diff --git a/src/Umbraco.Web.UI.Client/src/common/security/_module.js b/src/Umbraco.Web.UI.Client/src/common/security/_module.js index 15a7663d9b..c8289c754e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/_module.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/_module.js @@ -1,4 +1,4 @@ -// Based loosely around work by Witold Szczerba - https://github.com/witoldsz/angular-http-auth -angular.module('umbraco.security', [ - 'umbraco.security.retryQueue', - 'umbraco.security.interceptor']); \ No newline at end of file +//TODO: This is silly and unecessary to have a separate module for this +angular.module('umbraco.security.retryQueue', []); +angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']); +angular.module('umbraco.security', ['umbraco.security.retryQueue', 'umbraco.security.interceptor']); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js b/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js index e971719398..28d91dd610 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js @@ -1,3 +1,4 @@ +//TODO: This is silly and unecessary to have a separate module for this angular.module('umbraco.security.retryQueue', []) // This is a generic retry queue for security failures. Each item is expected to expose two functions: retry and cancel. diff --git a/src/Umbraco.Web.UI.Client/src/common/security/interceptor.js b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js similarity index 96% rename from src/Umbraco.Web.UI.Client/src/common/security/interceptor.js rename to src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js index 2b757707ba..b80754ef67 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js @@ -1,95 +1,95 @@ -angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']) - // This http interceptor listens for authentication successes and failures - .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'requestInterceptorFilter', function ($injector, queue, notifications, requestInterceptorFilter) { - return function(promise) { - - return promise.then( - function(originalResponse) { - // Intercept successful requests - - //Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it - //expires. Then we'll update the user in the user service accordingly. - var headers = originalResponse.headers(); - if (headers["x-umb-user-seconds"]) { - // We must use $injector to get the $http service to prevent circular dependency - var userService = $injector.get('userService'); - userService.setUserTimeout(headers["x-umb-user-seconds"]); - } - - return promise; - }, function(originalResponse) { - // Intercept failed requests - - //Here we'll check if we should ignore the error, this will be based on an original header set - var headers = originalResponse.config ? originalResponse.config.headers : {}; - if (headers["x-umb-ignore-error"] === "ignore") { - //exit/ignore - return promise; - } - var filtered = _.find(requestInterceptorFilter(), function(val) { - return originalResponse.config.url.indexOf(val) > 0; - }); - if (filtered) { - return promise; - } - - //A 401 means that the user is not logged in - if (originalResponse.status === 401) { - - // The request bounced because it was not authorized - add a new request to the retry queue - promise = queue.pushRetryFn('unauthorized-server', function retryRequest() { - // We must use $injector to get the $http service to prevent circular dependency - return $injector.get('$http')(originalResponse.config); - }); - } - else if (originalResponse.status === 404) { - - //a 404 indicates that the request was not found - this could be due to a non existing url, or it could - //be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it - - var errMsg = "The URL returned a 404 (not found):
" + originalResponse.config.url.split('?')[0] + ""; - if (originalResponse.data && originalResponse.data.ExceptionMessage) { - errMsg += "
with error:
" + originalResponse.data.ExceptionMessage + ""; - } - if (originalResponse.config.data) { - errMsg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; - } - - notifications.error( - "Request error", - errMsg); - - } - else if (originalResponse.status === 403) { - //if the status was a 403 it means the user didn't have permission to do what the request was trying to do. - //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was - //requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper, - // or completely custom for services calling resources. - - //http://issues.umbraco.org/issue/U4-2749 - - //It was decided to just put these messages into the normal status messages. - - var msg = "Unauthorized access to URL:
" + originalResponse.config.url.split('?')[0] + ""; - if (originalResponse.config.data) { - msg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; - } - - notifications.error( - "Authorization error", - msg); - } - - return promise; - }); - }; - }]) - - .value('requestInterceptorFilter', function() { - return ["www.gravatar.com"]; - }) - - // 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.responseInterceptors.push('securityInterceptor'); +angular.module('umbraco.security.interceptor') + // This http interceptor listens for authentication successes and failures + .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'requestInterceptorFilter', function ($injector, queue, notifications, requestInterceptorFilter) { + return function(promise) { + + return promise.then( + function(originalResponse) { + // Intercept successful requests + + //Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it + //expires. Then we'll update the user in the user service accordingly. + var headers = originalResponse.headers(); + if (headers["x-umb-user-seconds"]) { + // We must use $injector to get the $http service to prevent circular dependency + var userService = $injector.get('userService'); + userService.setUserTimeout(headers["x-umb-user-seconds"]); + } + + return promise; + }, function(originalResponse) { + // Intercept failed requests + + //Here we'll check if we should ignore the error, this will be based on an original header set + var headers = originalResponse.config ? originalResponse.config.headers : {}; + if (headers["x-umb-ignore-error"] === "ignore") { + //exit/ignore + return promise; + } + var filtered = _.find(requestInterceptorFilter(), function(val) { + return originalResponse.config.url.indexOf(val) > 0; + }); + if (filtered) { + return promise; + } + + //A 401 means that the user is not logged in + if (originalResponse.status === 401) { + + // The request bounced because it was not authorized - add a new request to the retry queue + promise = queue.pushRetryFn('unauthorized-server', function retryRequest() { + // We must use $injector to get the $http service to prevent circular dependency + return $injector.get('$http')(originalResponse.config); + }); + } + else if (originalResponse.status === 404) { + + //a 404 indicates that the request was not found - this could be due to a non existing url, or it could + //be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it + + var errMsg = "The URL returned a 404 (not found):
" + originalResponse.config.url.split('?')[0] + ""; + if (originalResponse.data && originalResponse.data.ExceptionMessage) { + errMsg += "
with error:
" + originalResponse.data.ExceptionMessage + ""; + } + if (originalResponse.config.data) { + errMsg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; + } + + notifications.error( + "Request error", + errMsg); + + } + else if (originalResponse.status === 403) { + //if the status was a 403 it means the user didn't have permission to do what the request was trying to do. + //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was + //requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper, + // or completely custom for services calling resources. + + //http://issues.umbraco.org/issue/U4-2749 + + //It was decided to just put these messages into the normal status messages. + + var msg = "Unauthorized access to URL:
" + originalResponse.config.url.split('?')[0] + ""; + if (originalResponse.config.data) { + msg += "
with data:
" + angular.toJson(originalResponse.config.data) + "
Contact your administrator for information."; + } + + notifications.error( + "Authorization error", + msg); + } + + return promise; + }); + }; + }]) + + .value('requestInterceptorFilter', function() { + return ["www.gravatar.com"]; + }) + + // 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.responseInterceptors.push('securityInterceptor'); }]); \ 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 30815027b0..be5e2e1232 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 @@ -58,6 +58,44 @@ function versionHelper() { } angular.module('umbraco.services').factory('versionHelper', versionHelper); +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/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index f9dedf5902..d046ac7104 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -9,7 +9,7 @@ small.umb-detail, label small, .guiDialogTiny { - color: #b3b3b3 !important; + color: @grayMed !important; text-decoration: none; display: block; font-weight: normal; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 83b61e48a5..aab0c5987c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -286,14 +286,14 @@ // form styles .umb-dialog .muted, .umb-panel .muted { - color: @grayLight; + color: @grayMed; } .umb-dialog a.muted:hover, .umb-dialog a.muted:focus, .umb-panel a.muted:hover, .umb-panel a.muted:focus { - color: darken(@grayLight, 10%); + color: darken(@grayMed, 10%); } .umb-dialog .text-warning, diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 5bddbd8022..8ebb0f08ef 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -14,7 +14,7 @@ @grayDarker: #222; @grayDark: #343434; @gray: #555; -@grayMed: #999; +@grayMed: #7f7f7f; @grayLight: #d9d9d9; @grayLighter: #f8f8f8; @white: #fff; 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..2c3495120a 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 !== undefined) { + $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,21 @@ function dateTimePickerController($scope, notificationsService, assetsService, a $scope.datePickerForm.datepicker.$setValidity("pickerError", true); } + $scope.serverTime = null; + $scope.serverTimeNeedsOffsetting = false; + if (Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { + // Will return something like 120 + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + + // Will return something like -120 + var localOffset = new Date().getTimezoneOffset(); + + // If these aren't equal then offsetting is needed + // note the minus in front of serverOffset needed + // because C# and javascript return the inverse offset + $scope.serverTimeNeedsOffsetting = (-serverOffset !== localOffset); + } + //get the current user to see if we can localize this picker userService.getCurrentUser().then(function (user) { @@ -97,8 +140,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 +169,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 +183,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..003e2ada60 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,10 @@ {{datePickerForm.datepicker.errorMsg}} Invalid date +

+ This translates to the following time on the server: {{serverTime}}
+ What does this mean? +

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.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 22d04a5a81..98278f890e 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -169,6 +169,8 @@ Not a member of group(s) Child items Target + This translates to the following time on the server: + What does this mean?]]> Click to upload diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 98270d2c4f..8c3f7f8d70 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -170,6 +170,8 @@ Not a member of group(s) Child items Target + This translates to the following time on the server: + What does this mean?]]> Click to upload diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 2103bedd1b..405ae75e9b 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -686,6 +686,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 efc9b7ced6..34cd64c265 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(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "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(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "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 6291afd195..64cdba4df6 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 622752e310..5c0d308e4f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -385,6 +385,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; + } } }