From 780c6e605525dddadab2a36a6709b141fa8fcba0 Mon Sep 17 00:00:00 2001 From: Leighton Whiting Date: Thu, 15 Nov 2012 12:19:35 -0700 Subject: [PATCH 1/2] Added project browser to core --- .../project_browser/css/jquery.multiselect.css | 23 + .../project_browser/css/project_browser.css | 203 +++++ core/modules/project_browser/images/arrow-asc.png | 5 + core/modules/project_browser/images/arrow-desc.png | 4 + core/modules/project_browser/images/circle.png | 3 + core/modules/project_browser/images/red-x.png | 7 + .../project_browser/js/jquery.multiselect.min.js | 44 + .../js/project_browser_categories_widget.js | 11 + .../js/project_browser_more_link.js | 28 + .../js/project_browser_multiselect.js | 13 + core/modules/project_browser/js/select_releases.js | 12 + .../project_browser/project_browser.admin.inc | 32 + core/modules/project_browser/project_browser.inc | 880 ++++++++++++++++++++ core/modules/project_browser/project_browser.info | 7 + .../modules/project_browser/project_browser.module | 467 +++++++++++ .../project_browser/project_browser.pages.inc | 521 ++++++++++++ core/modules/project_browser/project_browser.test | 75 ++ .../tests/project_browser_test.info | 12 + .../tests/project_browser_test.module | 329 ++++++++ .../theme/project-browser-block.tpl.php | 21 + .../theme/project-browser-install-queue.tpl.php | 16 + .../theme/project-browser-install.tpl.php | 21 + .../theme/project-browser-list.tpl.php | 26 + .../theme/project-browser-project.tpl.php | 59 ++ .../theme/project_browser.admin.inc | 32 + 25 files changed, 2851 insertions(+), 0 deletions(-) create mode 100644 core/modules/project_browser/css/jquery.multiselect.css create mode 100644 core/modules/project_browser/css/project_browser.css create mode 100644 core/modules/project_browser/images/arrow-asc.png create mode 100644 core/modules/project_browser/images/arrow-desc.png create mode 100644 core/modules/project_browser/images/circle.png create mode 100644 core/modules/project_browser/images/red-x.png create mode 100644 core/modules/project_browser/js/jquery.multiselect.min.js create mode 100644 core/modules/project_browser/js/project_browser_categories_widget.js create mode 100644 core/modules/project_browser/js/project_browser_more_link.js create mode 100644 core/modules/project_browser/js/project_browser_multiselect.js create mode 100644 core/modules/project_browser/js/select_releases.js create mode 100644 core/modules/project_browser/project_browser.admin.inc create mode 100644 core/modules/project_browser/project_browser.inc create mode 100644 core/modules/project_browser/project_browser.info create mode 100644 core/modules/project_browser/project_browser.module create mode 100644 core/modules/project_browser/project_browser.pages.inc create mode 100644 core/modules/project_browser/project_browser.test create mode 100644 core/modules/project_browser/tests/project_browser_test.info create mode 100644 core/modules/project_browser/tests/project_browser_test.module create mode 100644 core/modules/project_browser/theme/project-browser-block.tpl.php create mode 100644 core/modules/project_browser/theme/project-browser-install-queue.tpl.php create mode 100644 core/modules/project_browser/theme/project-browser-install.tpl.php create mode 100644 core/modules/project_browser/theme/project-browser-list.tpl.php create mode 100644 core/modules/project_browser/theme/project-browser-project.tpl.php create mode 100644 core/modules/project_browser/theme/project_browser.admin.inc diff --git a/core/modules/project_browser/css/jquery.multiselect.css b/core/modules/project_browser/css/jquery.multiselect.css new file mode 100644 index 0000000..e8f8e00 --- /dev/null +++ b/core/modules/project_browser/css/jquery.multiselect.css @@ -0,0 +1,23 @@ +.ui-multiselect { padding:2px 0 2px 4px; text-align:left } +.ui-multiselect span.ui-icon { float:right } +.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; } +.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important } + +.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px } +.ui-multiselect-header ul { font-size:0.9em } +.ui-multiselect-header ul li { float:left; padding:0 10px 0 0; list-style: none; } +.ui-multiselect-header a { text-decoration:none } +.ui-multiselect-header a:hover { text-decoration:underline } +.ui-multiselect-header span.ui-icon { float:left } +.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 } + +.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; width: 500px; } +.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:scroll } +.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px } +.ui-multiselect-checkboxes label input { position:relative; top:1px; margin-right: 5px; } +.ui-multiselect-checkboxes li { float:left; width: 225px; font-size:0.9em; padding-right:3px; list-style: none; } +.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid } +.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none } + +/* remove label borders in IE6 because IE6 does not support transparency */ +* html .ui-multiselect-checkboxes label { border:none } diff --git a/core/modules/project_browser/css/project_browser.css b/core/modules/project_browser/css/project_browser.css new file mode 100644 index 0000000..68b9b75 --- /dev/null +++ b/core/modules/project_browser/css/project_browser.css @@ -0,0 +1,203 @@ +#project-browser-install-button-form { + clear: both; + margin: 10px 0 0; +} + +#project-browser-install-queue { + margin: 5px 0 0; +} + +.project-browser-install-queue-item { + margin: 5px 0; +} + +.project-browser-install-queue-items { + clear: both; + margin: 0 0 15px; +} + +.project-browser-install-link { + clear: both; +} + +.project-browser-install-queue-item a { + padding-left: 20px; + background: url("../images/red-x.png") no-repeat scroll left 2px transparent; +} + +.project-browser-selected-release { + display: none; +} + +.project-browser-show-releases-link { + cursor: pointer; +} + +div.item-list ul.project-browser-sort-list, +div.item-list ul.project-browser-servers-list { + margin: 0; +} + +div.item-list ul.project-browser-sort-list li, +div.item-list ul.project-browser-servers-list li { + display: inline; + list-style-image: none; + margin-right: 10px; +} + +div.item-list ul.project-browser-sort-list li a, +div.item-list ul.project-browser-servers-list li a { + color: #0074BD; +} + +div.item-list ul.project-browser-sort-list li.sort-active a, +div.item-list ul.project-browser-servers-list li.server-active a { + color: #000; +} + +div.item-list ul.project-browser-sort-list li.sort-header, +div.item-list ul.project-browser-servers-list li.server-header { + font-weight: bold; +} + +div.item-list ul.project-browser-sort-list li.sort-asc { + background: url("../images/arrow-asc.png") no-repeat scroll right 2px transparent; + padding-right: 15px; +} + +div.item-list ul.project-browser-sort-list li.sort-desc { + background: url("../images/arrow-desc.png") no-repeat scroll right 2px transparent; + padding-right: 15px; +} + +a.show-more { + float: right; +} + +.project-extra { + clear: both; + color: gray; + text-align: right; +} + +.project-author { + color: gray; + font-size: 0.9em; +} + +.project-updated { + color: gray; + font-size: 0.9em; +} + +.project-image { + float: left; + margin-right: 10px; +} + +.project-image img { + max-height: 150px; + max-width: 200px; +} + +#project-browser-main ul, #project-browser-main ol { + list-style-position: inside; +} + +.project-item { + border-top: 1px solid #E0E0D8; + clear: both; + padding: 10px; + position: relative; +} + +.project-item-first { + clear: both; + padding: 10px; + position: relative; +} + +div.project-status { + position: absolute; + right: 10px; + text-align: right; + top: 5px; +} + +div.project-information { + +} + +.project-browser-install-main { + padding: 10px; + float: right; + width: 76%; +} + +.install-disabled { + color: gray; +} + +.install-enabled { + color: green; +} + +.project-browser-install-sidebar-left { + float: left; + width: 19%; + padding: 10px; +} + +div.project-title { + font-size: 18px; +} + +fieldset#edit-category, +fieldset#edit-version { + border: none; +} + +div.install-item-prefix { + float:left; + margin-left: 50px; +} + +#project-browser-main div.form-item-install { + float: right; +} + +#project-browser div.project-browser-region { + min-height: 1px; +} + +#project-browser div#project-browser-main { + width: 75%; + float: left; + margin-right: 1%; +} + +#project-browser div#project-browser-sidebar-right { + width: 23%; + float: right; +} + +#project-browser div.project-browser-block { + margin-bottom: 20px; + border: 1px solid #CCCCCC; +} + +#project-browser .project-browser-region .project-browser-block { + clear: both; +} + +#project-browser div.project-browser-block h2 { + float: none; + font-size: 1em; + margin: 0; + padding: 3px 10px; + background: none repeat scroll 0 0 #E0E0D8; +} + +#project-browser div#project-browser-sidebar-right div.project-browser-block div.content { + padding: 5px 10px; +} diff --git a/core/modules/project_browser/images/arrow-asc.png b/core/modules/project_browser/images/arrow-asc.png new file mode 100644 index 0000000..a3ccabc --- /dev/null +++ b/core/modules/project_browser/images/arrow-asc.png @@ -0,0 +1,5 @@ +PNG + + IHDR H%v?PLTEkR%tRNS@fIDATc`@L L LL + +Li|q)IENDB` \ No newline at end of file diff --git a/core/modules/project_browser/images/arrow-desc.png b/core/modules/project_browser/images/arrow-desc.png new file mode 100644 index 0000000..2edbb17 --- /dev/null +++ b/core/modules/project_browser/images/arrow-desc.png @@ -0,0 +1,4 @@ +PNG + + IHDR H%v?PLTEkR%tRNS@fIDATxc`@0`o``g`` +KzHIENDB` \ No newline at end of file diff --git a/core/modules/project_browser/images/circle.png b/core/modules/project_browser/images/circle.png new file mode 100644 index 0000000..6b2d63f --- /dev/null +++ b/core/modules/project_browser/images/circle.png @@ -0,0 +1,3 @@ +PNG + + IHDR6|JPLTEE<tRNS@fIDAT[c`cfe ( $b IENDB` \ No newline at end of file diff --git a/core/modules/project_browser/images/red-x.png b/core/modules/project_browser/images/red-x.png new file mode 100644 index 0000000..486390c --- /dev/null +++ b/core/modules/project_browser/images/red-x.png @@ -0,0 +1,7 @@ +PNG + + IHDRaIDATxڥ?HBQƣr #p(2hAk +!GP2ISiъ +:W_O"sw^gH^3כT(Ppc"31!h>_af Epd!H ߯! LNj Mp7<ǃpl2LIlo#6; a!vp˅r4 +u +RpNE>yHnlG.RWR B?%a<a2#=4}DrOu_ TWXTPjmA(`%xZZjKjFUtNjX'';GWXCW83p~zo$B{;rtAjX) |!uwsֆhx-=2ةQ-riIMQ(r?6{L~_ͧGIENDB` \ No newline at end of file diff --git a/core/modules/project_browser/js/jquery.multiselect.min.js b/core/modules/project_browser/js/jquery.multiselect.min.js new file mode 100644 index 0000000..256d8c7 --- /dev/null +++ b/core/modules/project_browser/js/jquery.multiselect.min.js @@ -0,0 +1,44 @@ +/* + * jQuery MultiSelect UI Widget 1.10a + * Modified by Leighton Whiting for Drupal's Project Browser + * Copyright (c) 2011 Eric Hynds + * + * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/ + * + * Depends: + * - jQuery 1.4.2+ + * - jQuery UI 1.8 widget factory + * + * Optional: + * - jQuery UI effects + * - jQuery UI position utility + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ + +(function($,undefined){var multiselectID=0;$.widget("ech.multiselect",{options:{header:true,height:175,minWidth:225,classes:'',checkAllText:'Check all',uncheckAllText:'Uncheck all',noneSelectedText:'Select options',selectedText:'# selected',selectedList:0,show:'',hide:'',autoOpen:false,multiple:true,position:{}},_create:function(){var el=this.element.hide(),o=this.options;this.speed=$.fx.speeds._default;this._isOpen=false;var +button=(this.button=$('')).addClass('ui-multiselect ui-widget ui-state-default ui-corner-all').addClass(o.classes).attr({'title':el.attr('title'),'aria-haspopup':true,'tabIndex':el.attr('tabIndex')}).insertAfter(el),buttonlabel=(this.buttonlabel=$('')).html(o.noneSelectedText).appendTo(button),menu=(this.menu=$('
')).addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all').addClass(o.classes).insertAfter(button),header=(this.header=$('
')).addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix').appendTo(menu),headerLinkContainer=(this.headerLinkContainer=$('
    ')).addClass('ui-helper-reset').html(function(){if(o.header===true){return'
  • '+o.checkAllText+'
  • '+o.uncheckAllText+'
  • ';}else if(typeof o.header==="string"){return'
  • '+o.header+'
  • ';}else{return'';}}).append('
  • ').appendTo(header),checkboxContainer=(this.checkboxContainer=$('
      ')).addClass('ui-multiselect-checkboxes ui-helper-reset').appendTo(menu);this._bindEvents();this.refresh(true);if(!o.multiple){menu.addClass('ui-multiselect-single');}},_init:function(){if(this.options.header===false){this.header.hide();} +if(!this.options.multiple){this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide();} +if(this.options.autoOpen){this.open();} +if(this.element.is(':disabled')){this.disable();}},refresh:function(init){var el=this.element,o=this.options,menu=this.menu,checkboxContainer=this.checkboxContainer,optgroups=[],html=[],id=el.attr('id')||multiselectID++;this.element.find('option').each(function(i){var $this=$(this),parent=this.parentNode,title=this.innerHTML,value=this.value,inputID=this.id||'ui-multiselect-'+id+'-option-'+i,isDisabled=this.disabled,isSelected=this.selected,labelClasses=['ui-corner-all'],optLabel;if(parent.tagName.toLowerCase()==='optgroup'){optLabel=parent.getAttribute('label');if($.inArray(optLabel,optgroups)===-1){html.push('
    • '+optLabel+'
    • ');optgroups.push(optLabel);}} +if(isDisabled){labelClasses.push('ui-state-disabled');} +if(isSelected&&!o.multiple){labelClasses.push('ui-state-active');} +html.push('
    • ');html.push('
    • ');});checkboxContainer.html(html.join(''));this.labels=menu.find('label');this._setButtonWidth();this._setMenuWidth();this.button[0].defaultValue=this.update();if(!init){this._trigger('refresh');}},update:function(){var o=this.options,$inputs=this.labels.find('input'),$checked=$inputs.filter(':checked'),numChecked=$checked.length,value;if(numChecked===0){value=o.noneSelectedText;}else{if($.isFunction(o.selectedText)){value=o.selectedText.call(this,numChecked,$inputs.length,$checked.get());}else if(/\d/.test(o.selectedList)&&o.selectedList>0&&numChecked<=o.selectedList){value=$checked.map(function(){return this.title;}).get().join(', ');}else{value=o.selectedText.replace('#',numChecked).replace('#',$inputs.length);}} +this.buttonlabel.html(value);return value;},_bindEvents:function(){var self=this,button=this.button;function clickHandler(){self[self._isOpen?'close':'open']();return false;} +button.find('span').bind('click.multiselect',clickHandler);button.bind({click:clickHandler,keypress:function(e){switch(e.which){case 27:case 38:case 37:self.close();break;case 39:case 40:self.open();break;}},mouseenter:function(){if(!button.hasClass('ui-state-disabled')){$(this).addClass('ui-state-hover');}},mouseleave:function(){$(this).removeClass('ui-state-hover');},focus:function(){if(!button.hasClass('ui-state-disabled')){$(this).addClass('ui-state-focus');}},blur:function(){$(this).removeClass('ui-state-focus');}});this.header.delegate('a','click.multiselect',function(e){if($(this).hasClass('ui-multiselect-close')){self.close();}else{self[$(this).hasClass('ui-multiselect-all')?'checkAll':'uncheckAll']();} +e.preventDefault();});this.menu.delegate('li.ui-multiselect-optgroup-label a','click.multiselect',function(e){e.preventDefault();var $this=$(this),$inputs=$this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'),nodes=$inputs.get(),label=$this.parent().text();if(self._trigger('beforeoptgrouptoggle',e,{inputs:nodes,label:label})===false){return;} +self._toggleChecked($inputs.filter(':checked').length!==$inputs.length,$inputs);self._trigger('optgrouptoggle',e,{inputs:nodes,label:label,checked:nodes[0].checked});}).delegate('label','mouseenter.multiselect',function(){if(!$(this).hasClass('ui-state-disabled')){self.labels.removeClass('ui-state-hover');$(this).addClass('ui-state-hover').find('input').focus();}}).delegate('label','keydown.multiselect',function(e){e.preventDefault();switch(e.which){case 9:case 27:self.close();break;case 38:case 40:case 37:case 39:self._traverse(e.which,this);break;case 13:$(this).find('input')[0].click();break;}}).delegate('input[type="checkbox"], input[type="radio"]','click.multiselect',function(e){var $this=$(this),val=this.value,checked=this.checked,tags=self.element.find('option');if(this.disabled||self._trigger('click',e,{value:val,text:this.title,checked:checked})===false){e.preventDefault();return;} +$this.attr('aria-selected',checked);tags.each(function(){if(this.value===val){this.selected=checked;}else if(!self.options.multiple){this.selected=false;}});if(!self.options.multiple){self.labels.removeClass('ui-state-active');$this.closest('label').toggleClass('ui-state-active',checked);self.close();} +setTimeout($.proxy(self.update,self),10);});$(document).bind('mousedown.multiselect',function(e){if(self._isOpen&&!$.contains(self.menu[0],e.target)&&!$.contains(self.button[0],e.target)&&e.target!==self.button[0]){self.close();}});$(this.element[0].form).bind('reset.multiselect',function(){setTimeout(function(){self.update();},10);});},_setButtonWidth:function(){var width=this.element.outerWidth(),o=this.options;this.button.width(width);},_setMenuWidth:function(){var m=this.menu,width=this.element.outerWidth(),o=this.options;if(/\d/.test(o.minWidth)&&width-1){self._toggleCheckbox('selected',flag).call(this);}});},_toggleDisabled:function(flag){this.button.attr({'disabled':flag,'aria-disabled':flag})[flag?'addClass':'removeClass']('ui-state-disabled');this.menu.find('input').attr({'disabled':flag,'aria-disabled':flag}).parent()[flag?'addClass':'removeClass']('ui-state-disabled');this.element.attr({'disabled':flag,'aria-disabled':flag});},open:function(e){var self=this,button=this.button,menu=this.menu,speed=this.speed,o=this.options;if(this._trigger('beforeopen')===false||button.hasClass('ui-state-disabled')||this._isOpen){return;} +var $container=menu.find('ul:last'),effect=o.show,pos=button.position();if($.isArray(o.show)){effect=o.show[0];speed=o.show[1]||self.speed;} +$container.scrollTop(0).height(o.height);if($.ui.position&&!$.isEmptyObject(o.position)){o.position.of=o.position.of||button;menu.show().position(o.position).hide().show(effect,speed);}else{menu.css({top:pos.top+button.outerHeight(),left:pos.left}).show(effect,speed);} +this.labels.eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');button.addClass('ui-state-active');this._isOpen=true;this._trigger('open');},close:function(){if(this._trigger('beforeclose')===false){return;} +var o=this.options,effect=o.hide,speed=this.speed;if($.isArray(o.hide)){effect=o.hide[0];speed=o.hide[1]||this.speed;} +this.menu.hide(effect,speed);this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');this._isOpen=false;this._trigger('close');},enable:function(){this._toggleDisabled(false);},disable:function(){this._toggleDisabled(true);},checkAll:function(e){this._toggleChecked(true);this._trigger('checkAll');},uncheckAll:function(){this._toggleChecked(false);this._trigger('uncheckAll');},getChecked:function(){return this.menu.find('input').filter(':checked');},destroy:function(){$.Widget.prototype.destroy.call(this);this.button.remove();this.menu.remove();this.element.show();return this;},isOpen:function(){return this._isOpen;},widget:function(){return this.menu;},_setOption:function(key,value){var menu=this.menu;switch(key){case'header':menu.find('div.ui-multiselect-header')[value?'show':'hide']();break;case'checkAllText':menu.find('a.ui-multiselect-all span').eq(-1).text(value);break;case'uncheckAllText':menu.find('a.ui-multiselect-none span').eq(-1).text(value);break;case'height':menu.find('ul:last').height(parseInt(value,10));break;case'minWidth':this.options[key]=parseInt(value,10);this._setButtonWidth();this._setMenuWidth();break;case'selectedText':case'selectedList':case'noneSelectedText':this.options[key]=value;this.update();break;case'classes':menu.add(this.button).removeClass(this.options.classes).addClass(value);break;} +$.Widget.prototype._setOption.apply(this,arguments);}});})(jQuery); \ No newline at end of file diff --git a/core/modules/project_browser/js/project_browser_categories_widget.js b/core/modules/project_browser/js/project_browser_categories_widget.js new file mode 100644 index 0000000..c28f738 --- /dev/null +++ b/core/modules/project_browser/js/project_browser_categories_widget.js @@ -0,0 +1,11 @@ +(function ($) { + $('#edit-categories').multiselect({ + noneSelectedText: '" . t('Choose') . "...', + selectedList: 99, + minWidth: 500, + position: { + my: 'right top', + at: 'right bottom' + } + }); +})(jQuery); \ No newline at end of file diff --git a/core/modules/project_browser/js/project_browser_more_link.js b/core/modules/project_browser/js/project_browser_more_link.js new file mode 100644 index 0000000..07dfc04 --- /dev/null +++ b/core/modules/project_browser/js/project_browser_more_link.js @@ -0,0 +1,28 @@ +(function ($) { + $(document).ready(function() { + // The height of the content block when it's not expanded + var adjustheight = 80; + // The "more" link text + var moreText = "More"; + // The "less" link text + var lessText = "Less"; + + $(".project-information .project-description").each(function(index) { + if ($(this).height() > adjustheight) + { + $(this).css('height', adjustheight).css('overflow', 'hidden'); + $(this).parents(".project-information").append(''); + } + }); + + $("a.show-more").text(moreText); + + $(".show-more").toggle(function() { + $(this).parents("div:first").find(".project-description").css('height', 'auto').css('overflow', 'visible'); + $(this).text(lessText); + }, function() { + $(this).parents("div:first").find(".project-description").css('height', adjustheight).css('overflow', 'hidden'); + $(this).text(moreText); + }); + }); +})(jQuery); diff --git a/core/modules/project_browser/js/project_browser_multiselect.js b/core/modules/project_browser/js/project_browser_multiselect.js new file mode 100644 index 0000000..bf1f49e --- /dev/null +++ b/core/modules/project_browser/js/project_browser_multiselect.js @@ -0,0 +1,13 @@ +(function ($) { + $(document).ready(function() { + $('#edit-categories').multiselect({ + noneSelectedText: Drupal.t("Choose"), + selectedList: 99, + minWidth: 500, + position: { + my: 'right top', + at: 'right bottom' + } + }); + }); +})(jQuery); diff --git a/core/modules/project_browser/js/select_releases.js b/core/modules/project_browser/js/select_releases.js new file mode 100644 index 0000000..97cc780 --- /dev/null +++ b/core/modules/project_browser/js/select_releases.js @@ -0,0 +1,12 @@ +(function ($) { + $(document).ready(function() { + $('.project-browser-releases-wrapper').hide(); + $('.project-browser-selected-release').show(); + + $('.project-browser-show-releases-link').click(function() { + var target = $(this).attr('rel'); + $('.project-browser-release-' + target).show(); + $('.project-browser-selected-release-' + target).hide(); + }) + }); +})(jQuery); diff --git a/core/modules/project_browser/project_browser.admin.inc b/core/modules/project_browser/project_browser.admin.inc new file mode 100644 index 0000000..831b050 --- /dev/null +++ b/core/modules/project_browser/project_browser.admin.inc @@ -0,0 +1,32 @@ + 'fieldset', + '#title' => t('Main settings'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + $form['main']['project_browser_servers'] = array( + '#type' => 'textarea', + '#title' => t('Repositories'), + '#default_value' => variable_get('project_browser_servers', ''), + '#description' => t("Add new repositories to use for the Project Browser, one per line, in + the 'url|method|Site Name' format. Drupal.org is added by default, and doesn't need to be + set here."), + '#required' => FALSE, + ); + + return system_settings_form($form); +} diff --git a/core/modules/project_browser/project_browser.inc b/core/modules/project_browser/project_browser.inc new file mode 100644 index 0000000..df7967a --- /dev/null +++ b/core/modules/project_browser/project_browser.inc @@ -0,0 +1,880 @@ + $queued_projects)); +} + +/** + * Shows an install button for the Install Queue block + */ +function project_browser_install_button_form($form, &$form_state) { + $form['#attributes']['id'] = 'project-browser-install-button-form'; + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Install', + ); + $form['#action'] = url('admin/modules/project-browser/install/select_versions'); + + return $form; +} + +/** + * Builds the filters form + */ +function project_browser_filters_form($form, &$form_state, $type) { + $form['search_text'] = array( + '#type' => 'textfield', + '#size' => '25', + '#title' => t('Search String'), + '#default_value' => isset($_SESSION['project_browser_text_filter_' . $type]) ? $_SESSION['project_browser_text_filter_' . $type] : '', + ); + + // Add the categories filter if there are categories + if ($categories = project_browser_get_categories($type)) { + $form['categories'] = array( + '#type' => 'select', + '#title' => t('Categories'), + '#multiple' => TRUE, + '#options' => $categories, + '#prefix' => '
      ', + '#suffix' => '
      ', + '#default_value' => isset($_SESSION['project_browser_category_filter_' . $type]) ? $_SESSION['project_browser_category_filter_' . $type] : array(), + ); + } + + $form['project_type'] = array( + '#type' => 'value', + '#value' => $type, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Filter'), + ); + + return $form; +} + +/** + * Returns a themed sort widget for the filters + */ +function project_browser_get_sort_widget($sort_options, $current_order_by, $current_sort) { + $sort_list = array(); + $sort_list[] = array('data' => t('Sort by:'), 'class' => array('sort-header')); + $current_path = drupal_get_path_alias(current_path()); + + foreach ($sort_options as $sort_option) { + $classes = array(); + $query = array( + 'order_by' => $sort_option['method'], + 'sort' => $sort_option['default_sort'], + ); + + // If the sort option is currently active, handle it differently + if ($current_order_by == $sort_option['method']) { + $classes[] = 'sort-active'; + $classes[] = 'sort-' . $current_sort; + + // Set the direction of the sort link to the opposite of what it currently is + if ($current_sort == $query['sort']) { + if ($query['sort'] == 'desc') { + $query['sort'] = 'asc'; + } + else { + $query['sort'] = 'desc'; + } + } + } + else { + $classes[] = 'sort-inactive'; + } + + $sort_list[] = array( + 'data' => l($sort_option['name'], $current_path, array('query' => $query, 'class' => array())), + 'class' => $classes, + ); + } + + return theme('item_list', array( + 'items' => $sort_list, + 'type' => 'ul', + 'attributes' => array('class' => array('project-browser-sort-list')))); +} + +/** + * Returns a themed sort widget for the filters + */ +function project_browser_get_server_widget($servers, $current_server) { + $list = array(); + $list[] = array('data' => t('Repository:'), 'class' => array('server-header')); + $current_path = drupal_get_path_alias($_GET['q']); + + $i = 0; + + foreach ($servers as $url => $server) { + $classes = array(); + $query = array( + 'repository' => $i, + ); + + // If the sort option is currently active, handle it differently + if ($current_server == $i) { + $classes[] = 'server-active'; + } + else { + $classes[] = 'server-inactive'; + } + + $list[] = array( + 'data' => l($server['name'], $current_path, array('query' => $query, 'class' => array())), + 'class' => $classes, + ); + + $i += 1; + } + + return theme('item_list', array( + 'items' => $list, + 'type' => 'ul', + 'attributes' => array('class' => array('project-browser-servers-list')))); +} + + +/** + * Returns a list of sort options + * + * @param $full + * Set this to TRUE if you want to get all of the supported sort methods + */ +function project_browser_get_sort_options($full = FALSE) { + $sort_options = array( + 'score' => array('method' => 'score', 'name' => t('Relevancy'), 'default_sort' => 'desc'), + 'usage' => array('method' => 'usage', 'name' => t('Most installed'), 'default_sort' => 'desc'), + 'title' => array('method' => 'title', 'name' => t('Title'), 'default_sort' => 'asc'), + 'name' => array('method' => 'name', 'name' => t('Author'), 'default_sort' => 'asc'), + 'latest_release' => array('method' => 'latest_release', 'name' => t('Latest release'), 'default_sort' => 'desc'), + ); + + if ($full) { + $sort_options['type'] = array('method' => 'type', 'name' => t('Type'), 'default_sort' => 'asc'); + $sort_options['created'] = array('method' => 'created', 'name' => t('Date created'), 'default_sort' => 'asc'); + $sort_options['latest_activity'] = array('method' => 'latest_activity', 'name' => t('Latest build'), 'default_sort' => 'desc'); + } + + return $sort_options; +} + +/** + * Handles the filters form submit + */ +function project_browser_filters_form_submit($form, &$form_state) { + $type = $form_state['values']['project_type']; + if (isset($form_state['values']['categories'])) { + $_SESSION['project_browser_category_filter_' . $type] = $form_state['values']['categories']; + } + else { + $_SESSION['project_browser_category_filter_' . $type] = array(); + } + $_SESSION['project_browser_text_filter_' . $type] = $form_state['values']['search_text']; +} + +/** + * Returns all available categories for a project type + * + * @param $type + * The type of project to get the categories for. Example: 'module' or 'theme' + * + * @return + * Array containing all available categories or FALSE if no categories + */ +function project_browser_get_categories($type) { + $categories = array(); + + // Get the server to use from SESSION + if (isset($_SESSION['project_browser_server_filter'])) { + $use_server = $_SESSION['project_browser_server_filter']; + } + else { + $use_server = 0; + } + + $categories_raw = project_browser_fetch_categories($type, $use_server); + + if (is_array($categories_raw) AND !empty($categories_raw)) { + foreach ($categories_raw as $url => $cats) { + foreach ($cats as $key => $value) { + // Create a new key so that there are no duplicate categories from different sites + $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value)); + $categories[$new_key] = $value; + } + } + } + + if (is_array($categories) AND !empty($categories)) { + ksort($categories); + + return $categories; + } + return FALSE; +} + +/** + * Prepares the categories for sending to the servers as filters + * + * @param $raw_cats + * An array of categories from $form_state['values'] + * @param $type + * The type of project to prepare the categories for, eg 'module' or 'theme' + */ +function project_browser_prepare_categories($raw_cats, $type) { + $categories = project_browser_fetch_categories($type); + + // Set the value of the categories to true if it is selected + foreach ($categories as $url => $cats) { + foreach ($cats as $key => $value) { + $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value)); + if (isset($raw_cats[$new_key]) AND $raw_cats[$new_key]) { + $categories[$url][$key] = TRUE; + } + else { + unset($categories[$url][$key]); + } + } + + // Unset the parent if there are no children + if (empty($categories[$url])) { + unset($categories[$url]); + } + } + + return $categories; +} + +/** + * Checks if a project is enabled + * + * @param $type + * The type of project. Could be 'theme' or 'module' + * @param $name + * The short name of the project + * + * @return + * TRUE if the project is enabled, FALSE otherwise + */ +function _project_browser_is_project_enabled($type, $name) { + switch ($type) { + case 'module': + return module_exists($name); + break; + case 'theme': + $themes = list_themes(); + return isset($themes[$name]); + break; + } + return FALSE; +} + +/** + * Gets the currently listed projects from the session + */ +function project_browser_get_listed_projects() { + if (isset($_SESSION['project_browser_listed_projects'])) { + return $_SESSION['project_browser_listed_projects']; + } + + return array(); +} + +/** + * Gets the currently queued projects from the session + */ +function project_browser_get_queued_projects($type = NULL) { + $projects = array(); + + if (isset($_SESSION['project_browser_install_list'])) { + foreach ($_SESSION['project_browser_install_list'] as $project) { + if (is_array($project) AND !empty($project)) { + if (isset($type) AND $type != $project['type']) { + continue; + } + else { + $projects[$project['name']] = $project; + } + } + } + } + + return $projects; +} + +/** + * Gets a release from a project and a release_name + */ +function project_browser_get_release($release_name, $project) { + $release_data = project_browser_get_project_release_data($project); + + return isset($release_data['releases'][$release_name]) ? $release_data['releases'][$release_name] : FALSE; +} + +/** + * Gets the newly installed projects from the session + */ +function project_browser_get_installed_projects() { + $projects = array(); + + if (isset($_SESSION['project_browser_installed_projects'])) { + foreach ($_SESSION['project_browser_installed_projects'] as $project) { + if (is_array($project) AND !empty($project)) { + $projects[$project['name']] = $project; + } + } + } + + return $projects; +} + +/** + * Adds a project to the install queue + */ +function project_browser_install_queue_add($project) { + $_SESSION['project_browser_install_list'][$project['name']] = $project; +} + +/** + * Removes a project from the install queue + */ +function project_browser_install_queue_remove($project_name) { + if (isset($_SESSION['project_browser_install_list'][$project_name])) { + unset($_SESSION['project_browser_install_list'][$project_name]); + } +} + +/** + * Gets the currently queued releases from the session + */ +function project_browser_get_queued_releases() { + $releases = array(); + + if (isset($_SESSION['project_browser_install_releases_list'])) { + foreach ($_SESSION['project_browser_install_releases_list'] as $release_name => $project) { + if (is_array($project) AND !empty($project)) { + $releases[$release_name] = $project; + } + } + } + + return $releases; +} + +// ====================================== +// Server Related Functions: +// ====================================== + +/** + * Fetches results from the servers based on the parameters passed in + * + * $filters should be an associative array with the following keys: + * array( + * 'version' => '7', // The Major Version of Drupal that is running on the Client + * 'text' => 'views', // The text that was entered as the search query, or '' if none + * 'categories' => array() // The categories that were selected, if any + * 'type' => 'module', // The type of project being searched + * 'page' => 3, // The zero-based page number + * 'requested' => 12, // How many results are requested per page + * ) + * + * The project_browser_fetch_results($filters) call returns an array like this: + * + * array( + * 'total' = 5, // The total number of results found for the filters + * 'projects' => array( // An array of projects returned for this page request + * 'views' => array( // A project array keyed by the machine name + * 'type' => 'module', // The type of project this is. Can be 'module' or 'theme' + * 'title' => 'Views', // The title of the project + * 'name' => 'views', // The machine name of the project + * 'author' => 'merlinofchaos', // The author's name + * 'description' => "Long project description ...", + * 'image' => 'http://www.example.com/image.jpg', // Absolute url to the image, if any + * 'usage' => '542312', // How many Downloads the module has had + * 'project url' => 'http://www.drupal.org/projects/views', // Absolute url to the project page, if any + * 'project status url' => 'http://updates.drupal.org/release-history/views/7.x', // The absolute url of the update checker, formatted like how Drupal.org Update Status does it + * 'last updated' => '12342523', // UNIX Timestamp of when the project was last updated + * 'maintenance status' => 'Actively maintained', // Maintenance status + * 'development status' => 'Under active development', // Development status + * 'rating' => '9.6', // A rating on a scale of 1 to 10 of the project, if available + * 'dependencies' => array( // An array of the dependencies of this module + * 'ctools', + * ), + * ), + * 'name_2 => array( ... ), + * ), + * ); + * + * @param $filters + * An associative array of queries to use to filter results + * + * @return + * Returns an array of results + */ +function project_browser_fetch_results($filters) { + $servers = project_browser_get_servers($filters['server']); + // Attempt to retrieve the cached version of this page + $cid = md5(serialize(array_merge($filters, $servers))); + + if ($cache = cache()->get($cid)) { + return $cache->data; + } + + $results = array( + 'projects' => array(), + 'total' => 0, + ); + + unset($filters['server']); + + foreach ($servers as $url => $server) { + $local_filters = $filters; + + // We are not using this right now because we only expect to handle 1 server at a time currently + // $local_filters['requested'] = floor($filters['requested'] / count($servers)); + + // Send only the relevant categories to the server + if (isset($filters['categories'])) { + if (!isset($filters['categories'][$url])) { + // Don't call a server for results if categories are being used, and none of them belong to the server + continue; + } + $local_filters['categories'] = $filters['categories'][$url]; + } + + // Use XMLRPC if it is set + if ($server['method'] == 'xmlrpc') { + $results_raw = xmlrpc($url, array( + 'project_browser_server.fetch_results' => array($local_filters), + )); + + // Check for errors + if ($error = xmlrpc_error() AND $error->is_error) { + drupal_set_message(t("Encountered an error when trying to fetch results from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $error->code, '@message' => $error->message))); + continue; + } + } + + // Use json if it is set + if ($server['method'] == 'json') { + $local_filters['method'] = 'query'; + if (isset($local_filters['categories'])) { + $local_filters['categories'] = serialize($local_filters['categories']); + } + + $query_url = $url . '/query/' . $local_filters['type'] . '/8?' . http_build_query($local_filters, FALSE, '&'); + $response = drupal_http_request($query_url); + if ($response->code == '200') { + $results_raw = drupal_json_decode($response->data); + } + else { + drupal_set_message(t("Encountered an error when trying to fetch results from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $response->code, '@message' => $response->error))); + continue; + } + } + + if (isset($results_raw['total'])) { + $results['total'] += $results_raw['total']; + } + + if (isset($results_raw['projects']) AND !empty($results_raw['projects'])) { + // Merge the results + $results['projects'] = array_merge($results['projects'], $results_raw['projects']); + } + } + + // Set the cached version of the results + cache()->set($cid, $results, strtotime("+24 hours")); + + return $results; +} + +/** + * Fetches categories from the servers based on the type of project + * + * @param $type + * The type of project we are getting categories for + * @param $use_server + * (Optional) The server to use. Defaults to 'all'. + * + * @return + * Returns an array of the categories + */ +function project_browser_fetch_categories($type, $use_server = 'all') { + $servers = project_browser_get_servers($use_server); + + // Attempt to retrieve the cached version of this page + $cid = md5('categories_' . $type . serialize($servers)); + + if ($cache = cache()->get($cid)) { + return $cache->data; + } + else { + $categories = array(); + + foreach ($servers as $url => $server) { + // Use xmlrpc if it is set + if ($server['method'] == 'xmlrpc') { + $categories_raw = xmlrpc($url, array( + 'project_browser_server.fetch_categories' => array($type), + )); + + // Check for errors + if ($error = xmlrpc_error() AND $error->is_error) { + drupal_set_message(t("Encountered an error when trying to fetch categories from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $error->code, '@message' => $error->message))); + continue; + } + } + + // Use json if it is set + if ($server['method'] == 'json') { + $params = array( + 'method' => 'categories', + 'type' => $type, + ); + $response = drupal_http_request($url . '/categories/' . $type . '?' . http_build_query($params, FALSE, '&')); + if ($response->code == '200') { + $categories_raw = drupal_json_decode($response->data); + } + else { + drupal_set_message(t("Encountered an error when trying to fetch categories from @name. Error @code : @message", + array('@name' => $server['name'], '@code' => $response->code, '@message' => $response->error))); + continue; + } + } + + if (is_array($categories_raw) AND !empty($categories_raw)) { + $categories[$url] = $categories_raw; + } + } + + // Cache this for 24 hours + cache()->set($cid, $categories, strtotime("+24 hours")); + } + + return $categories; +} + +/** + * Gets the servers to use for fetching results + * + * @return + * Returns an associative array of servers, populated from the project_browser_servers variable, + * in 'url => name' format + */ +function project_browser_get_servers($use_server = 'all') { + // FIXME - Change the link once drupal.org is ready + $servers = variable_get('project_browser_default_server', array( + 'http://drupal:drupal@pbs-drupal_7.redesign.devdrupal.org/project_browser/server' => array( + 'name' => 'Drupal.org', + 'method' => 'json', + ), + )); + + if ($servers_raw = variable_get('project_browser_servers', '')) { + // Process the variable and add the servers to the list + $custom_servers = array(); + + $list = explode("\n", $servers_raw); + $list = array_map('trim', $list); + $list = array_filter($list, 'strlen'); + + foreach ($list as $position => $text) { + $method = $name = $url = FALSE; + + $matches = array(); + if (preg_match('/(.*)\|(.*)\|(.*)/', $text, $matches)) { + $url = $matches[1]; + $method = $matches[2]; + $name = $matches[3]; + $custom_servers[$url] = array('name' => $name, 'method' => $method); + } + } + + $servers = array_merge($servers, $custom_servers); + } + + // Filter out servers if necessary + if ($use_server !== 'all') { + $i = 0; + foreach ($servers as $url => $server) { + if ($use_server != $i) { + unset($servers[$url]); + } + $i += 1; + } + } + + return $servers; +} + +/** + * Uses the project status url to get the available releases for a project + * + * @param $project + * The project to get the releases for + * + * @return + * An array of releases for this project + */ +function project_browser_get_project_release_data($project) { + $releases = array(); + $project['project_type'] = $project['type']; + $project['includes'] = array(); + + // Build the releases cache for this project + module_load_include('inc', 'update', 'update.fetch'); + if (_update_process_fetch_task($project)) { + $data = _update_cache_get('available_releases::' . $project['name']); + if (isset($data->data) AND isset($data->data['releases']) AND is_array($data->data['releases'])) { + return $data->data; + } + } + + return FALSE; +} + +// ====================================== +// Batch Operations: +// ====================================== + +/** + * Helper function to download a project. This code is mostly copied and pasted from + * modules/update/update.manager.inc + * + * There were no suitable functions that could be used besides + * drupal_form_submit('update_manager_install_form', $form_state, $project['type']); + * and it wouldn't work because this is being run from a Batch function + * + * @todo - Ideally, this should be in the update module as a standalone function, + * to reduce coupling and duplication + */ +function project_browser_download_project($url) { + module_load_include('inc', 'update', 'update.manager'); + // Download the file + $local_cache = update_manager_file_get($url); + if (!$local_cache) { + return array( + 'success' => FALSE, + 'message' => t('Unable to retrieve Drupal project from %url.', array('%url' => $url)), + ); + } + + // Try to extract it + $directory = _update_manager_extract_directory(); + try { + $archive = update_manager_archive_extract($local_cache, $directory); + } + catch (Exception $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } + $files = $archive->listContents(); + if (!$files) { + return array( + 'success' => FALSE, + 'message' => t('Provided archive contains no files.'), + ); + } + + $project = strtok($files[0], '/\\'); + + $archive_errors = update_manager_archive_verify($project, $local_cache, $directory); + if (!empty($archive_errors)) { + if (!empty($archive_errors)) { + foreach ($archive_errors as $error) { + drupal_set_message(check_plain($error), 'error'); + } + } + return array( + 'success' => FALSE, + 'message' => array_shift($archive_errors), + ); + } + //require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/Updater/Updater.php'; + + // Make sure the Updater registry is loaded. + drupal_get_updaters(); + + $project_location = $directory . '/' . $project; + try { + $updater = Updater::factory($project_location); + } + catch (Exception $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } + + try { + $project_title = Updater::getProjectTitle($project_location); + } + catch (Exception $e) { + return array( + 'success' => FALSE, + 'message' => $e->getMessage(), + ); + } + + if ($updater->isInstalled()) { + return array( + 'success' => FALSE, + 'message' => t('%project is already installed.', array('%project' => $project_title)), + ); + } + + $project_real_location = drupal_realpath($project_location); + $updater_name = get_class($updater); + + if (fileowner($project_real_location) == fileowner(conf_path())) { + module_load_include('inc', 'update', 'update.authorize'); + $filetransfer = new Local(DRUPAL_ROOT); + + // Initialize some variables in the Batch API $context array. + $updater = new $updater_name($project_real_location); + + try { + if ($updater->isInstalled()) { + // This is an update. + $tasks = $updater->update($filetransfer); + } + else { + $tasks = $updater->install($filetransfer); + } + } + catch (UpdaterException $e) { + return array( + 'success' => FALSE, + 'message' => t('Error installing / updating. Error: @error', array('@error' => $e->getMessage())), + ); + } + } + else { + return array( + 'success' => FALSE, + 'message' => t('Permissions are not set up properly.'), + ); + } + + return array( + 'success' => TRUE, + ); +} + +/** + * Installs a single release of a project during batch, for example + */ +function _project_browser_batch_install_release($release_name, $project, &$context) { + module_load_include('inc', 'project_browser', 'project_browser.pages'); + $release = project_browser_get_release($release_name, $project); + + $result = project_browser_download_project($release['download_link']); + + if ($result['success']) { + $context['results']['successes'][] = t('Successfully installed %project.', array('%project' => $project['title'])); + $context['message'] = t('Installed %project...', array('%project' => $project['title'])); + + // Add this to the session variable and remove it from the install_queue variable + $_SESSION['project_browser_installed_projects'][$project['name']] = $project; + unset($_SESSION['project_browser_install_list'][$project['name']]); + } + else { + watchdog('project_browser', 'There was an error while installing %project. + !message', + array('%project' => $project['title'], '!message' => $result['message']), WATCHDOG_ERROR); + $context['results']['failures'][] = t('Error installing %project. Errors have been logged.', + array('%project' => $project['title'])); + $context['message'] = t('Error installing %project. !message', + array('%project' => $project['title'], '!message' => $result['message'])); + } +} + +/** + * Shows a message and finish up the batch + */ +function _project_browser_batch_install_releases_finished($success, $results, $operations) { + drupal_get_messages(); + + // Restore the maintenance mode to what it was at the start + variable_set('maintenance_mode', $_SESSION['maintenance_mode']); + unset($_SESSION['maintenance_mode']); + + unset($_SESSION['project_browser_install_releases_list']); + if ($success) { + if (!empty($results)) { + if (!empty($results['failures'])) { + drupal_set_message(format_plural(count($results['failures']), 'Failed to install one project.', 'Failed to install @count projects.'), 'error'); + } + } + } + else { + drupal_set_message(t('Error installing projects.'), 'error'); + drupal_goto('admin/modules/project-browser/install/select_versions'); + } + + $projects = project_browser_get_installed_projects(); + $missing = project_browser_get_missing_dependencies($projects); + // If there are missing dependencies, go to install dependencies + if (count($missing) > 0) { + drupal_goto('admin/modules/project-browser/install/install_dependencies'); + } + else { + drupal_goto('admin/modules/project-browser/install/enable'); + } +} + +/** + * Gets the dependencies for installed projects + */ +function project_browser_get_missing_dependencies($projects) { + $modules = system_rebuild_module_data(); + + $missing = array(); + + foreach ($projects as $project) { + if ($project['type'] == 'module') { + $dependency_check = TRUE; + $dependencies = array(); + if (isset($modules[$project['name']])) { + foreach ($modules[$project['name']]->info['dependencies'] as $dependency) { + if (!isset($modules[$dependency])) { + $dependencies[] = $dependency; + } + } + if (count($dependencies) > 0) { + $missing[$project['name']] = $dependencies; + } + } + else { + drupal_set_message(t('There was an error getting information for @module', + array('@module' => $project['name'])), 'error'); + } + } + } + + return $missing; +} diff --git a/core/modules/project_browser/project_browser.info b/core/modules/project_browser/project_browser.info new file mode 100644 index 0000000..c6510a8 --- /dev/null +++ b/core/modules/project_browser/project_browser.info @@ -0,0 +1,7 @@ +name = Project Browser +description = A Project Browser that allows users to browse for and install modules and themes from their Drupal site admin area +dependencies[] = update +package = Core +version = VERSION +core = 8.x +files[] = project_browser.test \ No newline at end of file diff --git a/core/modules/project_browser/project_browser.module b/core/modules/project_browser/project_browser.module new file mode 100644 index 0000000..3744bfb --- /dev/null +++ b/core/modules/project_browser/project_browser.module @@ -0,0 +1,467 @@ +' . t("Provides a UI for users to browse for and install new modules and themes from + within their Drupal admin interface.") . '

      '; + break; + } + return $output; +} + +/** + * Implements of hook_perm(). + */ +function project_browser_permission() { + return array( + 'use project browser' => array( + 'title' => t('Use Project Browser'), + 'description' => t('This allows the user to browse for and install new modules and themes using Project Browser.'), + 'restrict access' => TRUE, + ) + ); +} + +/** + * Implements of hook_menu(). + */ +function project_browser_menu() { + $items = array(); + $items['admin/config/development/project_browser'] = array( + 'title' => 'Project Browser settings', + 'description' => 'Add new repositories and set other settings for Project Browser.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('project_browser_admin'), + 'access arguments' => array('access administration pages'), + 'file' => 'project_browser.admin.inc', + ); + + $items['admin/modules/project-browser'] = array( + 'title' => 'Project Browser', + 'description' => 'Browse and search for new modules', + 'page callback' => 'project_browser_page', + 'page arguments' => array('module'), + 'access arguments' => array('use project browser'), + 'file' => 'project_browser.pages.inc', + ); + + $items['admin/modules/project-browser/modules'] = array( + 'title' => 'Modules', + 'description' => 'Browse and search for new modules', + 'page callback' => 'project_browser_page', + 'page arguments' => array('module'), + 'access arguments' => array('use project browser'), + 'file' => 'project_browser.pages.inc', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + + $items['admin/modules/project-browser/themes'] = array( + 'title' => 'Themes', + 'description' => 'Browse and search for new themes', + 'page callback' => 'project_browser_page', + 'page arguments' => array('theme'), + 'access arguments' => array('use project browser'), + 'file' => 'project_browser.pages.inc', + 'type' => MENU_LOCAL_TASK, + ); + + $items['admin/modules/project-browser/install/%'] = array( + 'title' => 'Install', + 'page callback' => 'project_browser_installation_page', + 'page arguments' => array(4), + 'access arguments' => array('use project browser'), + 'type' => MENU_NORMAL_ITEM, + 'file' => 'project_browser.pages.inc', + ); + + $items['project-browser/%/install-queue/%/%'] = array( + 'page callback' => 'project_browser_install_queue_callback', + 'page arguments' => array(1, 3, 4), + 'access arguments' => array('use project browser'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Page callback that allows for adding to and removing from the install queue + */ +function project_browser_install_queue_callback($method, $op, $project_name) { + module_load_include('inc', 'project_browser', 'project_browser'); + + switch ($op) { + case 'add': + $projects = project_browser_get_listed_projects(); + + if (isset($projects[$project_name])) { + $project = $projects[$project_name]; + project_browser_install_queue_add($project); + } + else { + drupal_set_message(t('Error: The project was not found.'), 'error'); + } + break; + + case 'remove': + project_browser_install_queue_remove($project_name); + break; + } + + switch ($method) { + case 'nojs': + // Redirect to the page it came from + $redirect = (isset($_GET['destination'])) ? $_GET['destination'] : 'admin/modules/project-browser'; + + drupal_goto($redirect); + break; + case 'ajax': + $commands = array(); + + // Refresh the install queue + $commands[] = ajax_command_replace('#project-browser-install-queue', project_browser_get_install_list()); + // Refresh the add to queue link + $commands[] = ajax_command_replace('#add-to-queue-link-' . $project_name, project_browser_add_remove_queue_link($project_name)); + + return array('#type' => 'ajax', '#commands' => $commands); + break; + } +} + +/** + * Implements hook_menu_local_tasks_alter(). + * + * This is used to put the 'Project Browser' action on the 'Modules' page + */ +function project_browser_menu_local_tasks_alter(&$data, $router_item, $root_path) { + switch ($root_path) { + case 'admin/modules': + // Unset the install theme page + foreach ($data['actions']['output'] as $num => $item) { + if ($item['#link']['path'] == 'admin/modules/install') { + unset($data['actions']['output'][$num]); + } + } + $item = menu_get_item('admin/modules/project-browser/modules'); + if ($item['access']) { + $item['title'] = t('Install new modules'); + $data['actions']['output'][] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + ); + } + break; + case 'admin/modules/project-browser': + case 'admin/modules/project-browser/modules': + case 'admin/modules/project-browser/themes': + $item = menu_get_item('admin/modules/install'); + if ($item['access']) { + $item['title'] = t('Install manually'); + $data['actions']['output'][] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + ); + } + break; + case 'admin/appearance': + // Unset the install theme page + foreach ($data['actions']['output'] as $num => $item) { + if ($item['#link']['path'] == 'admin/appearance/install') { + unset($data['actions']['output'][$num]); + } + } + $item = menu_get_item('admin/modules/project-browser/themes'); + if ($item['access']) { + $item['title'] = t('Install new themes'); + $data['actions']['output'][] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + ); + } + break; + } +} + +// ====================================== +// Theme: +// ====================================== + +/** + * Implements hook_theme() + */ +function project_browser_theme($existing, $type, $theme, $path) { + return array( + // Template for installation page + 'project_browser_install' => array( + 'variables' => array('current_task' => NULL, 'main_content' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-install', + ), + // Template for list of projects + 'project_browser_list' => array( + 'variables' => array('projects_list' => NULL, 'type' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-list', + ), + // Template for list of projects + 'project_browser_block' => array( + 'render element' => 'element', + 'path' => $path . '/theme', + 'template' => 'project-browser-block', + ), + // Template for single project + 'project_browser_project' => array( + 'variables' => array('project' => NULL, 'first' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-project', + ), + // Template for install queue item + 'project_browser_install_queue' => array( + 'variables' => array('projects' => NULL), + 'path' => $path . '/theme', + 'template' => 'project-browser-install-queue', + ), + ); +} + +/** + * Add some variables for the projects install theme + * + * @param $variables + * An associative array containing: + * - current_task : the current task + */ +function project_browser_preprocess_project_browser_install(&$variables) { + module_load_include('inc', 'project_browser', 'project_browser.pages'); + // Add the themed list + $variables['task_list'] = project_browser_installation_task_list($variables['current_task']); +} + +/** + * Add some variables for the projects install queue theme + */ +function project_browser_preprocess_project_browser_install_queue(&$variables) { + $build = array(); + if (empty($variables['projects'])) { + $build['empty_text'] = array( + '#markup' => t('Install queue is empty.'), + ); + } + else { + foreach ($variables['projects'] as $project) { + $build['queued-item-' . $project['name']] = array( + '#prefix' => "
      ", + '#markup' => project_browser_add_remove_queue_link($project['name'], $project['title'], 'remove-queue-link'), + '#suffix' => "
      ", + ); + } + $build['install-link'] = drupal_get_form('project_browser_install_button_form'); + } + + // Add the install button + $variables['queue_html'] = drupal_render($build); +} + +/** + * Add some variables for the project browser block theme + * + * @param $variables + */ +function project_browser_preprocess_project_browser_block(&$variables) { + // Add the title and content variables + $variables['title'] = $variables['element']['#title']; + $variables['content'] = $variables['element']['#content']; +} + +/** + * Add some variables for the projects list theme + * + * @param $variables + * An associative array containing: + * - projects_list : array of all projects + */ +function project_browser_preprocess_project_browser_list(&$variables) { + module_load_include('inc', 'project_browser', 'project_browser'); + drupal_add_css(drupal_get_path('module', 'project_browser') . '/css/project_browser.css'); + + if (is_array($variables['projects_list']) AND !empty($variables['projects_list'])) { + $content = ''; + $first = TRUE; + // Theme each individual project and add to the list + foreach ($variables['projects_list'] as $project) { + $content .= theme('project_browser_project', array('project' => $project, 'first' => $first)); + $first = FALSE; + } + } + else { + $content = t('No results found.'); + } + + switch ($variables['type']) { + case 'module': + $title = t('Modules'); + break; + case 'theme': + $title = t('Themes'); + break; + default: + $title = t('Projects'); + break; + } + + $main_content['project_browser_main_block'] = array( + '#theme' => 'project_browser_block', + '#title' => $title, + '#content' => $content, + '#contextual_links' => array( + 'project_browser' => array('admin/config/development/project_browser', array()), + ), + ); + $variables['main_content'] = render($main_content); + + // Add the pager + $variables['pager'] = theme('pager', array('tags' => NULL)); + + // Add the filters + $filters_form = drupal_get_form('project_browser_filters_form', $variables['type']); + $filters['project_browser_filters_block'] = array( + '#theme' => 'project_browser_block', + '#title' => t('Filters'), + '#content' => drupal_render($filters_form), + ); + $variables['filters'] = render($filters); + + // Add the install list + $install_list['project_browser_filters_block'] = array( + '#theme' => 'project_browser_block', + '#title' => t('Install queue'), + '#content' => project_browser_get_install_list(), + ); + $variables['install_list'] = render($install_list); +} + +/** + * Add some variables for the project theme + * + * @param $variables + * An associative array containing: + * - project : associative array of project variables + */ +function project_browser_preprocess_project_browser_project(&$variables) { + module_load_include('inc', 'project_browser', 'project_browser'); + $project = $variables['project']; + + $variables['title'] = l($project['title'], check_url($project['project url']), + array('attributes' => array('target' => '_blank'), 'html' => TRUE)); + $variables['author'] = t('Author: @author', array('@author' => $project['author'])); + $variables['description'] = _filter_htmlcorrector(filter_xss($project['description'])); + $variables['image'] = $project['image']; + $variables['last_updated'] = ($project['last updated']) ? t('Last Updated: @date', array('@date' => format_date($project['last updated'], 'long'))) : ''; + + $extras = array(); + + if ($project['maintenance status']) { + $extras[] = check_plain($project['maintenance status']); + } + if ($project['development status']) { + // We are not showing this because it isn't a good indicator right now + // $extras[] = check_plain($project['development status']); + } + if ($project['usage'] AND is_numeric($project['usage'])) { + $extras[] = format_plural($project['usage'], '1 Install', '@count Installs'); + } + if ($project['rating']) { + $extras[] = check_plain($project['rating']); + } + + $variables['extras'] = implode(' | ', $extras); + + // Check if the project is installed + if (_project_browser_is_project_enabled($project['type'], $project['name'])) { + $variables['status'] = '
      Already installed
      '; + $variables['install'] = ''; + } + elseif (drupal_get_filename($project['type'], $project['name'])) { + $variables['status'] = '
      Already downloaded
      '; + $variables['install'] = ''; + } + else { + $variables['status'] = ''; + $variables['install'] = project_browser_add_remove_queue_link($project['name']); + } +} + +/** + * Builds the add/remove project to install queue link + */ +function project_browser_add_remove_queue_link($project_name, $title = NULL, $id_prefix = 'add-to-queue-link') { + $queued_projects = project_browser_get_queued_projects(); + if (!$title) { + $title = isset($queued_projects[$project_name]) ? t('Remove from Install queue') : t('Add to Install queue'); + } + $op = isset($queued_projects[$project_name]) ? 'remove' : 'add'; + + $build['ajax_link'] = array( + '#type' => 'link', + '#title' => $title, + '#href' => 'project-browser/nojs/install-queue/' . $op . '/'. $project_name, + '#options' => array( + 'query' => drupal_get_destination(), + ), + '#id' => $id_prefix . '-' . $project_name, + '#ajax' => array( + 'effect' => 'fade', + 'speed' => 1000, + 'progress' => array( + 'type' => 'throbber', + 'message' => '', + ), + ), + ); + + return drupal_render($build); +} + +/** + * Implements hook_library(). + * + * This is used for the categories multiselect widget library + */ +function project_browser_library() { + // Library One. + $libraries['multiselect'] = array( + 'title' => 'jQuery MultiSelect', + 'website' => 'http://example.com/library-1', + 'version' => '1.10a', + 'js' => array( + drupal_get_path('module', 'project_browser') . '/js/jquery.multiselect.min.js' => array(), + ), + 'css' => array( + drupal_get_path('module', 'project_browser') . '/css/jquery.multiselect.css' => array( + 'type' => 'file', + 'media' => 'screen', + ), + ), + 'dependencies' => array( + array('system', 'ui.widget'), + array('system', 'ui.dialog'), + array('system', 'ui.position'), + ), + ); + return $libraries; +} diff --git a/core/modules/project_browser/project_browser.pages.inc b/core/modules/project_browser/project_browser.pages.inc new file mode 100644 index 0000000..d042bf1 --- /dev/null +++ b/core/modules/project_browser/project_browser.pages.inc @@ -0,0 +1,521 @@ + $drupal_version[0], + 'type' => $type, + ); + + // Add filters + if (isset($_SESSION['project_browser_category_filter_' . $type])) { + $categories = array_filter($_SESSION['project_browser_category_filter_' . $type]); + if (!empty($categories)) { + $filters['categories'] = project_browser_prepare_categories($categories, $type); + } + } + if (isset($_SESSION['project_browser_text_filter_' . $type])) { + $filters['text'] = $_SESSION['project_browser_text_filter_' . $type]; + } + if (isset($_SESSION['project_browser_order_by_filter_' . $type])) { + $filters['order_by'] = $_SESSION['project_browser_order_by_filter_' . $type]; + } + if (isset($_SESSION['project_browser_sort_filter_' . $type])) { + $filters['sort'] = $_SESSION['project_browser_sort_filter_' . $type]; + } + if (isset($_SESSION['project_browser_server_filter'])) { + $filters['server'] = $_SESSION['project_browser_server_filter']; + } + else { + $filters['server'] = 0; + } + $filters['requested'] = 10; + $filters['page'] = isset($_GET['page']) ? $_GET['page'] : 0; + + // Get the projects to display here based on the filters + $results = project_browser_fetch_results($filters); + + // Save the listed projects in the session so it can be used + $_SESSION['project_browser_listed_projects'] = $results['projects']; + + $test = project_browser_get_listed_projects(); + + $list = array(); + foreach ($results['projects'] as $project) { + $list[] = $project; + } + + // Add the pager + $total = $results['total']; + $num_per_page = 10; + $page = pager_default_initialize($total, $num_per_page); + $offset = $num_per_page * $page; + $start = ($total) ? $offset + 1 : 0; + $finish = $offset + $num_per_page; + if ($finish > $total) { + $finish = $total; + } + + $sort_options = project_browser_get_sort_options(); + $current_order_by = isset($_SESSION['project_browser_order_by_filter_' . $type]) ? $_SESSION['project_browser_order_by_filter_' . $type] : 'score'; + $current_sort = isset($_SESSION['project_browser_sort_filter_' . $type]) ? $_SESSION['project_browser_sort_filter_' . $type] : 'desc';; + + $build = array(); + $build['content'] = array( + 'project_browser_header' => array( + '#markup' => t('Showing @start to @finish of @total.', array( + '@start' => $start, '@finish' => $finish, '@total' => $total)), + '#weight' => 0, + ), + 'project_browser_sort_header' => array( + '#type' => 'item', + '#weight' => 2, + '#markup' => project_browser_get_sort_widget($sort_options, $current_order_by, $current_sort), + ), + 'project_browser_list' => array( + '#markup' => theme('project_browser_list', array('projects_list' => $list, 'type' => $type)), + '#weight' => 3, + ), + 'pager' => array( + '#theme' => 'pager', + '#weight' => 99, + ), + ); + + $servers = project_browser_get_servers(); + + if (count($servers) > 1) { + $build['content']['project_browser_server_header'] = array( + '#type' => 'item', + '#weight' => 1, + '#markup' => project_browser_get_server_widget($servers, $filters['server']), + ); + } + + return $build; +} + +// ====================================== +// Installation Page: +// ====================================== + +/** + * Page to install projects + * + * @param $op + * Operation to preform. + */ +function project_browser_installation_page($op) { + drupal_add_css(drupal_get_path('module', 'project_browser') . '/css/project_browser.css', array('preprocess' => FALSE)); + + switch ($op) { + case 'select_versions': + drupal_set_title(t("Select versions")); + $content = project_browser_installation_select_versions_page(); + break; + case 'install_dependencies': + drupal_set_title(t("Install Dependencies")); + $content = project_browser_installation_install_dependencies_page(); + break; + case 'enable': + drupal_set_title(t("Enable modules")); + $content = project_browser_installation_enable_page(); + break; + } + return theme('project_browser_install', array('current_task' => $op, 'main_content' => drupal_render($content))); +} + +/** + * Task page for the Select versions installation task + * + * Shows a form where the user can select which versions to install for each + * project + */ +function project_browser_installation_select_versions_page() { + module_load_include('inc', 'project_browser', 'project_browser'); + // Show a form that lets the user select which version of the projects to install + $queued_projects = project_browser_get_queued_projects(); + unset($_SESSION['project_browser_installed_projects']); + + return drupal_get_form('project_browser_installation_select_versions_form', $queued_projects); +} + +/** + * Form builder for the select versions form + * + * @param $projects + * An array of projects to get the releases for + */ +function project_browser_installation_select_versions_form($form, &$form_state, $projects) { + module_load_include('inc', 'project_browser', 'project_browser'); + drupal_add_js(drupal_get_path('module', 'project_browser') . '/js/select_releases.js'); + + $form = array(); + + // First unset any old data + unset($_SESSION['project_browser_install_releases_list']); + + $form['#tree'] = TRUE; + + $form['releases-header'] = array( + '#type' => 'item', + '#markup' => t("You're about to install:"), + ); + + $form['releases'] = array(); + + foreach ($projects as $project) { + // Get the available releases for this project + if (!$release_data = project_browser_get_project_release_data($project)) { + drupal_set_message(t('Could not fetch releases for project %project.', + array('%project' => $project['title'])), 'warning'); + watchdog('project_browser', 'Could not fetch releases for project %project.', + array('%project' => $project['title']), WATCHDOG_ERROR); + project_browser_install_queue_remove($project['name']); + continue; + } + + // We use the update module to calculate the recommended version + $project_data = array( + 'existing_major' => 0, + 'existing_version' => 0, + 'install_type' => '', + ); + module_load_include('inc', 'update', 'update.compare'); + update_calculate_project_update_status($project_data, $release_data); + + $releases_list = array(); + + foreach ($release_data['releases'] as $version => $release) { + $release_title = t("@project @version - @date", array( + '@project' => $project['title'], + '@version' => $release['version'], + '@date' => format_date($release['date'], 'custom', 'M j, Y'), + )); + if (isset($release['terms']['Release type']) AND !empty($release['terms']['Release type'])) { + $release_title .= " (" . implode(', ', $release['terms']['Release type']) . ")"; + } + if (isset($release['release_link'])) { + $releases_list[$version] = l($release_title, $release['release_link']); + } + else { + $releases_list[$version] = $release_title; + } + } + + $form['releases'][$project['name']]['project'] = array( + '#type' => 'value', + '#value' => $project, + ); + + $form['releases'][$project['name']]['release_name'] = array( + '#type' => 'radios', + '#title' => t('Select release for @project', array('@project' => $project['title'])), + '#options' => $releases_list, + '#default_value' => key($releases_list), + '#prefix' => '
      ', + '#suffix' => '
      ', + '#attributes' => array( + 'class' => array('project-browser-releases-radios'), + ), + '#required' => TRUE, + ); + $form['releases'][$project['name']]['selected_text'] = array( + '#type' => 'item', + '#prefix' => '
      ', + '#suffix' => '
      ', + '#markup' => reset($releases_list), + ); + if (isset($project_data['recommended'])) { + // If there is a recommended release set, then only show it and show the jQuery link + $recommended_releases = array(); + $recommended_releases[$project_data['recommended']] = $releases_list[$project_data['recommended']]; + $form['releases'][$project['name']]['release_name']['#default_value'] = $project_data['recommended']; + $form['releases'][$project['name']]['selected_text']['#markup'] = $releases_list[$project_data['recommended']]; + } + if (count($releases_list) > 1) { + $form['releases'][$project['name']]['selected_text']['#markup'] .= + " (" . t('change release') . ")"; + } + } + + // If there is nothing to install, go to the enable page + if (empty($form['releases'])) { + drupal_set_message(t('No releases data found for any of the selected projects.'), 'warning'); + drupal_goto('admin/modules/project-browser/install/enable'); + } + + $form['backup_warning'] = array( + '#type' => 'markup', + '#markup' => t('Back up your database and site before you continue. !link.', + array('!link' => l(t('Learn how'), 'http://drupal.org/node/22281'))), + ); + $form['maintenance_mode'] = array( + '#type' => 'checkbox', + '#title' => t('Perform updates with site in maintenance mode (strongly recommended)'), + '#default_value' => TRUE, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Install'), + ); + + return $form; +} + +/** + * Submit handler for the select versions form + */ +function project_browser_installation_select_versions_form_submit($form, &$form_state) { + module_load_include('inc', 'project_browser', 'project_browser'); + // Store maintenance_mode setting so we can restore it when done. + $_SESSION['maintenance_mode'] = variable_get('maintenance_mode', FALSE); + if ($form_state['values']['maintenance_mode'] == TRUE) { + variable_set('maintenance_mode', TRUE); + } + + foreach ($form_state['values']['releases'] as $item) { + // Load the selected release + if ($release = project_browser_get_release($item['release_name'], $item['project'])) { + // Add the release to a session variable + $_SESSION['project_browser_install_releases_list'][$item['release_name']] = $item['project']; + } + } + + // Install the projects with batch + module_load_include('inc', 'update', 'update.manager'); + + $queued_releases = project_browser_get_queued_releases(); + + $operations = array(); + foreach ($queued_releases as $release_name => $project) { + $operations[] = array('_project_browser_batch_install_release', array($release_name, $project)); + } + $batch = array( + 'operations' => $operations, + 'finished' => '_project_browser_batch_install_releases_finished', + 'title' => t('Installing projects'), + 'init_message' => t('Installing modules...'), + 'progress_message' => t('Installed @current out of @total.'), + 'error_message' => t('Installation has encountered an error.'), + 'file' => drupal_get_path('module', 'project_browser') . '/project_browser.inc', + ); + batch_set($batch); +} + +/** + * Task page for the Install Dependencies installation task + * + * Show a form which lets the user select which version of dependencies to install + */ +function project_browser_installation_install_dependencies_page() { + module_load_include('inc', 'project_browser', 'project_browser'); + $projects = project_browser_get_installed_projects(); + $missing = project_browser_get_missing_dependencies($projects); + + if (count($missing) > 0) { + $missing_projects = array(); + // Add the project data in the array as best we can + foreach ($missing as $project_shortname => $dependencies) { + foreach ($dependencies as $shortname) { + $missing_projects[$shortname] = array( + 'name' => $shortname, + 'type' => 'module', // FIXME + 'title' => $shortname, + ); + } + } + + return drupal_get_form('project_browser_installation_select_versions_form', $missing_projects); + } + else { + drupal_goto('admin/modules/project-browser/install/enable'); + } +} + +/** + * Task page for the Enable projects installation task + * + * Show a form which lets the user enable the newly installed projects + */ +function project_browser_installation_enable_page() { + module_load_include('inc', 'project_browser', 'project_browser'); + $installed_projects = project_browser_get_installed_projects(); + + if (count($installed_projects) > 0) { + return drupal_get_form('project_browser_installation_enable_form', $installed_projects); + } + else { + drupal_goto('admin/modules/project-browser'); + } +} + +/** + * Form builder for the select versions form + * + * @param $projects + * An array of projects to get the releases for + */ +function project_browser_installation_enable_form($form, &$form_state, $projects) { + $modules = system_rebuild_module_data(); + $form['instructions'] = array( + '#type' => 'item', + '#markup' => t('The projects you selected have been successfully installed. + If you installed any new modules, you may enable them using the form below + or on the main !link page.', array('!link' => l(t('Modules'), 'admin/modules'))), + ); + + $options = array(); + $missing = array(); + + foreach ($projects as $project) { + if ($project['type'] == 'module') { + $dependency_check = TRUE; + $dependencies = array(); + if (isset($modules[$project['name']])) { + foreach ($modules[$project['name']]->info['dependencies'] as $dependency) { + if (isset($modules[$dependency])) { + $dependencies[] = $modules[$dependency]->info['name'] . ' (' . t('Installed') . ')'; + } + else { + $dependency_check = FALSE; + $dependencies[] = $dependency . ' (' . t('Missing') . ')'; + } + } + if ($dependency_check) { + $options[$project['name']] = array( + array('data' => $modules[$project['name']]->info['name']), + array('data' => $modules[$project['name']]->info['version']), + array('data' => implode(', ', $dependencies)), + ); + } + else { + $missing[$project['name']] = array( + array('data' => $modules[$project['name']]->info['name']), + array('data' => $modules[$project['name']]->info['version']), + array('data' => implode(', ', $dependencies)), + ); + } + } + else { + drupal_set_message(t('There was an error getting information for @module', + array('@module' => $project['name'])), 'error'); + } + } + } + + $headers = array( + array('data' => t('Title')), + array('data' => t('Version')), + array('data' => t('Dependencies')), + ); + + if (!empty($options)) { + $form['modules'] = array( + '#type' => 'tableselect', + '#title' => t('Enable modules'), + '#description' => t('Select which modules you would like to enable.'), + '#header' => $headers, + '#options' => $options, + '#empty' => t('No new modules installed.'), + '#multiple' => TRUE, + '#js_select' => TRUE, + '#weight' => 1, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#submit' => array('project_browser_installation_enable_form_submit'), + '#value' => t('Enable modules'), + '#weight' => 99, + ); + } + + if (!empty($missing)) { + $form['missing'] = array( + '#type' => 'item', + '#title' => t('Missing Dependencies'), + '#description' => t('These modules are missing one or more dependencies, + and so cannot be enabled.'), + '#markup' => theme('table', array('header' => $headers, 'rows' => $missing)), + '#weight' => 2, + ); + } + + return $form; +} + +/** + * Form submit handler + */ +function project_browser_installation_enable_form_submit($form, &$form_state) { + $enable_queue = array_filter($form_state['values']['modules']); + // Enable these all at once so that dependencies are handled properly + module_enable($enable_queue); + + /* + // Enable the modules one by one + foreach ($enable_queue as $project_name) { + if (!module_enable(array($project_name))) { + drupal_set_message(t('Error installing a module: @module', array('@module' => $project_name)), 'error'); + } + } + */ + drupal_flush_all_caches(); + + drupal_goto('admin/modules'); +} + +/** + * Get a task list to the sidebar area when installing projects + * + * This will need to be called from every page of the install process + * + * @param $active + * (Optional) Set the active task by key + */ +function project_browser_installation_task_list($active = NULL) { + // Default list of tasks. + $tasks = array( + 'select_versions' => t('Select versions'), + 'install_dependencies' => t('Install Dependencies'), + 'enable' => t('Enable projects'), + ); + + require_once DRUPAL_ROOT . '/core/includes/theme.maintenance.inc'; + + return theme_task_list(array('items' => $tasks, 'active' => $active)); +} diff --git a/core/modules/project_browser/project_browser.test b/core/modules/project_browser/project_browser.test new file mode 100644 index 0000000..71d1b13 --- /dev/null +++ b/core/modules/project_browser/project_browser.test @@ -0,0 +1,75 @@ + t('Project Browser Install Project Test'), + 'description' => t('Attempts to install a project.'), + 'group' => t('Project Browser'), + ); + } + + public function setUp() { + parent::setUp('project_browser', 'project_browser_test'); // Enable any modules required for the test + + // Set the default server variable + $server_url = url('project_browser_test/query', array('absolute' => TRUE)); + variable_set('project_browser_default_server', array( + $server_url => array( + 'name' => 'Test Server', + 'method' => 'json', + ), + )); + + // Create and log in our privileged user. + $this->privileged_user = $this->drupalCreateUser(array( + 'use project browser', + )); + $this->drupalLogin($this->privileged_user); + } + + public function testProjectBrowserSearchViews() { + // Create node to edit. + $edit = array(); + $edit['search_text'] = 'views'; + $this->drupalPost('admin/modules/project-browser/modules', $edit, t('Filter')); + $this->assertText('Showing 1 to'); + } + + public function testProjectBrowserGetProjects() { + // Attempt to fetch the default projects + $edit = array(); + $edit['search_text'] = ''; + $this->drupalPost('admin/modules/project-browser/modules', $edit, t('Filter')); + $this->assertText('Showing 1 to'); + } + + public function testProjectBrowserProjectEnabled() { + // Make sure project enabled detection works + module_load_include('inc', 'project_browser', 'project_browser'); + $this->assertTrue(_project_browser_is_project_enabled('module', 'project_browser'), t('Make sure project enabled detection works.')); + } + + public function testProjectBrowserAddRemoveQueue() { + // Refresh the page + $this->drupalGet('admin/modules/project-browser/modules'); + + // Simulate adding a project to the install queue + $this->drupalGet('project-browser/nojs/install-queue/add/views', array('query' => array('destination' => 'admin/modules/project-browser'))); + $this->assertNoText('Install queue is empty.'); + $this->assertNoText('Error: The project was not found.'); + + // Simulate removing a project from the install queue + $this->drupalGet('project-browser/nojs/install-queue/remove/views', array('query' => array('destination' => 'admin/modules/project-browser'))); + $this->assertText('Install queue is empty.'); + $this->assertNoText('Error: The project was not found.'); + } +} +?> \ No newline at end of file diff --git a/core/modules/project_browser/tests/project_browser_test.info b/core/modules/project_browser/tests/project_browser_test.info new file mode 100644 index 0000000..6db51d2 --- /dev/null +++ b/core/modules/project_browser/tests/project_browser_test.info @@ -0,0 +1,12 @@ +name = "Project Browser module tests" +description = "Support module for Project Browser related testing." +package = Testing +core = 7.x +hidden = TRUE + +; Information added by drupal.org packaging script on 2012-01-20 +version = "7.x-1.x-dev" +core = "7.x" +project = "project_browser" +datestamp = "1327063068" + diff --git a/core/modules/project_browser/tests/project_browser_test.module b/core/modules/project_browser/tests/project_browser_test.module new file mode 100644 index 0000000..6f9126a --- /dev/null +++ b/core/modules/project_browser/tests/project_browser_test.module @@ -0,0 +1,329 @@ + 'Test Query page', + 'description' => "Tests the ability to fetch and display projects, and filter them appropriately.", + 'page callback' => 'project_browser_test_query', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Page callback. Generates json based on the input filters + */ +function project_browser_test_query() { + if (!isset($_GET['method'])) { + print drupal_json_encode(t('You must specify a method.')); + exit(); + } + switch ($_GET['method']) { + case 'categories': + if (!isset($_GET['type'])) { + print drupal_json_encode(t('You must specify a project type.')); + exit(); + } + + $categories['project_browser_test'] = project_browser_test_get_categories($_GET['type']); + + print drupal_json_encode($categories); + exit(); + + case 'query': + // Check that we have valid data + if (!isset($_GET['drupal_version'])) { + print drupal_json_encode(t('You must specify a drupal version.')); + exit(); + } + if (!isset($_GET['type'])) { + print drupal_json_encode(t('You must specify a project type.')); + exit(); + } + + // Get the filters + $filters = array( + 'drupal_version' => $_GET['drupal_version'], + 'type' => $_GET['type'], + 'text' => (isset($_GET['text']) AND $_GET['text']) ? $_GET['text'] : '', + 'sort_method' => isset($_GET['sort_method']) ? $_GET['sort_method'] : 'usage', + 'sort_direction' => isset($_GET['sort_direction']) ? $_GET['sort_direction'] : 'desc', + 'requested' => isset($_GET['requested']) ? (int) $_GET['requested'] : 12, + 'page' => isset($_GET['page']) ? (int) $_GET['page'] : 0, + ); + + if (isset($_GET['categories'])) { + $categories = unserialize($_GET['categories']); + if (is_array($categories) AND !empty($categories)) { + $filters['categories'] = $categories; + } + } + + // Pass them off to the project_browser_server_get_results() function + $results = project_browser_test_get_results($filters); + + print drupal_json_encode($results); + + exit(); + } +} + +/** + * Returns some static categories + */ +function project_browser_test_get_categories($type) { + $categories = array(); + + switch ($type) { + case 'module': + $categories = array( + 'admin' => "Administrative", + 'search' => "Search", + 'user_management' => "User Management", + ); + break; + + case 'theme': + $categories = array( + 'dark' => "Dark", + 'light' => "Light", + ); + } + + return $categories; +} + +/** + * Returns projects based on the filters + */ +function project_browser_test_get_results($filters) { + $projects = project_browser_test_projects(); + + $results = array( + 'total' => count($projects), + 'projects' => array(), + ); + + // Filter out projects based on type + if (isset($filters['type']) AND $type = $filters['type']) { + foreach ($projects as $name => $project) { + if ($type != $project['type']) { + unset($projects[$name]); + } + } + } + + // Filter out projects based on drupal version number + if (isset($filters['drupal_version']) AND $version = $filters['drupal_version']) { + foreach ($projects as $name => $project) { + if ($version != $project['drupal version']) { + unset($projects[$name]); + } + } + } + + // Filter out projects based on categories number + if (isset($filters['categories']) AND is_array($filters['categories']) AND !empty($filters['categories'])) { + $filtered = array(); + foreach ($projects as $name => $project) { + foreach ($project['categories'] as $category) { + if (in_array($category, $filters['categories'])) { + $filtered[$name] = $project; + } + } + } + $projects = $filtered; + } + + // Filter out projects based on the text query + if (isset($filters['text']) AND $text = $filters['text']) { + foreach ($projects as $name => $project) { + if (!stristr($project['title'], $text) AND !stristr($project['description'], $text)) { + unset($projects[$name]); + } + } + } + + $results['total'] = count($projects); + + // Only send back the requested amount + $start = $filters['page'] * $filters['requested']; + $end = $start + $filters['requested']; + + $results['projects'] = $projects; + + return $results; +} + +/** + * Returns some static projects + */ +function project_browser_test_projects() { + $projects = array(); + + $projects['views'] = array( + 'type' => 'module', + 'title' => 'Views', + 'name' => 'views', + 'drupal version' => 7, + 'author' => 'merlinofchaos', + 'description' => "The Views module provides a flexible method for Drupal site + designers to control how lists and tables of content (nodes in Views 1, almost + anything in Views 2) are presented. Traditionally, Drupal has hard-coded most of + this, particularly in how taxonomy and tracker lists are formatted. ", + 'drupal_versions' => array(6, 7), + 'categories' => array('admin', 'search'), + 'image' => 'http://learnbythedrop.com/system/files/images/View-Edit_0.png', + 'usage' => '542312', + 'project url' => 'http://www.drupal.org/projects/views', + 'project status url' => 'http://updates.drupal.org/release-history/views/7.x', + 'last updated' => '12342523', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '9.6', + 'dependencies' => array( + 'ctools', + ), + ); + + $projects['ctools_test'] = array( + 'type' => 'module', + 'title' => 'CTools Test', + 'name' => 'ctools_test', + 'drupal version' => 7, + 'author' => 'merlinofchaos', + 'description' => "This suite is primarily a set of APIs and tools to improve + the developer experience. It also contains a module called the Page Manager + whose job is to manage pages. In particular it manages panel pages, but as + it grows it will be able to manage far more than just Panels.", + 'drupal_versions' => array(6, 7), + 'categories' => array(), + 'image' => '', + 'usage' => '4312', + 'project url' => 'http://www.drupal.org/projects/ctools', + 'project status url' => 'http://updates.drupal.org/release-history/ctools/7.x', + 'last updated' => '12354634', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.6', + 'dependencies' => array(), + ); + + $projects['ctools'] = array( + 'type' => 'module', + 'title' => 'Chaos Tool Suite', + 'name' => 'ctools', + 'drupal version' => 7, + 'author' => 'merlinofchaos', + 'description' => "This suite is primarily a set of APIs and tools to improve + the developer experience. It also contains a module called the Page Manager + whose job is to manage pages. In particular it manages panel pages, but as + it grows it will be able to manage far more than just Panels.", + 'drupal_versions' => array(6, 7), + 'categories' => array(), + 'image' => '', + 'usage' => '4312', + 'project url' => 'http://www.drupal.org/projects/ctools', + 'project status url' => 'http://updates.drupal.org/release-history/ctools/7.x', + 'last updated' => '12354634', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.6', + 'dependencies' => array(), + ); + + $projects['token'] = array( + 'type' => 'module', + 'title' => 'Token', + 'name' => 'token', + 'drupal version' => 7, + 'author' => 'eaton', + 'description' => "Tokens are small bits of text that can be placed into larger + documents via simple placeholders, like %site-name or [user]. The Token module + provides a central API for modules to use these tokens, and expose their own token values.", + 'categories' => array('admin'), + 'image' => 'http://drupal.org/files/images/token_08.thumbnail.png', + 'usage' => '4563', + 'project url' => 'http://www.drupal.org/projects/token', + 'project status url' => 'http://updates.drupal.org/release-history/token/7.x', + 'last updated' => '12357351', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '8.1', + 'dependencies' => array(), + ); + + $projects['zen'] = array( + 'type' => 'theme', + 'title' => 'Zen', + 'name' => 'zen', + 'drupal version' => 7, + 'author' => 'johnAlbin', + 'description' => "Zen is the ultimate starting theme for Drupal. If you are + building your own standards-compliant theme, you will find it much easier to + start with Zen than to start with Garland or Bluemarine. This theme has fantastic + online documentation and tons of code comments for both the PHP (template.php) + and HTML (page.tpl.php, node.tpl.php).", + 'categories' => array('light', 'dark'), + 'image' => 'http://drupal.org/files/images/zen-logo.thumbnail.png', + 'usage' => '4563', + 'project url' => 'http://www.drupal.org/project/zen', + 'project status url' => 'http://updates.drupal.org/release-history/zen/7.x', + 'last updated' => '12343634', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.1', + 'dependencies' => array(), + ); + + $projects['acquia_marina'] = array( + 'type' => 'theme', + 'title' => 'Acquia Marina', + 'name' => 'acquia_marina', + 'drupal version' => 7, + 'author' => 'stephthegeek', + 'description' => "The Fusion base theme and Skinr are required. Skinr for Drupal 7 + (dev release) is usable now but it is recommended that you proceed with caution + and do some of your own testing.", + 'categories' => array('light'), + 'image' => 'http://drupal.org/files/images/acquia_marina.thumbnail.png', + 'usage' => '14563', + 'project url' => 'http://www.drupal.org/project/acquia_marina', + 'project status url' => 'http://updates.drupal.org/release-history/acquia_marina/7.x', + 'last updated' => '12346574', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '7.8', + 'dependencies' => array( + 'fusion' + ), + ); + + $projects['fusion'] = array( + 'type' => 'theme', + 'title' => 'Fusion', + 'name' => 'fusion', + 'drupal version' => 7, + 'author' => 'stephthegeek', + 'description' => "Fusion is a powerful base theme, with layout and style configuration + options built in that you can control through Drupal's UI. It's based on a simplified + 960px or fluid 12/16-column grid. It's designed to be used with the Skinr module, + with numerous useful block styles included.", + 'categories' => array('light'), + 'image' => 'http://drupal.org/files/images/fusion-powering-small-banner.thumbnail.png', + 'usage' => '14563', + 'project url' => 'http://www.drupal.org/project/fusion', + 'project status url' => 'http://updates.drupal.org/release-history/fusion/7.x', + 'last updated' => '12342643', + 'maintenance status' => 'Actively maintained', + 'development status' => 'Under active development', + 'rating' => '', + 'dependencies' => array(), + ); + + return $projects; +} diff --git a/core/modules/project_browser/theme/project-browser-block.tpl.php b/core/modules/project_browser/theme/project-browser-block.tpl.php new file mode 100644 index 0000000..dc11389 --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-block.tpl.php @@ -0,0 +1,21 @@ + +
      + +

      + +
      + +
      +
      diff --git a/core/modules/project_browser/theme/project-browser-install-queue.tpl.php b/core/modules/project_browser/theme/project-browser-install-queue.tpl.php new file mode 100644 index 0000000..81e018c --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-install-queue.tpl.php @@ -0,0 +1,16 @@ + +
      + +
      diff --git a/core/modules/project_browser/theme/project-browser-install.tpl.php b/core/modules/project_browser/theme/project-browser-install.tpl.php new file mode 100644 index 0000000..fb7771b --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-install.tpl.php @@ -0,0 +1,21 @@ + + +
      + +
      +
      + +
      diff --git a/core/modules/project_browser/theme/project-browser-list.tpl.php b/core/modules/project_browser/theme/project-browser-list.tpl.php new file mode 100644 index 0000000..6468dbd --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-list.tpl.php @@ -0,0 +1,26 @@ + +
      +
      + +
      + +
      + + +
      + +
      diff --git a/core/modules/project_browser/theme/project-browser-project.tpl.php b/core/modules/project_browser/theme/project-browser-project.tpl.php new file mode 100644 index 0000000..ee77131 --- /dev/null +++ b/core/modules/project_browser/theme/project-browser-project.tpl.php @@ -0,0 +1,59 @@ + +
      + +
      + +
      + + +
      +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      +
      + +
      + +
      + +
      + + +
      +
      +
      diff --git a/core/modules/project_browser/theme/project_browser.admin.inc b/core/modules/project_browser/theme/project_browser.admin.inc new file mode 100644 index 0000000..831b050 --- /dev/null +++ b/core/modules/project_browser/theme/project_browser.admin.inc @@ -0,0 +1,32 @@ + 'fieldset', + '#title' => t('Main settings'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + $form['main']['project_browser_servers'] = array( + '#type' => 'textarea', + '#title' => t('Repositories'), + '#default_value' => variable_get('project_browser_servers', ''), + '#description' => t("Add new repositories to use for the Project Browser, one per line, in + the 'url|method|Site Name' format. Drupal.org is added by default, and doesn't need to be + set here."), + '#required' => FALSE, + ); + + return system_settings_form($form); +} -- 1.7.3.1.msysgit.0 From 58bb64a34dd801659f6fc86d87f7f52860cad6c3 Mon Sep 17 00:00:00 2001 From: Leighton Whiting Date: Fri, 16 Nov 2012 15:54:23 -0700 Subject: [PATCH 2/2] Fixed up a bunch of things --- .../js/project_browser_categories_widget.js | 11 - .../js/project_browser_more_link.js | 8 +- .../js/project_browser_multiselect.js | 1 + core/modules/project_browser/js/select_releases.js | 4 + .../project_browser/project_browser.admin.inc | 6 +- core/modules/project_browser/project_browser.inc | 205 +++++++++++++++----- .../modules/project_browser/project_browser.module | 62 ++++-- .../project_browser/project_browser.pages.inc | 140 ++++++++------ core/modules/project_browser/project_browser.test | 5 +- .../tests/project_browser_test.info | 11 +- .../theme/project-browser-project.tpl.php | 2 +- 11 files changed, 300 insertions(+), 155 deletions(-) delete mode 100644 core/modules/project_browser/js/project_browser_categories_widget.js diff --git a/core/modules/project_browser/js/project_browser_categories_widget.js b/core/modules/project_browser/js/project_browser_categories_widget.js deleted file mode 100644 index c28f738..0000000 --- a/core/modules/project_browser/js/project_browser_categories_widget.js +++ /dev/null @@ -1,11 +0,0 @@ -(function ($) { - $('#edit-categories').multiselect({ - noneSelectedText: '" . t('Choose') . "...', - selectedList: 99, - minWidth: 500, - position: { - my: 'right top', - at: 'right bottom' - } - }); -})(jQuery); \ No newline at end of file diff --git a/core/modules/project_browser/js/project_browser_more_link.js b/core/modules/project_browser/js/project_browser_more_link.js index 07dfc04..df42ba9 100644 --- a/core/modules/project_browser/js/project_browser_more_link.js +++ b/core/modules/project_browser/js/project_browser_more_link.js @@ -1,11 +1,15 @@ (function ($) { $(document).ready(function() { + /** + * The project descriptions are by default trimmed to a certain height. When the user + * clicks the more link, then the full text is shown. + */ // The height of the content block when it's not expanded var adjustheight = 80; // The "more" link text - var moreText = "More"; + var moreText = Drupal.t('More'); // The "less" link text - var lessText = "Less"; + var lessText = Drupal.t('Less'); $(".project-information .project-description").each(function(index) { if ($(this).height() > adjustheight) diff --git a/core/modules/project_browser/js/project_browser_multiselect.js b/core/modules/project_browser/js/project_browser_multiselect.js index bf1f49e..3978d14 100644 --- a/core/modules/project_browser/js/project_browser_multiselect.js +++ b/core/modules/project_browser/js/project_browser_multiselect.js @@ -1,5 +1,6 @@ (function ($) { $(document).ready(function() { + // This adds the widget to the multi-select element used for Categories $('#edit-categories').multiselect({ noneSelectedText: Drupal.t("Choose"), selectedList: 99, diff --git a/core/modules/project_browser/js/select_releases.js b/core/modules/project_browser/js/select_releases.js index 97cc780..a2bdc15 100644 --- a/core/modules/project_browser/js/select_releases.js +++ b/core/modules/project_browser/js/select_releases.js @@ -1,5 +1,9 @@ (function ($) { $(document).ready(function() { + /** + * This makes the Select Releases page show the default one and have the rest + * remain hidden until the 'Show All Releases' link is clicked + */ $('.project-browser-releases-wrapper').hide(); $('.project-browser-selected-release').show(); diff --git a/core/modules/project_browser/project_browser.admin.inc b/core/modules/project_browser/project_browser.admin.inc index 831b050..b3e0d96 100644 --- a/core/modules/project_browser/project_browser.admin.inc +++ b/core/modules/project_browser/project_browser.admin.inc @@ -9,7 +9,10 @@ // ====================================== /** - * Admin Settings Form + * Builds the Admin Settings Form + * + * @return + * The $form array for the admin settings */ function project_browser_admin() { $form['main'] = array( @@ -18,6 +21,7 @@ function project_browser_admin() { '#collapsible' => FALSE, '#collapsed' => FALSE, ); + // Because this is a pluggable system, there can be other repositories besided Drupal.org $form['main']['project_browser_servers'] = array( '#type' => 'textarea', '#title' => t('Repositories'), diff --git a/core/modules/project_browser/project_browser.inc b/core/modules/project_browser/project_browser.inc index df7967a..73c6efb 100644 --- a/core/modules/project_browser/project_browser.inc +++ b/core/modules/project_browser/project_browser.inc @@ -4,11 +4,15 @@ * Various functions that are required by project_browser */ +// Include the classes from the 'update' module use Drupal\Core\Updater\Updater; use Drupal\Core\FileTransfer\Local; /** - * Returns the themed install list form + * Gets the themed install list form + * + * @return + * HTML for the install queue block */ function project_browser_get_install_list() { $queued_projects = project_browser_get_queued_projects(); @@ -18,7 +22,13 @@ function project_browser_get_install_list() { } /** - * Shows an install button for the Install Queue block + * Builds the install button for the Install Queue block + * + * Since the selected projects are stored in the $_SESSION variable, + * no real processing is done, we just redirect to the install/select_versions page + * + * @return + * The $form array */ function project_browser_install_button_form($form, &$form_state) { $form['#attributes']['id'] = 'project-browser-install-button-form'; @@ -26,6 +36,7 @@ function project_browser_install_button_form($form, &$form_state) { '#type' => 'submit', '#value' => 'Install', ); + $form['#action'] = url('admin/modules/project-browser/install/select_versions'); return $form; @@ -33,6 +44,13 @@ function project_browser_install_button_form($form, &$form_state) { /** * Builds the filters form + * + * This includes categories and the string search box, and the $type is stored + * + * @param $type + * The type of project (module or theme) + * @return + * The $form array */ function project_browser_filters_form($form, &$form_state, $type) { $form['search_text'] = array( @@ -69,9 +87,21 @@ function project_browser_filters_form($form, &$form_state, $type) { } /** - * Returns a themed sort widget for the filters + * Builds a themed sort widget for the results + * + * These are links which can be clicked/toggled to select and change direction + * + * @param $sort_options + * An array of sort options + * @param $current_sort_option + * The currently selected sort option + * @param $current_sort_direction + * The currently selected sort direction + * + * @return + * A themed list of sort options */ -function project_browser_get_sort_widget($sort_options, $current_order_by, $current_sort) { +function project_browser_get_sort_widget($sort_options, $current_sort_option, $current_sort_direction) { $sort_list = array(); $sort_list[] = array('data' => t('Sort by:'), 'class' => array('sort-header')); $current_path = drupal_get_path_alias(current_path()); @@ -84,12 +114,12 @@ function project_browser_get_sort_widget($sort_options, $current_order_by, $curr ); // If the sort option is currently active, handle it differently - if ($current_order_by == $sort_option['method']) { + if ($current_sort_option == $sort_option['method']) { $classes[] = 'sort-active'; - $classes[] = 'sort-' . $current_sort; + $classes[] = 'sort-' . $current_sort_direction; // Set the direction of the sort link to the opposite of what it currently is - if ($current_sort == $query['sort']) { + if ($current_sort_direction == $query['sort']) { if ($query['sort'] == 'desc') { $query['sort'] = 'asc'; } @@ -115,7 +145,17 @@ function project_browser_get_sort_widget($sort_options, $current_order_by, $curr } /** - * Returns a themed sort widget for the filters + * Builds a themed widget to select the server + * + * This is only called if there are more than one server enabled in the settings. + * + * @param $server + * An array of servers that should be available as options + * @param $current_server + * The currently selected server + * + * @return + * A themed server select widget */ function project_browser_get_server_widget($servers, $current_server) { $list = array(); @@ -154,14 +194,17 @@ function project_browser_get_server_widget($servers, $current_server) { /** - * Returns a list of sort options + * Builds and returns an array of sort options, keyed by method * * @param $full - * Set this to TRUE if you want to get all of the supported sort methods + * (Optional) Set this to TRUE if you want to get all of the supported sort methods. Defaults to FALSE + * + * @return + * An array of sort options, keyed by method */ function project_browser_get_sort_options($full = FALSE) { $sort_options = array( - 'score' => array('method' => 'score', 'name' => t('Relevancy'), 'default_sort' => 'desc'), + 'score' => array('method' => 'score', 'name' => t('Relevancy'), 'default_sort' => 'desc'), 'usage' => array('method' => 'usage', 'name' => t('Most installed'), 'default_sort' => 'desc'), 'title' => array('method' => 'title', 'name' => t('Title'), 'default_sort' => 'asc'), 'name' => array('method' => 'name', 'name' => t('Author'), 'default_sort' => 'asc'), @@ -169,7 +212,7 @@ function project_browser_get_sort_options($full = FALSE) { ); if ($full) { - $sort_options['type'] = array('method' => 'type', 'name' => t('Type'), 'default_sort' => 'asc'); + $sort_options['type'] = array('method' => 'type', 'name' => t('Type'), 'default_sort' => 'asc'); $sort_options['created'] = array('method' => 'created', 'name' => t('Date created'), 'default_sort' => 'asc'); $sort_options['latest_activity'] = array('method' => 'latest_activity', 'name' => t('Latest build'), 'default_sort' => 'desc'); } @@ -179,6 +222,9 @@ function project_browser_get_sort_options($full = FALSE) { /** * Handles the filters form submit + * + * All that we do here is store the selected categories and search string + * in the $_SESSION variable. */ function project_browser_filters_form_submit($form, &$form_state) { $type = $form_state['values']['project_type']; @@ -215,11 +261,11 @@ function project_browser_get_categories($type) { if (is_array($categories_raw) AND !empty($categories_raw)) { foreach ($categories_raw as $url => $cats) { - foreach ($cats as $key => $value) { - // Create a new key so that there are no duplicate categories from different sites - $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value)); - $categories[$new_key] = $value; - } + foreach ($cats as $key => $value) { + // Create a new key so that there are no duplicate categories from different sites + $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value)); + $categories[$new_key] = $value; + } } } @@ -238,6 +284,9 @@ function project_browser_get_categories($type) { * An array of categories from $form_state['values'] * @param $type * The type of project to prepare the categories for, eg 'module' or 'theme' + * + * @return + * An array of server categories, keyed by server url */ function project_browser_prepare_categories($raw_cats, $type) { $categories = project_browser_fetch_categories($type); @@ -289,6 +338,9 @@ function _project_browser_is_project_enabled($type, $name) { /** * Gets the currently listed projects from the session + * + * @return + * An array of listed projects from the $_SESSION variable */ function project_browser_get_listed_projects() { if (isset($_SESSION['project_browser_listed_projects'])) { @@ -299,7 +351,14 @@ function project_browser_get_listed_projects() { } /** - * Gets the currently queued projects from the session + * Gets the currently queued projects from the $_SESSION variable + * + * @param $type + * (Optional) The type of project (module or theme). Defaults to NULL, which + * will return projects of all types + * + * @return + * An array of projects that are queued for install */ function project_browser_get_queued_projects($type = NULL) { $projects = array(); @@ -322,6 +381,14 @@ function project_browser_get_queued_projects($type = NULL) { /** * Gets a release from a project and a release_name + * + * @param $release_name + * The name of the release, such as '7.x-1.2' + * @param $project + * The $project data array + * + * @return + * The release data array or FALSE if the release doesn't exist */ function project_browser_get_release($release_name, $project) { $release_data = project_browser_get_project_release_data($project); @@ -331,6 +398,9 @@ function project_browser_get_release($release_name, $project) { /** * Gets the newly installed projects from the session + * + * @return + * An array of all of the newly installed projects */ function project_browser_get_installed_projects() { $projects = array(); @@ -347,23 +417,32 @@ function project_browser_get_installed_projects() { } /** - * Adds a project to the install queue + * Adds a project to the install queue $_SESSION variable + * + * @param $project + * An array of $project data for a single project */ function project_browser_install_queue_add($project) { $_SESSION['project_browser_install_list'][$project['name']] = $project; } /** - * Removes a project from the install queue + * Removes a project from the install queue $_SESSION variable + * + * @param $project_name + * The name of the project to remove, such as 'views' */ function project_browser_install_queue_remove($project_name) { - if (isset($_SESSION['project_browser_install_list'][$project_name])) { - unset($_SESSION['project_browser_install_list'][$project_name]); - } + if (isset($_SESSION['project_browser_install_list'][$project_name])) { + unset($_SESSION['project_browser_install_list'][$project_name]); + } } /** - * Gets the currently queued releases from the session + * Gets the currently queued releases from the $_SESSION variable + * + * @return + * An array of the currently selected releases */ function project_browser_get_queued_releases() { $releases = array(); @@ -480,8 +559,8 @@ function project_browser_fetch_results($filters) { if (isset($local_filters['categories'])) { $local_filters['categories'] = serialize($local_filters['categories']); } - - $query_url = $url . '/query/' . $local_filters['type'] . '/8?' . http_build_query($local_filters, FALSE, '&'); + + $query_url = $url . '/query/' . $local_filters['type'] . '/8?' . http_build_query($local_filters, FALSE, '&'); $response = drupal_http_request($query_url); if ($response->code == '200') { $results_raw = drupal_json_decode($response->data); @@ -515,7 +594,7 @@ function project_browser_fetch_results($filters) { * @param $type * The type of project we are getting categories for * @param $use_server - * (Optional) The server to use. Defaults to 'all'. + * (Optional) The server to use. Defaults to 'all' * * @return * Returns an array of the categories @@ -579,6 +658,9 @@ function project_browser_fetch_categories($type, $use_server = 'all') { /** * Gets the servers to use for fetching results * + * @param $use_server + * (Optional) The server to use. Defaults to 'all' + * * @return * Returns an associative array of servers, populated from the project_browser_servers variable, * in 'url => name' format @@ -636,12 +718,12 @@ function project_browser_get_servers($use_server = 'all') { * The project to get the releases for * * @return - * An array of releases for this project + * An array of releases for this project, or FALSE if it can't be found */ function project_browser_get_project_release_data($project) { $releases = array(); $project['project_type'] = $project['type']; - $project['includes'] = array(); + $project['includes'] = array(); // Build the releases cache for this project module_load_include('inc', 'update', 'update.fetch'); @@ -669,6 +751,13 @@ function project_browser_get_project_release_data($project) { * * @todo - Ideally, this should be in the update module as a standalone function, * to reduce coupling and duplication + * + * @param $url + * The url of the release download + * + * @return + * An array indicating whether or not this was successful, and an error message + * if applicable */ function project_browser_download_project($url) { module_load_include('inc', 'update', 'update.manager'); @@ -714,8 +803,7 @@ function project_browser_download_project($url) { 'message' => array_shift($archive_errors), ); } - //require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/Updater/Updater.php'; - + // Make sure the Updater registry is loaded. drupal_get_updaters(); @@ -787,6 +875,13 @@ function project_browser_download_project($url) { /** * Installs a single release of a project during batch, for example + * + * @param $release_name + * The name of the release, such as '7.x-1.2' + * @param $project + * The project data array + * @param &$context + * The context of the batch so that the results can be reported */ function _project_browser_batch_install_release($release_name, $project, &$context) { module_load_include('inc', 'project_browser', 'project_browser.pages'); @@ -814,7 +909,19 @@ function _project_browser_batch_install_release($release_name, $project, &$conte } /** - * Shows a message and finish up the batch + * Shows a message and finishes up the batch + * + * If there were any errors, they are reported here with drupal_set_message(). The + * user is then redirected to the select versions page if there were errors, the + * install dependencies page if there were any detected missing dependencies, or the + * enable modules page if there were no errors. + * + * @param $success + * Whether or not the whole operation was successful + * @param $results + * An array of messages about any failures + * @param $operations + * An array of operations that need to be performed */ function _project_browser_batch_install_releases_finished($success, $results, $operations) { drupal_get_messages(); @@ -835,25 +942,31 @@ function _project_browser_batch_install_releases_finished($success, $results, $o drupal_set_message(t('Error installing projects.'), 'error'); drupal_goto('admin/modules/project-browser/install/select_versions'); } - - $projects = project_browser_get_installed_projects(); - $missing = project_browser_get_missing_dependencies($projects); - // If there are missing dependencies, go to install dependencies - if (count($missing) > 0) { - drupal_goto('admin/modules/project-browser/install/install_dependencies'); - } - else { - drupal_goto('admin/modules/project-browser/install/enable'); + + $projects = project_browser_get_installed_projects(); + $missing = project_browser_get_missing_dependencies($projects); + // If there are missing dependencies, go to install dependencies + if (count($missing) > 0) { + drupal_goto('admin/modules/project-browser/install/install_dependencies'); + } + else { + drupal_goto('admin/modules/project-browser/install/enable'); } } /** * Gets the dependencies for installed projects + * + * @param $projects + * An array of projects to get the missing dependencies for + * + * @return + * An array of missing dependencies, if any were detected */ function project_browser_get_missing_dependencies($projects) { - $modules = system_rebuild_module_data(); - - $missing = array(); + $modules = system_rebuild_module_data(); + + $missing = array(); foreach ($projects as $project) { if ($project['type'] == 'module') { @@ -875,6 +988,6 @@ function project_browser_get_missing_dependencies($projects) { } } } - - return $missing; + + return $missing; } diff --git a/core/modules/project_browser/project_browser.module b/core/modules/project_browser/project_browser.module index 3744bfb..5319f6b 100644 --- a/core/modules/project_browser/project_browser.module +++ b/core/modules/project_browser/project_browser.module @@ -2,17 +2,15 @@ /** * @file * Project Browser module. - * Authored by Leighton Whiting for Google Summer of Code 2011 * * This module provides a new UI for admins to easily browse modules and themes from their * admin pages, and install them. - * - * TODO - Add a context to pages showing the filters currently used - * TODO - Create a test for the get releases HTML page */ /** - * Display help and module information + * Implements hook_help(). + * + * Displays help and module information */ function project_browser_help($path, $arg) { $output = ''; @@ -26,7 +24,7 @@ function project_browser_help($path, $arg) { } /** - * Implements of hook_perm(). + * Implements hook_perm(). */ function project_browser_permission() { return array( @@ -101,28 +99,30 @@ function project_browser_menu() { /** * Page callback that allows for adding to and removing from the install queue + * + * This is invoked via AJAX most of the time */ function project_browser_install_queue_callback($method, $op, $project_name) { module_load_include('inc', 'project_browser', 'project_browser'); - - switch ($op) { + + switch ($op) { case 'add': - $projects = project_browser_get_listed_projects(); + $projects = project_browser_get_listed_projects(); - if (isset($projects[$project_name])) { - $project = $projects[$project_name]; - project_browser_install_queue_add($project); - } - else { - drupal_set_message(t('Error: The project was not found.'), 'error'); - } + if (isset($projects[$project_name])) { + $project = $projects[$project_name]; + project_browser_install_queue_add($project); + } + else { + drupal_set_message(t('Error: The project was not found.'), 'error'); + } break; - - case 'remove': + + case 'remove': project_browser_install_queue_remove($project_name); break; } - + switch ($method) { case 'nojs': // Redirect to the page it came from @@ -240,7 +240,7 @@ function project_browser_theme($existing, $type, $theme, $path) { } /** - * Add some variables for the projects install theme + * Adds some variables for the projects install theme * * @param $variables * An associative array containing: @@ -253,7 +253,11 @@ function project_browser_preprocess_project_browser_install(&$variables) { } /** - * Add some variables for the projects install queue theme + * Adds some variables for the projects install queue theme + * + * @param $variables + * An associative array containing: + * - projects : an array of projects in the install queue */ function project_browser_preprocess_project_browser_install_queue(&$variables) { $build = array(); @@ -281,6 +285,9 @@ function project_browser_preprocess_project_browser_install_queue(&$variables) { * Add some variables for the project browser block theme * * @param $variables + * An associative array containing: + * - element['#title'] : the title of the block + * - element['#content'] : the content of the block */ function project_browser_preprocess_project_browser_block(&$variables) { // Add the title and content variables @@ -367,7 +374,7 @@ function project_browser_preprocess_project_browser_project(&$variables) { $project = $variables['project']; $variables['title'] = l($project['title'], check_url($project['project url']), - array('attributes' => array('target' => '_blank'), 'html' => TRUE)); + array('attributes' => array('target' => '_blank'), 'html' => TRUE)); $variables['author'] = t('Author: @author', array('@author' => $project['author'])); $variables['description'] = _filter_htmlcorrector(filter_xss($project['description'])); $variables['image'] = $project['image']; @@ -408,6 +415,17 @@ function project_browser_preprocess_project_browser_project(&$variables) { /** * Builds the add/remove project to install queue link + * + * @param $project_name + * The short name of the project, such as 'views' + * @param $title + * (Optional) The title of the project. Defaults to NULL + * @param $id_prefix + * (Optional) The prefix that should be prepended to the id, to ensure unique id names. Defaults + * to 'add-to-queue-link' + * + * @return + * A themed link to remove or add an item from the install queue */ function project_browser_add_remove_queue_link($project_name, $title = NULL, $id_prefix = 'add-to-queue-link') { $queued_projects = project_browser_get_queued_projects(); diff --git a/core/modules/project_browser/project_browser.pages.inc b/core/modules/project_browser/project_browser.pages.inc index d042bf1..e801e64 100644 --- a/core/modules/project_browser/project_browser.pages.inc +++ b/core/modules/project_browser/project_browser.pages.inc @@ -1,9 +1,7 @@ FALSE)); @@ -145,7 +146,7 @@ function project_browser_installation_page($op) { drupal_set_title(t("Select versions")); $content = project_browser_installation_select_versions_page(); break; - case 'install_dependencies': + case 'install_dependencies': drupal_set_title(t("Install Dependencies")); $content = project_browser_installation_install_dependencies_page(); break; @@ -162,12 +163,15 @@ function project_browser_installation_page($op) { * * Shows a form where the user can select which versions to install for each * project + * + * @return + * The form to select the versions of the projects the user wants to install */ function project_browser_installation_select_versions_page() { module_load_include('inc', 'project_browser', 'project_browser'); // Show a form that lets the user select which version of the projects to install $queued_projects = project_browser_get_queued_projects(); - unset($_SESSION['project_browser_installed_projects']); + unset($_SESSION['project_browser_installed_projects']); return drupal_get_form('project_browser_installation_select_versions_form', $queued_projects); } @@ -177,6 +181,9 @@ function project_browser_installation_select_versions_page() { * * @param $projects * An array of projects to get the releases for + * + * @return + * The form array to select the versions of the projects the user wants to install */ function project_browser_installation_select_versions_form($form, &$form_state, $projects) { module_load_include('inc', 'project_browser', 'project_browser'); @@ -199,13 +206,13 @@ function project_browser_installation_select_versions_form($form, &$form_state, foreach ($projects as $project) { // Get the available releases for this project if (!$release_data = project_browser_get_project_release_data($project)) { - drupal_set_message(t('Could not fetch releases for project %project.', - array('%project' => $project['title'])), 'warning'); - watchdog('project_browser', 'Could not fetch releases for project %project.', - array('%project' => $project['title']), WATCHDOG_ERROR); - project_browser_install_queue_remove($project['name']); - continue; - } + drupal_set_message(t('Could not fetch releases for project %project.', + array('%project' => $project['title'])), 'warning'); + watchdog('project_browser', 'Could not fetch releases for project %project.', + array('%project' => $project['title']), WATCHDOG_ERROR); + project_browser_install_queue_remove($project['name']); + continue; + } // We use the update module to calculate the recommended version $project_data = array( @@ -271,12 +278,12 @@ function project_browser_installation_select_versions_form($form, &$form_state, } } - // If there is nothing to install, go to the enable page - if (empty($form['releases'])) { - drupal_set_message(t('No releases data found for any of the selected projects.'), 'warning'); - drupal_goto('admin/modules/project-browser/install/enable'); - } - + // If there is nothing to install, go to the enable page + if (empty($form['releases'])) { + drupal_set_message(t('No releases data found for any of the selected projects.'), 'warning'); + drupal_goto('admin/modules/project-browser/install/enable'); + } + $form['backup_warning'] = array( '#type' => 'markup', '#markup' => t('Back up your database and site before you continue. !link.', @@ -298,6 +305,8 @@ function project_browser_installation_select_versions_form($form, &$form_state, /** * Submit handler for the select versions form + * + * This sets the batch to install the different selected releases one by one */ function project_browser_installation_select_versions_form_submit($form, &$form_state) { module_load_include('inc', 'project_browser', 'project_browser'); @@ -339,55 +348,66 @@ function project_browser_installation_select_versions_form_submit($form, &$form_ /** * Task page for the Install Dependencies installation task * - * Show a form which lets the user select which version of dependencies to install + * This shows a form which lets the user select which version of dependencies + * to install. This is only shown if there are missing dependencies. If there + * are no missing dependencies, then we redirect to the enable page. */ function project_browser_installation_install_dependencies_page() { module_load_include('inc', 'project_browser', 'project_browser'); $projects = project_browser_get_installed_projects(); - $missing = project_browser_get_missing_dependencies($projects); - - if (count($missing) > 0) { - $missing_projects = array(); - // Add the project data in the array as best we can - foreach ($missing as $project_shortname => $dependencies) { - foreach ($dependencies as $shortname) { - $missing_projects[$shortname] = array( - 'name' => $shortname, - 'type' => 'module', // FIXME - 'title' => $shortname, - ); - } - } - - return drupal_get_form('project_browser_installation_select_versions_form', $missing_projects); + $missing = project_browser_get_missing_dependencies($projects); + + if (count($missing) > 0) { + $missing_projects = array(); + // Add the project data in the array as best we can + foreach ($missing as $project_shortname => $dependencies) { + foreach ($dependencies as $shortname) { + $missing_projects[$shortname] = array( + 'name' => $shortname, + 'type' => 'module', // Missing dependencies only works for projects of type 'module' currently + 'title' => $shortname, + ); + } + } + + return drupal_get_form('project_browser_installation_select_versions_form', $missing_projects); + } + else { + drupal_goto('admin/modules/project-browser/install/enable'); } - else { - drupal_goto('admin/modules/project-browser/install/enable'); - } } /** * Task page for the Enable projects installation task * - * Show a form which lets the user enable the newly installed projects + * This shows a form which lets the user enable the newly installed projects. If + * there are unresolved dependencies, then the project is shown with a message + * about why it can't be enabled. This redirects to the project-browser page if + * there were no installed projects. + * + * @return + * The form to enable projects, or redirect to 'admin/modules/project-browser' */ function project_browser_installation_enable_page() { module_load_include('inc', 'project_browser', 'project_browser'); $installed_projects = project_browser_get_installed_projects(); - - if (count($installed_projects) > 0) { - return drupal_get_form('project_browser_installation_enable_form', $installed_projects); - } - else { - drupal_goto('admin/modules/project-browser'); - } + + if (count($installed_projects) > 0) { + return drupal_get_form('project_browser_installation_enable_form', $installed_projects); + } + else { + drupal_goto('admin/modules/project-browser'); + } } /** - * Form builder for the select versions form + * Builds the enable projects form * * @param $projects - * An array of projects to get the releases for + * An array of newly installed projects to enable + * + * @return + * The form array of newly installed projects to enable */ function project_browser_installation_enable_form($form, &$form_state, $projects) { $modules = system_rebuild_module_data(); @@ -479,22 +499,17 @@ function project_browser_installation_enable_form($form, &$form_state, $projects } /** - * Form submit handler + * Enables the selected projects from the enable projects form + * + * After the selected projects are enabled, we flush all caches and then + * redirect to the modules page. */ function project_browser_installation_enable_form_submit($form, &$form_state) { $enable_queue = array_filter($form_state['values']['modules']); // Enable these all at once so that dependencies are handled properly module_enable($enable_queue); - /* - // Enable the modules one by one - foreach ($enable_queue as $project_name) { - if (!module_enable(array($project_name))) { - drupal_set_message(t('Error installing a module: @module', array('@module' => $project_name)), 'error'); - } - } - */ - drupal_flush_all_caches(); + drupal_flush_all_caches(); drupal_goto('admin/modules'); } @@ -505,7 +520,10 @@ function project_browser_installation_enable_form_submit($form, &$form_state) { * This will need to be called from every page of the install process * * @param $active - * (Optional) Set the active task by key + * (Optional) Set the active task by key. Defaults to NULL + * + * @return + * The themed task list for the install projects process */ function project_browser_installation_task_list($active = NULL) { // Default list of tasks. diff --git a/core/modules/project_browser/project_browser.test b/core/modules/project_browser/project_browser.test index 71d1b13..dd7f352 100644 --- a/core/modules/project_browser/project_browser.test +++ b/core/modules/project_browser/project_browser.test @@ -1,7 +1,8 @@ - +
-- 1.7.3.1.msysgit.0