From c92b5d01660d47f3c145f9f5333ec7c58d019871 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 24 Aug 2020 19:12:26 +0530 Subject: [PATCH 001/121] fix: Handle duration fieldtype during export --- frappe/core/doctype/data_export/exporter.py | 4 ++- frappe/desk/query_report.py | 31 +++++++++++++++++++-- frappe/desk/reportview.py | 27 +++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index e4d2ff2af6..bec8cde7ea 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -8,7 +8,7 @@ from frappe import _ import frappe.permissions import re, csv, os from frappe.utils.csvutils import UnicodeWriter -from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint +from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration from frappe.core.doctype.data_import_legacy.importer import get_data_keys from six import string_types from frappe.core.doctype.access_log.access_log import make_access_log @@ -330,6 +330,8 @@ class DataExporter: value = formatdate(value) elif fieldtype == "Datetime": value = format_datetime(value) + elif fieldtype == "Duration": + value = format_duration(value, df.hide_days) row[_column_start_end.start + i + 1] = value diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index a1cfd02132..8e1c98c2af 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,14 +8,13 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, get_url_to_form +from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview from frappe.permissions import get_role_permissions from six import string_types, iteritems from datetime import timedelta -from frappe.utils import gzip_decompress from frappe.core.utils import ljust_list def get_report_doc(report_name): @@ -83,6 +82,8 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) if cint(report.add_total_row) and result and not skip_total_row: result = add_total_row(result, columns) + result = handle_duration_fieldtype_values(columns, result) + return { "result": result, "columns": columns, @@ -266,6 +267,32 @@ def get_columns_from_dict(columns, result): return reordered_result + +def handle_duration_fieldtype_values(columns, result, meta=None): + for i, col in enumerate(columns): + fieldtype, fieldname = None, None + if isinstance(col, string_types): + col = col.split(":") + if len(col) > 1: + if col[1]: + fieldtype = col[1] + if "/" in fieldtype: + fieldtype, options = fieldtype.split("/") + else: + fieldtype = "Data" + else: + fieldtype = col.get("fieldtype") + fieldname = col.get("fieldname") + + if fieldtype == "Duration": + for entry in range(0, len(result)): + val_in_seconds = result[entry][i] + if val_in_seconds: + duration_val = format_duration(val_in_seconds) + result[entry][i] = duration_val + return result + + def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} doc = None diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 6102be61ce..340e447f19 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -11,7 +11,7 @@ from frappe.model.db_query import DatabaseQuery from frappe import _ from six import string_types, StringIO from frappe.core.doctype.access_log.access_log import make_access_log -from frappe.utils import cstr +from frappe.utils import cstr, format_duration @frappe.whitelist() @@ -166,6 +166,8 @@ def export_query(): for i, row in enumerate(ret): data.append([i+1] + list(row)) + data = handle_duration_fieldtype_values(doctype, data, db_query.fields) + if file_format_type == "CSV": # convert to csv @@ -235,6 +237,29 @@ def get_labels(fields, doctype): return labels +def handle_duration_fieldtype_values(doctype, data, fields): + for field in fields: + key = field.split(" as ")[0] + + if key.startswith(('count(', 'sum(', 'avg(')): continue + + if "." in key: + parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") + else: + parenttype = doctype + fieldname = field.strip("`") + + df = frappe.get_meta(parenttype).get_field(fieldname) + + if df and df.fieldtype == 'Duration': + index = fields.index(field) + 1 + for i in range(1, len(data)): + val_in_seconds = data[i][index] + if val_in_seconds: + duration_val = format_duration(val_in_seconds, df.hide_days) + data[i][index] = duration_val + return data + @frappe.whitelist() def delete_items(): """delete selected items""" From c0b4532ea510c3e905ab3052d63d867badf87edd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 24 Aug 2020 19:12:59 +0530 Subject: [PATCH 002/121] fix: Handle duration fieldtype for Data Import --- frappe/core/doctype/data_import/exporter.py | 5 +++ frappe/core/doctype/data_import/importer.py | 4 ++- .../doctype/data_import_legacy/importer.py | 5 +-- frappe/utils/data.py | 33 +++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 3eef6ce016..2ef206f56a 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -8,6 +8,7 @@ from frappe.model import ( no_value_fields, table_fields as table_fieldtypes, ) +from frappe.utils import flt, format_duration from frappe.utils.csvutils import build_csv_response from frappe.utils.xlsxutils import build_xlsx_response @@ -148,6 +149,10 @@ class Exporter: continue row[i] = doc.get(df.fieldname, "") + if df.fieldtype == "Duration": + value = flt(doc.get(df.fieldname, 0)) + row[i] = format_duration(value, df.hide_days) + return rows def get_data_as_docs(self): diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 2c10c6b0a5..301c356e45 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -9,7 +9,7 @@ import timeit import json from datetime import datetime, date from frappe import _ -from frappe.utils import cint, flt, update_progress_bar, cstr +from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets from frappe.utils.xlsxutils import ( read_xlsx_file_from_attached_file, @@ -692,6 +692,8 @@ class Row: value = flt(value) elif df.fieldtype in ["Date", "Datetime"]: value = self.get_date(value, col) + elif df.fieldtype == "Duration": + value = duration_to_seconds(value) return value diff --git a/frappe/core/doctype/data_import_legacy/importer.py b/frappe/core/doctype/data_import_legacy/importer.py index 5bd0daf32b..f7f196da61 100644 --- a/frappe/core/doctype/data_import_legacy/importer.py +++ b/frappe/core/doctype/data_import_legacy/importer.py @@ -15,7 +15,7 @@ from frappe import _ from frappe.utils.csvutils import getlink from frappe.utils.dateutils import parse_date -from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url +from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds from six import string_types @@ -164,7 +164,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, d[fieldname] = get_datetime(_date + " " + _time) else: d[fieldname] = None - + elif df.fieldtype == "Duration": + d[fieldname] = duration_to_seconds(cstr(d[fieldname])) elif fieldtype in ("Image", "Attach Image", "Attach"): # added file to attachments list attachments.append(d[fieldname]) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index fd5c838b57..2a5ab8d5a3 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -346,6 +346,11 @@ def format_datetime(datetime_string, format_string=None): return formatted_datetime def format_duration(seconds, hide_days=False): + """Converts the given duration value in float(seconds) to duration format + + example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float + """ + total_duration = { 'days': math.floor(seconds / (3600 * 24)), 'hours': math.floor(seconds % (3600 * 24) / 3600), @@ -373,6 +378,34 @@ def format_duration(seconds, hide_days=False): return duration +def duration_to_seconds(duration): + """Converts the given duration formatted value to duration value in seconds + + example: converts '3h 34m 45s' to 12885 (value in seconds) + """ + value = 0 + if 'd' in duration: + val = duration.split('d') + days = val[0] + value += cint(days) * 24 * 60 * 60 + duration = val[1] + if 'h' in duration: + val = duration.split('h') + hours = val[0] + value += cint(hours) * 60 * 60 + duration = val[1] + if 'm' in duration: + val = duration.split('m') + mins = val[0] + value += cint(mins) * 60 + duration = val[1] + if 's' in duration: + val = duration.split('s') + secs = val[0] + value += cint(secs) + + return value + def get_weekdays(): return ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] From 22a8cc2ddaee2db053dfc7255dc92d72a9454269 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 2 Sep 2020 16:31:02 +0530 Subject: [PATCH 003/121] fix: Uninstall App even if it doesn't exist on bench --- frappe/commands/site.py | 2 +- frappe/installer.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index d343d10126..f85a9b2474 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -452,7 +452,7 @@ def uninstall(context, app, dry_run, yes, no_backup, force): try: frappe.init(site=site) frappe.connect() - remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force) + remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force, verbose=context.verbose) finally: frappe.destroy() if not context.sites: diff --git a/frappe/installer.py b/frappe/installer.py index 4994646890..53a8d878c7 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals, print_function from six.moves import input -import os, json, subprocess, shutil +import os, json, subprocess, shutil, sys import click import frappe import frappe.database @@ -119,9 +119,12 @@ def remove_from_installed_apps(app_name): if frappe.flags.in_install: post_install() -def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False): +def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, verbose=True): """Remove app and all linked to the app's module with the app from a site.""" + if not (verbose or dry_run): + sys.stdout = open(os.devnull, "w") + # dont allow uninstall app if not installed unless forced if not force: if app_name not in frappe.get_installed_apps(): @@ -143,11 +146,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) frappe.flags.in_uninstall = True drop_doctypes = [] - # remove modules, doctypes, roles - for module_name in frappe.get_module_list(app_name): - for doctype in frappe.get_list("DocType", filters={"module": module_name}, - fields=["name", "issingle"]): - print("removing DocType {0}...".format(doctype.name)) + modules = (x.name for x in frappe.get_all("Module Def", filters={"app_name": app_name})) + for module_name in modules: + print("Deleting Module '{0}'".format(module_name)) + + for doctype in frappe.get_list("DocType", filters={"module": module_name}, fields=["name", "issingle"]): + print("* removing DocType '{0}'...".format(doctype.name)) if not dry_run: frappe.delete_doc("DocType", doctype.name) @@ -155,24 +159,22 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) if not doctype.issingle: drop_doctypes.append(doctype.name) - linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent']) ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"] doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes] for doctype in doctypes_with_linked_modules: for record in frappe.get_list(doctype, filters={"module": module_name}): - print("removing {0} {1}...".format(doctype, record.name)) + print("* removing {0} '{1}'...".format(doctype, record.name)) if not dry_run: frappe.delete_doc(doctype, record.name) - print("removing Module {0}...".format(module_name)) + print("* removing Module Def '{0}'...".format(module_name)) if not dry_run: frappe.delete_doc("Module Def", module_name) - remove_from_installed_apps(app_name) - if not dry_run: + remove_from_installed_apps(app_name) # drop tables after a commit frappe.db.commit() From dbea31943a6f58582c116423f4a170f0b0935f94 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 2 Sep 2020 17:08:00 +0530 Subject: [PATCH 004/121] fix: Add verbosity --- frappe/commands/site.py | 5 +++-- frappe/installer.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index f85a9b2474..9de06b3723 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -444,15 +444,16 @@ def remove_from_installed_apps(context, app): @click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) @click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False) @click.option('--force', help='Force remove app from site', is_flag=True, default=False) +@click.option('--verbose', help='Add verbosity', is_flag=True, default=False) @pass_context -def uninstall(context, app, dry_run, yes, no_backup, force): +def uninstall(context, app, dry_run, yes, no_backup, force, verbose): "Remove app and linked modules from site" from frappe.installer import remove_app for site in context.sites: try: frappe.init(site=site) frappe.connect() - remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force, verbose=context.verbose) + remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force, verbose=context.verbose or verbose) finally: frappe.destroy() if not context.sites: diff --git a/frappe/installer.py b/frappe/installer.py index 53a8d878c7..d0b6db77d4 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -122,9 +122,6 @@ def remove_from_installed_apps(app_name): def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, verbose=True): """Remove app and all linked to the app's module with the app from a site.""" - if not (verbose or dry_run): - sys.stdout = open(os.devnull, "w") - # dont allow uninstall app if not installed unless forced if not force: if app_name not in frappe.get_installed_apps(): @@ -138,6 +135,9 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, if not confirm: return + if not (verbose or dry_run): + sys.stdout = open(os.devnull, "w") + if not no_backup: from frappe.utils.backups import scheduled_backup print("Backing up...") @@ -181,6 +181,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, for doctype in set(drop_doctypes): frappe.db.sql("drop table `tab{0}`".format(doctype)) + sys.stdout = sys.__stdout__ click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green") frappe.flags.in_uninstall = False From 5c01c7145a75f84474e05ea826dceacd905bea70 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 17:38:21 +0530 Subject: [PATCH 005/121] fix: handle total for duration fieldtype --- frappe/desk/query_report.py | 32 ++------------------------ frappe/public/js/frappe/model/model.js | 2 +- 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 8e1c98c2af..fa1df349c3 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,7 +8,7 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration +from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration, cstr from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview @@ -82,8 +82,6 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) if cint(report.add_total_row) and result and not skip_total_row: result = add_total_row(result, columns) - result = handle_duration_fieldtype_values(columns, result) - return { "result": result, "columns": columns, @@ -267,32 +265,6 @@ def get_columns_from_dict(columns, result): return reordered_result - -def handle_duration_fieldtype_values(columns, result, meta=None): - for i, col in enumerate(columns): - fieldtype, fieldname = None, None - if isinstance(col, string_types): - col = col.split(":") - if len(col) > 1: - if col[1]: - fieldtype = col[1] - if "/" in fieldtype: - fieldtype, options = fieldtype.split("/") - else: - fieldtype = "Data" - else: - fieldtype = col.get("fieldtype") - fieldname = col.get("fieldname") - - if fieldtype == "Duration": - for entry in range(0, len(result)): - val_in_seconds = result[entry][i] - if val_in_seconds: - duration_val = format_duration(val_in_seconds) - result[entry][i] = duration_val - return result - - def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} doc = None @@ -454,7 +426,7 @@ def add_total_row(result, columns, meta = None): if i >= len(row): continue cell = row.get(fieldname) if isinstance(row, dict) else row[i] - if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell): + if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(cell): total_row[i] = flt(total_row[i]) + flt(cell) if fieldtype == "Percent" and i not in has_percent: diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 663850d08c..308d9bd5f8 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -31,7 +31,7 @@ $.extend(frappe.model, { {fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, ], - numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"], + numeric_fieldtypes: ["Int", "Float", "Currency", "Percent", "Duration"], std_fields_table: [ {fieldname:'parent', fieldtype:'Data', label:__('Parent')}, From 0888fb48a73d42cd3f2eaae2eb77bbd4a9947ebe Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 19:01:42 +0530 Subject: [PATCH 006/121] feat: validate duration format in data import --- frappe/core/doctype/data_import/importer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 301c356e45..5271690527 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -664,6 +664,20 @@ class Row: } ) return + elif df.fieldtype == "Duration": + import re + is_valid_duration = re.match("^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) + if not is_valid_duration: + self.warnings.append( + { + "row": self.row_number, + "col": col.column_number, + "field": df_as_json(df), + "message": _("Value {0} must be in the valid duration format: d h m s").format( + frappe.bold(value) + ) + } + ) return value From 32e864bb6c55d5f84f3cf7be56294510e8d12b30 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 19:21:56 +0530 Subject: [PATCH 007/121] test: duration fieldtype import --- .../data_import/fixtures/sample_import_file.csv | 10 +++++----- frappe/core/doctype/data_import/test_importer.py | 7 ++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/data_import/fixtures/sample_import_file.csv b/frappe/core/doctype/data_import/fixtures/sample_import_file.csv index ef5b96df58..693f400878 100644 --- a/frappe/core/doctype/data_import/fixtures/sample_import_file.csv +++ b/frappe/core/doctype/data_import/fixtures/sample_import_file.csv @@ -1,5 +1,5 @@ -Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number -Test ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7 - , , , , ,child title 2 ,child description 2 ,title child ,30-10-2019 ,5 ,child title again 2 ,22-09-2021 , , -Test 2 ,test description 2 ,1 ,2 , ,child mandatory title , ,title child man , , ,child mandatory again , , , -Test 3 ,test description 3 ,4 ,5 ,"" ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019 ,6 ,child title again asdf ,22-09-2022 ,9 , 71 +Title ,Description ,Number ,Duration,another_number ,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number +Test ,test description ,1,3h,2, ,child title ,child description ,child title ,14-08-2019,4,child title again ,22-09-2020,5,7 + , , ,, , ,child title 2,child description 2,title child ,30-10-2019,5,child title again 2,22-09-2021, , +Test 2,test description 2,1,4d 3h,2, ,child mandatory title , ,title child man , , ,child mandatory again , , , +Test 3,test description 3,4,5d 5h 45m,5, ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019,6,child title again asdf ,22-09-2022,9,71 \ No newline at end of file diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index bdadad7890..3b573e64a1 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import getdate +from frappe.utils import getdate, format_duration doctype_name = 'DocType for Import' @@ -24,6 +24,7 @@ class TestImporter(unittest.TestCase): self.assertEqual(doc1.description, 'test description') self.assertEqual(doc1.number, 1) + self.assertEqual(format_duration(doc1.duration), '3h') self.assertEqual(doc1.table_field_1[0].child_title, 'child title') self.assertEqual(doc1.table_field_1[0].child_description, 'child description') @@ -40,7 +41,10 @@ class TestImporter(unittest.TestCase): self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22')) self.assertEqual(doc2.description, 'test description 2') + self.assertEqual(format_duration(doc2.duration), '4d 3h') + self.assertEqual(doc3.another_number, 5) + self.assertEqual(format_duration(doc3.duration), '5d 5h 45m') def test_data_import_preview(self): import_file = get_import_file('sample_import_file') @@ -146,6 +150,7 @@ def create_doctype_if_not_exists(doctype_name, force=False): {'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, {'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'}, {'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'}, + {'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'}, {'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, {'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}, {'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, From 5f51c201773bee983c6f36869eefcfed120e4677 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 3 Sep 2020 13:36:30 +0530 Subject: [PATCH 008/121] fix(reports): handle duration fieldtype during export --- .../doctype/report_column/report_column.json | 4 +-- frappe/desk/query_report.py | 29 +++++++++++++++++++ frappe/public/js/frappe/utils/utils.js | 8 ++++- .../js/frappe/views/reports/query_report.js | 3 ++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/report_column/report_column.json b/frappe/core/doctype/report_column/report_column.json index 53b5dff9b6..2e6a22d29a 100644 --- a/frappe/core/doctype/report_column/report_column.json +++ b/frappe/core/doctype/report_column/report_column.json @@ -31,7 +31,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Fieldtype", - "options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", + "options": "Check\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", "reqd": 1 }, { @@ -48,7 +48,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-17 14:32:17.174796", + "modified": "2020-09-03 10:52:03.895817", "modified_by": "Administrator", "module": "Core", "name": "Report Column", diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index fa1df349c3..dc42228f81 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -359,6 +359,7 @@ def export_query(): columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx + data['result'] = handle_duration_fieldtype_values(data.get('result'), data.get('columns')) xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation) xlsx_file = make_xlsx(xlsx_data, "Query Report") @@ -366,6 +367,30 @@ def export_query(): frappe.response['filecontent'] = xlsx_file.getvalue() frappe.response['type'] = 'binary' +def handle_duration_fieldtype_values(result, columns): + for i, col in enumerate(columns): + fieldtype, fieldname = None, None + if isinstance(col, string_types): + col = col.split(":") + if len(col) > 1: + if col[1]: + fieldtype = col[1] + if "/" in fieldtype: + fieldtype, options = fieldtype.split("/") + else: + fieldtype = "Data" + else: + fieldtype = col.get("fieldtype") + fieldname = col.get("fieldname") + + if fieldtype == "Duration": + for entry in range(0, len(result)): + val_in_seconds = result[entry][i] + if val_in_seconds: + duration_val = format_duration(val_in_seconds) + result[entry][i] = duration_val + + return result def build_xlsx_data(columns, data, visible_idx, include_indentation): result = [[]] @@ -385,7 +410,11 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation): for idx in range(len(data.columns)): label = columns[idx]["label"] fieldname = columns[idx]["fieldname"] + fieldtype = columns[idx]["fieldtype"] cell_value = row.get(fieldname, row.get(label, "")) + if fieldtype == "Duration": + cell_value = format_duration(value) + if cint(include_indentation) and 'indent' in row and idx == 0: cell_value = (' ' * cint(row['indent'])) + cell_value row_data.append(cell_value) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 38c22c9c9f..d64be06869 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -824,8 +824,14 @@ Object.assign(frappe.utils, { }; }, - get_formatted_duration(value, duration_options) { + get_formatted_duration(value, duration_options=null) { let duration = ''; + if (!duration_options) { + duration_options = { + hide_days: 0, + hide_seconds: 0 + } + } if (value) { let total_duration = frappe.utils.seconds_to_duration(value, duration_options); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1bec65e460..0817d8cfa5 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1322,6 +1322,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return row .slice(standard_column_count) .map((cell, i) => { + if (cell.column.fieldtype === "Duration") { + cell.content = frappe.utils.get_formatted_duration(cell.content) + } if (include_indentation && i===0) { cell.content = ' '.repeat(row.meta.indent) + (cell.content || ''); } From 14af05037abe8e24c66a1907eda37f15d09e2890 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Aug 2020 13:15:00 +0530 Subject: [PATCH 009/121] feat: Section with Collapsible Content --- frappe/public/scss/page-builder.scss | 38 ++++++++++++++ .../section_with_collapsible_content.html | 21 ++++++++ .../section_with_collapsible_content.json | 51 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html create mode 100644 frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss index f6446a9ba9..28db0b5a85 100644 --- a/frappe/public/scss/page-builder.scss +++ b/frappe/public/scss/page-builder.scss @@ -409,3 +409,41 @@ } } } + + +/* Section with Collapsible Content */ + +.collapsible-items { + max-width: 46rem; +} + +.collapsible-item { + padding: 1.75rem 0; + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } +} + +.collapsible-item a { + text-decoration: none; +} + +.collapsible-item h3 { + margin-bottom: 0; +} + +.collapsible-content { + margin-top: 1rem; + margin-bottom: 0; + color: $gray-700; +} + +.section-with-collapsible-content.align-center { + .section-title, .section-description { + text-align: center; + } + .section-description, .collapsible-items { + margin: 0 auto; + } +} diff --git a/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html new file mode 100644 index 0000000000..2b86e7f992 --- /dev/null +++ b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html @@ -0,0 +1,21 @@ +
+

{{ title }}

+ {%- if subtitle -%} +

{{ subtitle }}

+ {%- endif -%} + +
+ {%- for item in items -%} +
+ {%- set collapse_id = 'id-' + frappe.utils.generate_hash('Collapse', 12) -%} + +
+ {{ frappe.utils.md_to_html(item.content) }} +
+
+ {%- endfor -%} +
+
diff --git a/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json new file mode 100644 index 0000000000..f35b8d793e --- /dev/null +++ b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json @@ -0,0 +1,51 @@ +{ + "creation": "2020-08-07 16:27:38.265089", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "subtitle", + "fieldtype": "Data", + "label": "Subtitle", + "reqd": 0 + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nCenter", + "reqd": 0 + }, + { + "fieldname": "items", + "fieldtype": "Table Break", + "label": "Items", + "reqd": 0 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "content", + "fieldtype": "Markdown Editor", + "label": "Content", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-08-13 15:51:23.728803", + "modified_by": "Administrator", + "name": "Section with Collapsible Content", + "owner": "Administrator", + "standard": 1, + "template": "" +} \ No newline at end of file From c0734921de5d30e335c9dedd6282dbcfff321abe Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Aug 2020 13:23:03 +0530 Subject: [PATCH 010/121] feat: Section with Image align center --- frappe/public/scss/page-builder.scss | 14 +++++++++++-- .../section_with_image.html | 16 +++++++------- .../section_with_image.json | 21 ++++++++++++++----- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss index 28db0b5a85..81c06420bc 100644 --- a/frappe/public/scss/page-builder.scss +++ b/frappe/public/scss/page-builder.scss @@ -1,6 +1,6 @@ .hero-content { .btn-primary { - margin-top: 1rem; + margin-top: 1rem; margin-right: 0.5rem; @include media-breakpoint-up(lg) { @@ -35,6 +35,15 @@ } } +.section-with-image.align-center { + text-align: center; + + .section-description, .section-image { + margin-left: auto; + margin-right: auto; + } +} + .section-image { margin-top: 2rem; border-radius: 0.75rem; @@ -444,6 +453,7 @@ text-align: center; } .section-description, .collapsible-items { - margin: 0 auto; + margin-left: auto; + margin-right: auto; } } diff --git a/frappe/website/web_template/section_with_image/section_with_image.html b/frappe/website/web_template/section_with_image/section_with_image.html index ffa47d089e..cfd98064ac 100644 --- a/frappe/website/web_template/section_with_image/section_with_image.html +++ b/frappe/website/web_template/section_with_image/section_with_image.html @@ -1,8 +1,10 @@ -

{{ title }}

-

{{ subtitle }}

+
+

{{ title }}

+

{{ subtitle }}

-{{ frappe.render_template('templates/includes/image_with_blur.html', { - "src": image, - "alt": image_description, - "class": "section-image" -}) }} + {{ frappe.render_template('templates/includes/image_with_blur.html', { + "src": image, + "alt": image_description, + "class": "section-image" + }) }} +
diff --git a/frappe/website/web_template/section_with_image/section_with_image.json b/frappe/website/web_template/section_with_image/section_with_image.json index 5f610e5e2f..46169a8cc3 100644 --- a/frappe/website/web_template/section_with_image/section_with_image.json +++ b/frappe/website/web_template/section_with_image/section_with_image.json @@ -6,26 +6,37 @@ { "fieldname": "title", "fieldtype": "Data", - "label": "Title" + "label": "Title", + "reqd": 0 }, { "fieldname": "subtitle", "fieldtype": "Small Text", - "label": "Subtitle" + "label": "Subtitle", + "reqd": 0 }, { "fieldname": "image", "fieldtype": "Attach Image", - "label": "Image" + "label": "Image", + "reqd": 0 }, { "fieldname": "image_description", "fieldtype": "Data", - "label": "Image Description" + "label": "Image Description", + "reqd": 0 + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nCenter", + "reqd": 0 } ], "idx": 0, - "modified": "2020-04-17 19:31:33.474017", + "modified": "2020-08-06 16:08:12.005764", "modified_by": "Administrator", "name": "Section with Image", "owner": "Administrator", From 40a0c69255b7a3db0f5f1a8807ac90e576aaffb8 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Aug 2020 17:00:26 +0530 Subject: [PATCH 011/121] feat: Footer - Split footer in files - Footer grouped links styling - Put footer logo and extension in one row - Delete unused footer_items.html - Uncheck Right when adding Footer Items in Website Settings --- frappe/public/scss/footer.scss | 77 +++++++++++++++++++ frappe/public/scss/website.scss | 63 +-------------- frappe/templates/includes/footer/footer.html | 44 ++--------- .../includes/footer/footer_grouped_links.html | 56 ++++++++------ .../includes/footer/footer_info.html | 19 +++++ .../includes/footer/footer_items.html | 28 ------- .../includes/footer/footer_links.html | 8 +- .../footer/footer_logo_extension.html | 16 ++++ .../website_settings/website_settings.js | 6 +- 9 files changed, 158 insertions(+), 159 deletions(-) create mode 100644 frappe/public/scss/footer.scss create mode 100644 frappe/templates/includes/footer/footer_info.html delete mode 100644 frappe/templates/includes/footer/footer_items.html create mode 100644 frappe/templates/includes/footer/footer_logo_extension.html diff --git a/frappe/public/scss/footer.scss b/frappe/public/scss/footer.scss new file mode 100644 index 0000000000..9214907fbb --- /dev/null +++ b/frappe/public/scss/footer.scss @@ -0,0 +1,77 @@ +.web-footer { + padding: 5rem 0; + min-height: 140px; +} + +.footer-logo { + min-width: 5rem; + height: 1.5rem; + object-fit: contain; + object-position: left; +} + +.footer-child-item { + margin-top: 0.5rem; +} + +.footer-link, .footer-child-item a { + font-size: $font-size-sm; + font-weight: 500; + color: $gray-700; + + &:hover { + color: $primary; + text-decoration: none; + } +} + +.footer-col-left, .footer-col-right { + padding-top: 0.8rem; + padding-bottom: 1rem; + line-height: 2; + + &:empty { + padding: 0; + } +} + +.footer-col-right { + @include media-breakpoint-up(sm) { + text-align: right; + } +} + +.footer-col-left .footer-link { + margin-right: 1rem; +} + +.footer-col-right .footer-link { + margin-right: 1rem; + @include media-breakpoint-up(sm) { + margin-right: 0; + margin-left: 1rem; + } +} + +.footer-group { + margin-top: 2rem; +} + +.footer-group-label { + color: $text-muted; + font-size: $font-size-sm; + margin-bottom: 0.5rem; +} + +.footer-group-links { + display: flex; + flex-direction: column; + flex-wrap: wrap; + max-height: 10rem; +} + +.footer-info { + border-top: 1px solid $border-color; + color: $text-muted; + font-size: $font-size-sm; +} diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index e64c090ea8..59bbe4f19d 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -12,6 +12,7 @@ @import 'portal'; @import 'search'; @import 'doc'; +@import 'footer'; .ql-editor.read-mode { padding: 0; @@ -162,68 +163,6 @@ a.card { color: #d1d8dd !important; } -// footer - -.web-footer { - padding: 5rem 0; - min-height: 140px; -} - -.footer-logo { - width: 5rem; - height: 2rem; - object-fit: contain; - object-position: left; -} - -.footer-link, .footer-child-item a { - font-weight: 500; - color: $gray-700; - - &:hover { - color: $primary; - text-decoration: none; - } -} - -.footer-col-left, .footer-col-right { - padding-top: 0.8rem; - padding-bottom: 1rem; - line-height: 2; -} - -.footer-col-right { - @include media-breakpoint-up(sm) { - text-align: right; - } -} - -.footer-col-left .footer-link { - margin-right: 1rem; -} - -.footer-col-right .footer-link { - margin-right: 1rem; - @include media-breakpoint-up(sm) { - margin-right: 0; - margin-left: 1rem; - } -} - -.footer-group-label { - color: $text-muted; -} - -.footer-parent-item { - margin-bottom: 0.5rem; -} - -.footer-info { - border-top: 1px solid $border-color; - color: $text-muted; - font-size: $font-size-sm; -} - .no-underline { text-decoration: none !important; } diff --git a/frappe/templates/includes/footer/footer.html b/frappe/templates/includes/footer/footer.html index 671e928d32..2016c7e3d9 100644 --- a/frappe/templates/includes/footer/footer.html +++ b/frappe/templates/includes/footer/footer.html @@ -1,46 +1,12 @@
- {%- if footer_logo -%} -
- -
- {%- endif -%} -
-
- {% if footer_items -%} -
- {% include ["templates/includes/footer/footer_grouped_links.html", "templates/includes/footer/footer_items.html"] %} -
- {% endif %} -
+ {% include "templates/includes/footer/footer_logo_extension.html" %} -
- {% block extension %} - {% include "templates/includes/footer/footer_extension.html" %} - {% endblock %} -
-
+ {% if footer_items -%} + {% include "templates/includes/footer/footer_grouped_links.html" %} + {% endif %} {% include "templates/includes/footer/footer_links.html" %} - - + {% include "templates/includes/footer/footer_info.html" %}
diff --git a/frappe/templates/includes/footer/footer_grouped_links.html b/frappe/templates/includes/footer/footer_grouped_links.html index 6e20c51279..22cdb10824 100644 --- a/frappe/templates/includes/footer/footer_grouped_links.html +++ b/frappe/templates/includes/footer/footer_grouped_links.html @@ -1,28 +1,34 @@ -{% for page in footer_items if page.child_items %} -