diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js index b310526c7c..ef1952dc94 100644 --- a/cypress/integration/datetime.js +++ b/cypress/integration/datetime.js @@ -92,15 +92,15 @@ context('Control Date, Time and DateTime', () => { date_format: 'dd.mm.yyyy', time_format: 'HH:mm:ss', value: ' 02.12.2019 11:00:12', - doc_value: '2019-12-02 11:00:12', - input_value: '02.12.2019 11:00:12' + doc_value: '2019-12-02 00:30:12', // system timezone (America/New_York) + input_value: '02.12.2019 11:00:12' // admin timezone (Asia/Kolkata) }, { date_format: 'mm-dd-yyyy', time_format: 'HH:mm', value: ' 12-02-2019 11:00:00', - doc_value: '2019-12-02 11:00:00', - input_value: '12-02-2019 11:00' + doc_value: '2019-12-02 00:30:00', // system timezone (America/New_York) + input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata) } ]; datetime_formats.forEach(d => { diff --git a/frappe/boot.py b/frappe/boot.py index cf2b914436..e671d8b37d 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -17,6 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p from frappe.model.base_document import get_controller from frappe.social.doctype.post.post import frequently_visited_links from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo +from frappe.utils import get_time_zone def get_bootinfo(): """build and return boot info""" @@ -58,6 +59,7 @@ def get_bootinfo(): bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() bootinfo.notification_settings = get_notification_settings() + set_time_zone(bootinfo) # ipinfo if frappe.session.data.get('ipinfo'): @@ -220,8 +222,8 @@ def load_translations(bootinfo): bootinfo["__messages"] = messages def get_user_info(): - user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', - 'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'], + user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender', + 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'], filters=dict(enabled=1)) user_info_map = {d.name: d for d in user_info} @@ -324,3 +326,9 @@ def get_desk_settings(): def get_notification_settings(): return frappe.get_cached_doc('Notification Settings', frappe.session.user) + +def set_time_zone(bootinfo): + bootinfo.time_zone = { + "system": get_time_zone(), + "user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone() + } diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 12a8fa47fa..b4cfdf0a17 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -41,7 +41,19 @@ def run_server_script_for_doc_event(doc, event): if scripts: # run all scripts for this doctype + event for script_name in scripts: - frappe.get_doc('Server Script', script_name).execute_doc(doc) + try: + frappe.get_doc('Server Script', script_name).execute_doc(doc) + except Exception as e: + message = frappe._('Error executing Server Script {0}. Open Browser Console to see traceback.').format( + frappe.utils.get_link_to_form('Server Script', script_name) + ) + exception = type(e) + if getattr(frappe, 'request', None): + # all exceptions throw 500 which is internal server error + # however server script error is a user error + # so we should throw 417 which is expectation failed + exception.http_status_code = 417 + frappe.throw(title=frappe._('Server Script Error'), msg=message, exc=exception) def get_server_script_map(): # fetch cached server script methods diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js index c0c9074cbc..4eeab0274b 100644 --- a/frappe/core/doctype/system_settings/system_settings.js +++ b/frappe/core/doctype/system_settings/system_settings.js @@ -32,5 +32,11 @@ frappe.ui.form.on("System Settings", { frm.set_value('prepared_report_expiry_period', 7); } } + }, + on_update: function(frm) { + if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) { + // Clear cache after saving to refresh the values of boot. + frappe.ui.toolbar.clear_cache(); + } } }); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index dcec9b13c2..3e04643256 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -97,6 +97,7 @@ "fieldname": "time_zone", "fieldtype": "Select", "label": "Time Zone", + "read_only": 1, "reqd": 1 }, { diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 5b3a1affd9..77c199cdd4 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -77,7 +77,12 @@ frappe.ui.form.on('User', { } }, refresh: function(frm) { - var doc = frm.doc; + let doc = frm.doc; + + if (frm.is_new()) { + frm.set_value("time_zone", frappe.sys_defaults.time_zone); + } + if (in_list(['System User', 'Website User'], frm.doc.user_type) && !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) { frm.reload_doc(); @@ -267,6 +272,12 @@ frappe.ui.form.on('User', { } } }); + }, + on_update: function(frm) { + if (frappe.boot.time_zone && frappe.boot.time_zone.user !== frm.doc.time_zone) { + // Clear cache after saving to refresh the values of boot. + frappe.ui.toolbar.clear_cache(); + } } }); diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 6c729901e5..2d2ad1fed9 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -7,7 +7,7 @@ import frappe.defaults import frappe.permissions from frappe.model.document import Document from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime, - now_datetime, get_formatted_email, today) + now_datetime, get_formatted_email, today, get_time_zone) from frappe import throw, msgprint, _ from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit from frappe.desk.notifications import clear_notifications @@ -74,6 +74,7 @@ class User(Document): self.validate_roles() self.validate_allowed_modules() self.validate_user_image() + self.set_time_zone() if self.language == "Loading...": self.language = None @@ -227,11 +228,11 @@ class User(Document): def validate_share(self, docshare): pass # if docshare.user == self.name: - # if self.user_type=="System User": - # if docshare.share != 1: - # frappe.throw(_("Sorry! User should have complete access to their own record.")) - # else: - # frappe.throw(_("Sorry! Sharing with Website User is prohibited.")) + # if self.user_type=="System User": + # if docshare.share != 1: + # frappe.throw(_("Sorry! User should have complete access to their own record.")) + # else: + # frappe.throw(_("Sorry! Sharing with Website User is prohibited.")) def send_password_notification(self, new_password): try: @@ -596,6 +597,10 @@ class User(Document): return user + def set_time_zone(self): + if not self.time_zone: + self.time_zone = get_time_zone() + @frappe.whitelist() def get_timezones(): import pytz diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 579191efbe..252c463d3d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -120,6 +120,11 @@ class Meta(Document): or (not no_nulls and value is None)): out[key] = value + # set empty lists for unset table fields + for table_field in DOCTYPE_TABLE_FIELDS: + if not out.get(table_field.fieldname): + out[table_field.fieldname] = [] + return out return serialize(self) diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index b9945060cd..28e7f2a478 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -53,8 +53,6 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat let date_format = sysdefaults && sysdefaults.date_format ? sysdefaults.date_format : 'yyyy-mm-dd'; - let now_date = new Date(); - this.today_text = __("Today"); this.date_format = frappe.defaultDateFormat; this.datepicker_options = { @@ -62,7 +60,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat autoClose: true, todayButton: true, dateFormat: date_format, - startDate: now_date, + startDate: this.get_start_date(), keyboardNav: false, onSelect: () => { this.$input.trigger('change'); @@ -77,6 +75,11 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat ...(this.get_df_options()) }; } + + get_start_date() { + return new Date(this.get_now_date()); + } + set_datepicker() { this.$input.datepicker(this.datepicker_options); this.datepicker = this.$input.data('datepicker'); @@ -113,7 +116,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat this.datepicker.update('position', position); } get_now_date() { - return frappe.datetime.now_date(true); + return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true)); } set_t_for_today() { var me = this; diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js index f7a2798a99..5d0ecb9fe7 100644 --- a/frappe/public/js/frappe/form/controls/datetime.js +++ b/frappe/public/js/frappe/form/controls/datetime.js @@ -1,4 +1,22 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.ControlDate { + set_formatted_input(value) { + if (this.timepicker_only) return; + if (!this.datepicker) return; + if (!value) { + this.datepicker.clear(); + return; + } else if (value === "Today") { + value = this.get_now_date(); + } + value = this.format_for_input(value); + this.$input && this.$input.val(value); + this.datepicker.selectDate(frappe.datetime.user_to_obj(value)); + } + + get_start_date() { + let value = frappe.datetime.convert_to_user_tz(this.value); + return frappe.datetime.str_to_obj(value); + } set_date_options() { super.set_date_options(); this.today_text = __("Now"); @@ -14,10 +32,31 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co get_now_date() { return frappe.datetime.now_datetime(true); } + parse(value) { + if (value) { + value = frappe.datetime.user_to_str(value, false); + + if (!frappe.datetime.is_system_time_zone()) { + value = frappe.datetime.convert_to_system_tz(value, true); + } + + return value; + } + } + format_for_input(value) { + if (!value) return ""; + + + return frappe.datetime.str_to_user(value, false); + } set_description() { - const { description } = this.df; - const { time_zone } = frappe.sys_defaults; - if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) { + const description = this.df.description; + const time_zone = this.get_user_time_zone(); + + if (!this.df.hide_timezone) { + // Always show the timezone when rendering the Datetime field since the datetime value will + // always be in system_time_zone rather then local time. + if (!description) { this.df.description = time_zone; } else if (!description.includes(time_zone)) { @@ -26,6 +65,9 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co } super.set_description(); } + get_user_time_zone() { + return frappe.boot.time_zone ? frappe.boot.time_zone.user : frappe.sys_defaults.time_zone; + } set_datepicker() { super.set_datepicker(); if (this.datepicker.opts.timeFormat.indexOf('s') == -1) { diff --git a/frappe/public/js/frappe/form/controls/time.js b/frappe/public/js/frappe/form/controls/time.js index a7b6645681..f7fcc4c618 100644 --- a/frappe/public/js/frappe/form/controls/time.js +++ b/frappe/public/js/frappe/form/controls/time.js @@ -71,7 +71,7 @@ frappe.ui.form.ControlTime = class ControlTime extends frappe.ui.form.ControlDat set_description() { const { description } = this.df; const { time_zone } = frappe.sys_defaults; - if (!frappe.datetime.is_timezone_same()) { + if (!frappe.datetime.is_system_time_zone()) { if (!description) { this.df.description = time_zone; } else if (!description.includes(time_zone)) { diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 7e1f3fed06..851fbe3423 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -167,12 +167,8 @@ frappe.form.formatters = { }, Datetime: function(value) { if(value) { - var m = moment(frappe.datetime.convert_to_user_tz(value)); - if(frappe.boot.sysdefaults.time_zone) { - m = m.tz(frappe.boot.sysdefaults.time_zone); - } - return m.format(frappe.boot.sysdefaults.date_format.toUpperCase() - + ' ' + (frappe.boot.sysdefaults.time_format || 'HH:mm:ss')); + return moment(frappe.datetime.convert_to_user_tz(value)) + .format(frappe.boot.sysdefaults.date_format.toUpperCase() + ' ' + frappe.boot.sysdefaults.time_format || 'HH:mm:ss'); } else { return ""; } diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js index 99d47a6deb..7bb6076b72 100644 --- a/frappe/public/js/frappe/utils/datetime.js +++ b/frappe/public/js/frappe/utils/datetime.js @@ -13,33 +13,48 @@ frappe.provide("frappe.datetime"); $.extend(frappe.datetime, { convert_to_user_tz: function(date, format) { // format defaults to true - if(frappe.sys_defaults.time_zone) { - var date_obj = moment.tz(date, frappe.sys_defaults.time_zone).local(); + // Converts the datetime string to system time zone first since the database only stores datetime in + // system time zone and then convert the string to user time zone(from User doctype). + let date_obj = null; + if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) { + date_obj = moment.tz(date, frappe.boot.time_zone.system) + .clone() + .tz(frappe.boot.time_zone.user); } else { - var date_obj = moment(date); + date_obj = moment(date); } - return (format===false) ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); + return format === false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); }, convert_to_system_tz: function(date, format) { // format defaults to true - - if(frappe.sys_defaults.time_zone) { - var date_obj = moment(date).tz(frappe.sys_defaults.time_zone); + // Converts the datetime string to user time zone (from User doctype) first since this fn is called in datetime which accepts datetime + // in user time zone then convert the string to user time zone. + // This is done so that only one timezone is present in database and we do not end up storing local timezone since it changes + // as per the location of user. + let date_obj = null; + if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) { + date_obj = moment.tz(date, frappe.boot.time_zone.user) + .clone() + .tz(frappe.boot.time_zone.system); } else { - var date_obj = moment(date); + date_obj = moment(date); } - return (format===false) ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); + return format===false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); + }, + + is_system_time_zone: function() { + if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) { + return moment().tz(frappe.boot.time_zone.system).utcOffset() === moment().tz(frappe.boot.time_zone.user).utcOffset(); + } + + return true; }, is_timezone_same: function() { - if(frappe.sys_defaults.time_zone) { - return moment().tz(frappe.sys_defaults.time_zone).utcOffset() === moment().utcOffset(); - } else { - return true; - } + return frappe.datetime.is_system_time_zone(); }, str_to_obj: function(d) { @@ -98,11 +113,11 @@ $.extend(frappe.datetime, { return moment().endOf("quarter").format(); }, - year_start: function(){ + year_start: function() { return moment().startOf("year").format(); }, - year_end: function(){ + year_end: function() { return moment().endOf("year").format(); }, @@ -119,19 +134,25 @@ $.extend(frappe.datetime, { }, str_to_user: function(val, only_time = false) { - if(!val) return ""; + if (!val) return ""; + const user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase(); + const user_time_fmt = frappe.datetime.get_user_time_fmt(); + let user_format = user_time_fmt; - var user_time_fmt = frappe.datetime.get_user_time_fmt(); - if(only_time) { - return moment(val, frappe.defaultTimeFormat) - .format(user_time_fmt); - } - - var user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase(); - if(typeof val !== "string" || val.indexOf(" ")===-1) { - return moment(val).format(user_date_fmt); + if (only_time) { + let date_obj = moment(val, frappe.defaultTimeFormat); + return date_obj.format(user_format); } else { - return moment(val, "YYYY-MM-DD HH:mm:ss").format(user_date_fmt + " " + user_time_fmt); + let date_obj = moment.tz(val, frappe.boot.time_zone.system); + if (typeof val !== "string" || val.indexOf(" ") === -1) { + user_format = user_date_fmt; + } else { + user_format = user_date_fmt + " " + user_time_fmt; + } + return date_obj + .clone() + .tz(frappe.boot.time_zone.user) + .format(user_format); } }, @@ -186,23 +207,22 @@ $.extend(frappe.datetime, { }, _date: function(format, as_obj = false) { - const time_zone = frappe.sys_defaults && frappe.sys_defaults.time_zone; - let date; - if (time_zone) { - date = moment.tz(time_zone); - } else { - date = moment(); - } - if (as_obj) { - return frappe.datetime.moment_to_date_obj(date); - } else { - return date.format(format); - } + /** + * Whenever we are getting now_date/datetime, always make sure dates are fetched using user time zone. + * This is to make sure that time is as per user time zone set in User doctype, If a user had to change the timezone, + * we will end up having multiple timezone by not honouring timezone in User doctype. + * This will make sure that at any point we know which timezone the user if following and not have random timezone + * when the timezone of the local machine changes. + */ + let time_zone = frappe.boot.time_zone ? frappe.boot.time_zone.user || frappe.boot.time_zone.system : frappe.sys_defaults.time_zone; + let date = moment.tz(time_zone); + + return as_obj ? frappe.datetime.moment_to_date_obj(date) : date.format(format); }, - moment_to_date_obj: function(moment) { + moment_to_date_obj: function(moment_obj) { const date_obj = new Date(); - const date_array = moment.toArray(); + const date_array = moment_obj.toArray(); date_obj.setFullYear(date_array[0]); date_obj.setMonth(date_array[1]); date_obj.setDate(date_array[2]); diff --git a/frappe/public/js/frappe/utils/pretty_date.js b/frappe/public/js/frappe/utils/pretty_date.js index 3ebe2c1ae2..a5279682ce 100644 --- a/frappe/public/js/frappe/utils/pretty_date.js +++ b/frappe/public/js/frappe/utils/pretty_date.js @@ -6,7 +6,7 @@ function prettyDate(date, mini) { date = new Date((date || "").replace(/-/g, "/").replace(/[TZ]/g, " ").replace(/\.[0-9]*/, "")); } - let diff = (((new Date()).getTime() - date.getTime()) / 1000); + let diff = (((new Date(frappe.datetime.now_datetime())).getTime() - date.getTime()) / 1000); let day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff < 0) return ''; diff --git a/frappe/utils/data.py b/frappe/utils/data.py index d39d32d8df..de0242df07 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -882,7 +882,8 @@ number_format_info = { "#,##,###.##": (".", ",", 2), "#,###.###": (".", ",", 3), "#.###": ("", ".", 0), - "#,###": ("", ",", 0) + "#,###": ("", ",", 0), + "#.########": (".", "", 8) } def get_number_format_info(format): diff --git a/frappe/utils/image.py b/frappe/utils/image.py index fcde948f6c..3b0daf565c 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -30,6 +30,9 @@ def strip_exif_data(content, content_type): original_image = Image.open(io.BytesIO(content)) output = io.BytesIO() + # ref: https://stackoverflow.com/a/48248432 + if content_type == "image/jpeg" and original_image.mode in ("RGBA", "P"): + original_image = original_image.convert("RGB") new_image = Image.new(original_image.mode, original_image.size) new_image.putdata(list(original_image.getdata())) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 6f317855a0..f51efefc85 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -184,10 +184,32 @@ def get_safe_globals(): # allow iterators and list comprehension out._getiter_ = iter out._iter_unpack_sequence_ = RestrictedPython.Guards.guarded_iter_unpack_sequence - out.sorted = sorted + + # add common python builtins + out.update(get_python_builtins()) return out +def get_python_builtins(): + return { + 'abs': abs, + 'all': all, + 'any': any, + 'bool': bool, + 'dict': dict, + 'enumerate': enumerate, + 'isinstance': isinstance, + 'issubclass': issubclass, + 'list': list, + 'max': max, + 'min': min, + 'range': range, + 'set': set, + 'sorted': sorted, + 'sum': sum, + 'tuple': tuple, + } + def get_hooks(hook=None, default=None, app_name=None): hooks = frappe.get_hooks(hook=hook, default=default, app_name=app_name) return copy.deepcopy(hooks)