diff --git a/src/Umbraco.Web.UI.Client/src/common/security/requestinterceptor.js b/src/Umbraco.Web.UI.Client/src/common/security/requestinterceptor.js deleted file mode 100644 index 8557ce1435..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/security/requestinterceptor.js +++ /dev/null @@ -1,26 +0,0 @@ -angular.module('umbraco.security.interceptor').factory("requestInterceptor", - ['$q', 'requestInterceptorFilter', function ($q, requestInterceptorFilter) { - var requestInterceptor = { - request: function (config) { - - var filtered = _.find(requestInterceptorFilter(), function (val) { - return config.url.indexOf(val) > 0; - }); - if (filtered) { - return config; - } - - config.headers["Time-Offset"] = (new Date().getTimezoneOffset()); - return config; - } - }; - - return requestInterceptor; - } - ]) - // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block. - .config([ - '$httpProvider', function($httpProvider) { - $httpProvider.interceptors.push('requestInterceptor'); - } - ]); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 9fbf2947af..2f06d4c393 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -1,5 +1,43 @@ /*Contains multiple services for various helper tasks */ +function dateHelper() { + + return { + + convertToServerStringTime: function(momentLocal, serverOffsetMinutes, format) { + + //get the formatted offset time in HH:mm (server time offset is in minutes) + var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + + moment() + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); + + var server = moment.utc(momentLocal).zone(formattedOffset); + return server.format(format ? format : "YYYY-MM-DD HH:mm:ss"); + }, + + convertToLocalMomentTime: function (strVal, serverOffsetMinutes) { + + //get the formatted offset time in HH:mm (server time offset is in minutes) + var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + + moment() + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); + + //convert to the iso string format + var isoFormat = moment(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset; + + //create a moment with the iso format which will include the offset with the correct time + // then convert it to local time + return moment.parseZone(isoFormat).local(); + } + + }; +} +angular.module('umbraco.services').factory('dateHelper', dateHelper); + function packageHelper(assetsService, treeService, eventsService, $templateCache) { return { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 693bd49ec6..95d0a1bdf6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -1,4 +1,4 @@ -function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService, $element) { +function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService, $element, dateHelper) { //setup the default config var config = { @@ -22,6 +22,8 @@ function dateTimePickerController($scope, notificationsService, assetsService, a $scope.model.config.format = $scope.model.config.pickTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD"; } + + $scope.hasDatetimePickerValue = $scope.model.value ? true : false; $scope.datetimePickerValue = null; @@ -43,20 +45,46 @@ function dateTimePickerController($scope, notificationsService, assetsService, a if (e.date && e.date.isValid()) { $scope.datePickerForm.datepicker.$setValidity("pickerError", true); $scope.hasDatetimePickerValue = true; - $scope.datetimePickerValue = e.date.format($scope.model.config.format); - $scope.model.value = $scope.datetimePickerValue; + $scope.datetimePickerValue = e.date.format($scope.model.config.format); } else { $scope.hasDatetimePickerValue = false; $scope.datetimePickerValue = null; } - + + setModelValue(); + if (!$scope.model.config.pickTime) { $element.find("div:first").datetimepicker("hide", 0); } }); } + //sets the scope model value accordingly - this is the value to be sent up to the server and depends on + // if the picker is configured to offset time. We always format the date/time in a specific format for sending + // to the server, this is different from the format used to display the date/time. + function setModelValue() { + if ($scope.hasDatetimePickerValue) { + var elementData = $element.find("div:first").data().DateTimePicker; + if ($scope.model.config.pickTime) { + //check if we are supposed to offset the time + if ($scope.model.value && $scope.model.config.offsetTime === "1" && Umbraco.Sys.ServerVariables.application.serverTimeOffset) { + $scope.model.value = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset); + $scope.serverTime = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset, "YYYY-MM-DD HH:mm:ss Z"); + } + else { + $scope.model.value = elementData.getDate().format("YYYY-MM-DD HH:mm:ss"); + } + } + else { + $scope.model.value = elementData.getDate().format("YYYY-MM-DD"); + } + } + else { + $scope.model.value = null; + } + } + var picker = null; $scope.clearDate = function() { @@ -66,6 +94,8 @@ function dateTimePickerController($scope, notificationsService, assetsService, a $scope.datePickerForm.datepicker.$setValidity("pickerError", true); } + $scope.serverTime = null; + //get the current user to see if we can localize this picker userService.getCurrentUser().then(function (user) { @@ -97,8 +127,17 @@ function dateTimePickerController($scope, notificationsService, assetsService, a }); if ($scope.hasDatetimePickerValue) { - //assign value to plugin/picker - var dateVal = $scope.model.value ? moment($scope.model.value, "YYYY-MM-DD HH:mm:ss") : moment(); + var dateVal; + //check if we are supposed to offset the time + if ($scope.model.value && $scope.model.config.offsetTime === "1" && Umbraco.Sys.ServerVariables.application.serverTimeOffset) { + //get the local time offset from the server + dateVal = dateHelper.convertToLocalMomentTime($scope.model.value, Umbraco.Sys.ServerVariables.application.serverTimeOffset); + $scope.serverTime = dateHelper.convertToServerStringTime(dateVal, Umbraco.Sys.ServerVariables.application.serverTimeOffset, "YYYY-MM-DD HH:mm:ss Z"); + } + else { + //create a normal moment , no offset required + var dateVal = $scope.model.value ? moment($scope.model.value, "YYYY-MM-DD HH:mm:ss") : moment(); + } element.datetimepicker("setValue", dateVal); $scope.datetimePickerValue = dateVal.format($scope.model.config.format); @@ -117,18 +156,7 @@ function dateTimePickerController($scope, notificationsService, assetsService, a var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - if ($scope.hasDatetimePickerValue) { - var elementData = $element.find("div:first").data().DateTimePicker; - if ($scope.model.config.pickTime) { - $scope.model.value = elementData.getDate().format("YYYY-MM-DD HH:mm:ss"); - } - else { - $scope.model.value = elementData.getDate().format("YYYY-MM-DD"); - } - } - else { - $scope.model.value = null; - } + setModelValue(); }); //unbind doc click event! $scope.$on('$destroy', function () { @@ -142,17 +170,7 @@ function dateTimePickerController($scope, notificationsService, assetsService, a }); var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - if ($scope.hasDatetimePickerValue) { - if ($scope.model.config.pickTime) { - $scope.model.value = $element.find("div:first").data().DateTimePicker.getDate().format("YYYY-MM-DD HH:mm:ss"); - } - else { - $scope.model.value = $element.find("div:first").data().DateTimePicker.getDate().format("YYYY-MM-DD"); - } - } - else { - $scope.model.value = null; - } + setModelValue(); }); //unbind doc click event! diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index e6d49e5c6c..c9b83cc8ed 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -18,6 +18,9 @@ {{datePickerForm.datepicker.errorMsg}} Invalid date +

+ Server time: {{serverTime}} +

Clear date

diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index 0f323f03aa..ede7c20538 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -24,6 +24,7 @@ module.exports = function(karma) { 'lib/../build/belle/lib/underscore/underscore-min.js', + 'lib/../build/belle/lib/moment/moment-with-locales.js', 'lib/umbraco/Extensions.js', 'lib/../build/belle/lib/rgrove-lazyload/lazyload.js', 'lib/../build/belle/lib/angular-local-storage/angular-local-storage.min.js', diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/date-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/date-helper.spec.js new file mode 100644 index 0000000000..74d2b38cfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/date-helper.spec.js @@ -0,0 +1,53 @@ +describe('date helper tests', function () { + var dateHelper; + + beforeEach(module('umbraco.services')); + + beforeEach(inject(function ($injector) { + dateHelper = $injector.get('dateHelper'); + })); + + describe('converting to local moments', function () { + + it('converts from a positive offset', function () { + var offsetMin = 600; //+10 + var strDate = "2016-01-01 10:00:00"; + + var result = dateHelper.convertToLocalMomentTime(strDate, offsetMin); + + expect(result.format("YYYY-MM-DD HH:mm:ss Z")).toBe("2016-01-01 01:00:00 +01:00"); + }); + + it('converts from a negataive offset', function () { + var offsetMin = -420; //-7 + var strDate = "2016-01-01 10:00:00"; + + var result = dateHelper.convertToLocalMomentTime(strDate, offsetMin); + + expect(result.format("YYYY-MM-DD HH:mm:ss Z")).toBe("2016-01-01 18:00:00 +01:00"); + }); + + }); + + describe('converting to server strings', function () { + + it('converts to a positive offset', function () { + var offsetMin = 600; //+10 + var localDate = moment("2016-01-01 10:00:00"); + + var result = dateHelper.convertToServerStringTime(localDate, offsetMin, "YYYY-MM-DD HH:mm:ss Z"); + + expect(result).toBe("2016-01-01 19:00:00 +10:00"); + }); + + it('converts from a negataive offset', function () { + var offsetMin = -420; //-7 + var localDate = moment("2016-01-01 10:00:00"); + + var result = dateHelper.convertToServerStringTime(localDate, offsetMin, "YYYY-MM-DD HH:mm:ss Z"); + + expect(result).toBe("2016-01-01 02:00:00 -07:00"); + }); + + }); +}); \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index b5cea7a945..6ee6f260f3 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -677,6 +677,10 @@ namespace Umbraco.Web.Editors app.Add("cdf", ClientDependencySettings.Instance.Version); //useful for dealing with virtual paths on the client side when hosted in virtual directories especially app.Add("applicationPath", HttpContext.Request.ApplicationPath.EnsureEndsWith('/')); + + //add the server's GMT time offset in minutes + app.Add("serverTimeOffset", Convert.ToInt32(DateTimeOffset.Now.Offset.TotalMinutes)); + return app; } diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index bf9f2056b3..495217cec2 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -97,10 +97,11 @@ namespace Umbraco.Web.Editors var d = new Dictionary(); //add the files if any var files = contentItem.UploadedFiles.Where(x => x.PropertyAlias == p.Alias).ToArray(); - if (files.Any()) + if (files.Length > 0) { d.Add("files", files); } + var data = new ContentPropertyData(p.Value, p.PreValues, d); //get the deserialized value from the property editor @@ -113,7 +114,7 @@ namespace Umbraco.Web.Editors var valueEditor = p.PropertyEditor.ValueEditor; //don't persist any bound value if the editor is readonly if (valueEditor.IsReadOnly == false) - { + { var propVal = p.PropertyEditor.ValueEditor.ConvertEditorToDb(data, dboProperty.Value); var supportTagsAttribute = TagExtractor.GetAttribute(p.PropertyEditor); if (supportTagsAttribute != null) diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index ede1dfc78d..e44fc056da 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -179,7 +179,11 @@ namespace Umbraco.Web.Models.Mapping Label = localizedText.Localize("content/releaseDate"), Value = display.ReleaseDate.HasValue ? display.ReleaseDate.Value.ToIsoString() : null, //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains('P') ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + View = display.AllowedActions.Contains('P') ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, + Config = new Dictionary + { + {"offsetTime", "1"} + } //TODO: Fix up hard coded datepicker } , new ContentPropertyDisplay @@ -188,7 +192,11 @@ namespace Umbraco.Web.Models.Mapping Label = localizedText.Localize("content/unpublishDate"), Value = display.ExpireDate.HasValue ? display.ExpireDate.Value.ToIsoString() : null, //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains('P') ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + View = display.AllowedActions.Contains('P') ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, + Config = new Dictionary + { + {"offsetTime", "1"} + } //TODO: Fix up hard coded datepicker }, new ContentPropertyDisplay diff --git a/src/Umbraco.Web/PropertyEditors/DateTimePreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/DateTimePreValueEditor.cs new file mode 100644 index 0000000000..242893cbca --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/DateTimePreValueEditor.cs @@ -0,0 +1,10 @@ +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + internal class DateTimePreValueEditor : DatePreValueEditor + { + [PreValueField("offsetTime", "Offset time", "boolean", Description = "When enabled the time displayed will be offset with the server's timezone, this is useful for scenarios like scheduled publishing when an editor is in a different timezone than the hosted server")] + public bool OffsetTime { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs index 38674de14e..2a76c0d50a 100644 --- a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -14,7 +17,11 @@ namespace Umbraco.Web.PropertyEditors { //NOTE: This is very important that we do not use .Net format's there, this format // is the correct format for the JS picker we are using so you cannot capitalize the HH, they need to be 'hh' - {"format", "YYYY-MM-DD HH:mm:ss"} + {"format", "YYYY-MM-DD HH:mm:ss"}, + //a pre-value indicating if the client/server time should be offset, when set to true the date/time seen + // by the client will be offset with the server time. + // For example, this is forced to true for scheduled publishing date/time pickers + {"offsetTime", "0"} }; } @@ -40,7 +47,9 @@ namespace Umbraco.Web.PropertyEditors protected override PreValueEditor CreatePreValueEditor() { - return new DatePreValueEditor(); + return new DateTimePreValueEditor(); } } + + } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 48005b1209..a1968d5e3f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -354,6 +354,7 @@ + diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs index dda19f9ff9..195c488b1e 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs @@ -166,7 +166,7 @@ namespace Umbraco.Web.WebApi.Binders //create the dto from the persisted model if (model.PersistedContent != null) { - model.ContentDto = MapFromPersisted(model); + model.ContentDto = MapFromPersisted(model); } if (model.ContentDto != null) { @@ -186,9 +186,15 @@ namespace Umbraco.Web.WebApi.Binders /// private static void MapPropertyValuesFromSaved(TModelSave saveModel, ContentItemDto dto) { - foreach (var p in saveModel.Properties.Where(p => dto.Properties.Any(x => x.Alias == p.Alias))) + //NOTE: Don't convert this to linq, this is much quicker + foreach (var p in saveModel.Properties) { - dto.Properties.Single(x => x.Alias == p.Alias).Value = p.Value; + foreach (var propertyDto in dto.Properties) + { + if (propertyDto.Alias != p.Alias) continue; + propertyDto.Value = p.Value; + break; + } } }