+
diff --git a/sites/all/modules/better_exposed_filters/LICENSE.txt b/sites/all/modules/better_exposed_filters/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/sites/all/modules/better_exposed_filters/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/better_exposed_filters/README.txt b/sites/all/modules/better_exposed_filters/README.txt
new file mode 100644
index 0000000..0fd4c13
--- /dev/null
+++ b/sites/all/modules/better_exposed_filters/README.txt
@@ -0,0 +1,41 @@
+
+-- SUMMARY --
+
+The Better Exposed Filters module replaces the Views' default single-
+or multi-select boxes with radio buttons or checkboxes, respectively.
+
+Views Filters are a powerful tool to limit the results of a given view.
+When you expose a filter, you allow the user to interact with the view
+making it easy to build a customized advanced search. For example,
+exposing a taxonomy filter lets your site visitor search for articles
+with specific tags. Better Exposed Filters gives you greater control
+over the rendering of exposed filters.
+
+For a full description of the module, visit the project page:
+ https://drupal.org/project/better_exposed_filters
+
+To submit bug reports and feature suggestions, or to track changes:
+ https://drupal.org/project/issues/better_exposed_filters
+
+For more information on Views filters, see the Advanced Help documentation
+that comes with Views or visit the online version:
+ https://api.drupal.org/api/views/7
+
+
+-- REQUIREMENTS --
+
+This module requires the Views module:
+ https://drupal.org/project/views
+
+
+-- DOCUMENTATION --
+
+See:
+ https://drupal.org/node/766974
+
+
+-- CONTACT --
+
+The maintainer for this project is Mike Keran, known on drupal.org as mikeker
+(https://drupal.org/user/192273). He can be contacted for work on this module or
+other custom projects.
diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.api.php b/sites/all/modules/better_exposed_filters/better_exposed_filters.api.php
new file mode 100644
index 0000000..9148519
--- /dev/null
+++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.api.php
@@ -0,0 +1,35 @@
+display as $display) {
+ if (!empty($display->display_options['exposed_form']['type']) && 'better_exposed_filters' == $display->display_options['exposed_form']['type']) {
+ $warnings[] = t('The %display_title display in the %view_name view. (Update this display)',
+ array(
+ '%display_title' => $display->display_title,
+ '%view_name' => $view->human_name,
+ '@link' => url('admin/structure/views/view/' . $view->name . '/edit/' . $display->id),
+ )
+ );
+ }
+ }
+ }
+
+ if (!empty($warnings)) {
+ $message = t('The following Views displays are using the Better Exposed Filters plugin, which is no longer enabled. It is recommended that you update these displays (links open in a new window) before removing the code associated with this module. Not doing so may cause unexpected results.');
+ $message .= '
' . join('
', $warnings) . '
';
+ drupal_set_message($message, 'warning');
+ }
+}
+
+/*
+ * Implementations of hook_update_N.
+ *
+ * Comments above each function appear in the update database message.
+ */
+/**
+ * Rebuild the theme registry to discover new theme_bef_checkbox() function.
+ */
+function better_exposed_filters_update_7000() {
+ drupal_theme_rebuild();
+ return t('Theme registry has been rebuilt.');
+}
diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.js b/sites/all/modules/better_exposed_filters/better_exposed_filters.js
new file mode 100644
index 0000000..40da193
--- /dev/null
+++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.js
@@ -0,0 +1,448 @@
+/**
+ * @file better_exposed_filters.js
+ *
+ * Provides some client-side functionality for the Better Exposed Filters module
+ */
+(function ($) {
+ Drupal.behaviors.betterExposedFilters = {
+ attach: function(context) {
+ // Add highlight class to checked checkboxes for better theming
+ $('.bef-tree input[type=checkbox], .bef-checkboxes input[type=checkbox]')
+ // Highlight newly selected checkboxes
+ .change(function() {
+ _bef_highlight(this, context);
+ })
+ .filter(':checked').closest('.form-item', context).addClass('highlight')
+ ;
+ }
+ };
+
+ Drupal.behaviors.betterExposedFiltersSelectAllNone = {
+ attach: function(context) {
+
+ /*
+ * Add Select all/none links to specified checkboxes
+ */
+ var selected = $('.form-checkboxes.bef-select-all-none:not(.bef-processed)');
+ if (selected.length) {
+ var selAll = Drupal.t('Select All');
+ var selNone = Drupal.t('Select None');
+
+ // Set up a prototype link and event handlers
+ var link = $(''+ selAll +'')
+ link.click(function(event) {
+ // Don't actually follow the link...
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (selAll == $(this).text()) {
+ // Select all the checkboxes
+ $(this)
+ .html(selNone)
+ .siblings('.bef-checkboxes, .bef-tree')
+ .find('.form-item input:checkbox').each(function() {
+ $(this).attr('checked', true);
+ _bef_highlight(this, context);
+ })
+ .end()
+
+ // attr() doesn't trigger a change event, so we do it ourselves. But just on
+ // one checkbox otherwise we have many spinning cursors
+ .find('input[type=checkbox]:first').change()
+ ;
+ }
+ else {
+ // Unselect all the checkboxes
+ $(this)
+ .html(selAll)
+ .siblings('.bef-checkboxes, .bef-tree')
+ .find('.form-item input:checkbox').each(function() {
+ $(this).attr('checked', false);
+ _bef_highlight(this, context);
+ })
+ .end()
+
+ // attr() doesn't trigger a change event, so we do it ourselves. But just on
+ // one checkbox otherwise we have many spinning cursors
+ .find('input[type=checkbox]:first').change()
+ ;
+ }
+ });
+
+ // Add link to the page for each set of checkboxes.
+ selected
+ .addClass('bef-processed')
+ .each(function(index) {
+ // Clone the link prototype and insert into the DOM
+ var newLink = link.clone(true);
+
+ newLink.insertBefore($('.bef-checkboxes, .bef-tree', this));
+
+ // If all checkboxes are already checked by default then switch to Select None
+ if ($('input:checkbox:checked', this).length == $('input:checkbox', this).length) {
+ newLink.click();
+ }
+ })
+ ;
+ }
+
+ // Check for and initialize datepickers
+ var befSettings = Drupal.settings.better_exposed_filters;
+ if (befSettings && befSettings.datepicker && befSettings.datepicker_options && $.fn.datepicker) {
+ var opt = [];
+ $.each(befSettings.datepicker_options, function(key, val) {
+ if (key && val) {
+ opt[key] = JSON.parse(val);
+ }
+ });
+ $('.bef-datepicker').datepicker(opt);
+ }
+
+ } // attach: function() {
+ }; // Drupal.behaviors.better_exposed_filters = {
+
+ Drupal.behaviors.betterExposedFiltersAllNoneNested = {
+ attach:function (context, settings) {
+ $('.form-checkboxes.bef-select-all-none-nested li').has('ul').once('bef-all-none-nested', function () {
+ $(this)
+ // To respect term depth, check/uncheck child term checkboxes.
+ .find('input.form-checkboxes:first')
+ .click(function() {
+ var checkedParent = $(this).attr('checked');
+ if (!checkedParent) {
+ // Uncheck all children if parent is unchecked.
+ $(this).parents('li:first').find('ul input.form-checkboxes').removeAttr('checked');
+ }
+ else {
+ // Check all children if parent is checked.
+ $(this).parents('li:first').find('ul input.form-checkboxes').attr('checked', $(this).attr('checked'));
+ }
+ })
+ .end()
+ // When a child term is checked or unchecked, set the parent term's
+ // status.
+ .find('ul input.form-checkboxes')
+ .click(function() {
+ var checked = $(this).attr('checked');
+
+ // Determine the number of unchecked sibling checkboxes.
+ var ct = $(this).parents('ul:first').find('input.form-checkboxes:not(:checked)').size();
+
+ // If the child term is unchecked, uncheck the parent.
+ if (!checked) {
+ // Uncheck parent if any of the childres is unchecked.
+ $(this).parents('li:first').parents('li:first').find('input.form-checkboxes:first').removeAttr('checked');
+ }
+
+ // If all sibling terms are checked, check the parent.
+ if (!ct) {
+ // Check the parent if all the children are checked.
+ $(this).parents('li:first').parents('li:first').find('input.form-checkboxes:first').attr('checked', checked);
+ }
+ });
+ });
+ }
+ };
+
+ Drupal.behaviors.better_exposed_filters_slider = {
+ attach: function(context, settings) {
+ var befSettings = settings.better_exposed_filters;
+ if (befSettings && befSettings.slider && befSettings.slider_options) {
+ $.each(befSettings.slider_options, function(i, sliderOptions) {
+ var containing_parent = "#" + sliderOptions.viewId + " #edit-" + sliderOptions.id + "-wrapper .views-widget";
+ var $filter = $(containing_parent);
+
+ // If the filter is placed in a secondary fieldset, we may not have
+ // the usual wrapper element.
+ if (!$filter.length) {
+ containing_parent = "#" + sliderOptions.viewId + " .bef-slider-wrapper";
+ $filter = $(containing_parent);
+ }
+
+ // Only make one slider per filter.
+ $filter.once('slider-filter', function() {
+ var $input = $(this).find('input[type=text]');
+
+ // This is a "between" or "not between" filter with two values.
+ if ($input.length == 2) {
+ var $min = $input.parent().find('input#edit-' + sliderOptions.id + '-min'),
+ $max = $input.parent().find('input#edit-' + sliderOptions.id + '-max'),
+ default_min,
+ default_max;
+
+ if (!$min.length || !$max.length) {
+ return;
+ }
+
+ // Get the default values.
+ // We use slider min & max if there are no defaults.
+ default_min = parseFloat(($min.val() == '') ? sliderOptions.min : $min.val(), 10);
+ default_max = parseFloat(($max.val() == '') ? sliderOptions.max : $max.val(), 10);
+ // Set the element value in case we are using the slider min & max.
+ $min.val(default_min);
+ $max.val(default_max);
+
+ $min.parents(containing_parent).after(
+ $('').slider({
+ range: true,
+ min: parseFloat(sliderOptions.min, 10),
+ max: parseFloat(sliderOptions.max, 10),
+ step: parseFloat(sliderOptions.step, 10),
+ animate: sliderOptions.animate ? sliderOptions.animate : false,
+ orientation: sliderOptions.orientation,
+ values: [default_min, default_max],
+ // Update the textfields as the sliders are moved
+ slide: function (event, ui) {
+ $min.val(ui.values[0]);
+ $max.val(ui.values[1]);
+ },
+ // This fires when the value is set programmatically or the
+ // stop event fires.
+ // This takes care of the case that a user enters a value
+ // into the text field that is not a valid step of the slider.
+ // In that case the slider will go to the nearest step and
+ // this change event will update the text area.
+ change: function (event, ui) {
+ $min.val(ui.values[0]);
+ $max.val(ui.values[1]);
+ },
+ // Attach stop listeners.
+ stop: function(event, ui) {
+ // Click the auto submit button.
+ $(this).parents('form').find('.ctools-auto-submit-click').click();
+ }
+ })
+ );
+
+ // Update the slider when the fields are updated.
+ $min.blur(function() {
+ befUpdateSlider($(this), 0, sliderOptions);
+ });
+ $max.blur(function() {
+ befUpdateSlider($(this), 1, sliderOptions);
+ });
+ }
+ // This is single value filter.
+ else if ($input.length == 1) {
+ if ($input.attr('id') != 'edit-' + sliderOptions.id) {
+ return;
+ }
+
+ // Get the default value. We use slider min if there is no default.
+ var default_value = parseFloat(($input.val() == '') ? sliderOptions.min : $input.val(), 10);
+ // Set the element value in case we are using the slider min.
+ $input.val(default_value);
+
+ $input.parents(containing_parent).after(
+ $('').slider({
+ min: parseFloat(sliderOptions.min, 10),
+ max: parseFloat(sliderOptions.max, 10),
+ step: parseFloat(sliderOptions.step, 10),
+ animate: sliderOptions.animate ? sliderOptions.animate : false,
+ orientation: sliderOptions.orientation,
+ value: default_value,
+ // Update the textfields as the sliders are moved.
+ slide: function (event, ui) {
+ $input.val(ui.value);
+ },
+ // This fires when the value is set programmatically or the
+ // stop event fires.
+ // This takes care of the case that a user enters a value
+ // into the text field that is not a valid step of the slider.
+ // In that case the slider will go to the nearest step and
+ // this change event will update the text area.
+ change: function (event, ui) {
+ $input.val(ui.value);
+ },
+ // Attach stop listeners.
+ stop: function(event, ui) {
+ // Click the auto submit button.
+ $(this).parents('form').find('.ctools-auto-submit-click').click();
+ }
+ })
+ );
+
+ // Update the slider when the field is updated.
+ $input.blur(function() {
+ befUpdateSlider($(this), null, sliderOptions);
+ });
+ }
+ else {
+ return;
+ }
+ })
+ });
+ }
+ }
+ };
+
+ // This is only needed to provide ajax functionality
+ Drupal.behaviors.better_exposed_filters_select_as_links = {
+ attach: function(context, settings) {
+
+ $('.bef-select-as-links', context).once(function() {
+ var $element = $(this);
+
+ // Check if ajax submission is enabled. If it's not enabled then we
+ // don't need to attach our custom submission handling, because the
+ // links are already properly built.
+
+ // First check if any ajax views are contained in the current page.
+ if (typeof settings.views == 'undefined' || typeof settings.views.ajaxViews == 'undefined') {
+ return;
+ }
+
+ // Now check that the view for which the current filter block is used,
+ // is part of the configured ajax views.
+ var $uses_ajax = false;
+ $.each(settings.views.ajaxViews, function(i, item) {
+ var $view_name = item.view_name.replace(/_/g, '-');
+ var $view_display_id = item.view_display_id.replace(/_/g, '-');
+ var $id = 'views-exposed-form-' + $view_name + '-' + $view_display_id;
+ var $form_id = $element.parents('form').attr('id');
+ if ($form_id == $id) {
+ $uses_ajax = true;
+ return;
+ }
+ });
+
+ // If no ajax is used for form submission, we quit here.
+ if (!$uses_ajax) {
+ return;
+ }
+
+ // Attach selection toggle and form submit on click to each link.
+ $(this).find('a').click(function(event) {
+ var $wrapper = $(this).parents('.bef-select-as-links');
+ var $options = $wrapper.find('select option');
+ // We have to prevent the page load triggered by the links.
+ event.preventDefault();
+ event.stopPropagation();
+ // Un select old select value.
+ $wrapper.find('select option').removeAttr('selected');
+
+ // Set the corresponding option inside the select element as selected.
+ var link_text = $(this).text();
+ $selected = $options.filter(function() {
+ return $(this).text() == link_text;
+ });
+ $selected.attr('selected', 'selected');
+ $wrapper.find('.bef-new-value').val($selected.val());
+ $wrapper.find('a').removeClass('active');
+ $(this).addClass('active');
+ // Submit the form.
+ $wrapper.parents('form').find('.views-submit-button *[type=submit]').click();
+ });
+ });
+ }
+ };
+
+ Drupal.behaviors.betterExposedFiltersRequiredFilter = {
+ attach: function(context, settings) {
+ // Required checkboxes should re-check all inputs if a user un-checks
+ // them all.
+ $('.bef-select-as-checkboxes', context).once('bef-required-filter').ajaxComplete(function (e, xhr, s) {
+ var $element = $(this);
+
+ if (typeof settings.views == 'undefined' || typeof settings.views.ajaxViews == 'undefined') {
+ return;
+ }
+
+ // Now check that the view for which the current filter block is used,
+ // is part of the configured ajax views.
+ var $view_name;
+ var $view_display_id;
+ var $uses_ajax = false;
+ $.each(settings.views.ajaxViews, function(i, item) {
+ $view_name = item.view_name;
+ $view_display_id = item.view_display_id;
+ var $id = 'views-exposed-form-' + $view_name.replace(/_/g, '-') + '-' + $view_display_id.replace(/_/g, '-');
+ var $form_id = $element.parents('form').attr('id');
+ if ($form_id == $id) {
+ $uses_ajax = true;
+ return false;
+ }
+ });
+
+ //Check if we have any filters at all because of Views Selective Filter
+ if($('input', this).length > 0) {
+ var $filter_name = $('input', this).attr('name').slice(0, -2);
+ if (Drupal.settings.better_exposed_filters.views[$view_name].displays[$view_display_id].filters[$filter_name].required && $('input:checked', this).length == 0) {
+ $('input', this).prop('checked', true);
+ }
+ }
+ });
+ }
+ }
+
+ /*
+ * Helper functions
+ */
+
+ /**
+ * Adds/Removes the highlight class from the form-item div as appropriate
+ */
+ function _bef_highlight(elem, context) {
+ $elem = $(elem, context);
+ $elem.attr('checked')
+ ? $elem.closest('.form-item', context).addClass('highlight')
+ : $elem.closest('.form-item', context).removeClass('highlight');
+ }
+
+ /**
+ * Update a slider when a related input element is changed.
+ *
+ * We don't need to check whether the new value is valid based on slider min,
+ * max, and step because the slider will do that automatically and then we
+ * update the textfield on the slider's change event.
+ *
+ * We still have to make sure that the min & max values of a range slider
+ * don't pass each other though, however once this jQuery UI bug is fixed we
+ * won't have to. - http://bugs.jqueryui.com/ticket/3762
+ *
+ * @param $el
+ * A jQuery object of the updated element.
+ * @param valIndex
+ * The index of the value for a range slider or null for a non-range slider.
+ * @param sliderOptions
+ * The options for the current slider.
+ */
+ function befUpdateSlider($el, valIndex, sliderOptions) {
+ var val = parseFloat($el.val(), 10),
+ currentMin = $el.parents('div.views-widget').next('.bef-slider').slider('values', 0),
+ currentMax = $el.parents('div.views-widget').next('.bef-slider').slider('values', 1);
+ // If we have a range slider.
+ if (valIndex != null) {
+ // Make sure the min is not more than the current max value.
+ if (valIndex == 0 && val > currentMax) {
+ val = currentMax;
+ }
+ // Make sure the max is not more than the current max value.
+ if (valIndex == 1 && val < currentMin) {
+ val = currentMin;
+ }
+ // If the number is invalid, go back to the last value.
+ if (isNaN(val)) {
+ val = $el.parents('div.views-widget').next('.bef-slider').slider('values', valIndex);
+ }
+ }
+ else {
+ // If the number is invalid, go back to the last value.
+ if (isNaN(val)) {
+ val = $el.parents('div.views-widget').next('.bef-slider').slider('value');
+ }
+ }
+ // Make sure we are a number again.
+ val = parseFloat(val, 10);
+ // Set the slider to the new value.
+ // The slider's change event will then update the textfield again so that
+ // they both have the same value.
+ if (valIndex != null) {
+ $el.parents('div.views-widget').next('.bef-slider').slider('values', valIndex, val);
+ }
+ else {
+ $el.parents('div.views-widget').next('.bef-slider').slider('value', val);
+ }
+ }
+
+}) (jQuery);
diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.module b/sites/all/modules/better_exposed_filters/better_exposed_filters.module
new file mode 100644
index 0000000..7d93748
--- /dev/null
+++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.module
@@ -0,0 +1,206 @@
+ array(
+ 'function' => 'theme_select_as_checkboxes',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'select_as_checkboxes_fieldset' => array(
+ 'function' => 'theme_select_as_checkboxes_fieldset',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'select_as_radios' => array(
+ 'function' => 'theme_select_as_radios',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'select_as_radios_fieldset' => array(
+ 'function' => 'theme_select_as_radios_fieldset',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'select_as_hidden' => array(
+ 'function' => 'theme_select_as_hidden',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'select_as_tree' => array(
+ 'function' => 'theme_select_as_tree',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'select_as_links' => array(
+ 'function' => 'theme_select_as_links',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'secondary_exposed_elements' => array(
+ 'function' => 'theme_secondary_exposed_elements',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ 'bef_checkbox' => array(
+ 'function' => 'theme_bef_checkbox',
+ 'render element' => 'element',
+ 'file' => 'better_exposed_filters.theme',
+ ),
+ );
+}
+
+/*
+ * Views3 support
+ *
+ * Views3 adds the concept of exposed forms to the mix. In addition, elements
+ * injected into a Views dialog is no longer saved along with the Views form
+ * information (see the unpack_options() and option_definition() methods of the
+ * views_object object).
+ */
+
+/**
+ * Implements hook_views_api().
+ */
+function better_exposed_filters_views_api() {
+ return array(
+ 'api' => 3.0,
+ );
+}
+
+/**
+ * Unpacks sort_by and sort_order from the sort_bef_combine element.
+ */
+function bef_sort_combine_submit($form, &$form_state) {
+ // Same default as better_exposed_filters_exposed_form_plugin::options_form.
+ $combine_param = empty($form_state['#combine_param']) ? $form_state['complete form']['#info']['sort-sort_bef_combine']['value'] : $form_state['#combine_param'];
+ if (empty($form_state['values'][$combine_param])) {
+ $form_state['values']['sort_by'] = $form_state['values']['sort_order'] = '';
+ }
+ else {
+ list($form_state['values']['sort_by'], $form_state['values']['sort_order']) = explode(' ', $form_state['values'][$combine_param]);
+ }
+
+ // And pass this along to Views.
+ views_exposed_form_submit($form, $form_state);
+}
+
+/**
+ * Form element validation handler for BEF jQuery slider required fields.
+ */
+function better_exposed_filters_element_validate_slider_required($element, &$form_state) {
+ $value = $element['#value'];
+ // If a jQuery slider format has been selected make sure the min & max value
+ // fields are not empty.
+ if ($value == '' && _better_exposed_filters_slider_selected($element, $form_state)) {
+ form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
+ }
+}
+
+/**
+ * Form element validation handler for BEF jQuery slider animate setting.
+ */
+function better_exposed_filters_element_validate_slider_animate($element, &$form_state) {
+ $value = $element['#value'];
+ if ($value !== '' && _better_exposed_filters_slider_selected($element, $form_state) &&
+ ((!is_numeric($value) || intval($value) != $value || $value <= 0) &&
+ !in_array($value, array('slow', 'normal', 'fast')))) {
+ form_error($element, t('%name must be "slow", "normal", "fast" or the number of milliseconds to run the animation (e.g. 1000).', array('%name' => $element['#title'])));
+ }
+}
+
+/**
+ * Form element validation handler for BEF jQuery slider min and max settings.
+ *
+ * The max value must be greater than the min value.
+ */
+function better_exposed_filters_element_validate_slider_min_max($element, &$form_state) {
+ $value = $element['#value'];
+ $slider_min = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_min'];
+ $slider_max = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_max'];
+
+ if ($value !== '' && _better_exposed_filters_slider_selected($element, $form_state)) {
+ // Must not have more than 11 decimal places.
+ if (_better_exposed_filters_get_num_decimal_places($value) > 11) {
+ form_error($element, t('%name must not have more than 11 decimal places.', array('%name' => $element['#title'])));
+ }
+ // The slider min must be less than the slider max.
+ if (is_numeric($slider_min) && is_numeric($slider_max) && ($slider_max <= $slider_min)) {
+ form_error($element, t('The Range minimum value must be less than the Range maximum value.'));
+ }
+ }
+}
+
+/**
+ * Form element validation handler for BEF jQuery slider step setting.
+ *
+ * The full specified value range of the slider (range maximum - range minimum)
+ * should be evenly divisible by the step.
+ */
+function better_exposed_filters_element_validate_slider_step($element, &$form_state) {
+ $value = $element['#value'];
+
+ if ($value !== '' && _better_exposed_filters_slider_selected($element, $form_state)) {
+ $slider_min = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_min'];
+ $slider_max = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_max'];
+
+ // Must be positive.
+ if ($value < 0) {
+ form_error($element, t('%name must be a positive number.', array('%name' => $element['#title'])));
+ }
+ // Must not have more than 5 decimal places.
+ if (_better_exposed_filters_get_num_decimal_places($value) > 5) {
+ form_error($element, t('%name must not have more than 5 decimal places.', array('%name' => $element['#title'])));
+ }
+ // The slider range must be evenly divisible by the step.
+ // We check like this because of the issues PHP has with inaccurate floats,
+ // where 2 might actually be 1.9999999999.
+ // Because of this we can't reliably use fmod().
+ if (is_numeric($slider_min) && is_numeric($slider_max) && !ctype_digit((string) abs(($slider_max - $slider_min) / $value))) {
+ form_error($element, t('The range of the slider (Range maximum - Range minimum) should be evenly divisible by the step.'));
+ }
+ }
+}
+
+/**
+ * Return whether or not the slider has been selected for the given filter.
+ */
+function _better_exposed_filters_slider_selected($element, &$form_state) {
+ return (isset($element['#bef_filter_id']) &&
+ isset($form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['bef_format']) &&
+ $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['bef_format'] == 'bef_slider');
+}
+
+/**
+ * Return the number of decimal places of the given number.
+ */
+function _better_exposed_filters_get_num_decimal_places($number) {
+ $str = (string) $number;
+ return strlen(substr(strrchr($str, '.'), 1));
+}
+
+/**
+ * Implements hook_preprocess_views_view().
+ */
+function better_exposed_filters_preprocess_views_view(&$variables) {
+ $filters = array();
+ foreach ($variables['view']->filter as $filter) {
+ if ($filter->options['exposed']) {
+ $identifier = !empty($filter->options['is_grouped']) ? $filter->options['group_info']['identifier'] : $filter->options['expose']['identifier'];
+ $filters[$identifier] = array(
+ 'required' => $filter->options['expose']['required'] ? TRUE : FALSE,
+ );
+ }
+ }
+
+ $bef_js['views'][$variables['view']->name]['displays'][$variables['view']->current_display]['filters'] = $filters;
+ drupal_add_js(array('better_exposed_filters' => $bef_js), 'setting');
+}
diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.theme b/sites/all/modules/better_exposed_filters/better_exposed_filters.theme
new file mode 100644
index 0000000..98e7cd8
--- /dev/null
+++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.theme
@@ -0,0 +1,763 @@
+ '',
+ '#bef_description' => '',
+ '#bef_operator' => array(),
+ ),
+ $vars['element']
+ );
+
+ $fieldset = array(
+ '#title' => $element['#bef_title'],
+ '#description' => $element['#bef_description'],
+ '#attributes' => array(
+ 'class' => array(
+ 'bef-select-as-checkboxes-fieldset',
+ 'collapsible',
+ ),
+ ),
+ );
+ if (empty($element['#value'])) {
+ // Using the FAPI #collapsible and #collapsed attribute doesn't work here
+ // TODO: not sure why...
+ $fieldset['#attributes']['class'][] = 'collapsed';
+ }
+
+ // We rendered the description as part of the fieldset element, don't render
+ // it again along with the checkboxes.
+ unset($element['#bef_description']);
+
+ $children = '';
+ if (!empty($element['#bef_operator'])) {
+ // Put an exposed operator inside the fieldset.
+ $children = drupal_render($element['#bef_operator']);
+ }
+
+ // Render the checkboxes.
+ $children .= theme('select_as_checkboxes', array('element' => $element));
+
+ $fieldset['#children'] = $children;
+ return theme('fieldset', array('element' => $fieldset));
+}
+
+/**
+ * Themes a select element as a set of checkboxes.
+ *
+ * @see http://api.drupal.org/api/function/theme_select/7
+ *
+ * @param array $vars
+ * An array of arrays, the 'element' item holds the properties of the element.
+ *
+ * @return string
+ * HTML representing the form element.
+ */
+function theme_select_as_checkboxes($vars) {
+ $element = $vars['element'];
+ if (!empty($element['#bef_nested'])) {
+ if (empty($element['#attributes']['class'])) {
+ $element['#attributes']['class'] = array();
+ }
+ $element['#attributes']['class'][] = 'form-checkboxes';
+ return theme('select_as_tree', array('element' => $element));
+ }
+
+ // The selected keys from #options.
+ $selected_options = empty($element['#value']) ? (empty($element['#default_value']) ? array() : $element['#default_value']) : $element['#value'];
+ if (!is_array($selected_options)) {
+ $selected_options = array($selected_options);
+ }
+
+ // Grab exposed filter description. We'll put it under the label where it
+ // makes more sense.
+ $description = '';
+ if (!empty($element['#bef_description'])) {
+ $description = '
' . $element['#bef_description'] . '
';
+ }
+
+ $output = '
';
+ foreach ($element['#options'] as $option => $elem) {
+ if ('All' === $option) {
+ // TODO: 'All' text is customizable in Views.
+ // No need for an 'All' option -- either unchecking or checking all the
+ // checkboxes is equivalent.
+ continue;
+ }
+
+ // Check for Taxonomy-based filters.
+ if (is_object($elem)) {
+ $slice = array_slice($elem->option, 0, 1, TRUE);
+ list($option, $elem) = each($slice);
+ }
+
+ // Check for optgroups. Put subelements in the $element_set array and add
+ // a group heading. Otherwise, just add the element to the set.
+ $element_set = array();
+ $is_optgroup = FALSE;
+ if (is_array($elem)) {
+ $output .= '
';
+
+ // Fake theme_checkboxes() which we can't call because it calls
+ // theme_form_element() for each option.
+ $attributes['class'] = array('form-checkboxes', 'bef-select-as-checkboxes');
+ if (!empty($element['#bef_select_all_none'])) {
+ $attributes['class'][] = 'bef-select-all-none';
+ }
+ if (!empty($element['#bef_select_all_none_nested'])) {
+ $attributes['class'][] = 'bef-select-all-none-nested';
+ }
+ if (!empty($element['#attributes']['class'])) {
+ $attributes['class'] = array_merge($element['#attributes']['class'], $attributes['class']);
+ }
+
+ return '
$description$output
";
+}
+
+/**
+ * Themes a select element as a series of hidden fields.
+ *
+ * @see http://api.drupal.org/api/function/theme_select/7
+ *
+ * @param array $vars
+ * An array of arrays, the 'element' item holds the properties of the element.
+ *
+ * @return string
+ * HTML representing the form element.
+ */
+function theme_select_as_hidden($vars) {
+ $element = $vars['element'];
+ $output = '';
+ $selected_options = empty($element['#value']) ? $element['#default_value'] : $element['#value'];
+ $properties = array(
+ 'title' => isset($element['#title']) ? $element['#title'] : '',
+ 'description' => isset($element['#bef_description']) ? $element['#bef_description'] : '',
+ 'required' => FALSE,
+ );
+
+ foreach ($element['#options'] as $option => $elem) {
+ // Check for Taxonomy-based filters.
+ if (is_object($elem)) {
+ $slice = array_slice($elem->option, 0, 1, TRUE);
+ list($option, $elem) = each($slice);
+ }
+
+ // Check for optgroups. Put subelements in the $element_set array and add a
+ // group heading. Otherwise, just add the element to the set.
+ $element_set = array();
+ if (is_array($elem)) {
+ $element_set = $elem;
+ }
+ else {
+ $element_set[$option] = $elem;
+ }
+
+ foreach ($element_set as $key => $value) {
+ // Only render fields for selected values -- no selected values renders
+ // zero fields.
+ if (array_search($key, $selected_options) !== FALSE) {
+ // Custom ID for each hidden field based on the
\n";
+
+ return $output;
+}
+
+/**
+ * Format a hierarchical select.
+ *
+ * @param array $variables
+ * An associative array containing the properties of the element.
+ * @return string
+ * A themed HTML string representing the form element.
+ */
+function theme_hierarchical_select($variables) {
+ $element = $variables['element'];
+ $output = '';
+
+ // Update $element['#attributes']['class'].
+ if (!isset($element['#attributes']['class'])) {
+ $element['#attributes']['class'] = array();
+ }
+ $hsid = $element['hsid']['#value'];
+ $level_labels_style = variable_get('hierarchical_select_level_labels_style', 'none');
+ $classes = array(
+ 'hierarchical-select-wrapper',
+ "hierarchical-select-level-labels-style-$level_labels_style",
+ // Classes that make it possible to override the styling of specific
+ // instances of Hierarchical Select, based on either the ID of the form
+ // element or the config that it uses.
+ 'hierarchical-select-wrapper-for-name-' . $element['#id'],
+ (isset($element['#config']['config_id'])) ? 'hierarchical-select-wrapper-for-config-' . $element['#config']['config_id'] : NULL,
+ );
+ $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $classes);
+ $element['#attributes']['id'] = "hierarchical-select-$hsid-wrapper";
+ $element['#id'] = "hierarchical-select-$hsid-wrapper"; // This ensures the label's for attribute is correct.
+
+ return '
' . drupal_render_children($element) . '
';
+}
+
+/**
+ * Format the container for all selects in the hierarchical select.
+ *
+ * @param array $variables
+ * An associative array containing the properties of the element.
+ * @return string
+ * A themed HTML string representing the form element.
+ */
+function theme_hierarchical_select_selects_container($variables) {
+ $element = $variables['element'];
+ $output = '';
+ $output .= '
';
+ return $output;
+}
+
+/**
+ * Format a select in the .hierarchial-select div: prevent it from being
+ * wrapped in a div. This simplifies the CSS and JS code.
+ *
+ * @param array $variables
+ * An associative array containing the properties of the element.
+ * @return string
+ * A themed HTML string representing the form element.
+ */
+function theme_hierarchical_select_select($variables) {
+ $element = $variables['element'];
+ element_set_attributes($element, array('id', 'name', 'size'));
+ _form_set_class($element, array('form-select'));
+
+ return '' . _hierarchical_select_options($element) . '';
+}
+
+/**
+ * Format an item separator (for use in a lineage).
+ */
+function theme_hierarchical_select_item_separator($variables) {
+ $output = '';
+ $output .= '';
+ $output .= '›';
+ $output .= '';
+ return $output;
+}
+
+/**
+ * Format a special option in a Hierarchical Select select. For example the
+ * "none" option or the "create new item" option. This theme function allows
+ * you to change how a special option is indicated textually.
+ *
+ * @param array $variables
+ * A special option.
+ * @return string
+ * A textually indicated special option.
+ */
+function theme_hierarchical_select_special_option($variables) {
+ $option = $variables['option'];
+ return '<' . $option . '>';
+}
+
+/**
+ * Forms API theming callback for the dropbox. Renders the dropbox as a table.
+ *
+ * @param array $variables
+ * An element for which the #theme property was set to this function.
+ * @return string
+ * A themed HTML string.
+ */
+function theme_hierarchical_select_dropbox_table($variables) {
+ $element = $variables['element'];
+ $output = '';
+
+ $class = 'dropbox';
+ if (form_get_error($element) === '') {
+ $class .= ' error';
+ }
+
+ $title = $element['title']['#value'];
+ $separator = $element['separator']['#value'];
+ $is_empty = $element['is_empty']['#value'];
+
+ $separator_html = '' . $separator . '';
+
+ $output .= '
';
+ $output .= '
';
+ $output .= '
' . $title . '
';
+ $output .= '';
+
+ if (!$is_empty) {
+ // Each lineage in the dropbox corresponds to an entry in the dropbox table.
+ $lineage_count = count(element_children($element['lineages']));
+ for ($x = 0; $x < $lineage_count; $x++) {
+ $db_entry = $element['lineages']["lineage-$x"];
+ $zebra = $db_entry['#zebra'];
+ $first = $db_entry['#first'];
+ $last = $db_entry['#last'];
+ // The deepest level is the number of child levels minus one. This "one"
+ // is the element for the "Remove" checkbox.
+ $deepest_level = count(element_children($db_entry)) - 1;
+
+ $output .= '
';
+ $output .= '
';
+ // Each item in a lineage is separated by the separator string.
+ for ($depth = 0; $depth < $deepest_level; $depth++) {
+ $output .= drupal_render($db_entry[$depth]);
+
+ if ($depth < $deepest_level - 1) {
+ $output .= $separator_html;
+ }
+ }
+ $output .= '
';
+ $output .= '
' . drupal_render($db_entry['remove']) . '
';
+ $output .= '
';
+ }
+ }
+ else {
+ $output .= '
';
+ $output .= t('Nothing has been selected.');
+ $output .= '
';
+ }
+
+ $output .= '';
+ $output .= '
';
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Themeing function to render the level_labels settings as a table.
+ */
+// TODO: rename $form to $element for consistency (and update hook_theme() after that), make the comment consistent.
+/**
+ * @todo Please document this function.
+ * @see http://drupal.org/node/1354
+ */
+function theme_hierarchical_select_common_config_form_level_labels($variables) {
+ $form = $variables['form'];
+ // Recover the stored strings.
+ $strings = $form['#strings'];
+
+ $output = '';
+ $header = array(t('Level'), t('Label'));
+ $rows = array();
+
+ $output .= drupal_render($form['status']);
+
+ $output .= '
';
+ $output .= t(
+ 'The %item_type you enter for each level is what will be used in
+ each level to replace a "<create new item>" option with a
+ "<create new %item_type>" option, which is often more
+ intuitive.',
+ array(
+ '%item_type' => $strings['item_type'],
+ )
+ );
+ $output .= '
';
+ }
+ else {
+ // No levels exist yet in the hierarchy!
+ $output .= '
';
+ $output .= t('There are no levels yet in this !hierarchy!', array('!hierarchy' => $strings['hierarchy']));
+ $output .= '
';
+ }
+ $output .= '
';
+
+ // Render the remaining form items.
+ $output .= drupal_render_children($form);
+
+ return $output;
+}
+
+/**
+ * Themeing function to render a selection (of items) according to a given
+ * Hierarchical Select configuration as one or more lineages.
+ *
+ * @param $selection
+ * A selection of items of a hierarchy.
+ * @param $config
+ * A config array with at least the following settings:
+ * - module
+ * - save_lineage
+ * - params
+ */
+function theme_hierarchical_select_selection_as_lineages($variables) {
+ $selection = $variables['selection'];
+ $config = $variables['config'];
+ $output = '';
+
+ $selection = (!is_array($selection)) ? array($selection) : $selection;
+
+ // Generate a dropbox out of the selection. This will automatically
+ // calculate all lineages for us.
+ $selection = array_keys($selection);
+ $dropbox = _hierarchical_select_dropbox_generate($config, $selection);
+
+ // Actual formatting.
+ foreach ($dropbox->lineages as $id => $lineage) {
+ if ($id > 0) {
+ $output .= ' ';
+ }
+
+ $items = array();
+ foreach ($lineage as $level => $item) {
+ $items[] = $item['label'];
+ }
+ $output .= implode('›', $items);
+ }
+
+ // Add the CSS.
+ drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css');
+
+ return $output;
+}
+
+/**
+ * @} End of "ingroup themeable".
+ */
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * This is an altered clone of form_select_options(). The reason: I need to be
+ * able to set a class on an option element if it contains a level label, to
+ * allow for level label styles.
+ * TODO: rename to _hierarchical_select_select_options().
+ */
+function _hierarchical_select_options($element) {
+ if (!isset($choices)) {
+ $choices = $element['#options'];
+ }
+ // array_key_exists() accommodates the rare event where $element['#value'] is NULL.
+ // isset() fails in this situation.
+ $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
+ $value_is_array = isset($element['#value']) && is_array($element['#value']);
+ $options = '';
+ foreach ($choices as $key => $choice) {
+ $key = (string) $key;
+ if ($value_valid && (!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value'])))) {
+ $selected = ' selected="selected"';
+ }
+ else {
+ $selected = '';
+ }
+
+ // If an option DOES NOT have child info, then it's a special option:
+ // - label_\d+ (level label)
+ // - none ("")
+ // - create_new_item ("")
+ // Only when it's a level label, we have to add a class to this option.
+ if (!isset($element['#childinfo'][$key])) {
+ $class = (preg_match('/label_\d+/', $key)) ? ' level-label' : '';
+ }
+ else {
+ $class = ($element['#childinfo'][$key] == 0) ? 'has-no-children' : 'has-children';
+ }
+
+ $options .= '';
+ }
+ return $options;
+}
diff --git a/sites/all/modules/hierarchical_select/includes/views.js b/sites/all/modules/hierarchical_select/includes/views.js
new file mode 100644
index 0000000..d5b5e3e
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/includes/views.js
@@ -0,0 +1,29 @@
+
+
+/**
+ * @file
+ * Make Hierarchical Select work in Views' exposed filters form.
+ *
+ * Views' exposed filters form is a GET form, but since Hierarchical Select
+ * really is a combination of various form items, this will result in a very
+ * ugly and unnecessarily long GET URL, which also breaks the exposed filters.
+ * This piece of JavaScript is a necessity to make it work again, but it will
+ * of course only work when JavaScript is enabled!
+ */
+
+
+if (Drupal.jsEnabled) {
+ $(document).ready(function(){
+ $('.view-filters form').submit(function() {
+ // Remove the Hierarchical Select form build id and the form id, to
+ // prevent them from ending up in the GET URL.
+ $('#edit-hs-form-build-id').remove();
+
+ // Prepare the hierarchical select form elements that are used as
+ // exposed filters for a GET submit.
+ $('.view-filters form')
+ .find('.hierarchical-select-wrapper')
+ .trigger('prepare-GET-submit');
+ });
+ });
+}
diff --git a/sites/all/modules/hierarchical_select/modules/hs_flatlist.info b/sites/all/modules/hierarchical_select/modules/hs_flatlist.info
new file mode 100644
index 0000000..35e2f07
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_flatlist.info
@@ -0,0 +1,12 @@
+name = Hierarchical Select Flat List
+description = Allows Hierarchical Select's dropbox to be used for selecting multiple items in a flat list of options.
+dependencies[] = hierarchical_select
+package = Form Elements
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2016-06-03
+version = "7.x-3.0-beta7"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1464981541"
+
diff --git a/sites/all/modules/hierarchical_select/modules/hs_flatlist.module b/sites/all/modules/hierarchical_select/modules/hs_flatlist.module
new file mode 100644
index 0000000..d2fa4de
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_flatlist.module
@@ -0,0 +1,66 @@
+ t('None: flat list'),
+ 'entity type' => t('N/A'),
+ );
+}
diff --git a/sites/all/modules/hierarchical_select/modules/hs_menu.info b/sites/all/modules/hierarchical_select/modules/hs_menu.info
new file mode 100644
index 0000000..9de8489
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_menu.info
@@ -0,0 +1,13 @@
+name = Hierarchical Select Menu
+description = Use Hierarchical Select for menu parent selection.
+dependencies[] = hierarchical_select
+dependencies[] = menu
+package = Form Elements
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2016-06-03
+version = "7.x-3.0-beta7"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1464981541"
+
diff --git a/sites/all/modules/hierarchical_select/modules/hs_menu.install b/sites/all/modules/hierarchical_select/modules/hs_menu.install
new file mode 100644
index 0000000..3b4deb5
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_menu.install
@@ -0,0 +1,27 @@
+fields(array('weight' => 1))
+ ->condition('name', 'hs_menu')
+ ->execute();
+}
+
+/**
+ * Implementats hook_uninstall().
+ */
+function hs_menu_uninstall() {
+ db_delete('variable')
+ ->condition('name', 'hs_menu_%', 'LIKE')
+ ->execute();
+}
diff --git a/sites/all/modules/hierarchical_select/modules/hs_menu.module b/sites/all/modules/hierarchical_select/modules/hs_menu.module
new file mode 100644
index 0000000..6cee6e8
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_menu.module
@@ -0,0 +1,326 @@
+ 'Menu',
+ 'description' => 'Hierarchical Select configuration for Menu',
+ 'access arguments' => array('administer site configuration'),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('hs_menu_admin_settings'),
+ 'type' => MENU_LOCAL_TASK,
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the node form's menu form.
+ */
+function hs_menu_form_node_form_alter(&$form, &$form_state) {
+ $active_types = array_filter(variable_get('hs_menu_content_types', array()));
+ $active = empty($active_types) || in_array($form_state['node']->type, $active_types);
+ if ($active && isset($form['menu']['link']['parent']) && isset($form['menu']['#access']) && $form['menu']['#access']) {
+ unset($form['menu']['link']['parent']['#options']);
+ $form['menu']['link']['parent']['#type'] = 'hierarchical_select';
+ // Get menu name, needed to exclude current node.
+ $menu_name = explode(':', $form['menu']['link']['parent']['#default_value']);
+ _hs_menu_apply_config($form['menu']['link']['parent'], array(
+ 0 => $menu_name[0],
+ 1 => $form['menu']['link']['mlid']['#value'],
+ 'type' => $form['type']['#value'],
+ ));
+
+ // Set custom submit callback.
+ array_unshift($form['#submit'], 'hs_menu_node_form_submit');
+ // Change the loaded default value into an array so we can populate the
+ // Hierarchical Select element.
+ $form['menu']['link']['parent']['#default_value'] = array($form['menu']['link']['parent']['#default_value']);
+ }
+}
+
+/**
+ * Implements hook_form_BASE_FORMID_alter().
+ *
+ * Alter the widget type form; dynamically add the Hierarchical Select
+ * Configuration form when it is needed.
+ */
+function hs_menu_form_menu_edit_item_alter(&$form, &$form_state) {
+ unset($form['parent']['#options']);
+ $original_item = $form['original_item']['#value'];
+ $form['parent']['#type'] = 'hierarchical_select';
+ _hs_menu_apply_config($form['parent'], array('exclude' => array(
+ $original_item['menu_name'],
+ $original_item['mlid'],
+ )));
+
+ // Set custom submit callback.
+ array_unshift($form['#submit'], 'hs_menu_menu_edit_item_form_submit');
+}
+
+
+//----------------------------------------------------------------------------
+// Form API callbacks.
+
+/**
+ * Submit callback; menu edit item form.
+ */
+function hs_menu_menu_edit_item_form_submit(&$form, &$form_state) {
+ // Don't return an array, but a single item.
+ $form_state['values']['parent'] = $form_state['values']['parent'][0];
+}
+
+/**
+ * Submit callback; node edit form.
+ */
+function hs_menu_node_form_submit(&$form, &$form_state) {
+ // Don't return an array, but a single item.
+ $form_state['values']['menu']['parent'] = $form_state['values']['menu']['parent'][0];
+}
+
+//----------------------------------------------------------------------------
+// Menu callbacks.
+
+/**
+ * Form definition; admin settings.
+ */
+function hs_menu_admin_settings() {
+ $form['hs_menu_resizable'] = array(
+ '#type' => 'radios',
+ '#title' => t('Resizable'),
+ '#description' => t(
+ "When enabled, a handle appears below the Hierarchical Select to allow
+ the user to dynamically resize it. Double clicking will toggle between
+ the smallest and a sane 'big size'."
+ ),
+ '#options' => array(
+ 0 => t('Disabled'),
+ 1 => t('Enabled'),
+ ),
+ '#default_value' => variable_get('hs_menu_resizable', 1),
+ );
+
+ $form['hs_menu_content_types'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Content types'),
+ '#description' => t("Select the content types to use Hierarchical Select Menu on. If no content types are selected, then it will apply to all content types."),
+ '#options' => node_type_get_names(),
+ '#default_value' => variable_get('hs_menu_content_types', array()),
+ );
+
+ return system_settings_form($form);
+}
+
+
+//----------------------------------------------------------------------------
+// Hierarchical Select hooks.
+
+/**
+ * Implements hook_hierarchical_select_params().
+ */
+function hs_menu_hierarchical_select_params() {
+ $params = array(
+ 'exclude', // The menu_name and mlid (in an array) of a menu link that should be excluded from the hierarchy.
+ );
+ return $params;
+}
+
+/**
+ * Implements hook_hierarchical_select_root_level().
+ */
+function hs_menu_hierarchical_select_root_level($params) {
+ $menus = array();
+
+ $result = db_query("SELECT menu_name, title FROM {menu_custom} ORDER BY title");
+ // If the type is set, respect the core menu options setting.
+ if (isset($params['type'])) {
+ $type_menus = variable_get('menu_options_' . $params['type'], array('main-menu' => 'main-menu'));
+ while ($menu = $result->fetchObject()) {
+ if (in_array($menu->menu_name, $type_menus)) {
+ $menus[$menu->menu_name . ':0'] = $menu->title;
+ }
+ }
+ }
+ // Fall back to the legacy approach, show all menu's.
+ else {
+ while ($menu = $result->fetchObject()) {
+ $menus[$menu->menu_name . ':0'] = $menu->title;
+ }
+ }
+
+ return $menus;
+}
+
+/**
+ * Implements hook_hierarchical_select_children().
+ */
+function hs_menu_hierarchical_select_children($parent, $params) {
+ $children = array();
+ list($menu_name, $plid) = explode(':', $parent);
+ $tree = menu_tree_all_data($menu_name, NULL);
+ return _hs_menu_children($tree, $menu_name, $plid, $params['exclude']);
+}
+
+/**
+ * Implements hook_hierarchical_select_lineage().
+ */
+function hs_menu_hierarchical_select_lineage($item, $params) {
+ $lineage = array($item);
+
+ list($menu_name, $mlid) = explode(':', $item);
+
+ // If the initial mlid is zero, then this is the root level, so we don't
+ // have to get the lineage.
+ if ($mlid > 0) {
+ // Prepend each parent mlid (i.e. plid) to the lineage.
+ do {
+ $plid = db_query("SELECT plid FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchField();
+ array_unshift($lineage, "$menu_name:$plid");
+ if ($mlid == $plid) {
+ // Somehow we have an infinite loop situation. Bail out of the loop.
+ break;
+ }
+ $mlid = $plid;
+ } while ($plid > 0);
+ }
+
+ return $lineage;
+}
+
+/**
+ * Implements hook_hierarchical_select_valid_item().
+ */
+function hs_menu_hierarchical_select_valid_item($item, $params) {
+ $parts = explode(':', $item);
+
+ $valid = TRUE;
+
+ // Validate menu name.
+ $valid = (array_key_exists($parts[0], menu_get_menus()));
+
+ // Validate hierarchy of mlids.
+ for ($i = 1; $valid && $i < count($parts); $i++) {
+ $valid = $valid && is_numeric($parts[$i]);
+ }
+
+ // Ensure that this isn't the excluded menu link.
+ $valid = $valid && $item != $params['exclude'][0] . $params['exclude'][1];
+
+ return $valid;
+}
+
+/**
+ * Implements hook_hierarchical_select_item_get_label().
+ */
+function hs_menu_hierarchical_select_item_get_label($item, $params) {
+ static $labels = array();
+
+ $parts = explode(':', $item);
+ if (count($parts) == 1) { // Get the menu name.
+ $menu_name = $parts[0];
+ $labels[$item] = db_query("SELECT title FROM {menu_custom} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField();
+ }
+ else { // Get the menu link title.
+ $mlid = end($parts);
+ $menu_link = menu_link_load($mlid);
+ $labels[$item] = $menu_link['title'];
+ }
+
+ return $labels[$item];
+}
+
+/**
+ * Implements hook_hierarchical_select_implementation_info().
+ */
+function hs_menu_hierarchical_select_implementation_info() {
+ return array(
+ 'hierarchy type' => t('Menu'),
+ 'entity type' => t('N/A'),
+ );
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Recursive helper function for hs_menu_hierarchical_select_children().
+ */
+function _hs_menu_children($tree, $menu_name, $plid = 0, $exclude = FALSE) {
+ $children = array();
+
+ foreach ($tree as $data) {
+ if ($data['link']['plid'] == $plid && $data['link']['hidden'] >= 0) {
+ if ($exclude && $data['link']['menu_name'] === $exclude[0] && $data['link']['mlid'] == $exclude[1]) {
+ continue;
+ }
+
+ $title = truncate_utf8($data['link']['title'], 30, TRUE, FALSE);
+ if ($data['link']['hidden']) {
+ $title .= ' (' . t('disabled') . ')';
+ }
+ $children[$menu_name . ':' . $data['link']['mlid']] = $title;
+ if ($data['below']) {
+ $children += _hs_menu_children($data['below'], $menu_name, $plid, $exclude);
+ }
+ }
+ elseif ($data['below']) {
+ $children += _hs_menu_children($data['below'], $menu_name, $plid, $exclude);
+ }
+ }
+
+ return $children;
+}
+
+/**
+ * Helper function to apply the HS config to a form item.
+ */
+function _hs_menu_apply_config(&$form, $params) {
+ // The following is to ensure via javascript self is not listed.
+ if (!empty($params['exclude'])) {
+ $params['exclude'] = $params['exclude'][0] .':'. $params['exclude'][1];
+ drupal_add_js('jQuery(document).ready(function () {
+ jQuery("[value*=' . $params['exclude'] . ']").hide();
+ });', 'inline');
+ }
+ $form['#config'] = array(
+ 'module' => 'hs_menu',
+ 'params' => array(
+ 'exclude' => isset($params['exclude']) ? $params['exclude'] : NULL,
+ 'type' => isset($params['type']) ? $params['type'] : NULL,
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 0,
+ 'resizable' => variable_get('hs_menu_resizable', 1),
+ 'level_labels' => array(
+ 'status' => 0,
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'render_flat_select' => 0,
+ );
+}
diff --git a/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.info b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.info
new file mode 100644
index 0000000..7865b04
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.info
@@ -0,0 +1,12 @@
+name = Hierarchical Select Small Hierarchy
+description = Allows Hierarchical Select to be used for a hardcoded hierarchy. When it becomes to slow, you should move the hierarchy into the database and write a proper implementation.
+dependencies[] = hierarchical_select
+package = Form Elements
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2016-06-03
+version = "7.x-3.0-beta7"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1464981541"
+
diff --git a/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.module b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.module
new file mode 100644
index 0000000..be4c7eb
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.module
@@ -0,0 +1,214 @@
+ t('Custom'),
+ 'entity type' => t('N/A'),
+ );
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Automatically transform a given hierarchy with this format:
+ * array(
+ * 'win' => array(
+ * 'label' => 'Windows',
+ * 'children' => array(
+ * 'xp' => array('label' => 'XP'),
+ * 'vista' => array(
+ * 'label' => 'Vista',
+ * 'children' => array(
+ * 'x86' => array('label' => '32-bits'),
+ * 'x64' => array('label' => '64-bits'),
+ * ),
+ * ),
+ * ),
+ * ),
+ * )
+ *
+ * to one with this format:
+ * array(
+ * 'root' => array(
+ * 'children' => array(
+ * 'xp',
+ * 'vista',
+ * ),
+ * ),
+ * 'win' => array(
+ * 'label' => 'Windows',
+ * 'children' => array(
+ * 'win|xp',
+ * 'win|vista',
+ * ),
+ * ),
+ * 'win|xp' => array(
+ * 'label' => 'XP',
+ * ),
+ * 'win|vista' => array(
+ * 'label' => 'Vista',
+ * 'children' => array(
+ * 'win|vista|x86',
+ * 'win|vista|x64',
+ * ),
+ * ),
+ * 'win|vista|x86' => array(
+ * 'label' => '32-bits',
+ * ),
+ * 'win|vista|x64' => array(
+ * 'label' => '64-bits',
+ * ),
+ * )
+ *
+ * This new format:
+ * - ensures unique identifiers for each item
+ * - makes it very easy to find the parent of a given item.
+ * - makes it very easy to find the label and children of a given item.
+ *
+ * @params $hierarchy
+ * The hierarchy.
+ * @params $id
+ * A unique identifier for the hierarchy, for caching purposes.
+ * @params $separator
+ * The separator to use.
+ */
+function _hs_smallhierarchy_transform($hierarchy, $id, $separator = '|') {
+ // Make sure each hierarchy is only transformed once.
+ if (!isset($hs_hierarchy[$id])) {
+ $hs_hierarchy[$id] = array();
+
+ // Build the root level.
+ foreach ($hierarchy as $item => $children) {
+ $hs_hierarchy[$id]['root']['children'][] = $item;
+ $hs_hierarchy[$id][$item]['label'] = $children['label'];
+
+ // Build the subsequent levels.
+ if (isset($children['children'])) {
+ _hs_smallhierarchy_transform_recurse($item, $hs_hierarchy[$id], $children['children'], $separator);
+ }
+ }
+ }
+
+ return $hs_hierarchy[$id];
+}
+
+/**
+ * Helper function for _hs_smallhierarchy_transform().
+ *
+ * @params $parent
+ * The parent item of the current level.
+ * @params $hs_hierarchy
+ * The HS hierarchy.
+ * @params $relative_hierarchy
+ * The hierarchy relative to the current level.
+ * @params $separator
+ * The separator to use.
+ */
+function _hs_smallhierarchy_transform_recurse($parent, &$hs_hierarchy, $relative_hierarchy, $separator = '|') {
+ foreach ($relative_hierarchy as $item => $children) {
+ $generated_item = $parent . $separator . $item;
+ $hs_hierarchy[$parent]['children'][] = $generated_item;
+ $hs_hierarchy[$generated_item]['label'] = $children['label'];
+
+ // Build the subsequent levels.
+ if (isset($children['children'])) {
+ _hs_smallhierarchy_transform_recurse($generated_item, $hs_hierarchy, $children['children'], $separator);
+ }
+ }
+}
diff --git a/sites/all/modules/hierarchical_select/modules/hs_taxonomy.info b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.info
new file mode 100644
index 0000000..16811ea
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.info
@@ -0,0 +1,14 @@
+name = Hierarchical Select Taxonomy
+description = Use Hierarchical Select for Taxonomy.
+dependencies[] = hierarchical_select
+dependencies[] = taxonomy
+package = Form Elements
+
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2016-06-03
+version = "7.x-3.0-beta7"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1464981541"
+
diff --git a/sites/all/modules/hierarchical_select/modules/hs_taxonomy.install b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.install
new file mode 100644
index 0000000..72712c3
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.install
@@ -0,0 +1,123 @@
+condition('name', 'taxonomy_hierarchical_select_%', 'LIKE')
+ ->execute();
+ variable_del('taxonomy_override_selector');
+}
+
+/**
+ * Implementation of hook_enable().
+ */
+function hs_taxonomy_enable() {
+ variable_set('taxonomy_override_selector', TRUE);
+ drupal_set_message(t("Drupal core's taxonomy selects are now overridden on the
+ Taxonomy Term form. They've been replaced by
+ Hierarchical Selects for better scalability.
+ You can configure it to
+ be used on node forms too!",
+ array(
+ '!configure-url' => url('admin/config/content/hierarchical_select/configs'),
+ )));
+}
+
+/**
+ * Implementation of hook_disable().
+ */
+function hs_taxonomy_disable() {
+ variable_set('taxonomy_override_selector', FALSE);
+ drupal_set_message(t("Drupal core's taxonomy selects are now restored.
+ Please remember that they're not scalable!."),
+ 'warning');
+}
+
+
+//----------------------------------------------------------------------------
+// Schema updates.
+
+/**
+ * Upgrade path from Drupal 6 to Drupal 7 version of Hierarchical Select:
+ * - delete the taxonomy_override_selector variable if it exists.
+ */
+function hs_taxonomy_update_7300() {
+ variable_del('taxonomy_override_selector');
+}
+
+/**
+ * Apparently, taxonomy_override_selector still exists in *one* location in
+ * Drupal 7 core: on the form_taxonomy_form_term form (where you can create or
+ * edit a Taxonomy term).
+ */
+function hs_taxonomy_update_7301() {
+ variable_set('taxonomy_override_selector', TRUE);
+}
+
+/**
+ * Convert Taxonomy vocabulary config IDs to use machine name instead of serial
+ * vocabulary ID.
+ */
+function hs_taxonomy_update_7302() {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ $vocabularies = taxonomy_vocabulary_get_names();
+
+ foreach ($vocabularies as $machine_name => $vocabulary) {
+ $old_config_id = "taxonomy-{$vocabulary->vid}";
+ $new_config_id = "taxonomy-{$machine_name}";
+
+ $old_config = variable_get('hs_config_' . $old_config_id, NULL);
+
+ if (!empty($old_config)) {
+ hierarchical_select_common_config_set($new_config_id, $old_config);
+ hierarchical_select_common_config_del($old_config_id);
+ }
+ }
+}
+
+/**
+ * Convert Taxonomy vocabulary config IDs to use field name.
+ */
+function hs_taxonomy_update_7303() {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ foreach (field_info_instances() as $entity_type => $bundles) {
+ foreach ($bundles as $bundle => $field_list) {
+ foreach ($field_list as $field_name => $instance) {
+ if ($instance['widget']['type'] == 'taxonomy_hs') {
+ $field_info = field_info_field($field_name);
+ $allowed_value = $field_info['settings']['allowed_values'][0];
+ $vocabulary_name = $allowed_value['vocabulary'];
+ $old_config_id = "taxonomy-{$vocabulary_name}";
+ $new_config_id = "taxonomy-{$field_name}";
+ $old_config = hierarchical_select_common_config_get($old_config_id);
+ if (!empty($old_config)) {
+ hierarchical_select_common_config_set($new_config_id, $old_config);
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Cleanup old-named configs.
+ */
+function hs_taxonomy_update_7304() {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ $vocabularies = taxonomy_get_vocabularies();
+ foreach ($vocabularies as $vid => $vocabulary) {
+ $old_config_id = "taxonomy-{$vocabulary->machine_name}";
+ hierarchical_select_common_config_del($old_config_id);
+ }
+}
diff --git a/sites/all/modules/hierarchical_select/modules/hs_taxonomy.module b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.module
new file mode 100644
index 0000000..7da8106
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.module
@@ -0,0 +1,1273 @@
+ array(
+ 'variables' => array('lineage' => array()),
+ ),
+ );
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the Hierarchical Select admin settings form to add a checkbox to
+ * disable Hierarchical Select for taxonomy term edit forms.
+ */
+function hs_taxonomy_form_hierarchical_select_admin_settings_alter(&$form, &$form_state) {
+ $form['taxonomy_override_selector'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable for taxonomy term edit forms'),
+ '#description' => t(
+ 'If this is checked then the "Relations > Parent terms" field on taxonomy term edit pages will use hierarchical select.'
+ ),
+ '#default_value' => variable_get('taxonomy_override_selector', FALSE),
+ );
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the widget type form; dynamically add the Hierarchical Select
+ * Configuration form when it is needed.
+ */
+function hs_taxonomy_form_field_ui_widget_type_form_alter(&$form, &$form_state) {
+ form_load_include($form_state, 'inc', 'hierarchical_select', 'includes/common');
+
+ // Alter the widget type select: configure #ajax so that we can respond to
+ // changes in its value: whenever it is set to "taxonomy_hs", we add the HS
+ // config UI.
+ $form['basic']['widget_type']['#ajax'] = array(
+ 'event' => 'change',
+ 'callback' => 'hs_taxonomy_field_ui_widget_settings_ajax',
+ 'wrapper' => 'hs-config-replace',
+ 'method' => 'replace'
+ );
+
+ $current_widget_type = (isset($form_state['input']['widget_type'])) ? $form_state['input']['widget_type'] : $form_state['build_info']['args'][0]['widget']['type'];
+ if ($current_widget_type == 'taxonomy_hs') {
+ $field = field_info_field($form['#field_name']);
+ if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) {
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+ }
+
+ $vid = isset($vocabulary->vid) ? $vocabulary->vid : NULL;
+ $save_lineage = isset($vocabulary->hierarchy) ? (int) ($vocabulary->hierarchy == 2) : 0;
+ $instance = field_info_instance($form['#entity_type'], $form['#field_name'], $form['#bundle']);
+
+ // Add the Hierarchical Select config form.
+ $module = 'hs_taxonomy';
+ $params = array(
+ 'vid' => $vid,
+ 'exclude_tid' => NULL,
+ 'root_term' => NULL,
+ );
+ $config_id = hs_taxonomy_get_config_id($form['#field_name']);
+ $defaults = array(
+ // Enable the save_lineage setting by default if the multiple parents
+ // vocabulary option is enabled.
+ 'save_lineage' => $save_lineage,
+ 'editability' => array(
+ 'max_levels' => _hs_taxonomy_hierarchical_select_get_depth($vid),
+ ),
+ );
+ $strings = array(
+ 'hierarchy' => t('taxonomy_vocabulary'),
+ 'hierarchies' => t('vocabularies'),
+ 'item' => t('term'),
+ 'items' => t('terms'),
+ 'item_type' => t('term type'),
+ 'entity' => t('node'),
+ 'entities' => t('nodes'),
+ );
+ $max_hierarchy_depth = _hs_taxonomy_hierarchical_select_get_depth($vid);
+ $preview_is_required = ($instance['required'] == 1);
+ $form['hs'] = hierarchical_select_common_config_form($module, $params, $config_id, $defaults, $strings, $max_hierarchy_depth, $preview_is_required);
+
+ if (!module_exists('taxonomy_entity_index')) {
+ // Only allow person to select nodes.
+ $form['hs']['entity_count']['settings']['entity_types'] = array('node' => $form['hs']['entity_count']['settings']['entity_types']['node']);
+ $form['hs']['entity_count']['settings']['#collapsed'] = FALSE;
+ $form['hs']['entity_count']['settings']['entity_types']['node']['#collapsed'] = FALSE;
+ $form['hs']['entity_count']['#description'] = '
' . t('You can extend this functionality to other entities if you install !taxonomy_entity_index.', array('!taxonomy_entity_index' => l('Taxonomy entity index', 'https://www.drupal.org/project/taxonomy_entity_index'))) . '
';
+ }
+
+ // Make the config form AJAX-updateable.
+ $form['hs'] += array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+
+ // Add the submit handler for the Hierarchical Select config form. Make
+ // sure it is executed first.
+ $form['#hs_common_config_form_parents'] = array('hs');
+ array_unshift($form['#submit'], 'hierarchical_select_common_config_form_submit');
+
+ // Add a submit handler for HS Taxonomy that will update the field
+ // settings when necessary.
+ // @see hs_taxonomy_field_settings_submit() for details.
+ $form['#submit'][] = 'hs_taxonomy_field_settings_submit';
+ }
+ else {
+ $form['hs'] = array(
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ }
+}
+
+/**
+ * Submit callback; updates the field settings (i.e. sets the cardinality of
+ * the field to unlimited) whenever either the dropbox or "save lineage" is
+ * enabled.
+ */
+function hs_taxonomy_field_settings_submit(&$form, &$form_state) {
+ $field = field_info_field($form['#field_name']);
+ $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field_name']));
+
+ if ($config['dropbox']['status'] || $config['save_lineage']) {
+ $field = field_info_field($form['#field_name']);
+ $field['cardinality'] = -1; // -1 = unlimited
+ field_update_field($field);
+
+ drupal_set_message(t("Updated this field's cardinality to unlimited."));
+ }
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ *
+ * Alter the field settings form; dynamically disable the "cardinality" (or
+ * "Number of values" in the UI) setting on the form when either the dropbox
+ * or "save lineage" is enabled.
+ */
+function hs_taxonomy_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
+ if (isset($form['#field']['type']) && $form['#field']['type'] === 'taxonomy_term_reference' && $form['#instance']['widget']['type'] == 'taxonomy_hs') {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field']['field_name']));
+
+ if ($config['dropbox']['status'] || $config['save_lineage']) {
+ $form['field']['cardinality']['#disabled'] = TRUE;
+ $form['field']['cardinality']['#description'] .= ' ' . t('This setting is now managed by the Hierarchical Select configuration.') . '';
+ }
+ }
+}
+
+function hs_taxonomy_form_taxonomy_form_term_alter(&$form, &$form_state) {
+ // Don't alter the form when taxonomy_override_selector is not TRUE (or 1).
+ if (!variable_get('taxonomy_override_selector', FALSE)) {
+ return;
+ }
+
+ // Don't alter the form when it's in confirmation mode.
+ if (isset($form_state['confirm_delete']) || isset($form_state['confirm_parents'])) {
+ return;
+ }
+
+ // Build an appropriate config.
+ $vocabulary = $form['#vocabulary'];
+ $vid = $vocabulary->vid;
+ module_load_include('inc', 'hierarchical_select', 'includes/common');
+ $config = array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => $vid,
+ 'exclude_tid' => isset($form['#term']['tid']) ? $form['#term']['tid'] : NULL,
+ 'root_term' => TRUE,
+ ),
+ 'enforce_deepest' => 0,
+ 'save_lineage' => 0,
+ 'level_labels' => array('status' => FALSE, 'labels' => array()),
+ 'dropbox' => array(
+ 'status' => variable_get('hs_taxonomy_enable_dropbox_on_term_form', 0),
+ 'limit' => 0,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ ),
+ 'entity_count' => array(
+ 'enable' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'render_flat_select' => 0,
+ );
+
+ // Use Hierarchical Select for selecting the parent term(s).
+ $parent_tid = array_keys(taxonomy_get_parents($form['#term']['tid']));
+ $parent = !empty($parent_tid) ? $parent_tid : array(0);
+ $form['relations']['parent'] = array(
+ '#type' => 'hierarchical_select',
+ '#title' => t('Parents'),
+ '#required' => TRUE,
+ '#default_value' => $parent,
+ '#config' => $config,
+ );
+ $form['relations']['parent']['#config']['dropbox']['title'] = t('All parent terms');
+}
+
+/**
+ * Implements hook_field_delete().
+ *
+ * This enables us to delete HS configs when fields are deleted.
+ */
+function hs_taxonomy_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+ hierarchical_select_common_config_del(hs_taxonomy_get_config_id($field['field_name']));
+}
+
+//----------------------------------------------------------------------------
+// FAPI callbacks.
+
+/**
+ * AJAX callback; field UI widget settings form.
+ */
+function hs_taxonomy_field_ui_widget_settings_ajax($form, &$form_state) {
+ return $form['hs'];
+}
+
+
+//----------------------------------------------------------------------------
+// Field API widget hooks.
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function hs_taxonomy_field_widget_info() {
+ return array(
+ 'taxonomy_hs' => array(
+ 'label' => t('Hierarchical Select'),
+ 'field types' => array('taxonomy_term_reference'),
+ 'settings' => array(), // All set in hs_taxonomy_field_widget_form().
+ 'behaviors' => array(
+ // TODO: figure out how to map the "dropbox" behavior to Field API's
+ // "multiple values" system.
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function hs_taxonomy_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
+
+ if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) {
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+ }
+
+ // Build an array of existing term IDs.
+ $tids = array();
+ foreach ($items as $delta => $item) {
+ if (!empty($item['tid']) && $item['tid'] != 'autocreate') {
+ $tids[] = $item['tid'];
+ }
+ }
+
+ $element += array(
+ '#type' => 'hierarchical_select',
+ '#config' => array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => isset($vocabulary->vid) ? (int) $vocabulary->vid : NULL,
+ 'exclude_tid' => NULL,
+ 'root_term' => isset($field['settings']['allowed_values'][0]['parent']) ? (int) $field['settings']['allowed_values'][0]['parent'] : NULL,
+ ),
+ ),
+ '#default_value' => $tids,
+ );
+
+ hierarchical_select_common_config_apply($element, hs_taxonomy_get_config_id($field['field_name']));
+
+ // Append another #process callback that transforms #return_value to the
+ // format that Field API/Taxonomy Field expects.
+ // However, HS' default #process callback has not yet been set, since this
+ // typically happens automatically during FAPI processing. To ensure the
+ // order is right, we already set HS' own #process callback here explicitly.
+ $element_info = element_info('hierarchical_select');
+ $element['#process'] = array_merge($element_info['#process'], array('hs_taxonomy_widget_process'));
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function hs_taxonomy_field_widget_settings_form($field, $instance) {
+ // This poorly integrates with the Field UI. Hence we alter the
+ // field_ui_widget_type_form, to provide a more appropriate integration.
+ // @see hs_taxonomy_form_field_ui_widget_type_form_alter.
+ $form = array();
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function hs_taxonomy_field_widget_error($element, $error, $form, &$form_state) {
+ form_error($element, $error['message']);
+}
+
+/**
+ * #process callback that runs after HS' #process callback, to transform
+ * #return_value to the format that Field API/Taxonomy Field expects.
+ */
+function hs_taxonomy_widget_process($element, &$form_state, $complete_form) {
+ $tids = $element['#return_value'];
+
+ // If #return_value is array(NULL), then nothing was selected!
+ if (count($tids) == 1 && $tids[0] === NULL) {
+ $element['#return_value'] = array();
+ return $element;
+ }
+
+ $items = array();
+ foreach ($tids as $tid) {
+ $items[] = array('tid' => $tid);
+ }
+
+ $element['#return_value'] = $items;
+
+ return $element;
+}
+
+
+//----------------------------------------------------------------------------
+// Field API formatter hooks.
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function hs_taxonomy_field_formatter_info() {
+ return array(
+ 'hs_taxonomy_term_reference_hierarchical_text' => array(
+ 'label' => t('Hierarchical text'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_hierarchical_links' => array(
+ 'label' => t('Hierarchical links'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_hierarchical_links_last_text' => array(
+ 'label' => t('Hierarchical links last text'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_hierarchical_text_last_link' => array(
+ 'label' => t('Hierarchical text last link'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_last_link_only' => array(
+ 'label' => t('Last link only'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ 'hs_taxonomy_term_reference_last_text_only' => array(
+ 'label' => t('Last text only'),
+ 'field types' => array('taxonomy_term_reference'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_prepare_view().
+ */
+function hs_taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
+ // Extract required field information.
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+ $vid = $vocabulary->vid;
+
+ // Get the config for this field.
+ module_load_include('inc', 'hierarchical_select', 'includes/common');
+ $config_id = hs_taxonomy_get_config_id($field['field_name']);
+ $config = hierarchical_select_common_config_get($config_id);
+ $config += array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => $vid,
+ ),
+ );
+
+ // Collect every possible term attached to any of the fieldable entities.
+ // Copied from taxonomy_field_formatter_prepare_view().
+ foreach ($entities as $id => $entity) {
+ $selection = array();
+
+ foreach ($items[$id] as $delta => $item) {
+ // Force the array key to prevent duplicates.
+ if ($item['tid'] != 'autocreate') {
+ $selection[$item['tid']] = $item['tid'];
+ }
+ }
+
+ // Generate a dropbox out of the selection. This will automatically
+ // calculate all lineages for us.
+ $dropbox = _hierarchical_select_dropbox_generate($config, $selection);
+
+ // Store additional information in each item that's required for
+ // Hierarchical Select's custom formatters that are compatible with the
+ // save_lineage functionality.
+ if (!empty($dropbox->lineages)) {
+ foreach (array_keys($dropbox->lineages) as $lineage) {
+ foreach ($dropbox->lineages[$lineage] as $level => $details) {
+ $tid = $details['value'];
+
+ // Look up where this term (tid) is stored in the items array.
+ $key = array_search($tid, $selection);
+
+ // Store the additional information. One term can occur in multiple
+ // lineages: when Taxonomy's "multiple parents" functionality is
+ // being used.
+ $items[$id][$key]['hs_lineages'][] = array(
+ 'lineage' => $lineage,
+ 'level' => $level,
+ 'label' => $details['label'],
+ 'tid' => $tid,
+ );
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function hs_taxonomy_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ // Extract required field information.
+ $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
+
+ // Extract the lineage information from the items (this was added by
+ // hs_taxonomy_field_formatter_prepare_view()).
+ $lineages = array();
+ foreach ($items as $delta => $item) {
+ if (!empty($item['hs_lineages'])) {
+ $metadata = $item['hs_lineages'];
+
+ for ($i = 0; $i < count($metadata); $i++) {
+ $term = new StdClass();
+ $term->tid = $metadata[$i]['tid'];
+ $term->vid = $vocabulary->vid;
+ $term->vocabulary_machine_name = $vocabulary->machine_name;
+ $term->name = $metadata[$i]['label'];
+
+ $lineages[$metadata[$i]['lineage']][$metadata[$i]['level']] = $term;
+ }
+ }
+ }
+
+ // Actual formatting.
+ $element = array();
+ switch ($display['type']) {
+ case 'hs_taxonomy_term_reference_hierarchical_text':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]); $level++) {
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][$level] = array(
+ '#markup' => $term->name,
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_hierarchical_links':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]); $level++) {
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $uri['options']['html'] = TRUE;
+ $element[$l]['#lineage'][$level] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_hierarchical_links_last_text':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]) - 1; $level++) {
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $element[$l]['#lineage'][$level] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][$level] = array(
+ '#markup' => $term->name,
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_hierarchical_text_last_link':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ for ($level = 0; $level < count($lineages[$l]) - 1; $level++) {
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][$level] = array(
+ '#markup' => $term->name,
+ );
+ }
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $element[$l]['#lineage'][$level] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_last_text_only':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $element[$l]['#lineage'][0] = array(
+ '#markup' => $term->name,
+ );
+ }
+ }
+ break;
+
+ case 'hs_taxonomy_term_reference_last_link_only':
+ for ($l = 0; $l < count($lineages); $l++) {
+ $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
+ if (count($lineages[$l]) > 0) {
+ $level = count($lineages[$l]) - 1;
+ $term = $lineages[$l][$level];
+ $uri = entity_uri('taxonomy_term', $term);
+ $element[$l]['#lineage'][0] = array(
+ '#type' => 'link',
+ '#title' => $term->name,
+ '#href' => $uri['path'],
+ '#options' => $uri['options'],
+ );
+ }
+ }
+ break;
+ }
+
+ if (!empty($element)) {
+ $element['#attached']['css'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css';
+ }
+
+ return $element;
+}
+
+
+//----------------------------------------------------------------------------
+// Hierarchical Select hooks.
+
+/**
+ * Implementation of hook_hierarchical_select_params().
+ */
+function hs_taxonomy_hierarchical_select_params() {
+ $params = array(
+ 'vid',
+ 'exclude_tid', // Allows a term to be excluded (necessary for the taxonomy_form_term form).
+ 'root_term', // Displays a fake "" term in the root level (necessary for the taxonomy_form-term form).
+ 'entity_count_for_node_type', // Restrict the entity count to a specific node type.
+ );
+ return $params;
+}
+
+/**
+ * Implementation of hook_hierarchical_select_root_level().
+ */
+function hs_taxonomy_hierarchical_select_root_level($params) {
+ if (!isset($params['vid'])) {
+ return array();
+ }
+ // TODO: support multiple parents, i.e. support "save lineage".
+ $vocabulary = taxonomy_vocabulary_load($params['vid']);
+ $terms = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1);
+
+ // If the root_term parameter is enabled, then prepend a fake "" term.
+ if (isset($params['root_term']) && $params['root_term'] === TRUE) {
+ $root_term = new StdClass();
+ $root_term->tid = 0;
+ $root_term->name = '<' . t('root') . '>';
+ $terms = array_merge(array($root_term), $terms);
+ }
+
+ // Unset the term that's being excluded, if it is among the terms.
+ if (isset($params['exclude_tid'])) {
+ foreach ($terms as $key => $term) {
+ if ($term->tid == $params['exclude_tid']) {
+ unset($terms[$key]);
+ }
+ }
+ }
+
+ // If the Term Permissions module is installed, honor its settings.
+ if (function_exists('term_permissions_allowed')) {
+ global $user;
+ foreach ($terms as $key => $term) {
+ if (!term_permissions_allowed($term->tid, $user) ) {
+ unset($terms[$key]);
+ }
+ }
+ }
+
+ return _hs_taxonomy_hierarchical_select_terms_to_options($terms);
+}
+
+/**
+ * Implementation of hook_hierarchical_select_children().
+ */
+function hs_taxonomy_hierarchical_select_children($parent, $params) {
+ if (isset($params['root_term']) && $params['root_term'] && $parent == 0) {
+ return array();
+ }
+
+ $terms = taxonomy_get_children($parent, $params['vid']);
+
+ // Unset the term that's being excluded, if it is among the children.
+ if (isset($params['exclude_tid'])) {
+ unset($terms[$params['exclude_tid']]);
+ }
+
+ // If the Term Permissions module is installed, honor its settings.
+ if (function_exists('term_permissions_allowed')) {
+ global $user;
+ foreach ($terms as $key => $term) {
+ if (!term_permissions_allowed($term->tid, $user) ) {
+ unset($terms[$key]);
+ }
+ }
+ }
+
+ return _hs_taxonomy_hierarchical_select_terms_to_options($terms);
+}
+
+/**
+ * Implementation of hook_hierarchical_select_lineage().
+ */
+function hs_taxonomy_hierarchical_select_lineage($item, $params) {
+ $lineage = array();
+
+ if (isset($params['root_term']) && $params['root_term'] && $item == 0) {
+ return array(0);
+ }
+
+ $terms = array_reverse(hs_taxonomy_get_parents_all($item));
+ foreach ($terms as $term) {
+ $lineage[] = $term->tid;
+ }
+ return $lineage;
+}
+
+/**
+ * Alternative version of taxonomy_get_parents_all(): instead of using all
+ * parents of a term (i.e. when multiple parents are being used), only the
+ * first is kept.
+ */
+function hs_taxonomy_get_parents_all($tid) {
+ $parents = array();
+ if ($term = taxonomy_term_load($tid)) {
+ $parents[] = $term;
+ $n = 0;
+ while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
+ $parents = array_merge($parents, array(reset($parent)));
+ $n++;
+ }
+ }
+ return $parents;
+}
+
+/**
+ * Implementation of hook_hierarchical_select_valid_item().
+ */
+function hs_taxonomy_hierarchical_select_valid_item($item, $params) {
+ if (isset($params['root_term']) && $params['root_term'] && $item == 0) {
+ return TRUE;
+ }
+
+ if (!is_numeric($item) || $item < 1 || (isset($params['exclude_tid']) && $item == $params['exclude_tid'])) {
+ return FALSE;
+ }
+
+ $term = taxonomy_term_load($item);
+ if (!$term) {
+ return FALSE;
+ }
+
+ // If the Term Permissions module is installed, honor its settings.
+ if (function_exists('term_permissions_allowed')) {
+ global $user;
+ if (!term_permissions_allowed($term->tid, $user)) {
+ return FALSE;
+ }
+ }
+
+ return ($term->vid == $params['vid']);
+}
+
+/**
+ * Implementation of hook_hierarchical_select_item_get_label().
+ */
+function hs_taxonomy_hierarchical_select_item_get_label($item, $params) {
+ static $labels = array();
+
+ if (!isset($labels[$item])) {
+ if ($item === 0 && isset($params['root_term']) && $params['root_term'] === TRUE) {
+ $term = new StdClass();
+ $term->name = '<' . t('root') . '>';
+ }
+ else {
+ $term = taxonomy_term_load($item);
+ // Try to translate the label if the i18n module is available.
+ if (module_exists('i18n_taxonomy')) {
+ $term->name = i18n_taxonomy_term_name($term);
+ }
+ }
+ $labels[$item] = $term->name;
+ }
+
+ return $labels[$item];
+}
+
+/**
+ * Implementation of hook_hierarchical_select_create_item().
+ */
+function hs_taxonomy_hierarchical_select_create_item($label, $parent, $params) {
+ $term = new StdClass();
+ $term->vid = $params['vid'];
+ $term->name = html_entity_decode($label, ENT_QUOTES);
+ $term->description = '';
+ $term->parent = $parent;
+
+ $status = taxonomy_term_save($term);
+
+ if ($status !== FALSE) {
+ // Reset the cached tree.
+ _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1, TRUE);
+
+ // Retrieve the tid.
+ $children = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], $parent, 1);
+ foreach ($children as $term) {
+ if ($term->name == $label) {
+ return $term->tid;
+ }
+ }
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Implementation of hook_hierarchical_select_entity_count().
+ */
+function hs_taxonomy_hierarchical_select_entity_count($item, $params) {
+
+ $num_entities = 0;
+ $selected_bundles = $params['entity_count']['settings']['entity_types'];
+ $count_children = $params['entity_count']['settings']['count_children'];
+
+ // Maybe this needs some more caching and value-updates on entity_save()/
+ // _update()/delete().
+ if (empty($num_entities)) {
+ $index_table = 'taxonomy_index';
+ if (module_exists('taxonomy_entity_index')) {
+ $index_table = 'taxonomy_entity_index';
+ }
+
+ // Count entities associated to this term.
+ $query = db_select($index_table, 'ti');
+ $query->fields('ti');
+ $query->condition('ti.tid', $item);
+ if (module_exists('taxonomy_entity_index')) {
+ _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles);
+ }
+ $result = $query->execute();
+
+ $num_entities = $result->rowCount();
+
+ if ($count_children) {
+ $tids = array();
+ $tree = taxonomy_get_tree($params['vid'], $item);
+ foreach ($tree as $child_term) {
+ $tids[] = $child_term->tid;
+ }
+ if (count($tids)) {
+ // Count entities associated to child terms.
+ $query = db_select($index_table, 'ti');
+ $query->fields('ti');
+ $query->condition('ti.tid', $tids, 'IN');
+ if (module_exists('taxonomy_entity_index')) {
+ _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles);
+ }
+ $result = $query->execute();
+
+ $num_entities += $result->rowCount();
+ }
+ }
+ }
+
+ return $num_entities;
+}
+
+/**
+ * Implementation of hook_hierarchical_select_implementation_info().
+ */
+function hs_taxonomy_hierarchical_select_implementation_info() {
+ return array(
+ 'hierarchy type' => t('Taxonomy'),
+ 'entity type' => t('Node'),
+ );
+}
+
+/**
+ * Implementation of hook_hierarchical_select_config_info().
+ */
+function hs_taxonomy_hierarchical_select_config_info() {
+ static $config_info;
+
+ if (!isset($config_info)) {
+ $config_info = array();
+ $fields = field_info_fields();
+ foreach ($fields as $field_name => $field) {
+ foreach ($field['bundles'] as $entity_type => $bundles) {
+ foreach ($bundles as $bundle) {
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+ if ($instance['widget']['type'] == 'taxonomy_hs') {
+ $bundles_info = field_info_bundles($entity_type);
+ $entity_info = entity_get_info($entity_type);
+ $machine_name = $field['settings']['allowed_values'][0]['vocabulary'];
+ $vocabulary = taxonomy_vocabulary_machine_name_load($machine_name);
+ $config_id = hs_taxonomy_get_config_id($field_name);
+ $config_info[$config_id] = array(
+ 'config_id' => $config_id,
+ 'hierarchy type' => t('Taxonomy'),
+ 'hierarchy' => t('Vocabulary') . ': ' . l(t($vocabulary->name), "admin/structure/taxonomy/$vocabulary->machine_name") . ' ' . t('Field label') . ': ' . $instance['label'] . ' ' . t('Field machine name') . ': ' . $field_name . '',
+ 'entity type' => $entity_info['label'],
+ 'bundle' => l(t($bundles_info[$bundle]['label']), "admin/structure/types/manage/$bundle"),
+ 'context type' => '',
+ 'context' => '',
+ 'edit link' => isset($bundles_info[$bundle]['admin']['real path']) ? $bundles_info[$bundle]['admin']['real path'] . "/fields/$field_name/widget-type" : $bundles_info[$bundle]['admin']['path'] . "/fields/$field_name/widget-type",
+ );
+
+ }
+ }
+ }
+ }
+ }
+
+ return $config_info;
+}
+
+
+//----------------------------------------------------------------------------
+// Token hooks.
+
+/**
+ * Implementation of hook_token_values().
+ */
+/*
+// TODO: port this to D7.
+function hs_taxonomy_token_values($type, $object = NULL, $options = array()) {
+ static $hs_vids;
+ static $all_vids;
+
+ $separator = variable_get('hs_taxonomy_separator', variable_get('pathauto_separator', '-'));
+
+ $values = array();
+ switch ($type) {
+ case 'node':
+ $node = $object;
+
+ // Default values.
+ $values['save-lineage-termpath'] = $values['save-lineage-termpath-raw'] = '';
+
+ // If $node->taxonomy doesn't exist, these tokens cannot be created!
+ if (!is_object($node) || !isset($node->taxonomy) || !is_array($node->taxonomy)) {
+ return $values;
+ }
+
+ // Find out which vocabularies are using Hierarchical Select.
+ if (!isset($hs_vids)) {
+ $hs_vids = array();
+ // TODO Please convert this statement to the D7 database API syntax.
+ $result = db_query("SELECT SUBSTRING(name, 30, 3) AS vid FROM {variable} WHERE name LIKE 'taxonomy_hierarchical_select_%' AND value LIKE 'i:1\;';");
+ while ($o = db_fetch_object($result)) {
+ $hs_vids[] = $o->vid;
+ }
+ }
+
+ // Get a list of all existent vids, so we can generate an empty token
+ // when a token is requested for a vocabulary that's not associated with
+ // the current content type.
+ if (!isset($all_vids)) {
+ $all_vids = array();
+ $result = db_query("SELECT vid FROM {taxonomy_vocabulary}");
+ while ($row = db_fetch_object($result)) {
+ $all_vids[] = $row->vid;
+ }
+ }
+
+ // Generate the per-vid "save-lineage-termpath" tokens.
+ foreach ($all_vids as $vid) {
+ $terms = array();
+ if (in_array($vid, $hs_vids) && isset($node->taxonomy[$vid])) {
+ $selection = $node->taxonomy[$vid];
+ $terms = _hs_taxonomy_token_termpath_for_vid($selection, $vid);
+ }
+
+ $terms_raw = $terms;
+ $terms = array_map('check_plain', $terms);
+ $values["save-lineage-termpath:$vid"] = !empty($options['pathauto']) ? $terms : implode($separator, $terms);
+ $values["save-lineage-termpath-raw:$vid"] = !empty($options['pathauto']) ? $terms_raw : implode($separator, $terms_raw);
+ }
+
+ // We use the terms of the first vocabulary that uses Hierarchical
+ // Select for the default "save-lineage-termpath" tokens.
+ $vids = array_intersect(array_keys($node->taxonomy), $hs_vids);
+ if (!empty($vids)) {
+ $vid = $vids[0];
+ $values['save-lineage-termpath'] = $values["save-lineage-termpath:$vid"];
+ $values['save-lineage-termpath-raw'] = $values["save-lineage-termpath-raw:$vid"];
+ }
+ break;
+ }
+
+ return $values;
+}
+*/
+
+/**
+ * Implementation of hook_token_list().
+ */
+/*
+// TODO: port this to D7.
+function hs_taxonomy_token_list($type = 'all') {
+ if ($type == 'node' || $type == 'all') {
+ $tokens['node']['save-lineage-termpath'] = t('Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.');
+ $tokens['node']['save-lineage-termpath-raw'] = t('As [save-linage-termpath]. WARNING - raw user input.');
+
+ $tokens['node']['save-lineage-termpath:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.');
+ $tokens['node']['save-lineage-termpath-raw:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. As [save-linage-termpath]. WARNING - raw user input.');
+
+ return $tokens;
+ }
+}
+*/
+
+/**
+ * Helper function for hs_taxonomy_token_values().
+ */
+function _hs_taxonomy_token_termpath_for_vid($selection, $vid) {
+ $terms = array();
+ $selection = (is_array($selection)) ? $selection : array($selection);
+
+ // Generate the part we'll need of the Hierarchical Select configuration.
+ $config = array(
+ 'module' => 'hs_taxonomy',
+ 'save_lineage' => 1,
+ 'params' => array(
+ 'vid' => $vid,
+ 'exclude_tid' => NULL,
+ 'root_term' => NULL,
+ ),
+ );
+
+ // Validate all items in the selection, if any.
+ if (!empty($selection)) {
+ foreach ($selection as $key => $item) {
+ $valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $selection[$key], $config['params']);
+ if (!$valid) {
+ unset($selection[$key]);
+ }
+ }
+ }
+
+ // Generate a dropbox out of the selection. This will automatically
+ // calculate all lineages for us.
+ // If the selection is empty, then the tokens will be as well.
+ if (!empty($selection)) {
+ $dropbox = _hierarchical_select_dropbox_generate($config, $selection);
+
+ // If no lineages could be generated, these tokens cannot be created!
+ if (empty($dropbox->lineages)) {
+ return $terms;
+ }
+
+ // We pick the first lineage.
+ $lineage = $dropbox->lineages[0];
+
+ // Finally, we build the tokens.
+ foreach ($lineage as $item) {
+ $terms[] = $item['label'];
+ }
+ }
+
+ return $terms;
+}
+
+
+//----------------------------------------------------------------------------
+// Theme functions.
+
+/**
+ * Format a lineage for one of HS Taxonomy's custom Term reference formatters.
+ */
+function theme_hs_taxonomy_formatter_lineage($variables) {
+ $output = '';
+ $lineage = $variables['lineage'];
+ $separator = theme('hierarchical_select_item_separator');
+
+ // Render each item within a lineage.
+ $items = array();
+ foreach ($lineage as $level => $item ) {
+ $line = '';
+ $line .= drupal_render($item);
+ $line .= '';
+ $items[] = $line;
+ }
+ $output .= implode($separator, $items);
+
+ return $output;
+}
+
+
+//----------------------------------------------------------------------------
+// Private functions.
+
+/**
+ * Drupal core's taxonomy_get_tree() doesn't allow us to reset the cached
+ * trees, which obviously causes problems when you create new items between
+ * two calls to it.
+ */
+function _hs_taxonomy_hierarchical_select_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL, $reset = FALSE) {
+ static $children, $parents, $terms;
+
+ if ($reset) {
+ $children = $parents = $terms = array();
+ }
+
+ $tree = array();
+
+ if (!is_array($parent)) {
+ $parent = array($parent);
+ }
+
+ $max_depth = (is_null($max_depth)) ? 99999999 : $max_depth;
+ $depth++;
+
+ // We cache trees, so it's not CPU-intensive to call get_tree() on a term
+ // and its children, too.
+ if ($max_depth <= $depth) {
+ return $tree;
+ }
+ // Prepare queue for the "IN ( .. )" part of query.
+ $queue = array();
+ foreach ($parent as $single_parent) {
+ // Queue branch for processing if it's not cached yet.
+ if (!isset($children[$vid][$single_parent])) {
+ $queue[] = $single_parent;
+ // Use an empty array to distinguish between a stub (without children)
+ // term and a branch that is not loaded yet.
+ $children[$vid][$single_parent] = array();
+ }
+ }
+ if (!empty($queue)) {
+
+ $query = db_select('taxonomy_term_data', 't');
+ $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
+ $result = $query
+ ->addTag('translatable')
+ ->addTag('term_access')
+ ->addTag('hs_taxonomy_tree')
+ ->fields('t')
+ ->fields('h', array('parent'))
+ ->condition('t.vid', $vid)
+ ->condition('parent', array_merge(array($vid), $queue), 'IN')
+ ->orderBy('t.weight')
+ ->orderBy('t.name')
+ ->execute();
+
+ foreach ($result as $term) {
+ $children[$vid][$term->parent][] = $term->tid;
+ $parents[$vid][$term->tid][] = $term->parent;
+ $terms[$vid][$term->tid] = $term;
+ }
+ }
+
+ // Provide support for Title module. If Title module is enabled and this
+ // vocabulary uses translated term names we want output those terms with their
+ // translated version. Therefore a full taxonomy term entity load is required,
+ // similar to taxonomy_get_tree().
+ if (!empty($terms) && module_exists('title')) {
+ $vocabulary = taxonomy_vocabulary_load($vid);
+ if (title_field_replacement_enabled('taxonomy_term', $vocabulary->machine_name, 'name')) {
+ $term_entities = taxonomy_term_load_multiple(array_keys($terms[$vid]));
+ }
+ }
+
+ $next_parent = array();
+ foreach ($parent as $single_parent) {
+ foreach ($children[$vid][$single_parent] as $child) {
+ $term = isset($term_entities[$child]) ? $term_entities[$child] : $terms[$vid][$child];
+ $term = clone $term;
+ $term->depth = $depth;
+ // The "parent" attribute is not useful, as it would show one parent only.
+ unset($term->parent);
+ $term->parents = $parents[$vid][$child];
+ $tree[] = $term;
+ // Need more steps ?
+ if ($max_depth > $depth + 1) {
+ // Queue children for the next step down the tree. Do not process
+ // children which we already know as stub ones.
+ if (!isset($children[$vid][$child]) || !empty($children[$vid][$child])) {
+ $next_parent[] = $child;
+ }
+ }
+ }
+ }
+ if (!empty($next_parent)) {
+ // Process multiple children together i.e. next level.
+ $tree = array_merge($tree, _hs_taxonomy_hierarchical_select_get_tree($vid, $next_parent, $depth, $max_depth));
+ }
+
+ return isset($tree) ? $tree : array();
+}
+
+/**
+ * Returns the configuration ID that would be used for the specified field.
+ *
+ * @param string $field_name
+ * The field machine name.
+ *
+ * @return string
+ * The config id for the provided field.
+ */
+function hs_taxonomy_get_config_id($field_name) {
+ return "taxonomy-{$field_name}";
+}
+
+/**
+ * Drupal core's taxonomy_term_count_nodes() is buggy. See
+ * http://drupal.org/node/144969#comment-843000.
+ */
+function hs_taxonomy_term_count_nodes($tid, $type = 0) {
+ static $count;
+
+ $tids = array($tid);
+ if ($term = taxonomy_term_load($tid)) {
+ $tree = _hs_taxonomy_hierarchical_select_get_tree($term->vid, $tid);
+ foreach ($tree as $descendant) {
+ $tids[] = $descendant->tid;
+ }
+ }
+
+ if (!isset($count[$type][$tid])) {
+ $query = db_select('taxonomy_term_node','t');
+ $query->join('node', 'n', 't.nid = n.nid');
+ $query->addExpression('COUNT(DISTINCT(n.nid))', 'count')
+ ->condition('n.status', 1)
+ ->condition('t,tid', $tids);
+
+ if (!is_numeric($type)) {
+ $query->condition('n.type', $type);
+ }
+
+ $query->addTag('hs_taxonomy_term_count_nodes');
+ $query->addTag('term_access');
+ $result = $query->execute();
+
+ $count[$type][$tid] = $result->fetchField();
+ }
+ return $count[$type][$tid];
+}
+
+/**
+ * Transform an array of terms into an associative array of options, for use
+ * in a select form item.
+ *
+ * @param $terms
+ * An array of term objects.
+ * @return
+ * An associative array of options, keys are tids, values are term names.
+ */
+function _hs_taxonomy_hierarchical_select_terms_to_options($terms) {
+ $options = array();
+ $use_i18n = module_exists('i18n_taxonomy');
+ foreach ($terms as $key => $term) {
+ // Use the translated term when available!
+ $options[$term->tid] = $use_i18n && isset($term->vid) ? i18n_taxonomy_term_name($term) : $term->name;
+ }
+ return $options;
+}
+
+/**
+ * Get the depth of a vocabulary's tree.
+ *
+ * @param $vid
+ * A vocabulary id.
+ * @return
+ * The depth of the vocabulary's tree.
+ */
+function _hs_taxonomy_hierarchical_select_get_depth($vid) {
+ $depth = -99999;
+ $tree = _hs_taxonomy_hierarchical_select_get_tree($vid);
+ foreach ($tree as $term) {
+ if ($term->depth > $depth) {
+ $depth = $term->depth;
+ }
+ }
+ return $depth;
+}
+
+/**
+ * Helper function to add entity_type and bundles to count query in form of a
+ * and/or combination.
+ *
+ * @param object $query
+ * A db_select query object.
+ * @param array $selected_bundles
+ * An associative array of entities that contain bundles.
+ */
+function _hs_taxonomy_add_entity_bundles_condition_to_query(&$query, $selected_bundles) {
+ $db_or = db_or();
+ foreach ($selected_bundles as $entity_type => $bundles) {
+ $db_and = db_and();
+ $db_and->condition('ti.entity_type', $entity_type);
+ $db_and->condition('ti.bundle', array_shift($bundles) , 'IN');
+ $db_or->condition($db_and);
+ }
+ $query->condition($db_or);
+}
diff --git a/sites/all/modules/hierarchical_select/modules/hser/README.txt b/sites/all/modules/hierarchical_select/modules/hser/README.txt
new file mode 100644
index 0000000..4eb1737
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hser/README.txt
@@ -0,0 +1,8 @@
+This module allows you to use hierarchical_select (version 7.x-3.x) as a widget
+for a taxonomy-based entityreference field.
+
+To use it, create an entityreference field, with the Hierarchical Select widget,
+select "Taxonomy term" as the target type, and select one vocabulary (you must
+choose exactly one) as the target bundle.
+
+Credit: John Morahan, iO1 and iVillage.
diff --git a/sites/all/modules/hierarchical_select/modules/hser/hser.info b/sites/all/modules/hierarchical_select/modules/hser/hser.info
new file mode 100644
index 0000000..c3c87d1
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hser/hser.info
@@ -0,0 +1,18 @@
+name = Hierarchical Select Entity Reference
+description = Use the hierarchical select widget for entity reference fields, using taxonomy to provide hierarchy if appropriate, otherwise flat.
+package = Form Elements
+core = 7.x
+dependencies[] = hierarchical_select
+dependencies[] = hs_taxonomy
+dependencies[] = entityreference
+dependencies[] = entity
+dependencies[] = ctools
+dependencies[] = options
+dependencies[] = field
+
+; Information added by Drupal.org packaging script on 2016-06-03
+version = "7.x-3.0-beta7"
+core = "7.x"
+project = "hierarchical_select"
+datestamp = "1464981541"
+
diff --git a/sites/all/modules/hierarchical_select/modules/hser/hser.module b/sites/all/modules/hierarchical_select/modules/hser/hser.module
new file mode 100644
index 0000000..eaaf47d
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/modules/hser/hser.module
@@ -0,0 +1,132 @@
+ array(
+ 'label' => t('Hierarchical Select'),
+ 'field types' => array('entityreference'),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ 'settings' => array(
+ 'editable' => FALSE,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function hser_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $settings = $widget['settings'] + field_info_widget_settings($widget['type']);
+
+ $form = array();
+
+ if ($widget['type'] == 'hser_hierarchy') {
+ $form['editable'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Editable'),
+ '#default_value' => $settings['editable'],
+ '#description' => t('Select this to allow users to use the hierarchical select widget to create new terms in the selected vocabulary.'),
+ );
+ }
+
+ return $form;
+}
+
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function hser_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ if ($field['settings']['target_type'] == 'taxonomy_term') {
+ $vocabularies = $field['settings']['handler_settings']['target_bundles'];
+ if ((count($vocabularies) == 1) && ($vocabulary = taxonomy_vocabulary_machine_name_load(reset($vocabularies)))) {
+ $default_value = array();
+ foreach ($items as $item) {
+ $default_value[] = $item['target_id'];
+ }
+ $element += array(
+ '#type' => 'hierarchical_select',
+ '#size' => 1,
+ '#default_value' => $default_value,
+ '#element_validate' => array('_hser_element_validate'),
+ '#required' => $instance['required'],
+ '#config' => array(
+ 'module' => 'hs_taxonomy',
+ 'params' => array(
+ 'vid' => $vocabulary->vid,
+ ),
+ 'save_lineage' => FALSE,
+ 'enforce_deepest' => FALSE,
+ 'resizable' => FALSE,
+ 'level_labels' => array('status' => FALSE),
+ 'dropbox' => array(
+ 'status' => ($field['cardinality'] != 1),
+ 'limit' => $field['cardinality'],
+ ),
+ 'editability' => array(
+ 'status' => $instance['widget']['settings']['editable'],
+ 'allow_new_levels' => TRUE,
+ 'max_levels' => 0,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'render_flat_select' => FALSE,
+ ),
+ );
+ return $element;
+ }
+ }
+ // If we reach this point, we decided that hierarchical_select would not be
+ // appropriate for some reason (not taxonomy, no vocabulary selected, etc).
+ // So instead we fall back to a normal select widget.
+ $instance['widget']['type'] = 'options_select';
+ return options_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
+}
+
+/**
+ * Element validation callback for field widget hierarchical select element.
+ */
+function _hser_element_validate($element, &$form_state, $form) {
+ $value = array();
+ foreach ($element['#value'] as $delta => $target_id) {
+ $value[$delta]['target_id'] = $target_id;
+ }
+ form_set_value($element, $value, $form_state);
+
+ if ($element['#required'] && (!isset($form_state['submit_handlers'][0]) || $form_state['submit_handlers'][0] !== 'hierarchical_select_ajax_update_submit')) {
+ if (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0) || (array_key_exists(0, $element['#value'])) && !$element['#value'][0]) {
+ form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
+ _hierarchical_select_form_set_error_class($element);
+ }
+ }
+}
+
+/**
+ * Implements hook_node_validate().
+ *
+ * Temporary workaround for https://drupal.org/node/1293166 - remove when that
+ * bug is fixed.
+ */
+function hser_node_validate($node, $form, &$form_state) {
+ if (arg(0) == 'hierarchical_select_ajax') {
+ form_set_error('');
+ }
+}
diff --git a/sites/all/modules/hierarchical_select/tests/internals.test b/sites/all/modules/hierarchical_select/tests/internals.test
new file mode 100644
index 0000000..e734124
--- /dev/null
+++ b/sites/all/modules/hierarchical_select/tests/internals.test
@@ -0,0 +1,433 @@
+ array(
+ 'label' => LABEL_EURO,
+ 'children' => array(
+ EURO_BE => array(
+ 'label' => LABEL_EURO_BE,
+ 'children' => array(
+ EURO_BE_BRU => array(
+ 'label' => LABEL_EURO_BE_BRU,
+ ),
+ EURO_BE_HAS => array(
+ 'label' => LABEL_EURO_BE_HAS,
+ ),
+ ),
+ ),
+ EURO_FR => array(
+ 'label' => LABEL_EURO_FR,
+ ),
+ ),
+ ),
+ ASIA => array(
+ 'label' => LABEL_ASIA,
+ 'children' => array(
+ ASIA_CH => array(
+ 'label' => LABEL_ASIA_CH,
+ ),
+ ASIA_JP => array(
+ 'label' => LABEL_ASIA_JP,
+ 'children' => array(
+ ASIA_JP_TOK => array(
+ 'label' => LABEL_ASIA_JP_TOK,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+
+ /**
+ * Implementation of getInfo().
+ */
+ public function getInfo() {
+ return array(
+ 'name' => 'Internals',
+ 'description' => 'Checks whether all internals are working: the
+ building of the hierarchy and dropbox objects.',
+ 'group' => 'Hierarchical Select',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ public function setUp() {
+ parent::setUp('hierarchical_select', 'hs_smallhierarchy');
+ }
+
+ // In this test, all settings are disabled.
+ public function testAllSettingsOff() {
+ // Generate form item.
+ $form_item = array(
+ '#required' => FALSE,
+ '#config' => array(
+ 'module' => 'hs_smallhierarchy',
+ 'params' => array(
+ 'hierarchy' => $this->small_hierarchy,
+ 'id' => 'driverpack_platforms',
+ 'separator' => '|',
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 0,
+ 'resizable' => 1,
+ 'level_labels' => array(
+ 'status' => 0,
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ 'limit' => 0,
+ 'reset_hs' => 1,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ 'item_types' => array(),
+ 'allowed_levels' => array(),
+ 'allow_new_levels' => 0,
+ 'max_levels' => 3,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'animation_delay' => 400,
+ 'exclusive_lineages' => array(),
+ 'render_flat_select' => 0,
+ ),
+ );
+
+ // No selection
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(), array());
+ $reference = new stdClass();
+ $reference->lineage = array(
+ 0 => 'none',
+ );
+ $reference->levels = array(
+ 0 => array(
+ 'none' => '',
+ LINEAGE_EURO => LABEL_EURO,
+ LINEAGE_ASIA => LABEL_ASIA,
+ ),
+ );
+ $reference->childinfo = array(
+ 0 => array(
+ LINEAGE_EURO => 2,
+ LINEAGE_ASIA => 2,
+ ),
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => 'label_1',
+ );
+ $reference->levels[1] = array(
+ 'label_1' => '',
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > France
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_FR), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_FR,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > Belgium
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_BE), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_BE,
+ 2 => 'label_2',
+ );
+ $reference->levels[1] = array(
+ 'label_1' => '',
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->levels[2] = array(
+ 'label_2' => '',
+ LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU,
+ LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_EURO_BE_BRU => 0,
+ LINEAGE_EURO_BE_HAS => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => 'label_1',
+ );
+ $reference->levels[1] = array(
+ 'label_1' => '',
+ LINEAGE_ASIA_CH => LABEL_ASIA_CH,
+ LINEAGE_ASIA_JP => LABEL_ASIA_JP,
+ );
+ unset($reference->levels[2]);
+ $reference->childinfo[1] = array(
+ LINEAGE_ASIA_CH => 0,
+ LINEAGE_ASIA_JP => 1,
+ );
+ unset($reference->childinfo[2]);
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia > Japan > Tokyo
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA_JP_TOK), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => LINEAGE_ASIA_JP,
+ 2 => LINEAGE_ASIA_JP_TOK,
+ );
+ $reference->levels[2] = array(
+ 'label_2' => '',
+ LINEAGE_ASIA_JP_TOK => LABEL_ASIA_JP_TOK,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_ASIA_JP_TOK => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+ }
+
+ // In this test, only enforce_deepest enabled.
+ public function testEnforceDeepest() {
+ // Generate form item.
+ $form_item = array(
+ '#required' => FALSE,
+ '#config' => array(
+ 'module' => 'hs_smallhierarchy',
+ 'params' => array(
+ 'hierarchy' => $this->small_hierarchy,
+ 'id' => 'driverpack_platforms',
+ 'separator' => '|',
+ ),
+ 'save_lineage' => 0,
+ 'enforce_deepest' => 1,
+ 'resizable' => 1,
+ 'level_labels' => array(
+ 'status' => 0,
+ ),
+ 'dropbox' => array(
+ 'status' => 0,
+ 'limit' => 0,
+ 'reset_hs' => 1,
+ ),
+ 'editability' => array(
+ 'status' => 0,
+ 'item_types' => array(),
+ 'allowed_levels' => array(),
+ 'allow_new_levels' => 0,
+ 'max_levels' => 3,
+ ),
+ 'entity_count' => array(
+ 'enabled' => 0,
+ 'require_entity' => 0,
+ 'settings' => array(
+ 'count_children' => 0,
+ 'entity_types' => array(),
+ ),
+ ),
+ 'animation_delay' => 400,
+ 'exclusive_lineages' => array(),
+ 'render_flat_select' => 0,
+ ),
+ );
+
+ // No selection
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(), array());
+ $reference = new stdClass();
+ $reference->lineage = array(
+ 0 => 'label_0',
+ );
+ $reference->levels = array(
+ 0 => array(
+ 'none' => '',
+ LINEAGE_EURO => LABEL_EURO,
+ LINEAGE_ASIA => LABEL_ASIA,
+ ),
+ );
+ $reference->childinfo = array(
+ 0 => array(
+ LINEAGE_EURO => 2,
+ LINEAGE_ASIA => 2,
+ ),
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_BE,
+ 2 => LINEAGE_EURO_BE_BRU,
+ );
+ $reference->levels[1] = array(
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->levels[2] = array(
+ LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU,
+ LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_EURO_BE_BRU => 0,
+ LINEAGE_EURO_BE_HAS => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > France
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_FR), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_FR,
+ );
+ unset($reference->levels[2]);
+ unset($reference->childinfo[2]);
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Europe > Belgium
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_BE), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_EURO,
+ 1 => LINEAGE_EURO_BE,
+ 2 => LINEAGE_EURO_BE_BRU,
+ );
+ $reference->levels[1] = array(
+ LINEAGE_EURO_BE => LABEL_EURO_BE,
+ LINEAGE_EURO_FR => LABEL_EURO_FR,
+ );
+ $reference->levels[2] = array(
+ LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU,
+ LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS,
+ );
+ $reference->childinfo[1] = array(
+ LINEAGE_EURO_BE => 2,
+ LINEAGE_EURO_FR => 0,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_EURO_BE_BRU => 0,
+ LINEAGE_EURO_BE_HAS => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => LINEAGE_ASIA_CH,
+ );
+ $reference->levels[1] = array(
+ LINEAGE_ASIA_CH => LABEL_ASIA_CH,
+ LINEAGE_ASIA_JP => LABEL_ASIA_JP,
+ );
+ unset($reference->levels[2]);
+ $reference->childinfo[1] = array(
+ LINEAGE_ASIA_CH => 0,
+ LINEAGE_ASIA_JP => 1,
+ );
+ unset($reference->childinfo[2]);
+ $this->assertHierarchy($hierarchy, $reference);
+
+ // Asia > Japan > Tokyo
+ list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA_JP_TOK), array());
+ $reference->lineage = array(
+ 0 => LINEAGE_ASIA,
+ 1 => LINEAGE_ASIA_JP,
+ 2 => LINEAGE_ASIA_JP_TOK,
+ );
+ $reference->levels[2] = array(
+ LINEAGE_ASIA_JP_TOK => LABEL_ASIA_JP_TOK,
+ );
+ $reference->childinfo[2] = array(
+ LINEAGE_ASIA_JP_TOK => 0,
+ );
+ $this->assertHierarchy($hierarchy, $reference);
+ }
+
+
+ //--------------------------------------------------------------------------
+ // Private methods.
+
+ private function generate($element, $hs_selection, $db_selection, $op = 'Update') {
+ $config = $element['#config'];
+
+ // Generate the $hierarchy and $dropbox objects using the selections that
+ // were just calculated.
+ $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection);
+ $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox);
+
+ return array($hierarchy, $dropbox);
+ }
+
+ private function assertHierarchy($hierarchy, $reference) {
+ $this->assertIdentical($hierarchy->lineage, $reference->lineage, 'Hierarchy lineage is correct.');
+ $this->assertIdentical($hierarchy->levels, $reference->levels, 'Hierarchy levels is correct.');
+ $this->assertIdentical($hierarchy->childinfo, $reference->childinfo, 'Hierarchy child info is correct.');
+ }
+}
diff --git a/sites/all/modules/revisioning/LICENSE.txt b/sites/all/modules/revisioning/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/sites/all/modules/revisioning/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/revisioning/README.txt b/sites/all/modules/revisioning/README.txt
new file mode 100644
index 0000000..d6bc6e3
--- /dev/null
+++ b/sites/all/modules/revisioning/README.txt
@@ -0,0 +1,39 @@
+
+DESCRIPTION
+===========
+This module channels unpublished content as well as edits to current content
+into a queue for review by a moderator/publisher prior to becoming "live", i.e.
+visible to the public.
+
+We took our inspiration from the Revision Moderation module by Angie Byron,
+but found that a patch could not implement the deviating functionality our
+customers required, which would change the current behaviour of the RM module
+and surprise existing users.
+
+The Revisioning module now comes with two user interface models to chose from:
+"traditional" and "updated".
+
+Traditional: The traditional UI model is enabled by default. With the standard
+UI, the emphasis is on minimizing the number of controls, so each page is free
+of buttons and links that are not immediately relevant.
+
+Updated: The updated UI model can be selected by enabling the "revisioning_ux"
+submodule. In the updated UI, the emphasis is on maintaining consistent
+navigation controls. This allows revision moderators to flip between the tabs
+on the revisions operations page without ending up on a page that no longer
+contains all of the tabs formerly available.
+
+There are no database schema differences between the two UI models; you may
+enable revisioning_ux, try it out, and later go back to the traditional model
+without any difficulty.
+
+
+INSTALLATION & CONFIGURATION
+============================
+Step-by-step instructions can be found in the tutorials listed on the
+Revisioning project page, http://drupal.org/project/revisioning.
+
+AUTHORS
+=======
+Main module: Rik de Boer, Melbourne, Australia.
+Revisioning UX: Greg Anderson, USA
diff --git a/sites/all/modules/revisioning/THEMING.txt b/sites/all/modules/revisioning/THEMING.txt
new file mode 100644
index 0000000..0f077a5
--- /dev/null
+++ b/sites/all/modules/revisioning/THEMING.txt
@@ -0,0 +1,19 @@
+
+If you wish to override the way the revisions table is constructed,
+implement in your own module the function below.
+
+If you decided to do so, don't forget to register your theme_hook() via
+_theme() in a manner similar to what is done in revisioning_theme(),
+file revisioning_theme.inc.
+
+/*
+ * Custom alternative to the way Revisioning module themes its revisions table.
+ * @param $header
+ * @param $rows
+ * @return themed HTML, see for instance /includes/theme.inc/theme_table() and
+ * diff.module/theme_diff_table()
+ *
+ * @ingroup themeable
+ */
+ function theme_table_revisions($header, $rows) {
+ }
diff --git a/sites/all/modules/revisioning/css/revisioning-rtl.css b/sites/all/modules/revisioning/css/revisioning-rtl.css
new file mode 100644
index 0000000..da59e4b
--- /dev/null
+++ b/sites/all/modules/revisioning/css/revisioning-rtl.css
@@ -0,0 +1 @@
+/* Empty. Add your own. */
diff --git a/sites/all/modules/revisioning/css/revisioning.css b/sites/all/modules/revisioning/css/revisioning.css
new file mode 100644
index 0000000..92aed62
--- /dev/null
+++ b/sites/all/modules/revisioning/css/revisioning.css
@@ -0,0 +1,25 @@
+.table-revisions th.form-submit,
+.table-revisions td .form-type-checkbox {
+ text-align: center;
+}
+
+.revision-pending td {
+ background-color: #fcc; /* pale pink */
+}
+
+.revision-current td {
+ background-color: #faa; /* red-pink when current revision is unpublished */
+}
+
+.revision-current.published td {
+ background-color: #ffc; /* pale yellow, same as node/node.css */
+}
+
+div.block-revisioning ul {
+ padding: 0 0 0 1em;
+}
+
+div.block-revisioning ul li {
+/*list-style-type: square;*/
+ margin: 0;
+}
diff --git a/sites/all/modules/revisioning/js/revisioning-radios.js b/sites/all/modules/revisioning/js/revisioning-radios.js
new file mode 100644
index 0000000..fa8a5d8
--- /dev/null
+++ b/sites/all/modules/revisioning/js/revisioning-radios.js
@@ -0,0 +1,55 @@
+(function ($) {
+
+ Drupal.behaviors.suppressPublishedForModeratedContent = {
+ attach: function (context) {
+
+ var publishedDiv = $('.form-item-status', context);
+ var firstSummaryTab = $('.vertical-tabs span.summary:first', context)
+ var lastSummaryTab = $('.vertical-tabs span.summary:last', context);
+ var textNewRevision = Drupal.t('New revision');
+ var textNoRevision = Drupal.t('No revision');
+ var textPublished = Drupal.t('Published');
+ var textNotPublished = Drupal.t('Not published');
+ var checked = $('.form-item-revision-operation .form-radio:checked').val();
+
+ if (checked == 0) {
+ // REVISIONING_NO_REVISION.
+ firstSummaryTab.text(firstSummaryTab.text().replace(textNewRevision, textNoRevision));
+ }
+ else if (checked == 2) {
+ // REVISIONING_NEW_REVISION_WITH_MODERATION.
+ // Hide the "Published" check-box, as it does not apply in this mode
+ // because the new revision will always be unpublished.
+ publishedDiv.hide();
+ lastSummaryTab.text(lastSummaryTab.text().replace(textPublished, textNotPublished));
+ }
+
+ $('.form-radio').click(function() {
+ checked = $('.form-item-revision-operation .form-radio:checked').val();
+
+ if (checked == 2) {
+ // REVISIONING_NEW_REVISION_WITH_MODERATION.
+ publishedDiv.hide();
+ lastSummaryTab.text(lastSummaryTab.text().replace(textPublished, textNotPublished));
+ }
+ else {
+ // REVISIONING_NO_REVISION or REVISIONING_NEW_REVISION_NO_MODERATION.
+ publishedDiv.show();
+ if (checked == 0) {
+ // REVISIONING_NO_REVISION.
+ firstSummaryTab.text(firstSummaryTab.text().replace(textNewRevision, textNoRevision));
+ }
+ if ($('.form-item-status input').is(':checked')) {
+ updatedText = lastSummaryTab.text().replace(textNotPublished, textPublished);
+ }
+ else {
+ updatedText = lastSummaryTab.text().replace(textPublished, textNotPublished);
+ }
+ lastSummaryTab.text(updatedText);
+ }
+
+ });
+ }
+ };
+
+})(jQuery);
diff --git a/sites/all/modules/revisioning/revisioning.admin.inc b/sites/all/modules/revisioning/revisioning.admin.inc
new file mode 100644
index 0000000..a4f7aea
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning.admin.inc
@@ -0,0 +1,124 @@
+ 'fieldset',
+ '#title' => t('Configure which revision is loaded when clicking on a view or edit link'),
+ '#description' => t('This section allows you to configure which revision is loaded when clicking on links for viewing or editing moderated content, that is content for which the check-box New revision in draft, pending moderation is ticked on the %link page. If not ticked for this content type, current and latest revisions will be one and the same, so that the options behave identically.',
+ array('%link' => 'admin/structure/types/manage/')),
+ );
+ $form['revisioning_view_and_edit_links']['revisioning_view_callback'] = array(
+ '#type' => 'radios',
+ '#title' => t('Links to view content default to'),
+ '#options' => array(
+ REVISIONING_LOAD_CURRENT => t('displaying the current revision'),
+ REVISIONING_LOAD_LATEST => t('displaying the latest revision (reverts to current revision, if the user is not permitted to view revisions of the content type in question)')),
+ '#default_value' => variable_get('revisioning_view_callback', REVISIONING_LOAD_CURRENT),
+ '#description' => t('The first option represents default core behaviour.'),
+ );
+ $form['revisioning_view_and_edit_links']['revisioning_edit_callback'] = array(
+ '#type' => 'radios',
+ '#title' => t('Links to edit content default to'),
+ '#options' => array(
+ REVISIONING_LOAD_CURRENT => t('editing the current revision'),
+ REVISIONING_LOAD_LATEST => t('editing the latest revision (reverts to current revision, if the user is not permitted to view revisions of the content type in question)')),
+ '#default_value' => variable_get('revisioning_edit_callback', REVISIONING_LOAD_CURRENT),
+ '#description' => t('The first option represents default core behaviour.'),
+ );
+
+ $form['revisioning_publication_config'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Publication options'),
+ );
+ $form['revisioning_publication_config']['revisioning_require_update_to_publish'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Require update permission in order to publish or unpublish content.'),
+ '#default_value' => variable_get('revisioning_require_update_to_publish', TRUE),
+ '#description' => t('If you have enabled one or modules dealing with content access, then you would normally tick this box.'),
+ );
+ $form['revisioning_publication_config']['revisioning_publication_shortcuts'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show publication shortcut controls for all users permitted to publish nodes.'),
+ '#default_value' => variable_get('revisioning_publication_shortcuts', FALSE),
+ '#description' => t('Radio buttons are added immediately under the "Revisioning" pane in the content edit form for any user permitted to publish. If not selected, then only users with "administer nodes" will see these controls.'),
+ );
+ $form['revisioning_publication_config']['revisioning_no_moderation_by_default'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Publish content by default.'),
+ '#default_value' => variable_get('revisioning_no_moderation_by_default', FALSE),
+ '#description' => t('When publication shortcut controls are shown, publish content by default. If not, the default will be to moderate.'),
+ );
+
+ if (module_exists('taxonomy')) {
+ $form['revisioning_display_options'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Display options'),
+ );
+ $form['revisioning_display_options']['revisioning_show_taxonomy_terms'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Where applicable add Tags and Terms columns to the content revisions summary (Revisions tab).'),
+ '#default_value' => variable_get('revisioning_show_taxonomy_terms', TRUE),
+ '#description' => t('These columns will only appear if some tags or terms have been assigned to the content shown.'),
+ );
+ $form['revisioning_display_options']['revisioning_in_views_show_unpublished_content_terms'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('In feeds and Views, e.g. /content-summary, show Tags and Terms associated with unpublished content, subject to permissions'),
+ '#default_value' => variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE),
+ '#description' => t('Core behaviour is to suppress the display of tags and terms on all unpublished content even if the user has the permission.'),
+ );
+ }
+
+ $access_modules = module_implements('node_access');
+ $grants_modules = module_implements('node_grants');
+ $modules = array_merge($access_modules, $grants_modules);
+
+ $form['revisioning_additional_info'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Additional info'),
+ '#description' => t('The following content access modules are enabled and may affect whether users can view, edit, delete or publish content: %modules.',
+ array('%modules' => empty($modules) ? t('none') : implode(', ', $modules))),
+ );
+
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save configuration'),
+ );
+ $form['#submit'][] = 'revisioning_admin_configure_form_submit';
+
+ $form['#theme'] = 'system_settings_form';
+
+ return $form;
+}
+
+
+/**
+ * Execute the revisioning_admin_configure_form.
+ */
+function revisioning_admin_configure_form_submit($form, &$form_state) {
+ // Exclude unnecessary form elements.
+ form_state_values_clean($form_state);
+
+ foreach ($form_state['values'] as $key => $new_value) {
+ $old_value = variable_get($key);
+ if ($old_value != $new_value) {
+ variable_set($key, $new_value);
+ if ($key == 'revisioning_in_views_show_unpublished_content_terms') {
+ foreach (node_load_multiple(FALSE) as $node) {
+ revisioning_update_taxonomy_index($node, $new_value);
+ }
+ }
+ }
+ }
+
+ drupal_set_message(t('The revisioning configuration options have been saved.'));
+}
diff --git a/sites/all/modules/revisioning/revisioning.api.php b/sites/all/modules/revisioning/revisioning.api.php
new file mode 100644
index 0000000..dfe6c34
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning.api.php
@@ -0,0 +1,76 @@
+fields(array('weight' => 6))
+ ->condition('name', 'revisioning')
+ ->execute();
+
+ // Panels override fix, see http://drupal.org/node/519924.
+ variable_set('page_manager_override_anyway', TRUE);
+
+ // If requested, not yet published and unpublished nodes need to reveal their
+ // taxonomy terms in Views and in feeds (subject to permissions).
+ if (module_exists('taxonomy') && variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE)) {
+ foreach (node_load_multiple(FALSE) as $node) {
+ revisioning_update_taxonomy_index($node, TRUE);
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function revisioning_uninstall() {
+ // Delete all revisioning_* variables at once.
+ db_query("DELETE FROM {variable} WHERE name LIKE 'revisioning_%%'");
+ // See above.
+ variable_del('page_manager_override_anyway');
+ foreach (node_type_get_types() as $type) {
+ // Maybe revisioning_auto_publish_ and new_revisions_
+ // should be used in array, like 'revision_moderation' below?
+ variable_del('new_revisions_' . $type->type);
+ // Remove 'revision_moderation' from all node_options_ variables.
+ $variable_name = 'node_options_' . $type->type;
+ if ($node_options = variable_get($variable_name, NULL)) {
+ $node_options = array_diff($node_options, array('revision_moderation'));
+ variable_set($variable_name, $node_options);
+ }
+ }
+ // Make sure that unpublished nodes do not reveal their taxonomy terms, once
+ // Revisioning is uninstalled.
+ if (module_exists('taxonomy')) {
+ require_once 'revisioning.taxonomy.inc';
+
+ foreach (node_load_multiple(FALSE) as $node) {
+ // Modify node objects to be consistent with Revisioning being
+ // uninstalled, before updating the {taxonomy_index} table accordingly.
+ unset($node->revision_moderation);
+ revisioning_update_taxonomy_index($node, FALSE);
+ }
+ }
+}
+
+/**
+ * Adjust module weight.
+ *
+ * As of core 7.12 the new hooks taxonomy_node_insert() and
+ * taxonomy_node_update() must be executed before the associated revisioning
+ * hooks.
+ */
+function revisioning_update_7104() {
+ db_update('system')
+ ->fields(array('weight' => 1))
+ ->condition('name', 'revisioning')
+ ->execute();
+}
+
+/**
+ * To avoid problems when Revisioning is installed with Diff and Workbench too.
+ * See [#2142429].
+ */
+function revisioning_update_7107() {
+ db_update('system')
+ ->fields(array('weight' => '6'))
+ ->condition('name', 'revisioning')
+ ->execute();
+}
diff --git a/sites/all/modules/revisioning/revisioning.module b/sites/all/modules/revisioning/revisioning.module
new file mode 100644
index 0000000..326357d
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning.module
@@ -0,0 +1,1229 @@
+Revisioning project page',
+ array('@revisioning' => url('http://drupal.org/project/revisioning')));
+ break;
+
+ case 'node/%/revisions':
+ $s = t('To edit, publish or delete one of the revisions below, click on its saved date.');
+ break;
+
+ case 'admin/structure/trigger/revisioning':
+ $s = t("Below you can assign actions to run when certain publication-related events happen. For example, you could send an e-mail to an author when their pending content is pubished.");
+ break;
+
+ case 'accessible-content/i-created/pending':
+ $s = t('Showing all pending content you created and still have at least view access to.');
+ break;
+
+ case 'accessible-content/i-last-modified/pending':
+ $s = t('Showing all pending content you last modified and still have at least view access to.');
+ break;
+
+ case 'accessible-content/i-can-edit/pending':
+ $s = t('Showing all pending content you can edit.');
+ break;
+
+ case 'accessible-content/i-can-view/pending':
+ $s = t('Showing all pending content you have at least view access to.');
+ break;
+ }
+ return empty($s) ? '' : $s . ' ';
+}
+
+/**
+ * Implements hook_permission().
+ *
+ * Revisioning permissions. Note that permissions to view, revert and delete
+ * revisions already exist in node.module.
+ */
+function revisioning_permission() {
+
+ $edit_description = t('Also requires edit permission from either the Node or other content access module(s).');
+
+ $moderated_content_types = implode(', ', revisioning_moderated_content_types(FALSE));
+ $publish_description = empty($moderated_content_types)
+ ? t('Please select one or more content types for moderation by ticking the New revision in draft, pending moderationPublishing option at Structure >> Content types >> edit.')
+ : t('Applies to content types that are subject to moderation, i.e.: %moderated_content_types.',
+ array('%moderated_content_types' => $moderated_content_types));
+
+ if (!empty($moderated_content_types) && variable_get('revisioning_require_update_to_publish', TRUE)) {
+ $publish_description .= ' ' . $edit_description;
+ }
+
+ $permissions = array(
+ 'view revision status messages' => array(
+ 'title' => t('View revision status messages'),
+ 'description' => '',
+ ),
+ 'edit revisions' => array(
+ 'title' => t('Edit content revisions'),
+ 'description' => $edit_description,
+ ),
+ 'publish revisions' => array(
+ 'title' => t("Publish content revisions (of anyone's content)"),
+ 'description' => $publish_description,
+ ),
+ 'unpublish current revision' => array(
+ 'title' => t("Unpublish current revision (of anyone's content)"),
+ 'description' => $publish_description,
+ ),
+ );
+ // Add per node-type view permissions in same way as edit permissions of node
+ // module, but only for moderated content-types.
+ foreach (node_type_get_types() as $type) {
+ $machine_name = check_plain($type->type);
+ if (revisioning_content_is_moderated($machine_name)) {
+ $permissions['view revisions of own ' . $machine_name . ' content'] = array(
+ 'title' => t('%type-name: View revisions of own content', array('%type-name' => $type->name)));
+ $permissions['view revisions of any ' . $machine_name . ' content'] = array(
+ 'title' => t("%type-name: View revisions of anyone's content", array('%type-name' => $type->name)));
+ $permissions['publish revisions of own ' . $machine_name . ' content'] = array(
+ 'title' => t('%type-name: Publish revisions of own content', array('%type-name' => $type->name)));
+ $permissions['publish revisions of any ' . $machine_name . ' content'] = array(
+ 'title' => t("%type-name: Publish revisions of anyone's content", array('%type-name' => $type->name)));
+ }
+ }
+ return $permissions;
+}
+
+/**
+ * Implements hook_menu().
+ *
+ * Define new menu items.
+ * Existing menu items are modified through hook_menu_alter().
+ */
+function revisioning_menu() {
+ $items = array();
+
+ // Start with the Revisioning config menu item, put under Content Authoring.
+ $items['admin/config/content/revisioning'] = array(
+ 'title' => 'Revisioning',
+ 'description' => 'Configure how content view and edit links behave. Customise revision summary listing.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('revisioning_admin_configure'),
+ 'access arguments' => array('administer site configuration'),
+ 'file' => 'revisioning.admin.inc',
+ );
+
+ // Plain link, not a tab, to allow users to unpublish a node.
+ $items['node/%node/unpublish-current'] = array(
+ // 'title' => t(Unpublish current revision'),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('revisioning_unpublish_confirm', 1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('unpublish current revision', 1),
+ 'type' => MENU_CALLBACK,
+ );
+
+ // Revision tab local subtasks (i.e. secondary tabs), up to 8 of them:
+ // list, view, edit, publish, unpublish, revert, delete and compare.
+ // All revision operations 'node/%node/revisions/%vid/' are defined as
+ // local subtasks (subtabs) secondary to the primary 'node/%node/revisions'
+ // local task (primary tab).
+ //
+ // Subtab to the Revisions primary tab to allow going back to the revisions
+ // list without clicking the primary tab for a second time, which also works.
+ $items['node/%node/revisions/list'] = array(
+ 'title' => 'List all revisions',
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('view revision list', 1),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -20,
+ );
+
+ $items['node/%node/revisions/delete-archived'] = array(
+ 'title' => 'Delete archived revisions',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('revisioning_delete_archived_confirm', 1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('delete archived revisions', 1),
+ 'type' => MENU_CALLBACK,
+ );
+
+ // View revision local subtask.
+ // Note the use of %vid as opposed to %. This allows us to manipulate the
+ // second argument in the path through vid_to_arg().
+ $items['node/%node/revisions/%vid/view'] = array(
+ 'title' => 'View',
+ 'load arguments' => array(3),
+ 'page callback' => '_revisioning_view_revision',
+ 'page arguments' => array(1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('view revisions', 1),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -10,
+ // 'tab_parent' => 'node/%/revisions',
+ );
+ // Edit revision local subtask.
+ $items['node/%node/revisions/%vid/edit'] = array(
+ 'title' => 'Edit',
+ 'load arguments' => array(3),
+ 'page callback' => '_revisioning_edit_revision',
+ 'page arguments' => array(1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('edit revisions', 1),
+ 'file' => 'node.pages.inc',
+ 'file path' => drupal_get_path('module', 'node'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -7,
+ // 'tab_parent' => 'node/%/revisions',
+ );
+ // Publish revision local subtask.
+ // As the menu is content type unaware, a further check on
+ // node->revision_moderation must be made to determine whether it is
+ // appropriate to show this tab.
+ // This is done in _revisioning_access_node_revision.
+ $items['node/%node/revisions/%vid/publish'] = array(
+ 'title' => 'Publish',
+ 'load arguments' => array(3),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('revisioning_publish_confirm', 1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('publish revisions', 1),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -4,
+ );
+ // Unpublish node local subtask.
+ // As the menu is content type unaware, a further check on
+ // node->revision_moderation must be made to determine whether it is
+ // appropriate to show this tab.
+ // This is done in _revisioning_access_node_revision.
+ $items['node/%node/revisions/%vid/unpublish'] = array(
+ 'title' => 'Unpublish',
+ 'load arguments' => array(3),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('revisioning_unpublish_confirm', 1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('unpublish current revision', 1),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -3,
+ );
+ // Revert to revision local subtask.
+ $items['node/%node/revisions/%vid/revert'] = array(
+ 'title' => 'Revert to this',
+ 'load arguments' => array(3),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('node_revision_revert_confirm', 1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('revert revisions', 1),
+ 'file' => 'node.pages.inc',
+ 'file path' => drupal_get_path('module', 'node'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -2,
+ );
+ // Delete revision local subtask.
+ $items['node/%node/revisions/%vid/delete'] = array(
+ 'title' => 'Delete',
+ 'load arguments' => array(3),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('node_revision_delete_confirm', 1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('delete revisions', 1),
+ 'file' => 'node.pages.inc',
+ 'file path' => drupal_get_path('module', 'node'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 10,
+ );
+
+ // If Diff module is enabled, provide a "Compare to current" local subtask.
+ if (module_exists('diff')) {
+ $items['node/%node/revisions/%vid/compare'] = array(
+ 'title' => 'Compare to current',
+ 'load arguments' => array(3),
+ 'page callback' => '_revisioning_compare_to_current_revision',
+ 'page arguments' => array(1),
+ 'access callback' => '_revisioning_access_node_revision',
+ 'access arguments' => array('compare to current', 1),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 0,
+ // 'tab_parent' => 'node/%/revisions',
+ );
+ }
+ return $items;
+}
+
+/**
+ * Implements hook_menu_alter().
+ *
+ * Modify menu items defined in other modules (in particular the Node module).
+ */
+function revisioning_menu_alter(&$items) {
+
+ // Change to access callbacks for existing node paths so that we properly
+ // control revision-related operation.
+ // Some also have their page callbacks altered, e.g to load the latest
+ // rather than the current revision of a node.
+ // Can't change node load function to, say nid_load(), as we'll run into
+ // trouble elsewhere, e.g. menu_get_object(), due to the fact that the
+ // prefix, e.g. '%nid', is meant to be a type name, i.e. '%node'.
+ //
+ // Alter the 3 primary node page tabs: View tab, Edit tab, Revisions tab ...
+ $items['node/%node']['access callback'] = '_revisioning_view_edit_access_callback';
+ $items['node/%node']['access arguments'] = array('view', 1);
+ $items['node/%node']['page callback'] = '_revisioning_view';
+ $items['node/%node']['page arguments'] = array(1);
+ // This is the MENU_DEFAULT_LOCAL_TASK, so inherits the above.
+ $items['node/%node/view']['title callback'] = '_revisioning_title_for_tab';
+ $items['node/%node/view']['title arguments'] = array(1, 'view');
+ $items['node/%node/view']['weight'] = -10;
+
+ $items['node/%node/edit']['access callback'] = '_revisioning_view_edit_access_callback';
+ $items['node/%node/edit']['access arguments'] = array('edit', 1);
+ $items['node/%node/edit']['page callback'] = '_revisioning_edit';
+ $items['node/%node/edit']['page arguments'] = array(1);
+ $items['node/%node/edit']['title callback'] = '_revisioning_title_for_tab';
+ $items['node/%node/edit']['title arguments'] = array(1, 'edit');
+
+ // 'Revisions' tab remains, but points to new page callback, allowing users
+ // to pick the revision to view, edit, publish, revert, unpublish, delete.
+ // Need to override _node_revision_access() call back as it disallows access
+ // to the 'Revisions' tab when there's only one revision, which will prevent
+ // users from getting to the publish/unpublish links.
+ $items['node/%node/revisions']['access callback'] = '_revisioning_access_node_revision';
+ $items['node/%node/revisions']['access arguments'] = array('view revision list', 1);
+ $items['node/%node/revisions']['page callback'] = 'revisioning_node_overview';
+ $items['node/%node/revisions']['page arguments'] = array(1);
+ $items['node/%node/revisions']['title callback'] = '_revisioning_title_for_tab';
+ $items['node/%node/revisions']['title arguments'] = array(1, 'revisions');
+
+ // Remove the node.module links as we defined our own versions, using %vid
+ unset($items['node/%node/revisions/%/view']);
+ unset($items['node/%node/revisions/%/revert']);
+ unset($items['node/%node/revisions/%/delete']);
+
+ if (module_exists('diff')) {
+ // If Diff module is enabled, make sure it uses correct access callback.
+ $items['node/%node/revisions/view']['access callback'] = '_revisioning_access_node_revision';
+ $items['node/%node/revisions/view']['access arguments'] = array('view revisions', 1);
+ }
+}
+
+/**
+ * Perform path manipulations for menu items containing the %vid wildcard.
+ *
+ * For example the ones from revisioning_menu().
+ * @see http://drupal.org/node/500864
+ */
+function vid_to_arg($arg, &$map, $index) {
+ if (empty($arg)) {
+ // For e.g. node/%/revisions.
+ // Suppresses subtabs of Revisions tab where %vid is omitted.
+ $map = array();
+ }
+ return $arg;
+}
+
+/**
+ * Implements hook_node_load().
+ *
+ * The same load op may occur multiple times during the same HTTP request, so
+ * hooray for caching!
+ *
+ * hook_node_load is called when viewing a single node
+ * node_load() -> node_load_multiple() ->
+ * DrupalDefaultEntityController->attachLoad()
+ *
+ * hook_node_load is also called on the /content summary page:
+ * node_admin_nodes() -> node_load_multiple() ->
+ * DrupalDefaultEntityController->attachLoad()
+ *
+ * We do nothing in this 2nd case.
+ */
+function revisioning_node_load($nodes, $types) {
+ // The 'taxonomy/term/%' menu callback taxonomy_term_page() selects nodes
+ // based on presence of their nids in the {taxonomy_index} table, which is
+ // mainly based on publication status. Revisioning also updates the table for
+ // unpublished content so that in Views we can see the terms belonging to
+ // published as well as unpublished content. As a result we must re-apply
+ // access control when taxonomy feeds are displayed.
+ // See also revisioning_update_taxonomy_index().
+ //
+ $double_check_access = (strpos($_GET['q'], 'taxonomy/term') === 0) &&
+ variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE);
+
+ // At this point status, comment, promote and sticky have been set on all of
+ // the $nodes according to the {node_revision} table (not the {node} table),
+ // using {node.vid} as the foreign key into {node_revision}.
+ $nodes_to_be_fixed = array();
+ foreach ($nodes as $nid => $node) {
+ if ($double_check_access && !node_access('view', $node)) {
+ // At this point we cannot remove the node object from $nodes,
+ // but we can set a flag to be checked in a later hook.
+ $node->dont_display = TRUE;
+ }
+ else {
+ revisioning_set_node_revision_info($node);
+ if (!empty($node->revision_moderation) && !empty($node->is_current)) {
+ // Hack!
+ // Because of core issue [#1120272/#542290], if the current revision is
+ // loaded, $node fields may in fact be those belonging to LATEST
+ // revision.
+ // So reload with FIELD_LOAD_REVISION. We can rely on $node->vid, that
+ // attribute is set correctly.
+ // Make sure to unset the already loaded fields or we end up with 2
+ // copies of each field, e.g. 2 bodies, 2 tags, 2 image attachments etc.
+ list($nid, $vid, $bundle) = entity_extract_ids('node', $node);
+ $instances = _field_invoke_get_instances('node', $bundle, array('deleted' => FALSE));
+ foreach ($instances as $instance) {
+ $field_name = $instance['field_name'];
+ unset($node->{$field_name});
+ }
+ $nodes_to_be_fixed[$nid] = $node;
+ }
+ }
+ }
+ if (!empty($nodes_to_be_fixed)) {
+ field_attach_load_revision('node', $nodes_to_be_fixed);
+ foreach ($nodes_to_be_fixed as $nid => $node) {
+ $nodes[$nid] = $node;
+ }
+ }
+}
+
+/**
+ * Implements hook_entity_prepare_view().
+ *
+ * First of the dont_display hooks.
+ */
+function revisioning_entity_prepare_view($entities, $entity_type, $langcode) {
+ if ($entity_type == 'node') {
+ foreach ($entities as $node) {
+ if (!empty($node->dont_display)) {
+ $node->title = FALSE;
+ // This == COMMENT_NODE_HIDDEN if Comment module enabled.
+ $node->comment = 0;
+ $node->link = FALSE;
+ unset($node->body);
+ unset($node->rss_elements);
+ }
+ else {
+ // Display, however suppress comment form when revision is not current.
+ if (isset($node->comment) && !empty($node->revision_moderation) && empty($node->is_current)) {
+ // Prevent comment_node_view() from adding the comment form.
+ // This == COMMENT_NODE_HIDDEN;
+ $node->comment = 0;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_attach_view_alter().
+ */
+function revisioning_field_attach_view_alter(&$output, $context) {
+ if ($context['entity_type'] == 'node' && !empty($context['entity']->dont_display)) {
+ $output = array();
+ }
+}
+
+/**
+ * Implements hook_node_view().
+ */
+function revisioning_node_view($node, $view_mode, $langcode) {
+ if (!empty($node->dont_display)) {
+ // Suppress "Read more".
+ $node->content = array();
+ }
+}
+
+/**
+ * Output a status message, provided teh user has the required permission.
+ *
+ * @param string $message
+ * The status message to be displayed.
+ */
+function revisioning_set_status_message($message) {
+ if (user_access('view revision status messages')) {
+ drupal_set_message(filter_xss($message), 'status');
+ }
+}
+
+/**
+ * Implements hook_node_prepare().
+ *
+ * Called when presenting edit form.
+ */
+function revisioning_node_prepare($node) {
+ if (!empty($node->nid)) {
+ $count = _revisioning_get_number_of_revisions_newer_than($node->vid, $node->nid);
+ if ($count == 1) {
+ drupal_set_message(t('Please note there is one revision more recent than the one you are about to edit.'), 'warning');
+ }
+ elseif ($count > 1) {
+ drupal_set_message(t('Please note there are @count revisions more recent than the one you are about to edit.',
+ array('@count' => $count)), 'warning');
+ }
+ }
+}
+
+/**
+ * Implements hook_node_presave().
+ *
+ * Called when saving, be it an edit or when creating a node.
+ *
+ * Note that the following may be set programmatically on the $node object
+ * before calling node_save($node):
+ *
+ * o $node->revision_operation, one of:
+ *
+ * REVISIONING_NO_REVISION
+ * ($node->revision == $node->revision_moderation == FALSE)
+ *
+ * REVISIONING_NEW_REVISION_NO_MODERATION
+ * ($node->revision == TRUE, $node->revision_moderation == FALSE)
+ *
+ * REVISIONING_NEW_REVISION_WITH_MODERATION
+ * ($node->revision == $node->revision_moderation == TRUE)
+ *
+ * o $node->revision_condition (applies only to NEW_REVISION_WITH_MODERATION):
+ *
+ * REVISIONING_NEW_REVISION_EVERY_SAVE
+ * REVISIONING_NEW_REVISION_WHEN_NOT_PENDING
+ */
+function revisioning_node_presave($node) {
+ revisioning_set_node_revision_info($node);
+
+ if (isset($node->revision_operation)) {
+ $node->revision = ($node->revision_operation > REVISIONING_NO_REVISION);
+ $node->revision_moderation = ($node->revision_operation == REVISIONING_NEW_REVISION_WITH_MODERATION);
+ }
+
+ if (!empty($node->revision_moderation) && revisioning_user_may_auto_publish($node)) {
+ revisioning_set_status_message(t('Auto-publishing this revision.'));
+ // Follow the default saving process making this revision current and
+ // published, as opposed to pending.
+ unset($node->revision_moderation);
+ // This is not required for correct operation, as a revision becomes
+ // pending based on vid > current_revision_id. But it looks less confusing,
+ // when the "Published" box is in sync with the moderation radio buttons.
+ $node->status = NODE_PUBLISHED;
+ $node->auto_publish = TRUE;
+ }
+
+ if (!isset($node->nid)) {
+ // New node, if moderated without Auto-publish, ignore the default Publish
+ // tickbox.
+ if (isset($node->revision_moderation) && $node->revision_moderation == TRUE) {
+ $node->status = NODE_NOT_PUBLISHED;
+ }
+ // Set these for Rules, see [#1627400]
+ $node->current_status = $node->status;
+ $node->current_title = $node->title;
+ $node->current_promote = $node->promote;
+ $node->current_sticky = $node->sticky;
+ $node->current_comment = isset($node->comment) ? $node->comment : 0;
+ return;
+ }
+
+ if (!empty($node->revision_moderation) /* || !empty($auto_publish) */) {
+ // May want to do this for auto_publish too, to provide $node->current... to
+ // other modules, as a courtesy.
+ if (!isset($node->revision_condition) && !empty($node->revision) && !empty($node->is_pending)
+ && variable_get('new_revisions_' . $node->type, REVISIONING_NEW_REVISION_WHEN_NOT_PENDING) == REVISIONING_NEW_REVISION_WHEN_NOT_PENDING) {
+ revisioning_set_status_message(t('Updating existing draft, not creating new revision as this one is still pending.'));
+ // To tell revisioning_node_update().
+ $node->revision_condition = REVISIONING_NEW_REVISION_WHEN_NOT_PENDING;
+ }
+ if (isset($node->revision_condition)) {
+ // Tell node_save() whether a new revision should be created.
+ $node->revision = ($node->revision_condition == REVISIONING_NEW_REVISION_EVERY_SAVE);
+ }
+
+ $result = db_query("SELECT status, title, comment, promote, sticky FROM {node_revision} WHERE vid = :vid",
+ array(':vid' => $node->current_revision_id));
+ $current_revision = $result->fetchObject();
+ // Copy from {node_revision} the field values replicated on {node} before
+ // handing back to node_save(). This is a side-effect of core D7's somewhat
+ // "sick" table denormalisation.
+ // If the Scheduler module is used take the status from there. See
+ // revisioning_scheduler_api().
+ $node->current_status = isset($node->scheduled_status) ? $node->scheduled_status : $current_revision->status;
+ $node->current_title = $current_revision->title;
+ $node->current_promote = $current_revision->promote;
+ $node->current_sticky = $current_revision->sticky;
+ $node->current_comment = isset($current_revision->comment) ? $current_revision->comment : 0;
+ }
+}
+
+/**
+ * Implements hook_node_update().
+ *
+ * Note: $node->revision_moderation and $node->revision_condition may be set
+ * programmatically prior to calling node_save().
+ * See also: revisioning_node_pre_save().
+ */
+function revisioning_node_update($node) {
+
+ revisioning_update_taxonomy_index($node, variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE));
+
+ // Check whether the node has a current_status property and update the
+ // node->status to allow node_save()'s call to node_access_acquire_grants()
+ // to create the correct node_access entry for the current_status.
+ if (isset($node->current_status)) {
+ $node->status = $node->current_status;
+ }
+
+ if (!empty($node->revision_moderation) && (isset($node->revision_condition) || !empty($node->revision))) {
+ // Enter when revision moderation is on and revision_condition=0,1
+ // Have to do this due to D7's "sick" denormalisation of node revision data.
+ // Resetting the fields duplicated from new {node_revision} back to their
+ // originial values to match the current revision as opposed to the latest
+ // revision. The latter is done by node_save() just before it calls this
+ // function.
+ // By resetting {node.vid} {node.vid} < {node_revision.vid}, which makes
+ // the newly created revision a pending revision in Revisioning's books.
+ // Note: cannot use $node->old_vid as set by node_save(), as this refers to
+ // the revision edited, which may not be the current, which is what we are
+ // after here.
+ db_update('node')
+ ->fields(array(
+ 'vid' => $node->current_revision_id,
+ 'status' => $node->current_status,
+ 'title' => $node->current_title,
+ // In case Comment module enabled.
+ 'comment' => $node->current_comment,
+ 'promote' => $node->current_promote,
+ 'sticky' => $node->current_sticky))
+ ->condition('nid', $node->nid)
+ ->execute();
+ }
+ // Generate a 'post update' event in Rules.
+ module_invoke_all('revisionapi', 'post update', $node);
+
+ // Add new revision usage records to files to prevent them being deleted.
+ $fields = field_info_instances('node', $node->type);
+ foreach ($fields as $field_name => $value) {
+ $field_info = field_info_field($field_name);
+ if ($field_info['type'] == 'file' || $field_info['type'] == 'image') {
+ // See #1996412.
+ $file_fields[$field_name] = $value;
+ }
+ }
+ // Create file revision entries for files created using older versions.
+ // $old_node = isset($node->original) ? $node->original : NULL;
+ // [#2276657]
+ $old_node = isset($node->original) && !empty($node->original) ? $node->original : NULL;
+ if (isset($old_node) && !empty($file_fields)) {
+ foreach ($file_fields as $file_field) {
+ if ($old_files = field_get_items('node', $old_node, $file_field['field_name'], $old_node->language)) {
+ foreach ($old_files as $old_single_file) {
+ if (!empty($old_single_file)) {
+ $old_file = (object) $old_single_file;
+ file_usage_add($old_file, 'revisioning', 'revision', $old_node->vid);
+ }
+ }
+ }
+ }
+ }
+ if (!empty($file_fields)) {
+ foreach ($file_fields as $file_field) {
+ if ($files = field_get_items('node', $node, $file_field['field_name'], $node->language)) {
+ foreach ($files as $single_file) {
+ $file = (object) $single_file;
+ file_usage_add($file, 'revisioning', 'revision', $node->vid);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * New node.
+ */
+function revisioning_node_insert($node) {
+
+ revisioning_update_taxonomy_index($node, variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE));
+
+ if (!empty($node->revision_moderation)) {
+ revisioning_set_status_message($node->status ? t('Initial revision created and published.') : t('Initial draft created, pending publication.'));
+ }
+
+ // Add revision usage records to files to prevent them being deleted.
+ $fields = field_info_instances('node', $node->type);
+ foreach ($fields as $field_name => $value) {
+ $field_info = field_info_field($field_name);
+ if ($field_info['type'] == 'file') {
+ $file_fields[$field_name] = $value;
+ }
+ }
+ if (!empty($file_fields)) {
+ foreach ($file_fields as $file_field) {
+ if ($files = field_get_items('node', $node, $file_field['field_name'], $node->language)) {
+ foreach ($files as $single_file) {
+ $file = (object) $single_file;
+ file_usage_add($file, 'revisioning', 'revision', $node->vid);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ */
+function revisioning_node_delete($node) {
+ if ($revisions = node_revision_list($node)) {
+ $vids = array_keys($revisions);
+ db_delete('file_usage')->condition('module', 'revisioning')->condition('id', $vids, 'IN')->execute();
+ }
+}
+
+/**
+ * Implements hook_node_access_records_alter().
+ *
+ * If the node is not the current node this function clears the grants array and
+ * rebuilds it using the current node.
+ */
+function revisioning_node_access_records_alter(&$grants, $node) {
+
+ if (!revisioning_revision_is_current($node)) {
+ $current_node = node_load($node->nid, NULL, TRUE);
+ $grants = array();
+ foreach (module_implements('node_access_records') as $module) {
+ $function = $module . '_node_access_records';
+ if (function_exists($function)) {
+ $result = call_user_func_array($function, array($current_node));
+ if (isset($result)) {
+ if (is_array($result)) {
+ $grants = array_merge_recursive($grants, $result);
+ }
+ else {
+ $grants[] = $result;
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function revisioning_views_api() {
+ return array(
+ 'api' => views_api_version(),
+ 'path' => drupal_get_path('module', 'revisioning') . '/views',
+ );
+}
+
+/**
+ * Implements hook_user_node_access().
+ */
+function revisioning_user_node_access($revision_op, $node, $account = NULL) {
+ if (!isset($account)) {
+ $account = $GLOBALS['user'];
+ }
+
+ $type = check_plain($node->type);
+
+ switch ($revision_op) {
+ case 'view current':
+ break;
+
+ case 'compare to current':
+ case 'view revisions':
+ case 'view revision list':
+ if (user_access('view revisions', $account)) {
+ break;
+ }
+ if (user_access('view revisions of any ' . $type . ' content', $account)) {
+ break;
+ }
+ if (($node->uid == $account->uid) && user_access('view revisions of own ' . $type . ' content', $account)) {
+ break;
+ }
+ return FALSE;
+
+ case 'edit current':
+ return 'update';
+
+ case 'edit revisions':
+ case 'revert revisions':
+ return user_access($revision_op, $account) ? 'update' : FALSE;
+
+ case 'publish revisions':
+ $node_op = variable_get('revisioning_require_update_to_publish', TRUE) ? 'update' : 'view';
+ if (user_access('publish revisions', $account)) {
+ return $node_op;
+ }
+ if (user_access('publish revisions of any ' . $type . ' content', $account)) {
+ return $node_op;
+ }
+ if (($node->uid == $account->uid) && user_access('publish revisions of own ' . $type . ' content', $account)) {
+ return $node_op;
+ }
+ return FALSE;
+
+ case 'unpublish current revision':
+ $node_op = variable_get('revisioning_require_update_to_publish', TRUE) ? 'update' : 'view';
+ return user_access('unpublish current revision', $account) ? $node_op : FALSE;
+
+ case 'delete revisions':
+ case 'delete archived revisions':
+ if (!user_access('delete revisions', $account)) {
+ return FALSE;
+ }
+ case 'delete node':
+ return 'delete';
+
+ default:
+ drupal_set_message(t("Unknown Revisioning operation '%revision_op'. Treating as 'view'.",
+ array('%revision_op' => $revision_op)), 'warning', FALSE);
+ }
+ return 'view';
+}
+
+/**
+ * Implements hook_scheduler_api().
+ */
+function revisioning_scheduler_api($node, $action) {
+ if ($action == 'pre_publish') {
+ $node->scheduled_status = NODE_PUBLISHED;
+ }
+ elseif ($action == 'pre_unpublish') {
+ $node->scheduled_status = NODE_NOT_PUBLISHED;
+ }
+}
+
+/**
+ * Test whether the supplied revision operation is appropriate for the node.
+ *
+ * This is irrespective of user permissions, e.g. even for an administrator it
+ * doesn't make sense to publish a node that is already published or to
+ * "revert" to the current revision.
+ *
+ * @param string $revision_op
+ * For instance 'publish revisions', 'delete revisions'
+ * @param object $node
+ * The node object
+ *
+ * @return bool
+ * TRUE if the operation is appropriate for this node at this point
+ */
+function _revisioning_operation_appropriate($revision_op, $node) {
+
+ switch ($revision_op) {
+
+ case 'compare to current':
+ // Can't compare against itself.
+ case 'delete revisions':
+ // If the revision is the current one, suppress the delete operation
+ // @TODO ...unless it's the only revision, in which case delete the
+ // entire node; however this requires a different URL.
+ return !$node->is_current;
+
+ case 'delete archived revisions':
+ break;
+
+ case 'view revision list':
+ // For i.e. node revisions summary.
+ if (empty($node->revision_moderation) && isset($node->num_revisions) && $node->num_revisions == 1) {
+ // Suppress Revisions tab when when there's only 1 revision. This is
+ // consistent with core.
+ // However, when content is moderated (i.e. "New revision in draft,
+ // pending moderation" is ticked) we want to be able to get to the
+ // 'Unpublish current' link on this page and the 'Publish' tab on
+ // the next.
+ return FALSE;
+ }
+ break;
+
+ case 'edit revisions':
+ if (empty($node->revision_moderation) /* && !$node->is_current*/) {
+ return FALSE;
+ }
+ break;
+
+ case 'publish revisions':
+ // If the node isn't meant to be moderated,
+ // or the revision is not either pending or current but not published,
+ // then disallow publication.
+ if (empty($node->revision_moderation) ||
+ !($node->is_pending || ($node->is_current && !$node->status))) {
+ return FALSE;
+ }
+ break;
+
+ case 'unpublish current revision':
+ // If the node isn't meant to be moderated or it is unpublished already
+ // or we're not looking at the current revision, then unpublish is not an
+ // option.
+ if (empty($node->revision_moderation) || !$node->status || !$node->is_current) {
+ return FALSE;
+ }
+ break;
+
+ case 'revert revisions':
+ // If this revision is pending or current, suppress the reversion.
+ if ($node->is_pending || $node->is_current) {
+ return FALSE;
+ }
+ break;
+ }
+ return TRUE;
+}
+
+/**
+ * Determine whether the supplied revision operation is permitted on the node.
+ *
+ * This requires getting through three levels of defence:
+ * o Is the operation appropriate for this node at this time, e.g. a node must
+ * not be published if it already is or if it isn't under moderation control
+ * o Does the user have permission for the requested REVISION operation?
+ * o Does the user have the NODE access rights (view/update/delete) for this
+ * operation?
+ *
+ * @param string $revision_op
+ * For instance 'publish revisions', 'delete revisions'
+ * @param object $node
+ * The node object
+ *
+ * @return bool
+ * TRUE if the user has access
+ */
+function _revisioning_access_node_revision($revision_op, $node) {
+
+ if (!_revisioning_operation_appropriate($revision_op, $node)) {
+ return FALSE;
+ }
+ // Check the revision-aspect of the operation.
+ $node_op = revisioning_user_node_access($revision_op, $node);
+ // ... then check with core to assess node permissions
+ // node_access will invoke hook_node_access(), i.e. revisioning_node_access().
+ $access = $node_op && node_access($node_op, $node);
+
+ // Let other modules override the outcome, if there are any.
+ // If any module denies access that is the final result, otherwise allow.
+ $overrides = module_invoke_all('revisioning_access_node_revision', $revision_op, $node);
+ return empty($overrides) ? $access : !in_array(NODE_ACCESS_DENY, $overrides, TRUE);
+}
+
+/**
+ * Implements hook_node_access().
+ *
+ * This gets invoked from node.module/node_access() after it has checked the
+ * standard node permissions using node_node_access() and just before it checks
+ * the node_access grants table.
+ * We basically return "don't care" except for one 'view' case, which replicates
+ * the node.module. "Don't care" in this case would result in "access denied".
+ */
+function revisioning_node_access($node, $node_op, $account) {
+ // Taken from node.module/node_access():
+ // If no modules implement hook_node_grants(), the default behaviour is to
+ // allow all users to view published nodes, so reflect that here,
+ // augmented for the 'view revisions' family of permissions, which apply to
+ // both published and unpublished nodes.
+ if ($node_op == 'view' && !module_implements('node_grants')) {
+ if ($node->status == NODE_PUBLISHED || (!empty($node->revision_moderation) && revisioning_user_node_access('view revisions', $node, $account))) {
+ return NODE_ACCESS_ALLOW;
+ }
+ }
+ // [#1492246]
+ // Deny access to unpublished, moderated content by anonymous users.
+ if (empty($node->status) && !empty($node->revision_moderation) && empty($account->uid)) {
+ return NODE_ACCESS_DENY;
+ }
+ return NODE_ACCESS_IGNORE;
+}
+
+/**
+ * Access callback function.
+ *
+ * Access callback for 'node/%', 'node/%/view' and 'node/%/edit' links that
+ * may appear anywhere on the site.
+ * At the time that this function is called the CURRENT revision will already
+ * have been loaded by the system. However depending on the value of the
+ * 'revisioning_view_callback' and 'revisioning_edit_callback' variables (as
+ * set on the admin/config/content/revisioning page), this may not be the
+ * desired revision.
+ * If these variables state that the LATEST revision should be loaded, we need
+ * to check at this point whether the user has permission to view this revision.
+ *
+ * The 'View current' and/or 'Edit current' tabs are suppressed when the current
+ * revision is already displayed via one of the Revisions subtabs.
+ * The 'View latest' and/or 'Edit latest' tabs are suppressed when the latest
+ * revision is already displayed via one of the Revisions subtabs.
+ *
+ * @param string $op
+ * must be one of 'view' or 'edit'
+ * @param object $node
+ * the node object
+ *
+ * @return bool
+ * FALSE if access to the desired revision is denied
+ */
+function _revisioning_view_edit_access_callback($op, $node) {
+
+ $load_op = _revisioning_load_op($node, $op);
+
+ $vid = arg(3);
+ if (!empty($node->revision_moderation) && is_numeric($vid)) {
+ // The View, Edit primary tabs are requested indirectly, in the context of
+ // the secondary tabs under Revisions, e.g. node/%/revisions/%
+ if ($load_op == REVISIONING_LOAD_CURRENT && $vid == $node->current_revision_id) {
+ // Suppress 'View current' and 'Edit current' tabs when viewing current.
+ return FALSE;
+ }
+ if ($load_op == REVISIONING_LOAD_LATEST && $vid == revisioning_get_latest_revision_id($node->nid)) {
+ // Suppress 'View latest' and 'Edit latest' tabs when viewing latest.
+ return FALSE;
+ }
+ }
+ if ($load_op == REVISIONING_LOAD_LATEST) {
+ // _revisioning_load_op has already checked permission to view latest.
+ return TRUE;
+ }
+ $revision_op = ($op == 'view') ? 'view current' : 'edit current';
+ return _revisioning_access_node_revision($revision_op, $node);
+}
+
+/**
+ * Load a revision.
+ *
+ * Assuming that the node passed in is the current revision (core default),
+ * this function determines whether the lastest revision should be loaded
+ * instead, in which case it returns REVISIONING_LOAD_LATEST.
+ *
+ * @param object $node
+ * only nodes of content types subject to moderation are
+ * processed by this function
+ * @param string $op
+ * either 'edit' or 'view'
+ * @param bool $check_access
+ * whether revision access permissions should be checked; if the user has no
+ * permission to load the latest revisions, then the function returns
+ * REVISIONING_LOAD_CURRENT
+ *
+ * @return int
+ * REVISIONING_LOAD_LATEST or REVISIONING_LOAD_CURRENT
+ */
+function _revisioning_load_op($node, $op, $check_access = TRUE) {
+ if (!empty($node->revision_moderation)) {
+ $view_mode = (int) variable_get('revisioning_view_callback', REVISIONING_LOAD_CURRENT);
+ $edit_mode = (int) variable_get('revisioning_edit_callback', REVISIONING_LOAD_CURRENT);
+ $load_op = ($op == 'edit') ? $edit_mode : $view_mode;
+ if ($load_op == REVISIONING_LOAD_LATEST) {
+ // Site is configured to load latest revision, but we'll only do this if
+ // the latest isn't loaded already and user has the permission to do so.
+ $latest_vid = revisioning_get_latest_revision_id($node->nid);
+ if ($latest_vid != $node->current_revision_id) {
+ if (!$check_access) {
+ return REVISIONING_LOAD_LATEST;
+ }
+ $original_vid = $node->vid;
+ $node->vid = $latest_vid;
+ $node->is_current = revisioning_revision_is_current($node);
+ $revision_op = ($op == 'view') ? 'view revisions' : 'edit revisions';
+ $access = _revisioning_access_node_revision($revision_op, $node);
+ // Restore $node (even though called by value), to remain consistent.
+ $node->vid = $original_vid;
+ $node->is_current = revisioning_revision_is_current($node);
+ if ($access) {
+ return REVISIONING_LOAD_LATEST;
+ }
+ }
+ }
+ }
+ return REVISIONING_LOAD_CURRENT;
+}
+
+/**
+ * Display node overview.
+ *
+ * Display all revisions of the supplied node in a themed table with links for
+ * the permitted operations above it.
+ *
+ * @return array
+ * render array as returned by drupal_get_form()
+ */
+function revisioning_node_overview($node) {
+ return _revisioning_theme_revisions_summary($node);
+}
+
+/**
+ * Menu callback for the primary View tab.
+ *
+ * This is the same callback as used in core, except that in core current and
+ * latest revisions are always the same.
+ */
+function _revisioning_view($node) {
+ if (_revisioning_load_op($node, 'view') == REVISIONING_LOAD_LATEST) {
+ $vid_to_load = revisioning_get_latest_revision_id($node->nid);
+ $node = node_load($node->nid, $vid_to_load);
+ }
+ // This is the callback used by node.module for node/%node & node/%node/view
+ return node_page_view($node);
+}
+
+/**
+ * Callback for the primary Edit tab.
+ *
+ * This is the same callback as used in core, except that in core current and
+ * latest revisions are always the same.
+ */
+function _revisioning_edit($node) {
+ if (_revisioning_load_op($node, 'edit') == REVISIONING_LOAD_LATEST) {
+ $vid_to_load = revisioning_get_latest_revision_id($node->nid);
+ $node = node_load($node->nid, $vid_to_load);
+ }
+ _revisioning_set_custom_theme_if_necessary();
+ // This is the callback used by node.module for node/%node/edit
+ return node_page_edit($node);
+}
+
+/**
+ * Callback to view a particular revision.
+ */
+function _revisioning_view_revision($node) {
+ if (isset($node->nid)) {
+ /* For Panels: @todo test this thoroughly. See [#1567880]
+ $router_item = menu_get_item('node/' . $node->nid);
+ if (!empty($router_item['file'])) {
+ $path = $_SERVER['DOCUMENT_ROOT'] . base_path();
+ require_once $path . $router_item['file'];
+ }
+ // Call whatever function is assigned to the main node path but pass the
+ // current node as an argument. This approach allows for the reuse of Panel
+ // definition acting on node/%node.
+ if (isset($router_item['page_callback'])) {
+ return $router_item['page_callback']($node);
+ }*/
+ }
+ // This is the callback used by node.module for node/%node/revisions/%/view
+ return node_show($node, TRUE);
+}
+
+/**
+ * Callback to edit a particular revision.
+ *
+ * Note that there is no equivalent of this in core and we should not allow
+ * editing of a non-current revision, if $node->revision_moderation is not set.
+ * This is the job of the access callback _revisioning_access_node_revision().
+ */
+function _revisioning_edit_revision($node) {
+ _revisioning_set_custom_theme_if_necessary();
+ return node_page_edit($node);
+}
+
+/**
+ * Callback for the primary View, Edit and Revisions tabs titles.
+ *
+ * @param object $node
+ * the node object
+ * @param string $tab
+ * 'view', 'edit' or 'revisions'
+ *
+ * @return string
+ * translatable title string
+ */
+function _revisioning_title_for_tab($node, $tab) {
+ if ($tab == 'revisions') {
+ return is_numeric(arg(3)) ? t('Revision operations') : t('Revisions');
+ }
+ /*
+ if (empty($node->revision_moderation) || $node->num_revisions <= 1) {
+ return ($tab == 'edit' ? t('Edit') : t('View'));
+ }
+ if (_revisioning_load_op($node, $tab) == REVISIONING_LOAD_LATEST) {
+ return ($tab == 'edit' ? t('Edit latest') : t('View latest'));
+ }
+ return ($tab == 'edit' ? t('Edit current') : t('View current'));
+ */
+ if (!empty($node->revision_moderation) && $node->num_revisions > 1) {
+ if (_revisioning_load_op($node, $tab) == REVISIONING_LOAD_LATEST) {
+ return ($tab == 'edit' ? t('Edit latest') : t('View latest'));
+ }
+ if (_revisioning_access_node_revision('view revisions', $node)) {
+ return ($tab == 'edit' ? t('Edit current') : t('View current'));
+ }
+ }
+ return ($tab == 'edit' ? t('Edit') : t('View'));
+}
+
+/**
+ * Set custom theme.
+ */
+function _revisioning_set_custom_theme_if_necessary() {
+ // Use the admin theme if the user specified this at Appearance >> Settings.
+ // Note: first tick 'View the administration theme' at People >> Permissions.
+ if (variable_get('node_admin_theme', FALSE)) {
+ global $theme, $custom_theme;
+ $custom_theme = variable_get('admin_theme', $theme);
+ }
+}
+
+if (module_exists('diff')) {
+ /**
+ * Compare two revisions.
+ *
+ * Use diff's diff_diffs_show() function to compare specific revision to the
+ * current one.
+ */
+ function _revisioning_compare_to_current_revision($node) {
+ // For diff_diffs_show().
+ module_load_include('inc', 'diff', 'diff.pages');
+ // Make sure that latest of the two revisions is on the right.
+ if ($node->current_revision_id < $node->vid) {
+ return diff_diffs_show($node, $node->current_revision_id, $node->vid);
+ }
+ return diff_diffs_show($node, $node->vid, $node->current_revision_id);
+ }
+}
+
+/**
+ * Implements hook_page_manager_override().
+ *
+ * See http://drupal.org/node/1509674#comment-6702798
+ */
+function revisioning_page_manager_override($task_name) {
+ switch ($task_name) {
+ case 'node_view':
+ return '_revisioning_view';
+ }
+}
diff --git a/sites/all/modules/revisioning/revisioning.pages.inc b/sites/all/modules/revisioning/revisioning.pages.inc
new file mode 100644
index 0000000..cc92630
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning.pages.inc
@@ -0,0 +1,467 @@
+revision_moderation) ? $node->revision_moderation : revisioning_content_is_moderated($form['type']['#value'], $node);
+
+ // Alter the Create/Edit content form, if subject to moderation.
+ if ($is_moderated_content) {
+ // "Create new revision" must be set when the node is to be moderated.
+ $node->revision = TRUE;
+ // Next line is not essential, just ensures form options are
+ // consistent with the edited content being subject to moderation.
+ $form['revision_information']['revision']['#default_value'] = TRUE;
+ // For moderated content "Published" box will be treated as unticked,
+ // See revisioning_node_presave().
+ }
+
+ // Only add this radio selector if user has the appropriate permissions.
+ // 'administer nodes' is required by default, but if configured
+ // appropriately, then any user premitted to publish this node will get the
+ // shortcut controls.
+ $add_radio_selector = variable_get('revisioning_publication_shortcuts', FALSE) ?
+ revisioning_user_node_access('publish revisions', $node) :
+ user_access('administer nodes');
+ if ($add_radio_selector) {
+ // Expand and move this vertical tab to top, so that it's in user's face.
+ if (isset($form['menu'])) {
+ $form['menu']['#collapsed'] = TRUE;
+ }
+ $form['revision_information']['#collapsed'] = FALSE;
+ $form['revision_information']['#weight'] = -3;
+
+ $options = array();
+ if (isset($node->nid)) {
+ $options[REVISIONING_NO_REVISION] = t('Modify current revision, no moderation');
+ }
+ $options[REVISIONING_NEW_REVISION_NO_MODERATION] = t('Create new revision, no moderation');
+ $options[REVISIONING_NEW_REVISION_WITH_MODERATION] = t('Create new revision and moderate');
+
+ // This radio selection will appear in hook_node_presave as
+ // $node->revision_operation
+ $form['revision_information']['revision_operation'] = array(
+ '#title' => t('Revision creation and moderation options'),
+ '#description' => t('Moderation means that the new revision is not publicly visible until approved by someone with the appropriate permissions.'),
+ '#type' => 'radios',
+ '#options' => $options,
+ '#default_value' => isset($node->nid)
+ ? (int) $node->revision + (int) $is_moderated_content
+ : ($is_moderated_content ? REVISIONING_NEW_REVISION_WITH_MODERATION : REVISIONING_NEW_REVISION_NO_MODERATION),
+ );
+ unset($form['revision_information']['revision']);
+
+ // Add javascript to show/hide the "Published" checkbox if the user
+ // presses one of the first two radio buttons. Also updates summary tabs.
+ $js_file = drupal_get_path('module', 'revisioning') . '/js/revisioning-radios.js';
+ // After node.js.
+ drupal_add_js($js_file, array('weight' => 1));
+ if (variable_get('revisioning_no_moderation_by_default', FALSE)) {
+ $form['revision_information']['revision_operation']['#default_value'] = REVISIONING_NEW_REVISION_NO_MODERATION;
+ }
+ }
+ else {
+ // For non-admin don't show radios, just set default, hidden on form.
+ // Note that $form['revision_information']['revision'] is already set.
+ $form['revision_moderation'] = array(
+ '#type' => 'value',
+ '#value' => $is_moderated_content,
+ );
+ }
+
+ // In addition to node_form_submit() append our own handler to the list, so
+ // that we can redirect to the pending, as opposed to current, revision.
+ $form['actions']['submit']['#submit'][] = '_revisioning_form_submit';
+
+ if (isset($form['actions']['delete']) && isset($form['actions']['delete']['#type'])) {
+ $nid = $form['#node']->nid;
+ if (revisioning_get_number_of_revisions($nid) > 1) {
+ // Special treatment for Delete button when there are >= 2 revisions.
+ if ($form['#node']->vid == revisioning_get_current_node_revision_id($nid)) {
+ // Make it obvious to user that a 'Delete' is in fact 'Delete all'.
+ $form['actions']['delete']['#value'] = t('Delete (all revisions)');
+ }
+ elseif (user_access('delete revisions')) {
+ // Change the meaning of the 'Delete' button when editing a revision
+ // to be the deletion of the viewed revision, rather than the node
+ // node.
+ $form['actions']['delete']['#value'] = t('Delete this revision');
+ $form['actions']['delete']['#submit'][] = '_revisioning_delete_submit';
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_form_alter().
+ *
+ * On the content type edit form, add the "New revision in draft, pending
+ * moderation" tick-box and a couple of radio-boxes to select the new revision
+ * and auto-publish policies.
+ */
+function revisioning_form_node_type_form_alter(&$form, &$form_state) {
+ $form['workflow']['#collapsed'] = FALSE;
+ $form['workflow']['node_options']['#options']['revision_moderation'] = t('New revision in draft, pending moderation (requires "Create new revision")');
+
+ $form['workflow']['revisioning'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('New revision in draft'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+ $content_type = $form['#node_type']->type;
+ $form['workflow']['revisioning']['new_revisions'] = array(
+ '#title' => t('Create new revision:'),
+ '#type' => 'radios',
+ '#options' => array(
+ REVISIONING_NEW_REVISION_WHEN_NOT_PENDING => t('Only when saving %type content that is not already in draft/pending moderation', array('%type' => $content_type)),
+ REVISIONING_NEW_REVISION_EVERY_SAVE => t('Every time %type content is updated, even when saving content in draft/pending moderation', array('%type' => $content_type))),
+ '#default_value' => (int) variable_get('new_revisions_' . $content_type, REVISIONING_NEW_REVISION_WHEN_NOT_PENDING),
+ '#description' => t('Use less disk space and avoid cluttering your revisions list. With the first option ticked, modifications are saved to the same copy (i.e. no additional revisions are created) until the content is published.'),
+ );
+ $form['workflow']['revisioning']['revisioning_auto_publish'] = array(
+ '#title' => t('Auto-publish drafts of type %type (for moderators)', array('%type' => $content_type)),
+ '#type' => 'checkbox',
+ '#default_value' => (int) variable_get('revisioning_auto_publish_' . $content_type, FALSE),
+ '#description' => t('If this box is ticked and the user has one of the "Publish content revisions" permissions, then any draft of type %type is published immediately upon saving, without further review or the option to schedule a publication date.', array('%type' => $content_type)),
+ );
+}
+
+/**
+ * Return a confirmation page for publishing a revision.
+ */
+function revisioning_publish_confirm($form, &$form_state, $node) {
+ $form['node_id'] = array('#type' => 'value', '#value' => $node->nid);
+ $form['title'] = array('#type' => 'value', '#value' => $node->title);
+ $form['revision'] = array('#type' => 'value', '#value' => $node->vid);
+ $form['type'] = array('#type' => 'value', '#value' => $node->type);
+ return confirm_form($form,
+ t('Are you sure you want to publish this revision of %title?', array('%title' => $node->title)),
+ 'node/' . $node->nid . '/revisions',
+ t('Publishing this revision will make it visible to the public.'),
+ t('Publish'), t('Cancel'));
+}
+
+/**
+ * Submission handler for the publish_confirm form.
+ */
+function revisioning_publish_confirm_submit($form, &$form_state) {
+ $nid = $form_state['values']['node_id'];
+ $vid = $form_state['values']['revision'];
+ $node = node_load($nid, $vid);
+ _revisioning_publish_revision($node);
+ revisioning_set_status_message(t('Revision has been published.'));
+ // Redirect to the same page as unpublish and revert.
+ $form_state['redirect'] = "node/$nid/revisions";
+}
+
+/**
+ * Return a confirmation page for unpublishing the node.
+ */
+function revisioning_unpublish_confirm($form, &$form_state, $node) {
+ $form['node_id'] = array('#type' => 'value', '#value' => $node->nid);
+ $form['title'] = array('#type' => 'value', '#value' => $node->title);
+ $form['type'] = array('#type' => 'value', '#value' => $node->type);
+ return confirm_form($form,
+ t('Are you sure you want to unpublish %title?', array('%title' => $node->title)),
+ "node/$node->nid/revisions",
+ t('Unpublishing will remove this content from public view.'),
+ t('Unpublish'), t('Cancel'));
+}
+
+/**
+ * Submission handler for the unpublish_confirm form.
+ */
+function revisioning_unpublish_confirm_submit($form, &$form_state) {
+ $nid = $form_state['values']['node_id'];
+ _revisioning_unpublish_revision($nid);
+ $title = $form_state['values']['title'];
+ revisioning_set_status_message(t('%title is no longer publicly visible.', array('%title' => $title)));
+ // Redirect to the same page as publish and revert.
+ $form_state['redirect'] = "node/$nid/revisions";
+}
+
+/**
+ * Return a confirmation page for deleting archived revisione.
+ */
+function revisioning_delete_archived_confirm($form, &$form_state, $node) {
+ $node->num_archived = revisioning_get_number_of_archived_revisions($node);
+ $form['node'] = array('#type' => 'value', '#value' => $node);
+ $t = format_plural($node->num_archived,
+ 'Are you sure you want to delete the archived revision of %title?',
+ 'Are you sure you want to delete all @count archived revisions of %title?',
+ array('%title' => $node->title)
+ );
+ return confirm_form($form, $t, 'node/' . $node->nid . '/revisions',
+ t('This action cannot be undone.'),
+ t('Delete archived'), t('Cancel'));
+}
+
+/**
+ * Submission handler for the delete_archived_confirm form.
+ */
+function revisioning_delete_archived_confirm_submit($form, &$form_state) {
+ $node = $form_state['values']['node'];
+ revisioning_delete_archived_revisions($node);
+ $t = format_plural($node->num_archived, 'One archived revision deleted.', '@count archived revisions deleted.');
+ revisioning_set_status_message($t);
+ $form_state['redirect'] = ($node->num_revisions - $node->num_archived > 1) ? 'node/' . $node->nid . '/revisions' : 'node/' . $node->nid;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * @see node.pages.inc/node_revision_revert_confirm()
+ */
+function revisioning_form_node_revision_revert_confirm_alter(&$form, &$form_state) {
+ $node = $form['#node_revision'];
+ if (_revisioning_get_number_of_pending_revisions($node->nid) > 0) {
+ drupal_set_message(t('There is a pending revision. Are you sure you want to revert to an archived revision?'), 'warning');
+ }
+ array_unshift($form['#submit'], 'revisioning_revert_confirm_pre_submit');
+ $form['#submit'][] = 'revisioning_revert_confirm_post_submit';
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * We only add "pre" submit handler, because "post delete" event is already
+ * available via hook_nodeapi().
+ *
+ * @see node_revision_delete_confirm()
+ */
+function revisioning_form_node_revision_delete_confirm_alter(&$form, &$form_state) {
+ array_unshift($form['#submit'], 'revisioning_revision_delete_confirm_pre_submit');
+}
+
+/**
+ * Submission "pre" handler for the node_revision_delete_confirm form.
+ *
+ * Runs BEFORE the existing delete function in node.pages.inc
+ */
+function revisioning_revision_delete_confirm_pre_submit($form, &$form_state) {
+ $node = $form['#node_revision'];
+ module_invoke_all('revisionapi', 'pre delete', $node);
+}
+
+/**
+ * Submission "pre" handler the revert_confirm form.
+ *
+ * Runs BEFORE the existing revert function in node.pages.inc
+ */
+function revisioning_revert_confirm_pre_submit($form, &$form_state) {
+ $node = $form['#node_revision'];
+ $return = module_invoke_all('revisionapi', 'pre revert', $node);
+ if (in_array(FALSE, $return)) {
+ drupal_goto('node/' . $node->nid . '/revisions/' . $node->vid . '/view');
+ }
+}
+
+/**
+ * Submission "post" handler for the revert_confirm form.
+ *
+ * Runs AFTER the existing revert function in node.pages.inc
+ *
+ * Note:
+ * It would be nice if publish and revert were symmetrical operations and that
+ * node_revision_revert_confirm_submit didn't save a physical copy of the
+ * revision (under a new vid), as this has the side-effect of making all
+ * "pending" revisions "archived". This is because the definition of "pending"
+ * is: "node_vid > current_vid".
+ * It would be better if "pending" relied on a separate flag rather than a field
+ * such as vid or timestamp that changes every time a piece of code executes a
+ * node_save().
+ */
+function revisioning_revert_confirm_post_submit($form, &$form_state) {
+ $node = $form['#node_revision'];
+ // _revisioning_publish_node($node->nid); [#611988]
+ module_invoke_all('revisionapi', 'post revert', $node);
+}
+
+
+/**
+ * Return as a themed table a list of nodes that have pending revisions.
+ *
+ * Also checks access rights of the logged-in user.
+ *
+ * @param string $access
+ * Operation, one of 'view', 'update' or 'delete'.
+ * @param int $user_filter
+ * One of NO_FILTER, I_CREATED or I_LAST_MODIFIED.
+ *
+ * @return string
+ * themed HTML
+ */
+function _revisioning_show_pending_nodes($access = 'view', $user_filter = NO_FILTER) {
+ $is_moderated = user_access('administer nodes') ? NO_FILTER : TRUE;
+ $content_summary = module_grants_monitor_accessible_content_summary($access, NO_FILTER, $user_filter, $is_moderated, TRUE);
+ if (user_access('view revision status messages') && strpos($content_summary, 'No content') === FALSE && !user_access('administer nodes')) {
+ _revisioning_set_info_message();
+ }
+ return $content_summary;
+}
+
+/**
+ * Set info message.
+ */
+function _revisioning_set_info_message() {
+ if (user_access('publish revisions')) {
+ $moderated_types = array();
+ foreach (node_type_get_types() as $type) {
+ if (revisioning_content_is_moderated($type->type) &&
+ (user_access('view revisions') || user_access('view revisions of any ' . $type->type . ' content'))) {
+ $moderated_types[] = $type->name;
+ }
+ }
+ if (count($moderated_types) > 0) {
+ drupal_set_message(t('You have permission to publish content revisions of type(s): %moderated_types.',
+ array('%moderated_types' => implode(', ', $moderated_types))));
+ }
+ }
+}
+
+/**
+ * Handler for the 'Save' button on the edit form.
+ *
+ * When saving a new revision we shouldn't redirect to "View current", as
+ * that's not the one we've saved.
+ */
+function _revisioning_form_submit($form, &$form_state) {
+ // Don't redirect when creating new node, when not moderated or user doesn't
+ // have access to the revision.
+ if (isset($form_state['node']->nid) && !empty($form_state['node']->revision_moderation) && _revisioning_access_node_revision('view revisions', $form_state['node'])) {
+ $form_state['redirect'] = 'node/' . $form_state['node']->nid . '/revisions/' . $form_state['node']->vid . '/view';
+ }
+}
+
+/**
+ * Handler for the 'Delete this revision' button on the edit form.
+ *
+ * Redirect to node/%/revisions/%/delete as opposed to node/%/delete
+ */
+function _revisioning_delete_submit(&$form, &$form_state) {
+ $form_state['redirect'][0] = 'node/' . $form['#node']->nid . '/revisions/' . $form['#node']->vid . '/delete';
+}
+
+
+/**
+ * Implements hook_block_info().
+ *
+ * A block that may be placed on selected pages, alerting the moderator when
+ * new content has been submitted for review. Shows titles of pending revisions
+ * as a series of links. Clicking a link takes the moderator straight to the
+ * revision in question.
+ */
+function revisioning_block_info() {
+ $block['pending']['info'] = t('Pending revisions');
+ $block['pending']['cache'] = DRUPAL_NO_CACHE;
+ // Towards top of whatever region is chosen.
+ $block['pending']['weight'] = -10;
+ // Block is implemented by this module.
+ $block['pending']['custom'] = FALSE;
+ return $block;
+}
+
+/**
+ * Implements hook_block_configure().
+ */
+function revisioning_block_configure($delta = 'pending') {
+ $form['revisioning_block_num_pending'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Maximum number of pending revisions displayed'),
+ '#default_value' => variable_get('revisioning_block_num_pending', 5),
+ '#description' => t('Note: the title of this block mentions the total number of revisions pending, which may be greater than the number of revisions displayed.'),
+ );
+ $form['revisioning_block_order'] = array(
+ '#type' => 'radios',
+ '#title' => t('Order in which pending revisions are displayed'),
+ '#options' => array(
+ REVISIONING_REVISIONS_BLOCK_OLDEST_AT_TOP => t('Oldest at top'),
+ REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP => t('Newest at top')),
+ '#default_value' => variable_get('revisioning_block_order', REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP),
+ '#description' => t('Note: order is based on revision timestamps.'),
+ );
+ $form['revisioning_content_summary_page'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Page to go to when the block title is clicked'),
+ '#default_value' => variable_get('revisioning_content_summary_page', ''),
+ '#description' => t('When left blank this will default to either %view_content, if the Views module is enabled, or %admin_content, subject to permissions. For any of this to work the above Block title field must be left blank.', array(
+ '%view_content' => 'content-summary',
+ '%admin_content' => 'admin/content')),
+ );
+ return $form;
+}
+
+/**
+ * Implements hook_block_save().
+ */
+function revisioning_block_save($delta = '', $edit = array()) {
+ variable_set('revisioning_block_num_pending', (int) $edit['revisioning_block_num_pending']);
+ variable_set('revisioning_block_order', (int) $edit['revisioning_block_order']);
+ variable_set('revisioning_content_summary_page', $edit['revisioning_content_summary_page']);
+}
+
+/**
+ * Implements hook_block_view().
+ */
+function revisioning_block_view($delta = '') {
+ $order = variable_get('revisioning_block_order', REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP) == REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP ? 'DESC' : 'ASC';
+ $revisions = revisioning_get_revisions('view revision list', NO_FILTER, NO_FILTER, NO_FILTER, TRUE, TRUE, 100, 'timestamp ' . $order);
+ if (!empty($revisions)) {
+ return _revisioning_block_pending_revisions_content($revisions);
+ }
+}
+
+/**
+ * Define content for pending revisions block.
+ *
+ * @param array $revisions
+ * array of revision objects
+ *
+ * @return array
+ * the block array
+ */
+function _revisioning_block_pending_revisions_content($revisions) {
+ $num_revisions = count($revisions);
+ $max_num_shown = variable_get('revisioning_block_num_pending', 5);
+ $links = array();
+ foreach (array_slice($revisions, 0, $max_num_shown) as $revision) {
+ // If they exist, should we show multiple pending revisions on same node?
+ $links[] = l($revision->title, "node/$revision->nid/revisions/$revision->vid/view");
+ }
+ // Also loads /css/revisioning-rtl.css
+ drupal_add_css(drupal_get_path('module', 'revisioning') . '/css/revisioning.css');
+ $title_link = trim(variable_get('revisioning_content_summary_page', ''));
+ $link_options = array();
+ if (empty($title_link)) {
+ if (module_exists('views')) {
+ $title_link = 'content-summary';
+ $link_options['query'] = array('revision_moderation' => 1, 'state' => 2);
+ }
+ elseif (user_access('access content overview')) {
+ $title_link = 'admin/content';
+ }
+ }
+ $title = t('!num_revisions pending', array(
+ '!num_revisions' => format_plural($num_revisions, '1 revision', '@count revisions'),
+ ));
+ $block = array();
+ $block['subject'] = empty($title_link) ? $title : l($title, $title_link, $link_options);
+ $block['content'] = theme('item_list', array('items' => $links, 'title' => ''));
+ return $block;
+}
diff --git a/sites/all/modules/revisioning/revisioning.rules.inc b/sites/all/modules/revisioning/revisioning.rules.inc
new file mode 100644
index 0000000..fd6aa1f
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning.rules.inc
@@ -0,0 +1,297 @@
+ t('Revisioning'),
+ 'variables' => _revisioning_rules_event_variables(),
+ );
+ $events = array(
+ 'revisioning_post_update' => $default + array(
+ 'label' => t('Content revision has been updated'),
+ ),
+ 'revisioning_pre_publish' => $default + array(
+ 'label' => t('Content revision is going to be published'),
+ ),
+ 'revisioning_post_publish' => $default + array(
+ 'label' => t('Pending revision has been published'),
+ ),
+ 'revisioning_pre_revert' => $default + array(
+ 'label' => t('Content is going to be reverted to revision'),
+ ),
+ 'revisioning_post_revert' => $default + array(
+ 'label' => t('Content has been reverted to revision'),
+ ),
+ 'revisioning_post_unpublish' => $default + array(
+ 'label' => t('Content has been unpublished'),
+ ),
+ 'revisioning_pre_delete' => $default + array(
+ 'label' => t('Content revision is going to be deleted'),
+ ),
+ );
+ return $events;
+}
+
+/**
+ * Implements hook_rules_event_info_alter().
+ *
+ * Add revision variable to all events with node. [#2232451]
+ */
+function revisioning_rules_event_info_alter(&$events) {
+ foreach ($events as $key => $event) {
+ if (isset($event['variables'])) {
+ foreach ($event['variables'] as $name => $variable) {
+ if ($variable['type'] == 'node') {
+ $extra = _revisioning_rules_event_variables();
+ unset($extra['node']);
+ $events[$key]['variables'] += $extra;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Returns event variables suitable for use with a node revision event.
+ *
+ * (aka Data Selectors)
+ */
+function _revisioning_rules_event_variables() {
+ $vars = array(
+ 'user' => array(
+ 'type' => 'user',
+ 'label' => t('acting user'),
+ 'description' => t('The acting user.'),
+ 'handler' => 'revisioning_events_argument_global_user',
+ ),
+ 'node' => array(
+ 'type' => 'node',
+ 'label' => t('target node.'),
+ ),
+ 'revision' => array(
+ // Type revisioning_revision' ?
+ 'type' => 'node',
+ 'label' => t('current revision of target content.'),
+ 'description' => t('The current content revision'),
+ 'handler' => 'revisioning_events_argument_current_revision',
+ ),
+ );
+ return $vars;
+}
+
+/**
+ * Get global user argument.
+ */
+function revisioning_events_argument_global_user($arguments, $name, $info) {
+ global $user;
+ return user_load($user->uid);
+}
+
+/**
+ * Evaluate revision argument.
+ */
+function revisioning_events_argument_current_revision($arguments, $name, $info) {
+ if (empty($arguments['node'])) {
+ drupal_set_message(t('Revisioning: could not evaluate rule condition -- node variable missing.'), 'warning');
+ return FALSE;
+ }
+ $node = $arguments['node'];
+ // Use revisioning_get_current_node_revision_id($node->nid); ?
+ $current_vid = $node->current_revision_id;
+ if ($node->vid != $current_vid) {
+ $current = node_load($node->nid, $current_vid);
+ return $current;
+ }
+ return $node;
+}
+
+/**
+ * Target revision author argument handler.
+ */
+function revisioning_events_argument_target_revision_author($arguments, $name, $info) {
+ global $user;
+ $node = $arguments['node'];
+ return user_load($node->revision_uid);
+}
+
+function revisioning_events_argument_node_author($arguments, $name, $info) {
+ $node = $arguments['node'];
+ return user_load($node->uid);
+}
+
+function revisioning_events_argument_current_revision_author($arguments, $name, $info) {
+ $node = $arguments['node'];
+ $current_vid = $node->current_revision_id;
+ if ($node->vid != $current_vid) {
+ $current = node_load($node->nid, $current_vid);
+ return user_load($current->revision_uid);
+ }
+ return user_load($node->revision_uid);
+}
+
+/*************************** Rules Conditions ********************************/
+
+/**
+ * Implements hook_rules_condition_info().
+ */
+function revisioning_rules_condition_info() {
+ $defaults = array(
+ 'group' => t('Revisioning'),
+ 'parameter' => array(
+ 'node' => array('type' => 'node', 'label' => t('Content')),
+ ),
+ );
+ $items['revisioning_node_has_pending'] = $defaults + array(
+ 'label' => t('Content has pending revision'),
+ 'help' => t('Evaluates to TRUE, if the content has one or more pending revisions.'),
+ 'module' => 'revisioning',
+ );
+ $items['revisioning_condition_revision_is'] = $defaults + array(
+ 'label' => t('Content revision state'),
+ 'help' => t('Evaluates to TRUE, if the revision is in one of the selected states.'),
+ 'module' => 'revisioning',
+ 'base' => 'revisioning_condition_revision_is',
+ );
+ $items['revisioning_condition_revision_is']['parameter']['revision_type'] = array(
+ 'type' => 'list',
+ 'label' => t('Is one of'),
+ 'options list' => 'revisioning_revision_states_all',
+ 'description' => t('The content state(s) to check for.'),
+ );
+ return $items;
+}
+
+/**
+ * Wrapper function to always return all revisioning states.
+ */
+function revisioning_revision_states_all() {
+ return revisioning_revision_states();
+}
+
+/**
+ * Condition: check for pending revisions of the node.
+ */
+function revisioning_node_has_pending($node, $settings) {
+ revisioning_set_node_revision_info($node);
+ return ($node->revision_moderation && $node->num_revisions == 1 && !$node->status) || (_revisioning_get_number_of_pending_revisions($node->nid) > 0);
+}
+
+/**
+ * Condition: check revision state.
+ */
+function revisioning_condition_revision_is($node, $settings) {
+ revisioning_set_node_revision_info($node);
+ // Should we return FALSE here if $node->revision_moderation is not set?
+ $type = _revisioning_revision_is($node);
+ return in_array($type, $settings);
+}
+
+/**
+ * Label callback for "revisioning_revision_is" condition.
+ */
+function revisioning_condition_revision_is_label($settings, $argument_labels) {
+ $names = array_intersect_key(revisioning_revision_states(), $settings['revision_type']);
+ return t('Revision status of @node is: @type', $argument_labels + array('@type' => implode(t(' or '), $names)));
+}
+
+/**
+ * Label callback for "revisioning_node_has_pending" condition.
+ */
+function revisioning_node_has_pending_label($settings, $argument_labels) {
+ return t('Content "@node" has pending revision(s)', $argument_labels);
+}
+
+/*************************** Rules Actions ************************************/
+
+/**
+ * Implements hook_rules_action_info().
+ */
+function revisioning_rules_action_info() {
+ $default = array(
+ 'group' => t('Revisioning'),
+ );
+ return array(
+ 'revisioning_rules_action_publish_latest' => $default + array(
+ 'label' => t('Publish the most recent pending revision'),
+ 'parameter' => array(
+ 'node' => array('type' => 'node', 'label' => t('content')),
+ ),
+ ),
+ 'revisioning_rules_action_load_current' => $default + array(
+ 'label' => t('Load current revision of content'),
+ 'parameter' => array(
+ 'node' => array('type' => 'node', 'label' => t('content')),
+ ),
+ 'new variables' => array(
+ 'loaded_current_revision' => array(
+ 'type' => 'node',
+ 'label' => t('Loaded current revision of content'),
+ 'save' => FALSE,
+ 'label callback' => 'revisioning_rules_loaded_current_label',
+ ),
+ ),
+ ),
+ );
+}
+
+/**
+ * Label callback for "revisioning_rules_action_load_current" action.
+ * @obsolete ?
+ */
+function revisioning_rules_action_load_current_label($settings, $argument_labels) {
+ return t('Load current revision of "@node"', $argument_labels);
+}
+
+/**
+ * Label callback for "loaded_current_revision" variable.
+ * @obsolete ?
+ */
+function revisioning_rules_loaded_current_label($settings, $argument_labels) {
+ return t('Loaded current revision of "@node"', $argument_labels);
+}
+
+/**
+ * Action: load current revision of provided node.
+ */
+function revisioning_rules_action_load_current($node) {
+ // Or revisioning_get_current_node_revision_id($node->nid); ?
+ $current_vid = $node->current_revision_id;
+ if ($node->vid != $current_vid) {
+ $current = node_load($node->nid, $current_vid);
+ return array('loaded_current_revision' => $current);
+ }
+ return array('loaded_current_revision' => $node);
+}
+
+/**
+ * Action: publish most recent pending revision.
+ */
+function revisioning_rules_action_publish_latest($node) {
+ _revisioning_publish_latest_revision($node);
+}
+
+/**
+ * Implements hook_entity_property_info_alter().
+ */
+function revisioning_entity_property_info_alter(&$info) {
+ // $info['node']['properties']['author']['getter callback'] =
+ // 'revisioning_user_get_properties';
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/revisioning/revisioning.taxonomy.inc b/sites/all/modules/revisioning/revisioning.taxonomy.inc
new file mode 100644
index 0000000..27b163b
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning.taxonomy.inc
@@ -0,0 +1,75 @@
+condition('nid', $node->nid)->execute();
+ // Use of taxonomy_delete_node_index($node); requires core 7.12 or later
+ // Then add terms if node is published OR terms are requested explicitly.
+ if ($node->status || $show_unpublished_content_terms) {
+ $vid = empty($node->revision_moderation) || empty($node->current_revision_id) ? $node->vid : $node->current_revision_id;
+ // Find all the terms attached to this node revision.
+ $tids = revisioning_get_tids($vid);
+ if (!empty($tids)) {
+ // Core, via taxonomy_node_update(), only does this when node is
+ // published, but then we can't see the terms of unpublished content
+ // in Views!
+ $query = db_insert('taxonomy_index')->fields(
+ array('nid', 'tid', 'sticky', 'created')
+ );
+ foreach ($tids as $tid) {
+ $query->values(array(
+ 'nid' => $node->nid,
+ 'tid' => $tid,
+ // See [#1417658].
+ 'sticky' => empty($node->sticky) ? 0 : 1,
+ 'created' => $node->created,
+ ));
+ }
+ $query->execute();
+ }
+ }
+ }
+}
+
+/**
+ * Return array of all taxonomy term ids belonging to the supplied revision.
+ *
+ * @param int $vid
+ * the revision id
+ */
+function revisioning_get_tids($vid) {
+ $tids = array();
+ $conditions = array('type' => 'taxonomy_term_reference');
+ $fields = field_read_fields($conditions);
+ foreach ($fields as $field => $data) {
+ $sql = "SELECT {$field}_tid AS tid FROM {field_revision_$field} WHERE revision_id = :vid AND entity_type = 'node' AND deleted = 0 ORDER BY tid";
+ $result = db_query($sql, array(':vid' => $vid));
+ foreach ($result as $term) {
+ $tids[$term->tid] = $term->tid;
+ }
+ }
+ return $tids;
+}
diff --git a/sites/all/modules/revisioning/revisioning_api.inc b/sites/all/modules/revisioning/revisioning_api.inc
new file mode 100644
index 0000000..9ea147b
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_api.inc
@@ -0,0 +1,965 @@
+ current_vid) of ANY node
+ * OR single revision of UNPUBLISHED node
+ * Current, published:
+ * - revision with (vid == current_vid) of PUBLISHED node
+ * Archived:
+ * - all other revisions, i.e.
+ * revision with (vid < current_vid) of ANY node
+ * OR revision with (vid == current_vid) of UNPUBLISHED node
+ *
+ * Note: these will change when Revisioning is going to store revision states
+ * independently from vid number (e.g. in different table).
+ */
+
+/**
+ * Set node revision info.
+ *
+ * We use this in revisioning_node_load() to set up some useful node properties
+ * that may be read later, whether it be in this module or another, thus
+ * removing the need for multiple calls in various places to retrieve the same
+ * info.
+ */
+function revisioning_set_node_revision_info(&$node) {
+ if (!isset($node->num_revisions) && isset($node->nid)) {
+ // We need this info for updated content even if it is not moderated.
+ // Luckily this info is easily retrieved.
+ $node->num_revisions = revisioning_get_number_of_revisions($node->nid);
+ $node->current_revision_id = revisioning_get_current_node_revision_id($node->nid);
+ $node->is_current = revisioning_revision_is_current($node);
+ $node->is_pending = _revisioning_node_is_pending($node);
+ }
+ // The revision_moderation flag may be overridden on the node edit form by
+ // users with the "administer nodes" permission. By implication, the 'Publish'
+ // link needs to be available to those users, for any content with a pending
+ // revision, as the publish check box on the edit form applies to the current
+ // rather than the pending revision(s).
+ if (!isset($node->revision_moderation)) {
+ $node->revision_moderation = revisioning_content_is_moderated($node->type, $node);
+ }
+ // $node->uid and $node->revision_uid were already set in node_load()
+ // $node->revision is set as part of 'prepare'-op, see node_object_prepare()
+}
+
+/**
+ * Get the number of revisions belonging to a node.
+ *
+ * @param int $nid
+ * id of the node
+ *
+ * @return int
+ * A count representing the number of revisions associated with the node
+ */
+function revisioning_get_number_of_revisions($nid) {
+ $result = db_query("SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid", array(':nid' => $nid));
+ return $result->fetchField();
+}
+
+/**
+ * Get the number of archived revisions belonging to a node.
+ *
+ * @param object $node
+ * the node object
+ *
+ * @return int
+ * A count representing the number of archived revisions for the node
+ * Returns zero if there is only one (i.e. current) revision.
+ */
+function revisioning_get_number_of_archived_revisions($node) {
+ $result = db_query("SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid AND vid < :vid", array(
+ ':nid' => $node->nid,
+ ':vid' => $node->current_revision_id,
+ ));
+ return $result->fetchField();
+}
+
+/**
+ * Delete all revisions with a vid less than the current.
+ *
+ * Use node_revision_delete from node.module to ensure that we cleanup not
+ * only node revisions but also all attached field revisions as well.
+ */
+function revisioning_delete_archived_revisions($node) {
+ $revisions = db_select('node_revision', 'n')
+ ->fields('n', array('vid'))
+ ->condition('n.nid', $node->nid)
+ ->condition('n.vid', $node->current_revision_id, '<')
+ ->execute();
+
+ foreach ($revisions as $rev) {
+ node_revision_delete($rev->vid);
+ }
+}
+
+/**
+ * Get the id of the current revision that the supplied node is pointing to.
+ *
+ * Used in cases where the node object wasn't fully loaded or was loaded
+ * with a different revision.
+ *
+ * @param int $nid
+ * The id of the node whose current revision id is to be returned.
+ *
+ * @return int
+ * A single number being the current revision id (vid).
+ */
+function revisioning_get_current_node_revision_id($nid) {
+ $result = db_query("SELECT vid FROM {node} WHERE nid = :nid", array(':nid' => $nid));
+ return $result->fetchField();
+}
+
+/**
+ * Get the id of the user who last edited the supplied node.
+ *
+ * ie. the author of the latest revision.
+ *
+ * This is irrespective of whether this latest revision is pending or not,
+ * unless TRUE is specified for the second argument, in which case the uid
+ * of the creator of the current revision (published or not) is returned.
+ *
+ * @param int $nid
+ * The id of the node whose most recent editor id is to be returned.
+ * @param bool $current
+ * Whether the uid of the current or very latest revision should be returned.
+ *
+ * @return int
+ * A single number being the user id (uid).
+ */
+function revisioning_get_last_editor($nid, $current = FALSE) {
+ $sql = ($current) ? "SELECT vid FROM {node} WHERE nid = :nid" : "SELECT MAX(vid) FROM {node_revision} WHERE nid = :nid";
+ $vid = db_query($sql, array(':nid' => $nid))->fetchField();
+ $result = db_query("SELECT uid FROM {node_revision} WHERE vid = :vid", array(':vid' => $vid));
+ return $result->fetchField();
+}
+
+/**
+ * Return whether the currenly loaded revision is the current one.
+ *
+ * @param object $node
+ * The node object
+ *
+ * @return bool
+ * TRUE if the currently loaded node revision is the current revision
+ */
+function revisioning_revision_is_current($node) {
+ return isset($node->vid) && isset($node->current_revision_id) && $node->vid == $node->current_revision_id;
+}
+
+/**
+ * Return whether the supplied content type is subject to moderation.
+ *
+ * @param string $content_type
+ * i.e. machine name, ie. $node->type
+ * @param object $node
+ * (optional) argument to implement "moderation opt-in" for nodes that are not
+ * moderated by default (i.e by their content type).
+ *
+ * @return bool
+ * TRUE, if the supplied type has the "New revision in draft, pending
+ * moderation" box ticked on the Structure >> Content types >> edit page OR
+ * when a pending revision exists. The latter is to support the feature of
+ * "moderate at any time" available to users with the "administer nodes"
+ * permission.
+ */
+function revisioning_content_is_moderated($content_type, $node = NULL) {
+ $content_type_is_moderated = !empty($content_type) && in_array('revision_moderation', variable_get('node_options_' . $content_type, array()));
+ if (!$content_type_is_moderated && isset($node->nid) && isset($node->current_revision_id)) {
+ $latest_vid = revisioning_get_latest_revision_id($node->nid);
+ return $latest_vid > $node->current_revision_id;
+ }
+ return $content_type_is_moderated;
+}
+
+/**
+ * Return whether the user has permission to auto-publish the supplied node.
+ *
+ * Auto-publish applies only for content types for which this feature has been
+ * activated on the content type form and only when "New revision in draft,
+ * pending moderation" is ticked also.
+ *
+ * @param object $node
+ * The node object
+ */
+function revisioning_user_may_auto_publish($node) {
+ return variable_get('revisioning_auto_publish_' . $node->type, FALSE) && revisioning_user_node_access('publish revisions', $node);
+}
+
+/**
+ * Return a single or all possible revision state names.
+ *
+ * @param int $state
+ * (optional) state id, as defined in revisioning_api.inc
+ *
+ * @return array|string
+ * if $state is provided, state name. Otherwise, an array keyed by state id.
+ */
+function revisioning_revision_states($state = NULL) {
+ $states = array(
+ REVISION_ARCHIVED => t('Archived'),
+ REVISION_CURRENT => t('Current, published'),
+ REVISION_PENDING => t('Pending'),
+ );
+ return $state === NULL ? $states : $states[$state];
+}
+
+/**
+ * Check for a pending revision.
+ *
+ * Return TRUE when either of the following is true:
+ * o the supplied node has at least one revision more recent than the current
+ * o the node is not yet published and consists of a single revision
+ *
+ * Relies on vid, current_revision_id and num_revisions set on the node object,
+ * see function revisioning_set_node_revision_info()
+ *
+ * @param object $node
+ * The node object
+ *
+ * @return bool
+ * TRUE, if node is pending according to the above definition
+ */
+function _revisioning_node_is_pending($node) {
+ return isset($node->vid) && isset($node->current_revision_id) &&
+ ($node->vid > $node->current_revision_id || (!$node->status && $node->num_revisions == 1));
+}
+
+/**
+ * Implements hook_revisionapi().
+ *
+ * Act on various revision events.
+ *
+ * "Pre" operations can be useful to get values before they are lost or changed,
+ * for example, to save a backup of revision before it's deleted.
+ * Also, for "pre" operations vetoing mechanics could be implemented, so it
+ * would be possible to veto an operation via hook_revisionapi(). For example,
+ * when the hook is returning FALSE, operation will be vetoed.
+ */
+function revisioning_revisionapi($op, $node) {
+ switch ($op) {
+
+ case 'post update':
+ if (module_exists('rules')) {
+ rules_invoke_event('revisioning_post_update', $node);
+ }
+ break;
+
+ case 'pre publish':
+ if (module_exists('rules')) {
+ rules_invoke_event('revisioning_pre_publish', $node);
+ }
+ break;
+
+ case 'post publish':
+ // Called from _revisioning_publish_revision.
+ // Invoke hook_revision_publish() triggers, passing the node as argument.
+ module_invoke_all('revision_publish', $node);
+
+ if (module_exists('rules')) {
+ rules_invoke_event('revisioning_post_publish', $node);
+ }
+ break;
+
+ case 'post unpublish':
+ // Called from _revisioning_unpublish_revision().
+ // Invoke hook_revision_unpublish triggers passing the node as an arg.
+ module_invoke_all('revision_unpublish', $node);
+
+ if (module_exists('rules')) {
+ rules_invoke_event('revisioning_post_unpublish', $node);
+ }
+ break;
+
+ case 'pre revert':
+ if (module_exists('rules')) {
+ rules_invoke_event('revisioning_pre_revert', $node);
+ }
+ break;
+
+ case 'post revert':
+ // Called from revisioning_revert_confirm_post_submit().
+ // Invoke hook_revision_revert() triggers passing the node as an arg.
+ module_invoke_all('revision_revert', $node);
+
+ if (module_exists('rules')) {
+ rules_invoke_event('revisioning_post_revert', $node);
+ }
+ break;
+
+ case 'pre delete':
+ if (module_exists('rules')) {
+ rules_invoke_event('revisioning_pre_delete', $node);
+ }
+ break;
+
+ case 'post delete':
+ break;
+ }
+}
+
+/**
+ * Get the id of the latest revision belonging to a node.
+ *
+ * @param int $nid
+ * id of the node
+ *
+ * @return int
+ * ID of the latest revision.
+ */
+function revisioning_get_latest_revision_id($nid) {
+ $result = db_query("SELECT MAX(vid) FROM {node_revision} WHERE nid = :nid", array(':nid' => $nid));
+ return $result->fetchField();
+}
+
+/**
+ * Revert node to selected revision without changing its publication status.
+ *
+ * @param object|int $node
+ * Target $node object (loaded with target revision) or nid of target node
+ * @param int $vid
+ * (optional) vid of revision to revert to, if provided $node must not be an
+ * object.
+ */
+function _revisioning_revertpublish_revision(&$node, $vid = NULL) {
+ $node_revision = is_object($node) ? $node : node_load($node, $vid);
+ $return = module_invoke_all('revisionapi', 'pre revert', $node_revision);
+ if (in_array(FALSE, $return)) {
+ drupal_goto('node/' . $node_revision->nid . '/revisions/' . $node_revision->vid . '/view');
+ }
+ _revisioning_revert_revision($node_revision);
+ module_invoke_all('revisionapi', 'post revert', $node_revision);
+}
+
+/**
+ * Revert node to selected revision without publishing it.
+ *
+ * This is same as node_revision_revert_confirm_submit() in node_pages.inc,
+ * except it doesn't put any messages on screen.
+ *
+ * @param object|int $node
+ * Target $node object (loaded with target revision) or nid of target node
+ * @param int $vid
+ * (optional) vid of revision to revert to, if provided $node is not an
+ * object.
+ */
+function _revisioning_revert_revision(&$node, $vid = NULL) {
+ $node_revision = is_object($node) ? $node : node_load($node, $vid);
+ $node_revision->revision = 1;
+ $node_revision->log = t('Copy of the revision from %date.', array('%date' => format_date($node_revision->revision_timestamp)));
+ if (module_exists('taxonomy')) {
+ $node_revision->taxonomy = array_keys($node_revision->taxonomy);
+ }
+ node_save($node_revision);
+ watchdog('content', '@type: reverted %title revision %revision.', array(
+ '@type' => $node_revision->type,
+ '%title' => $node_revision->title,
+ '%revision' => $node_revision->vid,
+ ));
+}
+
+/**
+ * Unpublish node, without calling node_save().
+ *
+ * @param object|int $nid_or_node
+ * Target $node object or nid of target node
+ * @param bool $clear_cache
+ * Whether to clear the cache afterwards or not. Clearing the cache on every
+ * node during bulk operations can be time-consuming.
+ */
+function _revisioning_unpublish_node($nid_or_node, $clear_cache = TRUE) {
+ $node = is_object($nid_or_node) ? $nid_or_node : node_load($nid_or_node);
+
+ db_update('node')
+ ->fields(array(
+ 'changed' => time(),
+ 'status' => NODE_NOT_PUBLISHED))
+ ->condition('nid', $node->nid)
+ ->execute();
+
+ db_update('node_revision')
+ ->fields(array('status' => NODE_NOT_PUBLISHED))
+ ->condition('vid', $node->vid)
+ ->execute();
+
+ $node->status = NODE_NOT_PUBLISHED;
+
+ // Make sure the alias, if present, is not changed when unpublishing.
+ if (!isset($node->path['pathauto'])) {
+ $node->path = array(
+ // So that pathauto_node_update() does nothing.
+ 'alias' => '',
+ // So that pathauto_node_update() does nothing.
+ 'pathauto' => FALSE,
+ );
+ }
+ elseif (!isset($node->path['alias'])) {
+ // [#1328180], [#1576552]
+ $node->path['alias'] = '';
+ }
+ $node->original = clone $node;
+ $node->original->status = NODE_PUBLISHED;
+
+ module_invoke_all('node_update', $node);
+ module_invoke_all('entity_update', $node, 'node');
+
+ // Update node_access table.
+ node_access_acquire_grants($node);
+
+ if ($clear_cache) {
+ cache_clear_all();
+ }
+}
+
+/**
+ * Delete selected revision of node, provided it's not current.
+ *
+ * This is same as node_revision_delete_confirm_submit() in node_pages.inc,
+ * except it doesn't put any messages on the screen. This way it becomes
+ * reusable (eg. in actions).
+ *
+ * @param object $node
+ * Target $node object (loaded with target revision) or nid of target node
+ * @param int $vid
+ * (optional) vid of revision to delete, if provided $node is not object.
+ *
+ * @TODO: Insert check to prevent deletion of current revision of node.
+ */
+function _revisioning_delete_revision(&$node, $vid = NULL) {
+ $node_revision = is_object($node) ? $node : node_load($node, $vid);
+ module_invoke_all('revisionapi', 'pre delete', $node_revision);
+ db_delete('node_revision')
+ ->condition('vid', $node_revision->vid)
+ ->execute();
+ watchdog('content', '@type: deleted %title revision %revision.', array(
+ '@type' => $node_revision->type,
+ '%title' => $node_revision->title,
+ '%revision' => $node_revision->vid,
+ ));
+ module_invoke_all('revisionapi', 'post delete', $node_revision);
+}
+
+/**
+ * Unpublish revision (i.e. the node).
+ *
+ * Note that no check is made as to whether the initiating user has permission
+ * to unpublish this node.
+ *
+ * @param object $node
+ * Target $node object or nid of target node
+ */
+function _revisioning_unpublish_revision(&$node) {
+ $node_revision = is_object($node) ? $node : node_load($node);
+ module_invoke_all('revisionapi', 'pre unpublish', $node_revision);
+ _revisioning_unpublish_node($node_revision->nid);
+ watchdog('content', 'Unpublished @type %title', array('@type' => $node_revision->type, '%title' => $node_revision->title), WATCHDOG_NOTICE, l(t('view'), "node/$node_revision->nid"));
+ module_invoke_all('revisionapi', 'post unpublish', $node_revision);
+}
+
+/**
+ * Make the supplied revision of the node current and publish it.
+ *
+ * It is the caller's responsibility to provide a proper revision.
+ * Note that no check is made as to whether the initiating user has permission
+ * to publish this revision.
+ *
+ * @param int $node_revision
+ * Target $node object (loaded with target revision)
+ * @param bool $clear_cache
+ * Whether to clear the cache afterwards or not. Clearing the cache on every
+ * node during bulk operations can be time-consuming.
+ */
+function _revisioning_publish_revision(&$node_revision, $clear_cache = TRUE) {
+
+ $return = module_invoke_all('revisionapi', 'pre publish', $node_revision);
+ if (in_array(FALSE, $return)) {
+ drupal_goto('node/' . $node_revision->nid . '/revisions/' . $node_revision->vid . '/view');
+ }
+ // Update {node} and {node_revision} tables setting status and other flags.
+ db_update('node')
+ ->fields(array(
+ 'vid' => $node_revision->vid,
+ 'title' => $node_revision->title,
+ 'changed' => time(),
+ 'status' => NODE_PUBLISHED,
+ 'comment' => $node_revision->comment,
+ 'promote' => $node_revision->promote,
+ 'sticky' => $node_revision->sticky))
+ ->condition('nid', $node_revision->nid)
+ ->execute();
+
+ db_update('node_revision')
+ ->fields(array('status' => NODE_PUBLISHED))
+ ->condition('vid', $node_revision->vid)
+ ->execute();
+
+ if (empty($node_revision->is_current)) {
+ // Need to set up $node_revision correctly before calling
+ // revisioning_update_taxonomy_index(), via revisioning_node_update().
+ $node_revision->current_revision_id = $node_revision->vid;
+ }
+ $node_revision->status = $node_revision->current_status = NODE_PUBLISHED;
+
+ // Make sure the alias, if present, is not changed when publishing.
+ if (!isset($node_revision->path['pathauto'])) {
+ $node_revision->path = array(
+ // So that path_node_update() does nothing.
+ 'alias' => '',
+ // So that pathauto_node_update() does nothing.
+ 'pathauto' => FALSE,
+ );
+ }
+ elseif (!isset($node_revision->path['alias'])) {
+ // [#1328180], [#1576552]
+ $node_revision->path['alias'] = '';
+ }
+ // Make sure the menu, if present, is not changed when publishing [#1698024]
+ if (!isset($node_revision->menu)) {
+ $node_revision->menu = array(
+ 'enabled' => '',
+ 'mlid' => '',
+ 'link_title' => '',
+ 'link_path' => '',
+ 'description' => '',
+ );
+ }
+ elseif (!isset($node_revision->menu)) {
+ $node_revision->menu = '';
+ }
+ $node_revision->original = clone $node_revision;
+ $node_revision->original->status = NODE_NOT_PUBLISHED;
+
+ module_invoke_all('node_update', $node_revision);
+ module_invoke_all('entity_update', $node_revision, 'node');
+
+ // Update node_access table only for existing nodes. When the node is newly
+ // created via the node/add page, node_access_acquire_grants() is called by
+ // node_save() anyway. See [#1243018].
+ if (empty($node_revision->is_new)) {
+ node_access_acquire_grants($node_revision);
+ }
+
+ if ($clear_cache) {
+ cache_clear_all();
+ }
+
+ watchdog('content', 'Published rev #%revision of @type %title',
+ array(
+ '@type' => $node_revision->type,
+ '%title' => $node_revision->title,
+ '%revision' => $node_revision->vid,
+ ),
+ WATCHDOG_NOTICE,
+ l(t('view'), "node/$node_revision->nid/revisions/$node_revision->vid/view")
+ );
+ module_invoke_all('revisionapi', 'post publish', $node_revision);
+}
+
+/**
+ * Publish latest revision.
+ *
+ * Find the most recent pending revision, make it current, unless it already is
+ * and publish it and its node.
+ *
+ * Note that no check is made as to whether the initiating user has permission
+ * to publish this node.
+ *
+ * Note that this is a post-save operation that should not be called in
+ * response to hook_node_presave(), as invoked from node_save().
+ *
+ * @param object $node
+ * The node object whose latest pending revision is to be published
+ *
+ * @return bool
+ * TRUE if operation was successful, FALSE if there is no pending revision to
+ * publish
+ */
+function _revisioning_publish_latest_revision(&$node) {
+ // Get the latest pending revision.
+ $pending_revisions = _revisioning_get_pending_revisions($node->nid);
+ $latest_pending = array_shift($pending_revisions);
+ if ($latest_pending) {
+ $node_revision = node_load($node->nid, $latest_pending->vid);
+ _revisioning_publish_revision($node_revision);
+ return TRUE;
+ }
+ // If there is no pending revision, take the current revision, provided it is
+ // NOT published.
+ if (!$node->status) {
+ if (!isset($node->is_current)) {
+ $node->current_revision_id = revisioning_get_current_node_revision_id($node->nid);
+ $node->is_current = revisioning_revision_is_current($node);
+ }
+ if ($node->is_current) {
+ _revisioning_publish_revision($node);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * Return a count of the number of revisions newer than the supplied vid.
+ *
+ * @param int $vid
+ * The reference vid.
+ * @param int $nid
+ * The id of the node.
+ *
+ * @return int
+ * count of the number of revisions newer than the supplied vid
+ */
+function _revisioning_get_number_of_revisions_newer_than($vid, $nid) {
+ $result = db_query("SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid AND vid > :vid",
+ array(
+ ':vid' => $vid,
+ ':nid' => $nid,
+ )
+ );
+ return $result->fetchField();
+}
+
+/**
+ * Return a count of the number of revisions newer than the current revision.
+ *
+ * @param int $nid
+ * The id of the node.
+ *
+ * @return int
+ * count of the number of revisions newer than the current revision
+ */
+function _revisioning_get_number_of_pending_revisions($nid) {
+ $result = db_query("SELECT COUNT(r.vid) FROM {node} n INNER JOIN {node_revision} r ON n.nid = r.nid WHERE (r.vid > n.vid AND n.nid = :nid)", array(
+ ':nid' => $nid)
+ );
+ return $result->fetchField();
+}
+
+/**
+ * Retrieve a list of revisions with a vid greater than the current.
+ *
+ * @param int $nid
+ * The node id to retrieve.
+ *
+ * @return array
+ * An array of revisions (latest first), each containing vid, title and
+ * content type.
+ */
+function _revisioning_get_pending_revisions($nid) {
+ $sql = "SELECT r.vid, r.title, n.type FROM {node} n INNER JOIN {node_revision} r ON n.nid = r.nid WHERE (r.vid > n.vid AND n.nid = :nid) ORDER BY r.vid DESC";
+ $result = db_query($sql, array(
+ ':nid' => $nid)
+ );
+ $revisions = array();
+ foreach ($result as $revision) {
+ $revisions[$revision->vid] = $revision;
+ }
+ return $revisions;
+}
+
+/**
+ * Return revision type of the supplied node.
+ *
+ * @param object $node
+ * Node object to check
+ *
+ * @return int
+ * Revision type
+ */
+function _revisioning_revision_is(&$node) {
+ if ($node->is_pending) {
+ return REVISION_PENDING;
+ }
+ return ($node->is_current && $node->status == NODE_PUBLISHED) ? REVISION_CURRENT : REVISION_ARCHIVED;
+}
+
+/**
+ * Return a string with details about the node that is about to be displayed.
+ *
+ * @param object $node
+ * The node that is about to be viewed
+ *
+ * @return string
+ * A translatable message containing details about the node
+ */
+function _revisioning_node_info_msg($node) {
+ // Get username for the revision, not the creator of the node.
+ $revision_author = user_load($node->revision_uid);
+ $placeholder_data = array(
+ '@content_type' => $node->type,
+ '%title' => $node->title,
+ '!author' => theme('username', array('account' => $revision_author)),
+ '@date' => format_date($node->revision_timestamp, 'short'),
+ );
+ $revision_type = _revisioning_revision_is($node);
+ switch ($revision_type) {
+
+ case REVISION_PENDING:
+ return t('Displaying pending revision of @content_type %title, last modified by !author on @date', $placeholder_data);
+
+ case REVISION_CURRENT:
+ return t('Displaying current, published revision of @content_type %title, last modified by !author on @date', $placeholder_data);
+
+ case REVISION_ARCHIVED:
+ return t('Displaying archived revision of @content_type %title, last modified by !author on @date', $placeholder_data);
+ }
+}
+
+/**
+ * Return TRUE only if the user account has ALL of the supplied permissions.
+ *
+ * @param array $permissions
+ * An array of permissions (strings)
+ * @param object $account
+ * (optional) The user account object. Defaults to current user if omitted.
+ *
+ * @return bool
+ * Whether the user has access to all permissions
+ */
+function revisioning_user_access_all($permissions, $account = NULL) {
+ foreach ($permissions as $permission) {
+ if (!user_access($permission, $account)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Return an array of names of content types that are subject to moderation.
+ *
+ * @return array
+ * array of strings, may be empty
+ */
+function revisioning_moderated_content_types($machine_name = TRUE) {
+ $moderated_content_types = array();
+ foreach (node_type_get_types() as $type) {
+ $content_type = check_plain($type->type);
+ if (revisioning_content_is_moderated($content_type)) {
+ $moderated_content_types[] = ($machine_name ? $content_type : $type->name);
+ }
+ }
+ return $moderated_content_types;
+}
+
+define('NO_FILTER', '-1');
+
+/**
+ * Get list of revisions accessible to the logged-in user via the operation.
+ *
+ * @param string $op
+ * Revision operation, eg 'view revision list' (as used by Pending Revisions
+ * block)
+ * @param int $is_published
+ * (optional) 1 to return only published content
+ * 0 to return only content that isn't published
+ * -1 (default) no filter, return content regardles of publication status
+ * @param int $creator_uid
+ * (optional) Only return content created by the user with the supplied id.
+ * Defaults to -1, which means don't care who the creator is.
+ * @param int $modifier_uid
+ * (optional) Only return content last modified by user with the supplied id.
+ * Defaults to -1, which means don't care who last modifed the node.
+ * @param bool|int $is_moderated
+ * (optional) TRUE to return only content of types subject to moderation
+ * FALSE to return only content that isn't subject to moderation
+ * -1 (default) no filter, return content regardles of moderation flag
+ * @param bool $is_pending
+ * (optional) bool indicating whether only nodes pending publication should be
+ * returned; a pending node is defined as a node that has a revision newer
+ * than the current OR a node with a single revision that is not published.
+ * @param int $max
+ * (optional) Maximum number of nodes to be returned, defaults to 1000
+ * @param string $order_by_override
+ * (optional) "ORDER BY ..." clause to be added, defaults to "timestamp DESC".
+ *
+ * @return array
+ * An array of revision objects each containing nid, content type, published
+ * flag, creator-id, title+vid+modifier-id+timestamp of the current revision,
+ * plus tags and taxonomy terms.
+ *
+ * @todo
+ * This code may need to be reviewed if used for purposes other than the
+ * Pending Revisions block.
+ */
+function revisioning_get_revisions($op, $is_published = -1, $creator_uid = -1, $modifier_uid = -1,
+ $is_moderated = -1, $is_pending = FALSE, $max = 1000, $order_by_override = NULL) {
+ $sql_select = 'SELECT n.nid, r.vid, n.uid AS creator_uid, r.uid, n.type, n.status, r.title, r.timestamp';
+ // Join on current revision (vid) except when looking for pending revisions.
+ $sql_from = ' FROM {node} n INNER JOIN {node_revision} r ' . ($is_pending ? 'ON n.nid=r.nid' : 'ON n.vid=r.vid');
+ $sql_where = ($is_published < 0) ? '' : " WHERE n.status=$is_published";
+ if ($creator_uid >= 0) {
+ $sql_where = empty($sql_where) ? " WHERE n.uid=$creator_uid" : $sql_where . " AND n.uid=$creator_uid";
+ }
+ if ($modifier_uid >= 0) {
+ $sql_where = empty($sql_where) ? " WHERE r.uid=$modifier_uid" : $sql_where . " AND r.uid=$modifier_uid";
+ }
+ if ($is_pending) {
+ $sql_where = empty($sql_where) ? ' WHERE' : $sql_where . ' AND';
+ $sql_where .= ' (r.vid>n.vid OR (n.status=0 AND (SELECT COUNT(vid) FROM {node_revision} WHERE nid=n.nid)=1))';
+ }
+ $sql_order = " ORDER BY " . (empty($order_by_override) ? _revisioning_extract_order_clause_from_URI() : $order_by_override);
+ $include_taxonomy_terms = module_exists('taxonomy') &&
+ variable_get('revisioning_show_taxonomy_terms', TRUE) && (count(taxonomy_get_vocabularies()) > 0);
+ if ($include_taxonomy_terms) {
+ $conditions = array('type' => 'taxonomy_term_reference');
+ $fields = field_read_fields($conditions);
+ foreach ($fields as $field => $data) {
+ $sql_select .= ", ttd_$field.name AS " . ($field == 'field_tags' ? 'tags' : 'term');
+ $sql_from .= " LEFT JOIN {field_revision_$field} r_$field ON r.vid = r_$field.revision_id LEFT JOIN {taxonomy_term_data} ttd_$field ON r_$field.{$field}_tid=ttd_$field.tid";
+ }
+ }
+ $sql = $sql_select . $sql_from . $sql_where . $sql_order;
+ $node_query_result = db_query_range($sql, 0, $max);
+ $revisions = array();
+ foreach ($node_query_result as $revision) {
+ // Need to set revision_moderation for revisioning_node_access() to work
+ // properly.
+ $revision->revision_moderation = revisioning_content_is_moderated($revision->type/*, $revision*/);
+
+ $filter = ($is_moderated < 0) || ($is_moderated == $revision->revision_moderation);
+
+ if ($filter && _revisioning_access_node_revision($op, $revision)) {
+ if (empty($revisions[$revision->vid])) {
+ $revisions[$revision->vid] = $revision;
+ }
+ // If a revision has more than one taxonomy term, these will be returned
+ // by the query as seperate objects differing only in their terms.
+ elseif ($include_taxonomy_terms) {
+ $existing_revision = $revisions[$revision->vid];
+ if (!empty($revision->term)) {
+ if (strpos($existing_revision->term, $revision->term) === FALSE) {
+ // Bit of a quick & dirty -- goes wrong if a term is substr of
+ // another.
+ $existing_revision->term .= ", $revision->term";
+ }
+ }
+ if (!empty($revision->tags)) {
+ if (strpos($existing_revision->tags, $revision->tags) === FALSE) {
+ $existing_revision->tags .= ", $revision->tags";
+ }
+ }
+ }
+ }
+ }
+ return $revisions;
+}
+
+/**
+ * Retrieve a list of all revisions belonging to the supplied node.
+ *
+ * Includes archive, current, and pending revisions.
+ *
+ * @param int $nid
+ * The node id to retrieve.
+ * @param bool $include_taxonomy_terms
+ * Whether to also retrieve the taxonomy terms for each revision
+ *
+ * @return array
+ * An array of revision objects, each with published flag, log message, vid,
+ * title, timestamp and name of user that created the revision
+ */
+function _revisioning_get_all_revisions_for_node($nid, $include_taxonomy_terms = FALSE) {
+ $node = node_load($nid);
+
+ $sql_select = 'SELECT r.vid, r.status, r.title, r.log, r.uid, r.timestamp, u.name';
+ $sql_from = ' FROM {node_revision} r INNER JOIN {users} u ON r.uid=u.uid';
+ $sql_where = ' WHERE r.nid = :nid ORDER BY r.vid DESC';
+ if ($include_taxonomy_terms) {
+ $conditions = array('type' => 'taxonomy_term_reference');
+ $fields = field_read_fields($conditions);
+ foreach ($fields as $field => $data) {
+ $field_instance = field_read_instance('node', $field, $node->type);
+ if ($field_instance) {
+ $sql_select .= ", ttd_$field.name AS " . ($field == 'field_tags' ? 'tags' : 'term');
+ $sql_from .= " LEFT JOIN {field_revision_$field} r_$field ON r.vid = r_$field.revision_id LEFT JOIN {taxonomy_term_data} ttd_$field ON r_$field.{$field}_tid=ttd_$field.tid";
+ }
+ }
+ }
+ $sql = $sql_select . $sql_from . $sql_where;
+ $result = db_query($sql, array(':nid' => $nid));
+ $revisions = array();
+ foreach ($result as $revision) {
+ if (empty($revisions[$revision->vid])) {
+ $revision->current = $node->vid;
+ $revisions[$revision->vid] = $revision;
+ }
+ // If a revision has more than one tag or taxonomy term, these will be
+ // returned by the query as seperate objects differing only in their terms.
+ elseif ($include_taxonomy_terms) {
+ $existing_revision = $revisions[$revision->vid];
+ if (!empty($revision->term)) {
+ if (strpos($existing_revision->term, $revision->term) === FALSE) {
+ // Bit of a quick & dirty -- goes wrong if a term is substr of
+ // another.
+ $existing_revision->term .= ", $revision->term";
+ }
+ }
+ if (!empty($revision->tags)) {
+ if (strpos($existing_revision->tags, $revision->tags) === FALSE) {
+ $existing_revision->tags .= ", $revision->tags";
+ }
+ }
+ }
+ }
+ return $revisions;
+}
+
+/**
+ * Extract order clause.
+ *
+ * Extract from the incoming URI (as in the table column header href)
+ * the sort field and order for use in an SQL 'ORDER BY' clause.
+ *
+ * @return string
+ * db table field name and sort direction as a string
+ */
+function _revisioning_extract_order_clause_from_URI() {
+ // We shouldn't have to do this, as tablesort.inc/tablesort_header(), called
+ // from theme_table() is meant to look after it, but it's got a bug [#480382].
+ // Note: this function is secure, as we're only allowing recognised values,
+ // all unknown values, result in a descending sort by 'timestamp'.
+ switch ($order_by = drupal_strtolower($_REQUEST['order'])) {
+ case 'creator':
+ $order_by = 'n.uid';
+ break;
+
+ case 'by':
+ $order_by = 'r.uid';
+ break;
+
+ case 'published?':
+ $order_by = 'status';
+ break;
+
+ case 'workflow state':
+ $order_by = 'state';
+ break;
+
+ // List names that are fine the way they are here:
+ case 'title':
+ case 'type':
+ case 'term':
+ break;
+
+ default:
+ $order_by = 'timestamp';
+ break;
+ }
+ $direction = (drupal_strtolower($_REQUEST['sort']) == 'asc') ? 'ASC' : 'DESC';
+ return "$order_by $direction";
+}
diff --git a/sites/all/modules/revisioning/revisioning_scheduler/README.txt b/sites/all/modules/revisioning/revisioning_scheduler/README.txt
new file mode 100644
index 0000000..7bf936f
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_scheduler/README.txt
@@ -0,0 +1,33 @@
+
+DESCRIPTION
+===========
+The Revisioning Scheduler is an optional add-on for the Revisioning module.
+It allows users to schedule a publication date for a new or existing content
+revision (in the latter case we speak of reversion rather than publication).
+
+The publication date and time may be set when clicking the Publish or Revert
+links or on the content edit form prior to pressing Save.
+
+The publication date text field on the content edit form appears only when:
+o the user has publish or "administer content" permission for this piece of
+ content
+o "Auto-publish drafts of type ..." is NOT ticked on the content type edit form
+ (or it is ticked, but the user does not have permission to publish)
+o a) the node is currently not published yet (Published box NOT ticked), or
+ b) if it is published, "Create new revision and moderate" IS ticked
+ under "Publishing options".
+
+INSTALLATION & CONFIGURATION
+============================
+Enable like any other module. Then have a look at the self-explanatory
+configuration options at Configurartion >> Revisioning Scheduler.
+
+CHANGELOG
+=========
+http://drupalcode.org/project/revisioning.git/shortlog/refs/heads/7.x-1.x
+
+AUTHORS
+=======
+Original version by Adam Bramley, adam at catalyst dot net dot nz
+Edit form extension by Zvi Epner (zepner on http://drupal.org)
+Further developed by Rik de Boer, rik at flink dot com dot au
diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revision-schedule.js b/sites/all/modules/revisioning/revisioning_scheduler/revision-schedule.js
new file mode 100644
index 0000000..7166894
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_scheduler/revision-schedule.js
@@ -0,0 +1,40 @@
+(function ($) {
+
+ /* This reflect the logic of revisioning_scheduler_node_presave().
+ * Scheduling a publication date is only appropriate if:
+ * o the node is NOT published
+ * o the node is already published, but the new revision to be created goes in
+ * moderation, i.e will not be published yet
+ */
+ Drupal.behaviors.showPublicationDateTimeField = {
+ attach: function (context) {
+ var publishedBox = $('.form-item-status input');
+ var newRevisionInModerationRadio = $('#edit-revision-operation-2');
+ var publicationDate = $('.form-item-publication-date');
+
+ // Page-load: hide the publication date textbox, if necessary
+ if ((publishedBox.is(':checked') && !newRevisionInModerationRadio.is(':checked'))) {
+ publicationDate.hide();
+ }
+
+ // Define handlers for clicks on the Published box and moderation radio buttons.
+ publishedBox.click(function() {
+ if (!publishedBox.is(':checked') || newRevisionInModerationRadio.is(':checked')) {
+ publicationDate.show();
+ }
+ else {
+ publicationDate.hide();
+ }
+ });
+ $('#edit-revision-operation input').click(function() {
+ if (!publishedBox.is(':checked') || newRevisionInModerationRadio.is(':checked')) {
+ publicationDate.show();
+ }
+ else {
+ publicationDate.hide();
+ }
+ });
+ }
+ };
+
+})(jQuery);
diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.info b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.info
new file mode 100644
index 0000000..49bcbe1
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.info
@@ -0,0 +1,12 @@
+name = Revisioning Scheduler
+description = "Allows revisions to be published or reverted at specified times."
+dependencies[] = revisioning
+configure = admin/config/content/revisioning_scheduler
+core = "7.x"
+
+; Information added by Drupal.org packaging script on 2014-06-26
+version = "7.x-1.9"
+core = "7.x"
+project = "revisioning"
+datestamp = "1403741628"
+
diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.install b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.install
new file mode 100644
index 0000000..5a67a4d
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.install
@@ -0,0 +1,62 @@
+ 'The base table for revisions that are scheduled to be published',
+ 'fields' => array(
+ 'revision_vid' => array(
+ 'description' => 'The identifier for this revision',
+ 'type' => 'int',
+ 'not null' => TRUE),
+ 'revision_nid' => array(
+ 'description' => 'The primary identifier for this node',
+ 'type' => 'int',
+ 'not null' => TRUE),
+ 'revision_uid' => array(
+ 'description' => 'The user who submitted this revision',
+ 'type' => 'int',
+ 'not null' => TRUE),
+ 'revision_date' => array(
+ 'description' => 'The date when this revision will be published',
+ 'type' => 'int',
+ 'not null' => TRUE),
+ ),
+ 'primary key' => array('revision_vid'),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function revisioning_scheduler_install() {
+ db_update('system')
+ ->fields(array('weight' => 1))
+ ->condition('name', 'revisioning_scheduler')
+ ->execute();
+}
+
+/**
+ * Weight of revisioning_scheduler should be same as revisioning.
+ */
+function revisioning_scheduler_update_7014() {
+ db_update('system')
+ ->fields(array('weight' => 1))
+ ->condition('name', 'revisioning_scheduler')
+ ->execute();
+}
+
+/**
+ * Delete scheduled publication of revisions of already deleted nodes.
+ */
+function revisioning_scheduler_update_7015() {
+ db_query('DELETE FROM {revisioning_scheduler} WHERE revision_nid NOT IN (SELECT nid FROM {node})');
+}
diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.module b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.module
new file mode 100644
index 0000000..e9b09ac
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.module
@@ -0,0 +1,432 @@
+
+ * zepner (drupal.org)
+ * RdeBoer (drupal.org)
+ */
+
+define('REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT', 'd-m-Y H:i');
+define('REVISIONING_SCHEDULER_SLACK', 120);
+
+/**
+ * Implements hook_menu().
+ */
+function revisioning_scheduler_menu() {
+ $items = array();
+ // Put the administrative settings under Content on the Configuration page.
+ $items['admin/config/content/revisioning_scheduler'] = array(
+ 'title' => 'Revisioning Scheduler',
+ 'description' => 'Set the format for entering publication dates',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('revisioning_scheduler_admin_configure'),
+ 'access arguments' => array('administer site configuration'),
+ );
+ return $items;
+}
+
+/**
+ * Menu callback for admin settings.
+ */
+function revisioning_scheduler_admin_configure() {
+ $date_format = variable_get('revisioning_scheduler_date_format');
+ $default_date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT;
+ if (empty($date_format)) {
+ $date_format = $default_date_format;
+ }
+ $help_text = t('Date and time must be separated by a space. See this manual page for available symbols and their meaning.', array(
+ '!php_manual_page' => 'http://php.net/manual/en/function.date.php'));
+ $t_args = array(
+ '%date_format' => $default_date_format,
+ '%date' => date($date_format),
+ );
+ $form['revisioning_scheduler_date_format'] = array(
+ '#type' => 'textfield',
+ '#size' => 25,
+ '#title' => t('Format used for entering publication dates'),
+ '#default_value' => $date_format,
+ '#description' => $help_text . ' ' . ($date_format == $default_date_format
+ ? t('The default input format %date_format is used. Time now in this format: %date.', $t_args)
+ : t('Time now in above format: %date If left blank the date input format defaults to %date_format', $t_args)
+ ),
+ );
+ $form['revisioning_scheduler_on_edit_form'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Allow publication dates to be scheduled on the content edit form'),
+ '#default_value' => variable_get('revisioning_scheduler_on_edit_form', TRUE),
+ '#description' => t('In addition publication dates may be scheduled when you press the Publish or Revert links.'),
+ );
+ return system_settings_form($form);
+}
+
+/**
+ * Implements hook_form_BASEFORMID_alter().
+ *
+ * This function adds a publication date & time field to the Edit form.
+ * It also loads a small javascript file, which controls visibility of the field
+ * in response to clicks on the revision moderation radio-buttons.
+ */
+function revisioning_scheduler_form_node_form_alter(&$form, &$form_state, $form_id) {
+
+ if (!empty($form['#node_edit_form']) && variable_get('revisioning_scheduler_on_edit_form', TRUE)) {
+
+ $node = $form_state['node'];
+ if (!user_access('administer nodes')) {
+ // If the node is already published then scheduling a publication date is
+ // inappropriate, unless the new revision goes into moderation.
+ // The exception are users with 'administer nodes' permission, as they
+ // can change the Published checkbox and moderation radio buttons, so we
+ // have to deal with that client-side, see file revision-schedule.js
+ if ($node->status == NODE_PUBLISHED) {
+ return;
+ }
+ if (empty($node->revision_moderation) && !(empty($node->nid) && revisioning_content_is_moderated($node->type, $node))) {
+ // No moderation specified and not a new node of a type subject to
+ // moderation.
+ return;
+ }
+ if (!revisioning_user_node_access('publish revisions', $node)) {
+ return;
+ }
+ }
+ // Don't offer the form if auto-publish is enabled for this node and user.
+ if (revisioning_user_may_auto_publish($node)) {
+ return;
+ }
+
+ $date_format = variable_get('revisioning_scheduler_date_format');
+ if (empty($date_format)) {
+ $date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT;
+ }
+ $description1 = t('Please use this format: %format, e.g %datetime. If you enter "now" this content will be published immediately.',
+ array(
+ '%format' => $date_format,
+ '%datetime' => format_date(time(), 'custom', $date_format),
+ )
+ );
+ $description2 = t('If you do not wish to schedule publication, leave the field blank.');
+
+ if (isset($node->vid)) {
+ $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_vid = :vid',
+ array(':vid' => $node->vid)
+ );
+ $revision = $result->fetchAssoc();
+ }
+ $scheduled_datetime = empty($revision) ? '' : format_date($revision['revision_date'], 'custom', $date_format);
+
+ $form['revision_information']['publication_date'] = array(
+ '#type' => 'textfield',
+ '#size' => 25,
+ '#maxlength' => 25,
+ '#title' => t('Optionally schedule a date and time for publication'),
+ '#description' => $description1 . ' ' . $description2,
+ '#default_value' => $scheduled_datetime,
+ '#weight' => 10,
+ '#attributes' => array('class' => array('publication-date')),
+ );
+ $form['#attached']['js'][] = drupal_get_path('module', 'revisioning_scheduler') . '/revision-schedule.js';
+ }
+}
+
+/**
+ * Implements hook_form_alter().
+ *
+ * Adds date and time fields to the publication and reverting forms.
+ * Also shows the entered date and time on the revisions summary.
+ */
+function revisioning_scheduler_form_alter(&$form, $form_state, $form_id) {
+
+ switch ($form_id) {
+
+ case 'revisioning_publish_confirm':
+ case 'node_revision_revert_confirm':
+ $vid = arg(3);
+ $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_vid = :vid',
+ array(':vid' => $vid)
+ );
+ $revision = $result->fetchAssoc();
+ if (!empty($revision)) {
+ drupal_set_message(t('This revision was already scheduled by !username for publication on %date. You may override this date and time.',
+ array(
+ '%date' => format_date($revision['revision_date']),
+ '!username' => theme('username', array('account' => user_load($revision['revision_uid']))),
+ )),
+ 'warning', FALSE);
+ }
+ $date_format = variable_get('revisioning_scheduler_date_format');
+ if (empty($date_format)) {
+ $date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT;
+ }
+ $date_and_time = explode(' ', date($date_format));
+ $form['revisioning_scheduler_date'] = array(
+ '#title' => $form_id == 'node_revision_revert_confirm' ? t('Date for reversion') : t('Date for publication'),
+ '#type' => 'textfield',
+ '#description' => t('Enter the date you want this revision to be published.'),
+ '#maxlength' => 10,
+ '#size' => 10,
+ '#default_value' => $date_and_time[0],
+ '#weight' => -1,
+ );
+ $form['revisioning_scheduler_time'] = array(
+ '#title' => $form_id == 'node_revision_revert_confirm' ? t('Time for reversion') : t('Time for publication'),
+ '#type' => 'textfield',
+ '#maxlength' => 5,
+ '#size' => 5,
+ '#default_value' => $date_and_time[1],
+ '#description' => t('Enter the time you want this revision to be published. Use the 24 hour clock.'),
+ '#weight' => 0,
+ );
+ break;
+
+ case 'revisioning_revisions_summary':
+ $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_nid = :nid',
+ array(':nid' => arg(1))
+ );
+ foreach ($result as $revision) {
+ if ($revision->revision_date > time()) {
+ $form['info'][$revision->revision_vid]['#markup'] .= ' ' . t('Scheduled for publication on %date.',
+ array('%date' => format_date($revision->revision_date, 'long')));
+ }
+ else {
+ $form['info'][$revision->revision_vid]['#markup'] .= ' ' . t('Scheduled for publication at next cron run.');
+ }
+ }
+ break;
+ }
+}
+
+/**
+ * Implements hook_validate().
+ */
+function revisioning_publish_confirm_validate($node, &$form) {
+ $date = check_plain($_POST['revisioning_scheduler_date']);
+ $date_format = variable_get('revisioning_scheduler_date_format');
+ if (empty($date_format)) {
+ $date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT;
+ }
+ $date_only_format = drupal_substr($date_format, 0, strpos($date_format, ' '));
+ if (strtotime($date) < strtotime(date($date_only_format))) {
+ form_set_error('revisioning_scheduler_date', t('The publication date you set is in the past.'));
+ }
+ else {
+ $time = check_plain($_POST['revisioning_scheduler_time']);
+ $scheduled_time = strtotime($date . $time);
+ // Add 90 seconds of slack to give user a chance to publish instantly by
+ // leaving time as is.
+ if ($scheduled_time < time() - REVISIONING_SCHEDULER_SLACK) {
+ form_set_error('revisioning_scheduler_time', t('The publication time you set is in the past.'));
+ }
+ }
+}
+
+/**
+ * Implements hook_revisionapi().
+ *
+ * @see revisioning/revisioning_api.inc
+ */
+function revisioning_scheduler_revisionapi($op, $node) {
+ switch ($op) {
+ case 'pre publish':
+ case 'post revert':
+ if (empty($_POST['revisioning_scheduler_date'])) {
+ break;
+ }
+ $date = check_plain($_POST['revisioning_scheduler_date']);
+ $time = check_plain($_POST['revisioning_scheduler_time']);
+ $result = _revisioning_scheduler_schedule_publication($date, $time, $node);
+ if (isset($result)) {
+ // This will abort the current publication operation.
+ return FALSE;
+ }
+ break;
+
+ // The revision is being deleted. If it is scheduled for publishing, i.e.
+ // vid exists in {revisioning_scheduler} table, remove the scheduler entry.
+ case 'pre delete':
+ _revisioning_scheduler_unschedule($node->vid);
+ break;
+ }
+}
+
+/**
+ * Implements hook_node_presave().
+ *
+ * Called when saving, be it an edit or when creating a node.
+ *
+ * Picks up the value for the scheduled publication date (if entered) and
+ * decides whether the node should be published immediately or scheduled for a
+ * later date.
+ */
+function revisioning_scheduler_node_presave($node) {
+ if (!isset($node->nid)) {
+ // This may happen when importing files using Feeds module.
+ return;
+ }
+ if (empty($node->revision_moderation) || !empty($node->auto_publish)) {
+ _revisioning_scheduler_unschedule_all_revisions($node->nid);
+ }
+ elseif (!empty($node->publication_date)) {
+ $datetime = explode(' ', trim($node->publication_date));
+ $date = $datetime[0];
+ $time = isset($datetime[1]) ? $datetime[1] : '00:00';
+ $node->publication_date = "$date $time";
+ $scheduled_time = strtotime($node->publication_date);
+ if ($date == 'now' || ($scheduled_time > time() - REVISIONING_SCHEDULER_SLACK && $scheduled_time <= time())) {
+ // Publish immediately without scheduling.
+ // Follow the default saving process making this revision current and
+ // published, as opposed to pending.
+ unset($node->revision_moderation);
+ $node->status = NODE_PUBLISHED;
+ _revisioning_scheduler_unschedule_all_revisions($node->nid);
+ }
+ else {
+ // Schedule publication date.
+ return;
+ }
+ }
+ // Publication date does not apply in this situation.
+ unset($node->publication_date);
+}
+
+/**
+ * Implements hook_node_insert().
+ *
+ * Called when a new node has just been created.
+ */
+function revisioning_scheduler_node_insert($node) {
+ revisioning_scheduler_node_update($node);
+}
+
+/**
+ * Implements hook_node_update().
+ *
+ * This hook was chosen to invoke the scheduler because at this point vid has
+ * the new value.
+ */
+function revisioning_scheduler_node_update($node) {
+ if (empty($node->publication_date)) {
+ _revisioning_scheduler_unschedule($node->vid);
+ }
+ else {
+ $datetime = explode(' ', $node->publication_date);
+ _revisioning_scheduler_schedule_publication($datetime[0], $datetime[1], $node);
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ */
+function revisioning_scheduler_node_delete($node) {
+ // Delete scheduled publication of revisions of the deleted node.
+ _revisioning_scheduler_unschedule_all_revisions($node->nid);
+}
+
+/**
+ * Implements hook_cron().
+ *
+ * If there are any revisions with times that have passed, then publish them
+ * and delete them from the database.
+ */
+function revisioning_scheduler_cron() {
+ module_load_include('inc', 'revisioning', 'revisioning_api');
+ $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_date <= :date',
+ array(':date' => time())
+ );
+ foreach ($result as $revision) {
+ if ($node_revision = node_load($revision->revision_nid, $revision->revision_vid)) {
+ _revisioning_publish_revision($node_revision);
+ }
+ _revisioning_scheduler_unschedule_all_revisions($revision->revision_nid);
+ }
+}
+
+/**
+ * Schedule the supplied node for publication at the supplied date & time.
+ *
+ * @param string $date
+ * Publication date as a string, e.g. '25-12-2012'
+ * @param string $time
+ * Publication time as a string, e.g. '23:59'
+ * @param object $node
+ * The node object
+ *
+ * @return null|bool
+ * TRUE: revision successfully scheduled
+ * empty: revision not scheduled, should be published immediately
+ * FALSE: error, date & time in the past or database error
+ */
+function _revisioning_scheduler_schedule_publication($date, $time, $node) {
+ $date = trim($date);
+ $time = trim($time);
+ if (empty($time)) {
+ $time = '00:00';
+ }
+ $scheduled_time = strtotime($date . $time);
+ if ($scheduled_time > time() - REVISIONING_SCHEDULER_SLACK) {
+ if ($scheduled_time <= time()) {
+ // Schedule immediately.
+ return;
+ }
+
+ _revisioning_scheduler_unschedule_all_revisions($node->nid);
+
+ global $user;
+ $data = array(
+ 'revision_nid' => $node->nid,
+ 'revision_vid' => $node->vid,
+ 'revision_uid' => $user->uid,
+ 'revision_date' => $scheduled_time,
+ );
+ if (drupal_write_record('revisioning_scheduler', $data)) {
+ if ($scheduled_time > time()) {
+ drupal_set_message(t('Revision scheduled for publication at %time on %date.', array('%time' => $time, '%date' => $date)));
+ }
+ else {
+ // Should never get here.
+ drupal_set_message(t('Revision will be published at next cron run.'));
+ }
+ return TRUE;
+ }
+ }
+ drupal_set_message(t('Publication could not be scheduled at this date & time: %date %time.', array('%date' => $date, '%time' => $time)), 'error');
+ return FALSE;
+}
+
+/**
+ * Delete all scheduled publication dates for this node, if any.
+ *
+ * @param int $nid
+ * the unique node id
+ */
+function _revisioning_scheduler_unschedule_all_revisions($nid) {
+ return db_delete('revisioning_scheduler')
+ ->condition('revision_nid', $nid)
+ ->execute();
+}
+
+/**
+ * Check if there is a scheduled publication date for this revision.
+ *
+ * If so delete that date.
+ *
+ * @param int $vid
+ * the unique revision id
+ */
+function _revisioning_scheduler_unschedule($vid) {
+ return db_delete('revisioning_scheduler')
+ ->condition('revision_vid', $vid)
+ ->execute();
+}
+
+/**
+ * Register View API information.
+ */
+function revisioning_scheduler_views_api() {
+ return array(
+ 'api' => views_api_version(),
+ 'path' => drupal_get_path('module', 'revisioning_scheduler'),
+ );
+}
diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views.inc b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views.inc
new file mode 100644
index 0000000..f958bd1
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views.inc
@@ -0,0 +1,126 @@
+ 'revision_vid',
+ 'title' => t('Content revisions scheduled'),
+ 'help' => t('A list of all revisions currently scheduled for publication.'),
+ 'defaults' => array(
+ 'field' => 'revision_date',
+ ),
+ );
+
+ // For other base tables (node_revision, users), explain how we join.
+ $data['revisioning_scheduler']['table']['join'] = array(
+ // Directly links to node_revision table. See also relationship details at
+ // $data['revisioning_scheduler']['revision_vid'].
+ 'node_revision' => array(
+ 'left_field' => 'vid',
+ 'field' => 'revision_vid',
+ ),
+ 'node' => array(
+ 'left_field' => 'nid',
+ 'field' => 'revision_nid',
+ ),
+ // Directly links to the users table. See also relationship details at
+ // $data['revisioning_scheduler']['revision_uid'].
+ 'users' => array(
+ 'left_field' => 'uid',
+ 'field' => 'revision_uid',
+ ),
+ );
+
+ $data['revisioning_scheduler']['revision_nid'] = array(
+ 'title' => t('Nid'),
+ 'help' => t('Node ID.'),
+ 'field' => array(
+ 'views_handler_field',
+ 'click sortable' => FALSE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_integer',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'relationship' => array(
+ 'base' => 'node',
+ 'field' => 'revision_nid',
+ 'handler' => 'views_handler_relationship',
+ 'label' => t('Content revision (NID)'),
+ ),
+ );
+
+ $data['revisioning_scheduler']['revision_date'] = array(
+ 'title' => t('Scheduled publication date'),
+ 'help' => t('Date & time on this content revision will be published.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ );
+
+ $data['revisioning_scheduler']['revision_vid'] = array(
+ 'title' => t('Vid'),
+ 'help' => t('Revision ID.'),
+ 'field' => array(
+ 'views_handler_field',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_integer',
+ ),
+ 'relationship' => array(
+ 'base' => 'node_revision',
+ 'field' => 'revision_vid',
+ 'handler' => 'views_handler_relationship',
+ 'label' => t('Content revision (VID)'),
+ ),
+ );
+
+ $data['revisioning_scheduler']['revision_uid'] = array(
+ 'title' => t('Scheduled by'),
+ 'help' => t('User ID.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_integer',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'relationship' => array(
+ 'base' => 'users',
+ 'field' => 'revision_uid',
+ 'handler' => 'views_handler_relationship',
+ 'label' => t('User'),
+ ),
+ );
+
+ return $data;
+}
diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views_default.inc b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views_default.inc
new file mode 100644
index 0000000..95a1ae8
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views_default.inc
@@ -0,0 +1,222 @@
+name = 'scheduled_publications';
+ $view->description = 'A summary table of content revisions currently scheduled for publication.';
+ $view->tag = 'default';
+ $view->base_table = 'revisioning_scheduler';
+ $view->human_name = 'Scheduled publications';
+ $view->core = 7;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Master */
+ $handler = $view->new_display('default', 'Master', 'default');
+ $handler->display->display_options['title'] = 'Scheduled publications';
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'publish revisions';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'full';
+ $handler->display->display_options['pager']['options']['items_per_page'] = '10';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['group_rendered'] = 0;
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'title' => 'title',
+ 'revision_vid' => 'revision_vid',
+ 'last_editor' => 'last_editor',
+ 'revision_date' => 'revision_date',
+ 'name' => 'name',
+ );
+ $handler->display->display_options['style_options']['default'] = 'revision_date';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'title' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'revision_vid' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-right',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'last_editor' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'revision_date' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'name' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ );
+ $handler->display->display_options['style_options']['override'] = 1;
+ $handler->display->display_options['style_options']['sticky'] = 0;
+ $handler->display->display_options['style_options']['empty_table'] = 0;
+ /* Relationship: Scheduled content: Scheduled by */
+ $handler->display->display_options['relationships']['revision_uid']['id'] = 'revision_uid';
+ $handler->display->display_options['relationships']['revision_uid']['table'] = 'revisioning_scheduler';
+ $handler->display->display_options['relationships']['revision_uid']['field'] = 'revision_uid';
+ $handler->display->display_options['relationships']['revision_uid']['required'] = 1;
+ /* Relationship: Scheduled content: Vid */
+ $handler->display->display_options['relationships']['revision_vid']['id'] = 'revision_vid';
+ $handler->display->display_options['relationships']['revision_vid']['table'] = 'revisioning_scheduler';
+ $handler->display->display_options['relationships']['revision_vid']['field'] = 'revision_vid';
+ $handler->display->display_options['relationships']['revision_vid']['required'] = 1;
+ /* Field: Content revision: Title */
+ $handler->display->display_options['fields']['title']['id'] = 'title';
+ $handler->display->display_options['fields']['title']['table'] = 'node_revision';
+ $handler->display->display_options['fields']['title']['field'] = 'title';
+ $handler->display->display_options['fields']['title']['relationship'] = 'revision_vid';
+ $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['make_link'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['absolute'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['external'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['replace_spaces'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['trim_whitespace'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['nl2br'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 1;
+ $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 1;
+ $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['trim'] = 0;
+ $handler->display->display_options['fields']['title']['alter']['html'] = 0;
+ $handler->display->display_options['fields']['title']['element_label_colon'] = 1;
+ $handler->display->display_options['fields']['title']['element_default_classes'] = 1;
+ $handler->display->display_options['fields']['title']['hide_empty'] = 0;
+ $handler->display->display_options['fields']['title']['empty_zero'] = 0;
+ $handler->display->display_options['fields']['title']['hide_alter_empty'] = 1;
+ $handler->display->display_options['fields']['title']['link_to_node'] = 0;
+ $handler->display->display_options['fields']['title']['link_to_node_revision'] = 1;
+ /* Field: Scheduled content: Vid */
+ $handler->display->display_options['fields']['revision_vid']['id'] = 'revision_vid';
+ $handler->display->display_options['fields']['revision_vid']['table'] = 'revisioning_scheduler';
+ $handler->display->display_options['fields']['revision_vid']['field'] = 'revision_vid';
+ $handler->display->display_options['fields']['revision_vid']['label'] = 'Rev. ID';
+ $handler->display->display_options['fields']['revision_vid']['alter']['alter_text'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['make_link'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['absolute'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['external'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['replace_spaces'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['trim_whitespace'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['nl2br'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['word_boundary'] = 1;
+ $handler->display->display_options['fields']['revision_vid']['alter']['ellipsis'] = 1;
+ $handler->display->display_options['fields']['revision_vid']['alter']['strip_tags'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['trim'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['alter']['html'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['element_label_colon'] = 1;
+ $handler->display->display_options['fields']['revision_vid']['element_default_classes'] = 1;
+ $handler->display->display_options['fields']['revision_vid']['hide_empty'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['empty_zero'] = 0;
+ $handler->display->display_options['fields']['revision_vid']['hide_alter_empty'] = 1;
+ /* Field: Content: Last edited by */
+ $handler->display->display_options['fields']['last_editor']['id'] = 'last_editor';
+ $handler->display->display_options['fields']['last_editor']['table'] = 'node';
+ $handler->display->display_options['fields']['last_editor']['field'] = 'last_editor';
+ $handler->display->display_options['fields']['last_editor']['relationship'] = 'revision_vid';
+ $handler->display->display_options['fields']['last_editor']['label'] = 'Edited by';
+ $handler->display->display_options['fields']['last_editor']['alter']['alter_text'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['make_link'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['path'] = 'display->display_options['fields']['last_editor']['alter']['absolute'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['external'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['replace_spaces'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['trim_whitespace'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['nl2br'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['word_boundary'] = 1;
+ $handler->display->display_options['fields']['last_editor']['alter']['ellipsis'] = 1;
+ $handler->display->display_options['fields']['last_editor']['alter']['strip_tags'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['trim'] = 0;
+ $handler->display->display_options['fields']['last_editor']['alter']['html'] = 0;
+ $handler->display->display_options['fields']['last_editor']['element_label_colon'] = 1;
+ $handler->display->display_options['fields']['last_editor']['element_default_classes'] = 1;
+ $handler->display->display_options['fields']['last_editor']['hide_empty'] = 0;
+ $handler->display->display_options['fields']['last_editor']['empty_zero'] = 0;
+ $handler->display->display_options['fields']['last_editor']['hide_alter_empty'] = 1;
+ /* Field: Scheduled content: Scheduled publication date */
+ $handler->display->display_options['fields']['revision_date']['id'] = 'revision_date';
+ $handler->display->display_options['fields']['revision_date']['table'] = 'revisioning_scheduler';
+ $handler->display->display_options['fields']['revision_date']['field'] = 'revision_date';
+ $handler->display->display_options['fields']['revision_date']['label'] = 'Scheduled for publication';
+ $handler->display->display_options['fields']['revision_date']['alter']['alter_text'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['make_link'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['absolute'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['external'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['replace_spaces'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['trim_whitespace'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['nl2br'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['word_boundary'] = 1;
+ $handler->display->display_options['fields']['revision_date']['alter']['ellipsis'] = 1;
+ $handler->display->display_options['fields']['revision_date']['alter']['strip_tags'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['trim'] = 0;
+ $handler->display->display_options['fields']['revision_date']['alter']['html'] = 0;
+ $handler->display->display_options['fields']['revision_date']['element_label_colon'] = 1;
+ $handler->display->display_options['fields']['revision_date']['element_default_classes'] = 1;
+ $handler->display->display_options['fields']['revision_date']['hide_empty'] = 0;
+ $handler->display->display_options['fields']['revision_date']['empty_zero'] = 0;
+ $handler->display->display_options['fields']['revision_date']['hide_alter_empty'] = 1;
+ $handler->display->display_options['fields']['revision_date']['date_format'] = 'long';
+ /* Field: User: Name */
+ $handler->display->display_options['fields']['name']['id'] = 'name';
+ $handler->display->display_options['fields']['name']['table'] = 'users';
+ $handler->display->display_options['fields']['name']['field'] = 'name';
+ $handler->display->display_options['fields']['name']['relationship'] = 'revision_uid';
+ $handler->display->display_options['fields']['name']['label'] = 'Scheduled by';
+ $handler->display->display_options['fields']['name']['alter']['alter_text'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['make_link'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['absolute'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['external'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['replace_spaces'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['trim_whitespace'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['nl2br'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['word_boundary'] = 1;
+ $handler->display->display_options['fields']['name']['alter']['ellipsis'] = 1;
+ $handler->display->display_options['fields']['name']['alter']['strip_tags'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['trim'] = 0;
+ $handler->display->display_options['fields']['name']['alter']['html'] = 0;
+ $handler->display->display_options['fields']['name']['element_label_colon'] = 1;
+ $handler->display->display_options['fields']['name']['element_default_classes'] = 1;
+ $handler->display->display_options['fields']['name']['hide_empty'] = 0;
+ $handler->display->display_options['fields']['name']['empty_zero'] = 0;
+ $handler->display->display_options['fields']['name']['hide_alter_empty'] = 1;
+ $handler->display->display_options['fields']['name']['link_to_user'] = 1;
+ $handler->display->display_options['fields']['name']['overwrite_anonymous'] = 0;
+ $handler->display->display_options['fields']['name']['format_username'] = 1;
+
+ /* Display: Page */
+ $handler = $view->new_display('page', 'Page', 'page');
+ $handler->display->display_options['path'] = 'scheduled-publications';
+ $handler->display->display_options['menu']['type'] = 'normal';
+ $handler->display->display_options['menu']['title'] = 'Scheduled publications';
+
+ $views[$view->name] = $view;
+ return $views;
+}
diff --git a/sites/all/modules/revisioning/revisioning_theme.inc b/sites/all/modules/revisioning/revisioning_theme.inc
new file mode 100644
index 0000000..fd01eb7
--- /dev/null
+++ b/sites/all/modules/revisioning/revisioning_theme.inc
@@ -0,0 +1,362 @@
+ 'form',
+ );
+ $theme['revisions_pending_block'] = array(
+ // Uses revisions-pending-block.tpl.php.
+ 'template' => 'revisions-pending-block',
+ );
+ $theme['revisions_summary'] = array(
+ // Uses revisions-summary.tpl.php.
+ 'template' => 'revisions-summary',
+ );
+ return $theme;
+}
+
+/**
+ * Return revisions summary table data.
+ *
+ * If the Diff modules is enabled, the
+ * object returned includes a column of checkboxes allowing the user to select
+ * two revisions for side-by-side comparison.
+ *
+ * @param array $form
+ * typically an empty array as passed in by drupal_get_form()
+ * @param array $form_state
+ * the form state
+ * @param int $extra
+ * parameters as passed into drupal_get_form(), see _theme_revisions_summary()
+ *
+ * @return array
+ * updated form containing all data to be themed
+ */
+function revisioning_revisions_summary($form, &$form_state, $extra) {
+
+ // #type=>'value' form field values will not appear in the HTML. Used here
+ // to pass the node id to theme_revisioning_revisions_summary().
+ // Or: $form_state['build_info']['args'][0]; ?
+ $nid = $extra;
+ $form['nid'] = array('#type' => 'value', '#value' => $nid);
+
+ $show_taxonomy_terms = module_exists('taxonomy') &&
+ variable_get('revisioning_show_taxonomy_terms', TRUE) && (count(taxonomy_get_vocabularies()) > 0);
+
+ $revisions = _revisioning_get_all_revisions_for_node($nid, $show_taxonomy_terms);
+
+ $revision_ids = array();
+ $published = FALSE;
+ foreach ($revisions as $revision) {
+ $vid = $revision->vid;
+ if ($vid == $revision->current) {
+ $title = $revision->title;
+ $published = $revision->status;
+ }
+ // No text next to check boxes (see below).
+ $revision_ids[$vid] = '';
+ $base_url = "node/$nid/revisions/$vid";
+
+ // First column: saved date + author.
+ $first_cell = t('Saved !date by !username',
+ array(
+ '!date' => l(format_date($revision->timestamp, 'short'), "$base_url/view"),
+ '!username' => theme('username', array('account' => $revision))))
+ . (empty($revision->log) ? '' : '