diff --git a/app/assets/javascripts/rails_admin/jquery.ui.timepicker.js b/app/assets/javascripts/rails_admin/jquery.ui.timepicker.js index 63ffa9a5a2..60a3d610f6 100644 --- a/app/assets/javascripts/rails_admin/jquery.ui.timepicker.js +++ b/app/assets/javascripts/rails_admin/jquery.ui.timepicker.js @@ -1,54 +1,54 @@ /* -* jQuery UI Timepicker 0.3.1 -* -* Copyright 2010-2011, Francois Gelinas -* Dual licensed under the MIT or GPL Version 2 licenses. -* http://jquery.org/license -* -* http://fgelinas.com/code/timepicker -* -* Depends: -* jquery.ui.core.js -* jquery.ui.position.js (only if position settngs are used) -* -* Change version 0.1.0 - moved the t-rex up here -* -____ -___ .-~. /_"-._ -`-._~-. / /_ "~o\ :Y -\ \ / : \~x. ` ') -] Y / | Y< ~-.__j -/ ! _.--~T : l l< /.-~ -/ / ____.--~ . ` l /~\ \<|Y -/ / .-~~" /| . ',-~\ \L| -/ / / .^ \ Y~Y \.^>/l_ "--' -/ Y .-"( . l__ j_j l_/ /~_.-~ . -Y l / \ ) ~~~." / `/"~ / \.__/l_ -| \ _.-" ~-{__ l : l._Z~-.___.--~ -| ~---~ / ~~"---\_ ' __[> -l . _.^ ___ _>-y~ -\ \ . .-~ .-~ ~>--" / -\ ~---" / ./ _.-' -"-.,_____.,_ _.--~\ _.-~ -~~ ( _} -Row -`. ~( -) \ -/,`--'~\--'~\ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->T-Rex<- + * jQuery UI Timepicker + * + * Copyright 2010-2013, Francois Gelinas + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://fgelinas.com/code/timepicker + * + * Depends: + * jquery.ui.core.js + * jquery.ui.position.js (only if position settngs are used) + * + * Change version 0.1.0 - moved the t-rex up here + * + ____ + ___ .-~. /_"-._ + `-._~-. / /_ "~o\ :Y + \ \ / : \~x. ` ') + ] Y / | Y< ~-.__j + / ! _.--~T : l l< /.-~ + / / ____.--~ . ` l /~\ \<|Y + / / .-~~" /| . ',-~\ \L| + / / / .^ \ Y~Y \.^>/l_ "--' + / Y .-"( . l__ j_j l_/ /~_.-~ . + Y l / \ ) ~~~." / `/"~ / \.__/l_ + | \ _.-" ~-{__ l : l._Z~-.___.--~ + | ~---~ / ~~"---\_ ' __[> + l . _.^ ___ _>-y~ + \ \ . .-~ .-~ ~>--" / + \ ~---" / ./ _.-' + "-.,_____.,_ _.--~\ _.-~ + ~~ ( _} -Row + `. ~( + ) \ + /,`--'~\--'~\ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ->T-Rex<- */ (function ($) { - $.extend($.ui, { timepicker: { version: "0.3.1"} }); + $.extend($.ui, { timepicker: { version: "0.3.2"} }); var PROP_NAME = 'timepicker', tpuuid = new Date().getTime(); /* Time picker manager. -Use the singleton instance of this class, $.timepicker, to interact with the time picker. -Settings for (groups of) time pickers are maintained in an instance object, -allowing multiple different settings on the same page. */ + Use the singleton instance of this class, $.timepicker, to interact with the time picker. + Settings for (groups of) time pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ function Timepicker() { this.debug = true; // Change this to true to start debugging @@ -64,61 +64,61 @@ allowing multiple different settings on the same page. */ this.regional = []; // Available regional settings, indexed by language code this.regional[''] = { // Default regional settings - hourText: 'Hour', // Display text for hours section - minuteText: 'Minute', // Display text for minutes link - amPmText: ['AM', 'PM'], // Display text for AM PM - closeButtonText: 'Done', // Text for the confirmation button (ok button) - nowButtonText: 'Now', // Text for the now button - deselectButtonText: 'Deselect' // Text for the deselect button + hourText: 'Hour', // Display text for hours section + minuteText: 'Minute', // Display text for minutes link + amPmText: ['AM', 'PM'], // Display text for AM PM + closeButtonText: 'Done', // Text for the confirmation button (ok button) + nowButtonText: 'Now', // Text for the now button + deselectButtonText: 'Deselect' // Text for the deselect button }; this._defaults = { // Global defaults for all the time picker instances - showOn: 'focus', // 'focus' for popup on focus, + showOn: 'focus', // 'focus' for popup on focus, // 'button' for trigger button, or 'both' for either (not yet implemented) - button: null, // 'button' element that will trigger the timepicker - showAnim: 'fadeIn', // Name of jQuery animation for popup - showOptions: {}, // Options for enhanced animations - appendText: '', // Display text following the input box, e.g. showing the format - - beforeShow: null, // Define a callback function executed before the timepicker is shown - onSelect: null, // Define a callback function when a hour / minutes is selected - onClose: null, // Define a callback function when the timepicker is closed - - timeSeparator: ':', // The character to use to separate hours and minutes. - periodSeparator: ' ', // The character to use to separate the time from the time period. - showPeriod: false, // Define whether or not to show AM/PM with selected time - showPeriodLabels: true, // Show the AM/PM labels on the left of the time picker - showLeadingZero: true, // Define whether or not to show a leading zero for hours < 10. [true/false] - showMinutesLeadingZero: true, // Define whether or not to show a leading zero for minutes < 10. - altField: '', // Selector for an alternate field to store selected time into - defaultTime: 'now', // Used as default time when input field is empty or for inline timePicker + button: null, // 'button' element that will trigger the timepicker + showAnim: 'fadeIn', // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + appendText: '', // Display text following the input box, e.g. showing the format + + beforeShow: null, // Define a callback function executed before the timepicker is shown + onSelect: null, // Define a callback function when a hour / minutes is selected + onClose: null, // Define a callback function when the timepicker is closed + + timeSeparator: ':', // The character to use to separate hours and minutes. + periodSeparator: ' ', // The character to use to separate the time from the time period. + showPeriod: false, // Define whether or not to show AM/PM with selected time + showPeriodLabels: true, // Show the AM/PM labels on the left of the time picker + showLeadingZero: true, // Define whether or not to show a leading zero for hours < 10. [true/false] + showMinutesLeadingZero: true, // Define whether or not to show a leading zero for minutes < 10. + altField: '', // Selector for an alternate field to store selected time into + defaultTime: 'now', // Used as default time when input field is empty or for inline timePicker // (set to 'now' for the current time, '' for no highlighted time) - myPosition: 'left top', // Position of the dialog relative to the input. + myPosition: 'left top', // Position of the dialog relative to the input. // see the position utility for more info : http://jqueryui.com/demos/position/ - atPosition: 'left bottom', // Position of the input element to match + atPosition: 'left bottom', // Position of the input element to match // Note : if the position utility is not loaded, the timepicker will attach left top to left bottom //NEW: 2011-02-03 - onHourShow: null, // callback for enabling / disabling on selectable hours ex : function(hour) { return true; } - onMinuteShow: null, // callback for enabling / disabling on time selection ex : function(hour,minute) { return true; } + onHourShow: null, // callback for enabling / disabling on selectable hours ex : function(hour) { return true; } + onMinuteShow: null, // callback for enabling / disabling on time selection ex : function(hour,minute) { return true; } hours: { - starts: 0, // first displayed hour - ends: 23 // last displayed hour + starts: 0, // first displayed hour + ends: 23 // last displayed hour }, minutes: { - starts: 0, // first displayed minute - ends: 55, // last displayed minute - interval: 5 // interval of displayed minutes + starts: 0, // first displayed minute + ends: 55, // last displayed minute + interval: 5 // interval of displayed minutes }, - rows: 4, // number of rows for the input tables, minimum 2, makes more sense if you use multiple of 2 + rows: 4, // number of rows for the input tables, minimum 2, makes more sense if you use multiple of 2 // 2011-08-05 0.2.4 - showHours: true, // display the hours section of the dialog - showMinutes: true, // display the minute section of the dialog - optionalMinutes: false, // optionally parse inputs of whole hours with minutes omitted - + showHours: true, // display the hours section of the dialog + showMinutes: true, // display the minute section of the dialog + optionalMinutes: false, // optionally parse inputs of whole hours with minutes omitted + // buttons - showCloseButton: false, // shows an OK button to confirm the edit - showNowButton: false, // Shows the 'now' button - showDeselectButton: false // Shows the deselect time button + showCloseButton: false, // shows an OK button to confirm the edit + showNowButton: false, // Shows the 'now' button + showDeselectButton: false // Shows the deselect time button }; $.extend(this._defaults, this.regional['']); @@ -141,16 +141,16 @@ allowing multiple different settings on the same page. */ }, /* Override the default settings for all instances of the time picker. -@param settings object - the new settings to use as defaults (anonymous object) -@return the manager object */ + @param settings object - the new settings to use as defaults (anonymous object) + @return the manager object */ setDefaults: function (settings) { extendRemove(this._defaults, settings || {}); return this; }, /* Attach the time picker to a jQuery selection. -@param target element - the target input field or division or span -@param settings object - the new settings to use for this time picker instance (anonymous) */ + @param target element - the target input field or division or span + @param settings object - the new settings to use for this time picker instance (anonymous) */ _attachTimepicker: function (target, settings) { // check for settings on the control itself - in namespace 'time:' var inlineSettings = null; @@ -192,7 +192,7 @@ allowing multiple different settings on the same page. */ id: id, input: target, // associated target inline: inline, // is timepicker inline or not : tpDiv: (!inline ? this.tpDiv : // presentation div - $('
')) + $('
')) }; }, @@ -229,8 +229,8 @@ allowing multiple different settings on the same page. */ $.timepicker._updateSelectedValue(inst); $.timepicker._hideTimepicker(); -return false; // don't submit the form -break; // select the value on enter + return false; // don't submit the form + break; // select the value on enter case 27: $.timepicker._hideTimepicker(); break; // hide on escape default: handled = false; @@ -315,8 +315,8 @@ break; // select the value on enter }, /* Pop-up the time picker for a given input field. -@param input element - the input field attached to the time picker or -event - if triggered by focus */ + @param input element - the input field attached to the time picker or + event - if triggered by focus */ _showTimepicker: function (input) { input = input.target || input; if (input.nodeName.toLowerCase() != 'input') { input = $('input', input.parentNode)[0]; } // find from button/image trigger @@ -348,10 +348,6 @@ event - if triggered by focus */ isFixed |= $(this).css('position') == 'fixed'; return !isFixed; }); - if (isFixed && $.browser.opera) { // correction for Opera when fixed and scrolled - $.timepicker._pos[0] -= document.documentElement.scrollLeft; - $.timepicker._pos[1] -= document.documentElement.scrollTop; - } var offset = { left: $.timepicker._pos[0], top: $.timepicker._pos[1] }; @@ -362,7 +358,7 @@ event - if triggered by focus */ // position with the ui position utility, if loaded - if ( ( ! inst.inline ) && ( typeof $.ui.position == 'object' ) ) { + if ( ( ! inst.inline ) && ( typeof $.ui.position == 'object' ) ) { inst.tpDiv.position({ of: inst.input, my: $.timepicker._get( inst, 'myPosition' ), @@ -384,7 +380,7 @@ event - if triggered by focus */ // and adjust position before showing offset = $.timepicker._checkOffset(inst, offset, isFixed); inst.tpDiv.css({ position: ($.timepicker._inDialog && $.blockUI ? -'static' : (isFixed ? 'fixed' : 'absolute')), display: 'none', + 'static' : (isFixed ? 'fixed' : 'absolute')), display: 'none', left: offset.left + 'px', top: offset.top + 'px' }); if ( ! inst.inline ) { @@ -395,9 +391,9 @@ event - if triggered by focus */ $.timepicker._timepickerShowing = true; var borders = $.timepicker._getBorders(inst.tpDiv); inst.tpDiv.find('iframe.ui-timepicker-cover'). // IE6- only -css({ left: -borders[0], top: -borders[1], -width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() -}); + css({ left: -borders[0], top: -borders[1], + width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() + }); }; // Fixed the zIndex problem for real (I hope) - FG - v 0.2.9 @@ -416,24 +412,31 @@ width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() } }, - // This is a copy of the zIndex function of UI core 1.8.?? - // Copied in the timepicker to stay backward compatible. + // This is an enhanced copy of the zIndex function of UI core 1.8.?? For backward compatibility. + // Enhancement returns maximum zindex value discovered while traversing parent elements, + // rather than the first zindex value found. Ensures the timepicker popup will be in front, + // even in funky scenarios like non-jq dialog containers with large fixed zindex values and + // nested zindex-influenced elements of their own. _getZIndex: function (target) { - var elem = $( target ), position, value; - while ( elem.length && elem[ 0 ] !== document ) { - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { - value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; + var elem = $(target); + var maxValue = 0; + var position, value; + while (elem.length && elem[0] !== document) { + position = elem.css("position"); + if (position === "absolute" || position === "relative" || position === "fixed") { + value = parseInt(elem.css("zIndex"), 10); + if (!isNaN(value) && value !== 0) { + if (value > maxValue) { maxValue = value; } } } elem = elem.parent(); } + + return maxValue; }, /* Refresh the time picker -@param target element - The target input field or inline container element. */ + @param target element - The target input field or inline container element. */ _refreshTimepicker: function(target) { var inst = this._getInst(target); if (inst) { @@ -453,11 +456,11 @@ width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() var borders = $.timepicker._getBorders(inst.tpDiv), self = this; inst.tpDiv -.find('iframe.ui-timepicker-cover') // IE6- only -.css({ left: -borders[0], top: -borders[1], -width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() -}) -.end() + .find('iframe.ui-timepicker-cover') // IE6- only + .css({ left: -borders[0], top: -borders[1], + width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() + }) + .end() // after the picker html is appended bind the click & double click events (faster in IE this way // then letting the browser interpret the inline events) // the binding for the minute cells also exists in _updateMinuteDisplay @@ -471,25 +474,25 @@ width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectHours, this)) .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectHours, this)) .end() -.find('.ui-timepicker td a') + .find('.ui-timepicker td a') .unbind() -.bind('mouseout', function () { -$(this).removeClass('ui-state-hover'); -if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).removeClass('ui-timepicker-prev-hover'); -if (this.className.indexOf('ui-timepicker-next') != -1) $(this).removeClass('ui-timepicker-next-hover'); -}) -.bind('mouseover', function () { -if ( ! self._isDisabledTimepicker(inst.inline ? inst.tpDiv.parent()[0] : inst.input[0])) { -$(this).parents('.ui-timepicker-calendar').find('a').removeClass('ui-state-hover'); -$(this).addClass('ui-state-hover'); -if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).addClass('ui-timepicker-prev-hover'); -if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-timepicker-next-hover'); -} -}) -.end() -.find('.' + this._dayOverClass + ' a') -.trigger('mouseover') -.end() + .bind('mouseout', function () { + $(this).removeClass('ui-state-hover'); + if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).removeClass('ui-timepicker-prev-hover'); + if (this.className.indexOf('ui-timepicker-next') != -1) $(this).removeClass('ui-timepicker-next-hover'); + }) + .bind('mouseover', function () { + if ( ! self._isDisabledTimepicker(inst.inline ? inst.tpDiv.parent()[0] : inst.input[0])) { + $(this).parents('.ui-timepicker-calendar').find('a').removeClass('ui-state-hover'); + $(this).addClass('ui-state-hover'); + if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).addClass('ui-timepicker-prev-hover'); + if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-timepicker-next-hover'); + } + }) + .end() + .find('.' + this._dayOverClass + ' a') + .trigger('mouseover') + .end() .find('.ui-timepicker-now').bind("click", function(e) { $.timepicker.selectNow(e); }).end() @@ -548,7 +551,7 @@ if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-tim pmItems++; } } - hourCounter = 0; + hourCounter = 0; amRows = Math.floor(amItems / hours.length * rows); pmRows = Math.floor(pmItems / hours.length * rows); @@ -639,11 +642,12 @@ if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-tim html += buttonPanel + ''; } html += ''; + return html; }, /* Special function that update the minutes selection in currently visible timepicker -* called on hour selection when onMinuteShow is defined */ + * called on hour selection when onMinuteShow is defined */ _updateMinuteDisplay: function (inst) { var newHtml = this._generateHTMLMinutes(inst); inst.tpDiv.find('td.ui-timepicker-minutes').html(newHtml); @@ -651,17 +655,17 @@ if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-tim // after the picker html is appended bind the click & double click events (faster in IE this way // then letting the browser interpret the inline events) // yes I know, duplicate code, sorry -/* .find('.ui-timepicker-minute-cell') -.bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectMinutes, this)) -.bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectMinutes, this)); +/* .find('.ui-timepicker-minute-cell') + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectMinutes, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectMinutes, this)); */ }, /* -* Generate the minutes table -* This is separated from the _generateHTML function because is can be called separately (when hours changes) -*/ + * Generate the minutes table + * This is separated from the _generateHTML function because is can be called separately (when hours changes) + */ _generateHTMLMinutes: function (inst) { var m, row, html = '', @@ -686,8 +690,8 @@ if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-tim minutesPerRow = Math.round(minutes.length / rows + 0.49); // always round up /* -* The minutes table -*/ + * The minutes table + */ // if currently selected minute is not enabled, we have a problem and need to select a new minute. if (onMinuteShow && (onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours , inst.minutes]) == false) ) { @@ -744,7 +748,7 @@ if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-tim var html = ""; var enabled = true; - var onHourShow = this._get(inst, 'onHourShow'); //custom callback + var onHourShow = this._get(inst, 'onHourShow'); //custom callback if (hour == undefined) { html = ' '; @@ -752,7 +756,7 @@ if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-tim } if (onHourShow) { - enabled = onHourShow.apply((inst.input ? inst.input[0] : null), [hour]); + enabled = onHourShow.apply((inst.input ? inst.input[0] : null), [hour]); } if (enabled) { @@ -764,26 +768,26 @@ if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-tim ''; } else { - html = - '' + -'' + -displayHour.toString() + -'' + -''; + html = + '' + + '' + + displayHour.toString() + + '' + + ''; } return html; }, /* Generate the content of a "Hour" cell */ _generateHTMLMinuteCell: function (inst, minute, displayText) { - var html = ""; + var html = ""; var enabled = true; - var onMinuteShow = this._get(inst, 'onMinuteShow'); //custom callback + var onMinuteShow = this._get(inst, 'onMinuteShow'); //custom callback if (onMinuteShow) { - //NEW: 2011-02-03 we should give the hour as a parameter as well! - enabled = onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours,minute]); //trigger callback + //NEW: 2011-02-03 we should give the hour as a parameter as well! + enabled = onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours,minute]); //trigger callback } if (minute == undefined) { @@ -792,19 +796,19 @@ displayHour.toString() + } if (enabled) { -html = '' + -'' + -displayText + -''; + html = '' + + '' + + displayText + + ''; } else { - html = '' + -'' + -displayText + -'' + + html = '' + + '' + + displayText + + '' + ''; } return html; @@ -812,7 +816,7 @@ displayText + /* Detach a timepicker from its control. -@param target element - the target input field or division or span */ + @param target element - the target input field or division or span */ _destroyTimepicker: function(target) { var $target = $(target); var inst = $.data(target, PROP_NAME); @@ -832,7 +836,7 @@ displayText + }, /* Enable the date picker to a jQuery selection. -@param target element - the target input field or division or span */ + @param target element - the target input field or division or span */ _enableTimepicker: function(target) { var $target = $(target), target_id = $target.attr('id'), @@ -861,7 +865,7 @@ displayText + }, /* Disable the time picker to a jQuery selection. -@param target element - the target input field or division or span */ + @param target element - the target input field or division or span */ _disableTimepicker: function(target) { var $target = $(target); var inst = $.data(target, PROP_NAME); @@ -893,8 +897,8 @@ displayText + }, /* Is the first field in a jQuery collection disabled as a timepicker? -@param target_id element - the target input field or division or span -@return boolean - true if disabled, false if enabled */ + @param target_id element - the target input field or division or span + @return boolean - true if disabled, false if enabled */ _isDisabledTimepicker: function (target_id) { if ( ! target_id) { return false; } for (var i = 0; i < this._disabledInputs.length; i++) { @@ -918,9 +922,9 @@ displayText + // now check if datepicker is showing outside window viewport - move to a better place if so. offset.left -= Math.min(offset.left, (offset.left + tpWidth > viewWidth && viewWidth > tpWidth) ? -Math.abs(offset.left + tpWidth - viewWidth) : 0); + Math.abs(offset.left + tpWidth - viewWidth) : 0); offset.top -= Math.min(offset.top, (offset.top + tpHeight > viewHeight && viewHeight > tpHeight) ? -Math.abs(tpHeight + inputHeight) : 0); + Math.abs(tpHeight + inputHeight) : 0); return offset; }, @@ -937,14 +941,14 @@ Math.abs(tpHeight + inputHeight) : 0); }, /* Retrieve the size of left and top borders for an element. -@param elem (jQuery object) the element of interest -@return (number[2]) the left and top borders */ + @param elem (jQuery object) the element of interest + @return (number[2]) the left and top borders */ _getBorders: function (elem) { var convert = function (value) { return { thin: 1, medium: 2, thick: 3}[value] || value; }; return [parseFloat(convert(elem.css('border-left-width'))), -parseFloat(convert(elem.css('border-top-width')))]; + parseFloat(convert(elem.css('border-top-width')))]; }, @@ -953,15 +957,15 @@ parseFloat(convert(elem.css('border-top-width')))]; if (!$.timepicker._curInst) { return; } var $target = $(event.target); if ($target[0].id != $.timepicker._mainDivId && -$target.parents('#' + $.timepicker._mainDivId).length == 0 && -!$target.hasClass($.timepicker.markerClassName) && -!$target.hasClass($.timepicker._triggerClass) && -$.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) + $target.parents('#' + $.timepicker._mainDivId).length == 0 && + !$target.hasClass($.timepicker.markerClassName) && + !$target.hasClass($.timepicker._triggerClass) && + $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) $.timepicker._hideTimepicker(); }, /* Hide the time picker from view. -@param input element - the input field attached to the time picker */ + @param input element - the input field attached to the time picker */ _hideTimepicker: function (input) { var inst = this._curInst; if (!inst || (input && inst != $.data(input, PROP_NAME))) { return; } @@ -977,7 +981,7 @@ $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) } else { inst.tpDiv[(showAnim == 'slideDown' ? 'slideUp' : -(showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))]((showAnim ? duration : null), postProcess); + (showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))]((showAnim ? duration : null), postProcess); } if (!showAnim) { postProcess(); } @@ -997,7 +1001,7 @@ $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) if (onClose) { onClose.apply( (inst.input ? inst.input[0] : null), - [(inst.input ? inst.input.val() : ''), inst]); // trigger custom callback + [(inst.input ? inst.input.val() : ''), inst]); // trigger custom callback } } @@ -1011,9 +1015,9 @@ $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) }, /* Retrieve the instance data for the target control. -@param target element - the target input field or division or span -@return object - the associated instance data -@throws error if a jQuery problem getting data */ + @param target element - the target input field or division or span + @return object - the associated instance data + @throws error if a jQuery problem getting data */ _getInst: function (target) { try { return $.data(target, PROP_NAME); @@ -1026,7 +1030,7 @@ $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) /* Get a setting value, defaulting if necessary. */ _get: function (inst, name) { return inst.settings[name] !== undefined ? -inst.settings[name] : this._defaults[name]; + inst.settings[name] : this._defaults[name]; }, /* Parse existing time and initialise time picker. */ @@ -1057,13 +1061,13 @@ inst.settings[name] : this._defaults[name]; }, /* Update or retrieve the settings for an existing time picker. -@param target element - the target input field or division or span -@param name object - the new settings to update or -string - the name of the setting to change or retrieve, -when retrieving also 'all' for all instance settings or -'defaults' for all global defaults -@param value any - the new value for the setting -(omit if above is an object or to retrieve a value) */ + @param target element - the target input field or division or span + @param name object - the new settings to update or + string - the name of the setting to change or retrieve, + when retrieving also 'all' for all instance settings or + 'defaults' for all global defaults + @param value any - the new value for the setting + (omit if above is an object or to retrieve a value) */ _optionTimepicker: function(target, name, value) { var inst = this._getInst(target); if (arguments.length == 2 && typeof name == 'string') { @@ -1087,16 +1091,16 @@ when retrieving also 'all' for all instance settings or /* Set the time for a jQuery selection. -@param target element - the target input field or division or span -@param time String - the new time */ -_setTimeTimepicker: function(target, time) { -var inst = this._getInst(target); -if (inst) { -this._setTime(inst, time); - this._updateTimepicker(inst); -this._updateAlternate(inst, time); -} -}, + @param target element - the target input field or division or span + @param time String - the new time */ + _setTimeTimepicker: function(target, time) { + var inst = this._getInst(target); + if (inst) { + this._setTime(inst, time); + this._updateTimepicker(inst); + this._updateAlternate(inst, time); + } + }, /* Set the time directly. */ _setTime: function(inst, time, noChange) { @@ -1129,8 +1133,8 @@ this._updateAlternate(inst, time); }, /* -* Parse a time string into hours and minutes -*/ + * Parse a time string into hours and minutes + */ parseTime: function (inst, timeVal) { var retVal = new Object(); retVal.hours = -1; @@ -1218,7 +1222,7 @@ this._updateAlternate(inst, time); // added for onMinuteShow callback var onMinuteShow = this._get(inst, 'onMinuteShow'); if (onMinuteShow) { - // this will trigger a callback on selected hour to make sure selected minute is allowed. + // this will trigger a callback on selected hour to make sure selected minute is allowed. this._updateMinuteDisplay(inst); } @@ -1301,7 +1305,7 @@ this._updateAlternate(inst, time); if (displayHours == -1) { displayHours = 0 } if (selectedMinutes == -1) { selectedMinutes = 0 } - if (showPeriod) { + if (showPeriod) { if (inst.hours == 0) { displayHours = 12; } @@ -1383,15 +1387,15 @@ this._updateAlternate(inst, time); /* Invoke the timepicker functionality. -@param options string - a command, optionally followed by additional parameters or -Object - settings for attaching new timepicker functionality -@return jQuery object */ + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new timepicker functionality + @return jQuery object */ $.fn.timepicker = function (options) { /* Initialise the time picker. */ if (!$.timepicker.initialized) { $(document).mousedown($.timepicker._checkExternalClick). -find('body').append($.timepicker.tpDiv); + find('body').append($.timepicker.tpDiv); $.timepicker.initialized = true; } @@ -1400,15 +1404,15 @@ find('body').append($.timepicker.tpDiv); var otherArgs = Array.prototype.slice.call(arguments, 1); if (typeof options == 'string' && (options == 'getTime' || options == 'getTimeAsDate' || options == 'getHour' || options == 'getMinute' )) return $.timepicker['_' + options + 'Timepicker']. -apply($.timepicker, [this[0]].concat(otherArgs)); + apply($.timepicker, [this[0]].concat(otherArgs)); if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string') return $.timepicker['_' + options + 'Timepicker']. apply($.timepicker, [this[0]].concat(otherArgs)); return this.each(function () { typeof options == 'string' ? -$.timepicker['_' + options + 'Timepicker']. -apply($.timepicker, [this].concat(otherArgs)) : -$.timepicker._attachTimepicker(this, options); + $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this].concat(otherArgs)) : + $.timepicker._attachTimepicker(this, options); }); }; @@ -1424,7 +1428,7 @@ $.timepicker._attachTimepicker(this, options); $.timepicker = new Timepicker(); // singleton instance $.timepicker.initialized = false; $.timepicker.uuid = new Date().getTime(); - $.timepicker.version = "0.3.1"; + $.timepicker.version = "0.3.2"; // Workaround for #4055 // Add another global to avoid noConflict issues with inline event handlers diff --git a/app/assets/javascripts/rails_admin/ui.coffee b/app/assets/javascripts/rails_admin/ui.coffee index f1e232dd44..991468c508 100644 --- a/app/assets/javascripts/rails_admin/ui.coffee +++ b/app/assets/javascripts/rails_admin/ui.coffee @@ -68,3 +68,10 @@ $(document).on 'rails_admin.dom_ready', -> $(this).siblings('.control-group').hide() $(".table").tooltip selector: "th[rel=tooltip]" + +$(document).on 'click', '#fields_to_export label input#check_all', () -> + elems = $('#fields_to_export label input') + if $('#fields_to_export label input#check_all').is ':checked' + $(elems).prop('checked', true) + else + $(elems).prop('checked',false) diff --git a/app/assets/stylesheets/rails_admin/base/theming.scss b/app/assets/stylesheets/rails_admin/base/theming.scss index 014409179b..4458d48a72 100644 --- a/app/assets/stylesheets/rails_admin/base/theming.scss +++ b/app/assets/stylesheets/rails_admin/base/theming.scss @@ -47,7 +47,8 @@ body.rails_admin { .controls .nav { margin-bottom:5px; } - .remove_nested_fields { + .remove_nested_fields, + .remove_nested_one_fields { position:absolute; } margin:0px; diff --git a/app/controllers/rails_admin/main_controller.rb b/app/controllers/rails_admin/main_controller.rb index 1ceafb171f..cbce110bed 100644 --- a/app/controllers/rails_admin/main_controller.rb +++ b/app/controllers/rails_admin/main_controller.rb @@ -62,7 +62,7 @@ def get_sort_hash(model_config) "#{abstract_model.table_name}.#{params[:sort]}" elsif field.sortable == false # use default sort, asked field is not sortable "#{abstract_model.table_name}.#{model_config.list.sort_by}" - elsif field.sortable.is_a?(String) && field.sortable.include?('.') # just provide sortable, don't do anything smart + elsif (field.sortable.is_a?(String) || field.sortable.is_a?(Symbol)) && field.sortable.to_s.include?('.') # just provide sortable, don't do anything smart field.sortable elsif field.sortable.is_a?(Hash) # just join sortable hash, don't do anything smart "#{field.sortable.keys.first}.#{field.sortable.values.first}" diff --git a/app/helpers/rails_admin/application_helper.rb b/app/helpers/rails_admin/application_helper.rb index c9c751de88..d7343028ad 100644 --- a/app/helpers/rails_admin/application_helper.rb +++ b/app/helpers/rails_admin/application_helper.rb @@ -79,9 +79,10 @@ def navigation nodes_stack, nodes, level=0 model_param = node.abstract_model.to_param url = url_for(:action => :index, :controller => 'rails_admin/main', :model_name => model_param) level_class = " nav-level-#{level}" if level > 0 + nav_icon = node.navigation_icon ? %{}.html_safe : '' li = content_tag :li, "data-model"=>model_param do - link_to node.label_plural, url, :class => "pjax#{level_class}" + link_to nav_icon + node.label_plural, url, :class => "pjax#{level_class}" end li + navigation(nodes_stack, nodes_stack.select{ |n| n.parent.to_s == node.abstract_model.model_name}, level+1) end.join.html_safe diff --git a/app/views/rails_admin/main/_form_filtering_multiselect.html.haml b/app/views/rails_admin/main/_form_filtering_multiselect.html.haml index 8244c0c992..cd0acfb6e5 100644 --- a/app/views/rails_admin/main/_form_filtering_multiselect.html.haml +++ b/app/views/rails_admin/main/_form_filtering_multiselect.html.haml @@ -41,7 +41,7 @@ - selected_ids = (hdv = field.html_default_value).nil? ? selected_ids : hdv = form.select field.method_name, collection, { :selected => selected_ids, :object => form.object }, field.html_attributes.reverse_merge({:data => { :filteringmultiselect => true, :options => js_data.to_json }, :multiple => true}) -- if authorized?(:new, config.abstract_model) && !field.parent_readonly +- if authorized?(:new, config.abstract_model) && !field.parent_readonly && field.inline_add - path_hash = { :model_name => config.abstract_model.to_param, :modal => true } - path_hash.merge!({ :associations => { field.inverse_of => (form.object.persisted? ? form.object.id : 'new') } }) if field.inverse_of = link_to " ".html_safe + wording_for(:link, :new, config.abstract_model), '#', :data => { :link => new_path(path_hash) }, :class => "create btn btn-info", :style => 'margin-left:10px' diff --git a/app/views/rails_admin/main/_form_filtering_select.html.haml b/app/views/rails_admin/main/_form_filtering_select.html.haml index ac619cfd90..3a93ff0708 100644 --- a/app/views/rails_admin/main/_form_filtering_select.html.haml +++ b/app/views/rails_admin/main/_form_filtering_select.html.haml @@ -28,10 +28,10 @@ - selected_id = (hdv = field.html_default_value).nil? ? selected_id : hdv = form.select field.method_name, collection, { :selected => selected_id, :include_blank => true }, field.html_attributes.reverse_merge({ :data => { :filteringselect => true, :options => js_data.to_json }, :placeholder => t('admin.misc.search') }) -- if authorized? :new, config.abstract_model +- if authorized?(:new, config.abstract_model) && field.inline_add - path_hash = { :model_name => config.abstract_model.to_param, :modal => true } - path_hash.merge!({ :associations => { field.inverse_of => (form.object.persisted? ? form.object.id : 'new') } }) if field.inverse_of = link_to " ".html_safe + wording_for(:link, :new, config.abstract_model), '#', :data => { :link => new_path(path_hash) }, :class => "btn btn-info create", :style => 'margin-left:10px' -- if edit_url.present? +- if edit_url.present? && field.inline_edit = link_to " ".html_safe + wording_for(:link, :edit, config.abstract_model), '#', :data => { :link => edit_url }, :class => "btn btn-info update #{field.value.nil? && 'disabled'}", :style => 'margin-left:10px' diff --git a/app/views/rails_admin/main/_form_nested_many.html.haml b/app/views/rails_admin/main/_form_nested_many.html.haml index c70b42272d..b8f7d41aa3 100644 --- a/app/views/rails_admin/main/_form_nested_many.html.haml +++ b/app/views/rails_admin/main/_form_nested_many.html.haml @@ -2,7 +2,7 @@ .btn-group %a.btn.btn-info.toggler{:'data-toggle' => "button", :'data-target' => "#{form.jquery_namespace(field)} > .tab-content, #{form.jquery_namespace(field)} > .controls > .nav", :class => (field.active? ? 'active' : '')} %i.icon-white - - unless field.nested_form[:update_only] + - unless field.nested_form[:update_only] || !field.inline_add = form.link_to_add " #{wording_for(:link, :new, field.associated_model_config.abstract_model)}".html_safe, field.name, { :class => 'btn btn-info' } = form.errors_for(field) = form.help_for(field) diff --git a/app/views/rails_admin/main/export.html.haml b/app/views/rails_admin/main/export.html.haml index 5517b42387..a2fa5f0eaf 100644 --- a/app/views/rails_admin/main/export.html.haml +++ b/app/views/rails_admin/main/export.html.haml @@ -4,7 +4,12 @@ = form_tag export_path(params.merge(:all => true)), :method => 'post', :class => 'form-horizontal denser' do %input{:name => "send_data", :type => "hidden", :value => "true"}/ - %fieldset + %fieldset{:id => 'fields_to_export'} + %div.control-group + %div.controls + %label.checkbox{:for => 'check_all'} + = 'Select All Fields' + = check_box_tag 'all', 'all', true, { :id => 'check_all' } %legend %i.icon-chevron-down = t('admin.export.select') diff --git a/lib/rails_admin/adapters/active_record.rb b/lib/rails_admin/adapters/active_record.rb index 1bfb79123a..462602efa0 100644 --- a/lib/rails_admin/adapters/active_record.rb +++ b/lib/rails_admin/adapters/active_record.rb @@ -107,6 +107,10 @@ def embedded? false end + def cyclic? + false + end + def adapter_supports_joins? true end diff --git a/lib/rails_admin/adapters/mongoid.rb b/lib/rails_admin/adapters/mongoid.rb index 317a62804b..71a07071a5 100644 --- a/lib/rails_admin/adapters/mongoid.rb +++ b/lib/rails_admin/adapters/mongoid.rb @@ -6,7 +6,7 @@ module RailsAdmin module Adapters module Mongoid STRING_TYPE_COLUMN_NAMES = [:name, :title, :subject] - DISABLED_COLUMN_TYPES = ['Range'] + DISABLED_COLUMN_TYPES = ['Range', 'Moped::BSON::Binary'] ObjectId = (::Mongoid::VERSION >= '3' ? ::Moped::BSON::ObjectId : ::BSON::ObjectId) def new(params = {}) @@ -105,6 +105,10 @@ def embedded? @embedded ||= !!model.associations.values.find{|a| a.macro.to_sym == :embedded_in } end + def cyclic? + @cyclic ||= !!model.cyclic? + end + def object_id_from_string(str) ObjectId.from_string(str) end @@ -327,7 +331,7 @@ def association_foreign_inverse_of_lookup(association) def association_nested_attributes_options_lookup(association) nested = model.nested_attributes_options.try { |o| o[association.name.to_sym] } - if !nested && [:embeds_one, :embeds_many].include?(association.macro.to_sym) + if !nested && [:embeds_one, :embeds_many].include?(association.macro.to_sym) && !association.cyclic raise <<-MSG.gsub(/^\s+/, '') Embbeded association without accepts_nested_attributes_for can't be handled by RailsAdmin, because embedded model doesn't have top-level access. @@ -426,10 +430,14 @@ def perform_search_on_associated_collection(field_name, conditions) def sort_by(options, scope) return scope unless options[:sort] - field_name, collection_name = options[:sort].to_s.split('.').reverse - if collection_name && collection_name != table_name - # sorting by associated model column is not supported, so just ignore - return scope + case options[:sort] + when String + field_name, collection_name = options[:sort].split('.').reverse + if collection_name && collection_name != table_name + raise "sorting by associated model column is not supported in Non-Relational databases" + end + when Symbol + field_name = options[:sort].to_s end if options[:sort_reverse] scope.asc field_name diff --git a/lib/rails_admin/config.rb b/lib/rails_admin/config.rb index d52032ddae..17d690e3b5 100644 --- a/lib/rails_admin/config.rb +++ b/lib/rails_admin/config.rb @@ -315,7 +315,7 @@ def reset_model(model) # @see RailsAdmin::Config::Hideable def visible_models(bindings) - models.map{|m| m.with(bindings) }.select{|m| m.visible? && bindings[:controller].authorized?(:index, m.abstract_model) && !m.abstract_model.embedded?}.sort do |a, b| + models.map{|m| m.with(bindings) }.select{|m| m.visible? && bindings[:controller].authorized?(:index, m.abstract_model) && (!m.abstract_model.embedded? || m.abstract_model.cyclic?)}.sort do |a, b| (weight_order = a.weight <=> b.weight) == 0 ? a.label.downcase <=> b.label.downcase : weight_order end end diff --git a/lib/rails_admin/config/fields/base.rb b/lib/rails_admin/config/fields/base.rb index 3c5854165e..24e7b5c782 100644 --- a/lib/rails_admin/config/fields/base.rb +++ b/lib/rails_admin/config/fields/base.rb @@ -215,10 +215,28 @@ def virtual? def editable? return false if @properties && @properties[:read_only] - active_model_attr_accessible = !bindings[:object].class.active_authorizer[bindings[:view].controller.send(:_attr_accessible_role)].deny?(self.method_name) + role = bindings[:view].controller.send(:_attr_accessible_role) + active_model_attr_accessible = !bindings[:object].class.active_authorizer[role].deny?(self.method_name) + return true if active_model_attr_accessible if RailsAdmin::Config.yell_for_non_accessible_fields - Rails.logger.debug "\n\n[RailsAdmin] Please add 'attr_accessible :#{self.method_name}' in your '#{bindings[:object].class}' model definition if you want to make it editable.\nYou can also explicitely mark this field as read-only: \n\nconfig.model #{bindings[:object].class} do\n field :#{self.name} do\n read_only true\n end\nend\n\nAdd 'config.yell_for_non_accessible_fields = false' in your 'rails_admin.rb' initializer if you do not want to see these warnings\n\n" + accessible = "attr_accessible :#{self.method_name}#{role == :default ? '' : ", :as => :#{role}"}" + + Rails.logger.debug <<-MESSAGE.strip_heredoc + + + [RailsAdmin] Please add '#{accessible}' in your '#{bindings[:object].class}' model definition if you want to make it editable. + You can also explicitely mark this field as read-only: + + config.model #{bindings[:object].class} do + field :#{self.name} do + read_only true + end + end + + Add 'config.yell_for_non_accessible_fields = false' in your 'rails_admin.rb' initializer if you do not want to see these warnings + + MESSAGE end false end diff --git a/lib/rails_admin/config/fields/types/belongs_to_association.rb b/lib/rails_admin/config/fields/types/belongs_to_association.rb index 300b2773a2..e7ba09807b 100644 --- a/lib/rails_admin/config/fields/types/belongs_to_association.rb +++ b/lib/rails_admin/config/fields/types/belongs_to_association.rb @@ -23,6 +23,14 @@ class BelongsToAssociation < RailsAdmin::Config::Fields::Association nested_form ? :form_nested_one : :form_filtering_select end + register_instance_option :inline_add do + true + end + + register_instance_option :inline_edit do + true + end + def selected_id bindings[:object].send(foreign_key) end diff --git a/lib/rails_admin/config/fields/types/has_many_association.rb b/lib/rails_admin/config/fields/types/has_many_association.rb index fe0d0ec680..577add3e5c 100644 --- a/lib/rails_admin/config/fields/types/has_many_association.rb +++ b/lib/rails_admin/config/fields/types/has_many_association.rb @@ -22,6 +22,10 @@ class HasManyAssociation < RailsAdmin::Config::Fields::Association self.associated_model_config.excluded? end + register_instance_option :inline_add do + true + end + def method_name nested_form ? "#{super}_attributes".to_sym : "#{super.to_s.singularize}_ids".to_sym # name_ids end diff --git a/lib/rails_admin/config/fields/types/has_one_association.rb b/lib/rails_admin/config/fields/types/has_one_association.rb index dacde0aa4d..1df74bdf2b 100644 --- a/lib/rails_admin/config/fields/types/has_one_association.rb +++ b/lib/rails_admin/config/fields/types/has_one_association.rb @@ -17,6 +17,15 @@ class HasOneAssociation < RailsAdmin::Config::Fields::Association (o = value) && o.send(associated_model_config.object_label_method) end + register_instance_option :inline_add do + true + end + + register_instance_option :inline_edit do + true + end + + def editable? (nested_form || abstract_model.model.new.respond_to?("#{self.name}_id=")) && super end diff --git a/lib/rails_admin/config/fields/types/serialized.rb b/lib/rails_admin/config/fields/types/serialized.rb index 2792bcc17e..0281ad9004 100644 --- a/lib/rails_admin/config/fields/types/serialized.rb +++ b/lib/rails_admin/config/fields/types/serialized.rb @@ -13,7 +13,9 @@ class Serialized < RailsAdmin::Config::Fields::Types::Text end def parse_input(params) - params[name] = (params[name].blank? ? nil : YAML.safe_load(params[name])) if params[name].is_a?(::String) + if params[name].is_a?(::String) + params[name] = (params[name].blank? ? nil : (YAML.safe_load(params[name]) || nil)) + end end end end diff --git a/lib/rails_admin/config/model.rb b/lib/rails_admin/config/model.rb index 6d1fa0a892..e16ff594c7 100644 --- a/lib/rails_admin/config/model.rb +++ b/lib/rails_admin/config/model.rb @@ -82,6 +82,10 @@ def pluralize(count) @navigation_label ||= (parent_module = abstract_model.model.parent) != Object ? parent_module.to_s : nil end + register_instance_option :navigation_icon do + nil + end + # Act as a proxy for the base section configuration that actually # store the configurations. def method_missing(m, *args, &block) diff --git a/lib/rails_admin/version.rb b/lib/rails_admin/version.rb index 490e0f97d1..6cb552bed3 100644 --- a/lib/rails_admin/version.rb +++ b/lib/rails_admin/version.rb @@ -2,7 +2,7 @@ module RailsAdmin class Version MAJOR = 0 unless defined? MAJOR MINOR = 4 unless defined? MINOR - PATCH = 5 unless defined? PATCH + PATCH = 7 unless defined? PATCH PRE = nil unless defined? PRE class << self diff --git a/spec/controllers/rails_admin/main_controller_spec.rb b/spec/controllers/rails_admin/main_controller_spec.rb index bb896f77cc..ca70646746 100644 --- a/spec/controllers/rails_admin/main_controller_spec.rb +++ b/spec/controllers/rails_admin/main_controller_spec.rb @@ -38,6 +38,24 @@ end describe "#get_sort_hash" do + context "options sortable is a hash" do + before do + RailsAdmin.config('Player') do + configure :team do + sortable do + :'team.name' + end + end + end + end + + it "returns the option with no changes" do + controller.params = { :sort => "team", :model_name =>"players" } + expect(controller.send(:get_sort_hash, RailsAdmin.config(Player))).to eq({:sort=>:"team.name", :sort_reverse=>true}) + end + end + + it "works with belongs_to associations with label method virtual" do controller.params = { :sort => "parent_category", :model_name =>"categories" } expect(controller.send(:get_sort_hash, RailsAdmin.config(Category))).to eq({:sort=>"categories.parent_category_id", :sort_reverse=>true}) diff --git a/spec/dummy_app/app/mongoid/field_test.rb b/spec/dummy_app/app/mongoid/field_test.rb index 0816d9ebf0..780c3941df 100644 --- a/spec/dummy_app/app/mongoid/field_test.rb +++ b/spec/dummy_app/app/mongoid/field_test.rb @@ -11,6 +11,7 @@ class FieldTest field :big_decimal_field, :type => BigDecimal field :boolean_field, :type => Boolean field :bson_object_id_field, :type => RailsAdmin::Adapters::Mongoid::ObjectId + field :bson_binary_field, :type => Moped::BSON::Binary field :date_field, :type => Date field :datetime_field, :type => DateTime field :time_with_zone_field, :type => ActiveSupport::TimeWithZone @@ -30,7 +31,7 @@ class FieldTest field :protected_field, :type => String has_mongoid_attached_file :paperclip_asset, :styles => { :thumb => "100x100>" } - basic_accessible_fields = [:comment_attributes, :nested_field_tests_attributes, :embed_attributes, :embeds_attributes, :dragonfly_asset, :remove_dragonfly_asset, :retained_dragonfly_asset, :carrierwave_asset, :carrierwave_asset_cache, :remove_carrierwave_asset, :paperclip_asset, :delete_paperclip_asset, :comment_id, :name, :array_field, :big_decimal_field, :boolean_field, :bson_object_id_field, :date_field, :datetime_field, :time_with_zone_field, :default_field, :float_field, :hash_field, :integer_field, :object_field, :range_field, :string_field, :symbol_field, :text_field, :time_field, :created_at, :updated_at, :format] + basic_accessible_fields = [:comment_attributes, :nested_field_tests_attributes, :embed_attributes, :embeds_attributes, :dragonfly_asset, :remove_dragonfly_asset, :retained_dragonfly_asset, :carrierwave_asset, :carrierwave_asset_cache, :remove_carrierwave_asset, :paperclip_asset, :delete_paperclip_asset, :comment_id, :name, :array_field, :big_decimal_field, :boolean_field, :bson_object_id_field, :bson_binary_field, :date_field, :datetime_field, :time_with_zone_field, :default_field, :float_field, :hash_field, :integer_field, :object_field, :range_field, :string_field, :symbol_field, :text_field, :time_field, :created_at, :updated_at, :format] attr_accessible *basic_accessible_fields attr_accessible *(basic_accessible_fields + [:restricted_field, {:as => :custom_role}]) attr_accessible *(basic_accessible_fields + [:protected_field, {:as => :extra_safe_role}]) diff --git a/spec/integration/config/edit/rails_admin_config_edit_spec.rb b/spec/integration/config/edit/rails_admin_config_edit_spec.rb index eef2ba178f..97e2ef008e 100644 --- a/spec/integration/config/edit/rails_admin_config_edit_spec.rb +++ b/spec/integration/config/edit/rails_admin_config_edit_spec.rb @@ -593,6 +593,79 @@ class HelpTest < Tableless expect(find("#team_division_id_field .help-block")).to have_content("Optional") expect(find("#team_name_field .help-block")).to have_content("Required") end + + it "can hide the add button on an associated field" do + RailsAdmin.config Player do + edit do + field :team do + inline_add false + end + field :draft do + inline_add false + end + field :comments do + inline_add false + end + end + end + visit new_path(:model_name => "player") + should have_no_selector('a', :text => 'Add a new Team') + should have_no_selector('a', :text => 'Add a new Draft') + should have_no_selector('a', :text => 'Add a new Comment') + end + + it "can show the add button on an associated field" do + RailsAdmin.config Player do + edit do + field :team do + inline_add true + end + field :draft do + inline_add true + end + field :comments do + inline_add true + end + end + end + visit new_path(:model_name => "player") + should have_selector('a', :text => 'Add a new Team') + should have_selector('a', :text => 'Add a new Draft') + should have_selector('a', :text => 'Add a new Comment') + end + + it "can hide the edit button on an associated field" do + RailsAdmin.config Player do + edit do + field :team do + inline_edit false + end + field :draft do + inline_edit false + end + end + end + visit new_path(:model_name => "player") + should have_no_selector('a', :text => 'Edit this Team') + should have_no_selector('a', :text => 'Edit this Draft') + end + + it "can show the edit button on an associated field" do + RailsAdmin.config Player do + edit do + field :team do + inline_edit true + end + field :draft do + inline_edit true + end + end + end + visit new_path(:model_name => "player") + should have_selector('a', :text => 'Edit this Team') + should have_selector('a', :text => 'Edit this Draft') + end + end describe "bindings" do @@ -1023,4 +1096,5 @@ def color_enum should have_selector(".color_type input") end end + end diff --git a/spec/rails_admin/adapters/mongoid_spec.rb b/spec/rails_admin/adapters/mongoid_spec.rb index afa362ec62..3f47b0d323 100644 --- a/spec/rails_admin/adapters/mongoid_spec.rb +++ b/spec/rails_admin/adapters/mongoid_spec.rb @@ -243,12 +243,24 @@ class MongoEmbedded embedded_in :mongo_embeds_many end + class MongoRecursivelyEmbedsOne + include Mongoid::Document + recursively_embeds_one + end + + class MongoRecursivelyEmbedsMany + include Mongoid::Document + recursively_embeds_many + end + expect(lambda{ RailsAdmin::AbstractModel.new(MongoEmbedsOne).associations }).to raise_error(RuntimeError, "Embbeded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embedded' line to `MongoEmbedsOne' model.\n" ) expect(lambda{ RailsAdmin::AbstractModel.new(MongoEmbedsMany).associations }).to raise_error(RuntimeError, "Embbeded association without accepts_nested_attributes_for can't be handled by RailsAdmin,\nbecause embedded model doesn't have top-level access.\nPlease add `accepts_nested_attributes_for :mongo_embeddeds' line to `MongoEmbedsMany' model.\n" ) + expect(lambda{ RailsAdmin::AbstractModel.new(MongoRecursivelyEmbedsOne).associations }).not_to raise_error + expect(lambda{ RailsAdmin::AbstractModel.new(MongoRecursivelyEmbedsMany).associations }).not_to raise_error end it "works with inherited embeds_many model" do @@ -276,10 +288,10 @@ class MongoEmbedsChild < MongoEmbedsParent; end it "maps Mongoid column types to RA types" do expect(@abstract_model.properties.select{|p| %w(_id array_field big_decimal_field - boolean_field bson_object_id_field date_field datetime_field time_with_zone_field default_field float_field - hash_field integer_field name object_field range_field short_text string_field subject - symbol_field text_field time_field title). - include? p[:name].to_s}).to match_array [ + boolean_field bson_object_id_field bson_binary_field date_field datetime_field + time_with_zone_field default_field float_field hash_field integer_field name + object_field range_field short_text string_field subject symbol_field text_field + time_field title).include? p[:name].to_s}).to match_array [ { :name => :_id, :pretty_name => "Id", :nullable? => true, diff --git a/spec/rails_admin/config/fields/base_spec.rb b/spec/rails_admin/config/fields/base_spec.rb index a9ae9392b6..ee8ec003a2 100644 --- a/spec/rails_admin/config/fields/base_spec.rb +++ b/spec/rails_admin/config/fields/base_spec.rb @@ -394,6 +394,18 @@ class FieldVisibilityTest < Tableless expect(editable).to be_false end + it "yells for non attr_accessible fields specified role if config.yell_for_non_accessible_fields is true" do + RailsAdmin.config do |config| + config.yell_for_non_accessible_fields = true + config.model FieldTest do + field :protected_field + end + end + Rails.logger.should_receive(:debug).with {|msg| msg =~ /Please add 'attr_accessible :protected_field, :as => :admin'/ } + editable = RailsAdmin.config(FieldTest).field(:protected_field).with(:object => FactoryGirl.create(:field_test), :view => double(:controller => double(:_attr_accessible_role => :admin))).editable? + expect(editable).to be_false + end + it "does not yell for non attr_accessible fields if config.yell_for_non_accessible_fields is false" do RailsAdmin.config do |config| config.yell_for_non_accessible_fields = false diff --git a/spec/rails_admin/config_spec.rb b/spec/rails_admin/config_spec.rb index adc60a1a4f..f1256cffc3 100644 --- a/spec/rails_admin/config_spec.rb +++ b/spec/rails_admin/config_spec.rb @@ -267,6 +267,21 @@ expect(RailsAdmin.config.visible_models(:controller => double(:_current_user => double(:role => :admin), :authorized? => true)).map(&:abstract_model).map(&:model)).to match_array [FieldTest, Comment] end + + it "basically does not contain embedded model except model using recursively_embeds_many or recursively_embeds_one", :mongoid => true do + class RecursivelyEmbedsOne + include Mongoid::Document + recursively_embeds_one + end + class RecursivelyEmbedsMany + include Mongoid::Document + recursively_embeds_many + end + RailsAdmin.config do |config| + config.included_models = [FieldTest, Comment, Embed, RecursivelyEmbedsMany, RecursivelyEmbedsOne] + end + expect(RailsAdmin.config.visible_models(:controller => double(:_current_user => double(:role => :admin), :authorized? => true)).map(&:abstract_model).map(&:model)).to match_array [FieldTest, Comment, RecursivelyEmbedsMany, RecursivelyEmbedsOne] + end end end