Merge pull request #1284 from umbraco/temp-U4-265
U4-265 Time zone adjustment in Umbraco backend for Date/Time picker
This commit is contained in:
@@ -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']);
|
||||
//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']);
|
||||
@@ -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.
|
||||
|
||||
@@ -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): <br/><i>" + originalResponse.config.url.split('?')[0] + "</i>";
|
||||
if (originalResponse.data && originalResponse.data.ExceptionMessage) {
|
||||
errMsg += "<br/> with error: <br/><i>" + originalResponse.data.ExceptionMessage + "</i>";
|
||||
}
|
||||
if (originalResponse.config.data) {
|
||||
errMsg += "<br/> with data: <br/><i>" + angular.toJson(originalResponse.config.data) + "</i><br/>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: <br/><i>" + originalResponse.config.url.split('?')[0] + "</i>";
|
||||
if (originalResponse.config.data) {
|
||||
msg += "<br/> with data: <br/><i>" + angular.toJson(originalResponse.config.data) + "</i><br/>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): <br/><i>" + originalResponse.config.url.split('?')[0] + "</i>";
|
||||
if (originalResponse.data && originalResponse.data.ExceptionMessage) {
|
||||
errMsg += "<br/> with error: <br/><i>" + originalResponse.data.ExceptionMessage + "</i>";
|
||||
}
|
||||
if (originalResponse.config.data) {
|
||||
errMsg += "<br/> with data: <br/><i>" + angular.toJson(originalResponse.config.data) + "</i><br/>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: <br/><i>" + originalResponse.config.url.split('?')[0] + "</i>";
|
||||
if (originalResponse.config.data) {
|
||||
msg += "<br/> with data: <br/><i>" + angular.toJson(originalResponse.config.data) + "</i><br/>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');
|
||||
}]);
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
small.umb-detail,
|
||||
label small, .guiDialogTiny {
|
||||
color: #b3b3b3 !important;
|
||||
color: @grayMed !important;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
@grayDarker: #222;
|
||||
@grayDark: #343434;
|
||||
@gray: #555;
|
||||
@grayMed: #999;
|
||||
@grayMed: #7f7f7f;
|
||||
@grayLight: #d9d9d9;
|
||||
@grayLighter: #f8f8f8;
|
||||
@white: #fff;
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
<span class="help-inline" val-msg-for="datepicker" val-toggle-msg="valServer">{{datePickerForm.datepicker.errorMsg}}</span>
|
||||
<span class="help-inline" val-msg-for="datepicker" val-toggle-msg="pickerError">Invalid date</span>
|
||||
|
||||
<p ng-if="model.config.offsetTime === '1' && serverTimeNeedsOffsetting && model.value" class="muted">
|
||||
<small><localize key="content_scheduledPublishServerTime">This translates to the following time on the server:</localize> {{serverTime}}</small><br/>
|
||||
<small><localize key="content_scheduledPublishDocumentation">What does this mean?</localize></small>
|
||||
</p>
|
||||
<p ng-show="hasDatetimePickerValue === true || datePickerForm.datepicker.$error.pickerError === true">
|
||||
<a href ng-click="clearDate()"><i class="icon-delete"></i><small><localize key="content_removeDate">Clear date</localize></small></a>
|
||||
</p>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user