diff --git a/sites/all/modules/cck/CHANGELOG.txt b/sites/all/modules/cck/CHANGELOG.txt new file mode 100644 index 0000000..b4285ef --- /dev/null +++ b/sites/all/modules/cck/CHANGELOG.txt @@ -0,0 +1,149 @@ +CCK 6.x-3.0-alpha4 +================== + +- Security: Open Redirect - SA-CONTRIB-2015-126 +- #1401950 by KarenS, Add empty test module with dependency on schema to make schema available to testbot. +- #1363036 by markdorison, Fix Class 'ContentCrudTestCase' not found message in tests. +- #1097548 by DeFr: Fixed warning on node forms for multigroups without any required fields. +- #998494 by zhangtaihao: Fix Node Reference field always displaying 10 results + when using the Advanced View option (with Views 6.x-3.x) +- #1167004 by DeFr: Dramatically enhance node display performance issues. +- #1189090 by gaurishankar, yched: Fixed warnings on multigroups with no + required fields. +- #1423432 by sun: Fixed testInfo() methods throws PHP notices. +- #1533506 by yched: Fixed field tokens not available in hook_update_N(). +- #1442886 by pdrake: Fixed fatal error "failed opening theme.inc" in some edge + cases. + +CCK 6.x-3.0-alpha3 +================== + +- #1081072 Imagefield validation does not use error_element, needs a tweak so that the empty required field gets highlighted correctly. +- #1081072 Imagefield validation requires that we alter the field_info values or it will still require an image in an empty group. +- Fix miscellaneous undefined index errors. +- #1081072 Tweak fix for required fields inside non-required group so not all fields are highlighted. +- #1093928 Required option lists do not have an empty option available. Add one to avoid 'An illegal choice has been detected' errors for empty groups. +- #1081072 Fix Cannot use a scalar value as an array problem when using optionwidgets. + +CCK 6.x-3.0-alpha2 +================== + +Features: +- #536260 by Agileware: Make "Add more values" button text themeable. + +Bugfixes: +- Fix Use of undefined constant _add_new_group. +- Fix undefined index '#depth'. +- Fix warning: Creating default object from empty value in template_preprocess_content_display_overview_form() (line 134 of ..theme.inc). +- #1063338 by DeFr: Fix undefined indexes for FIELD_NAME_rendered and others. +- Fix undefined group name index in fieldgroup_nodeapi. +- #1053566 by DamienMcKenna: Fix undefined index errors in _fieldgroup_get_tree(). +- #1081072 Must be able to save empty, non-required, multigroup, even if it contains required fields. +- #522564 Remove empty groups even if not marked 'remove'. +- Multigroup validation was defaulting to 'required' when created, should default to 'not required'. +- #1092044 Make sure all multigroup formats are preserved, even if not visible on the current tab. + +CCK 6.x-3.0-alpha1 +================== + +Features: +- #1049790 Add tests for fieldgroup. +- #849420 by Roberto Gerola, rconstantine, thekevinday, ericduran, KarenS Add ability to nest fieldgroups within other fieldgroups. +- #1008184 by merlinofchaos, bojanz, dereine, yched: Adapt to Views 3 "semantic views" feature (backwards compatible with Views 2) +- #932680 by Dave Reid: Token integration : allow using of Token API's new $options +- #863226 by KarenS, make sure we have a function that will return inactive instances when other instances of the same field are still active. +- #692822 by Dave Reid, add authoring info and publishing options to CCK extra fields. +- #428650 Conditional cache/menu rebuild for content CRUD methods. +- #334945 Save default values when field is hidden because of access permissions. +- #196421 Deleting unwanted multiple values / multiple values delta issues. +- #119102 Combo field - group different fields into one. +- #505278 Panels 3 and multiple node type fields. +- #495582 Reviewed Panels 3 integration (prep work for combo / multigroups). + Implementation of fieldgroup_view_group() that can be used to render field groups. +- #417122 by quicksketch: allow drupal_alter on field and widget settings. +- #514452 Add new argument $node to content_access() to enhance the context for hook_field_access(). +- #523072 by merlinofchaos - Have nodereference relationships limit CCK field availability as well. +- #519870 by joachim - Add a note to say offset starts from 0 in grouping options for views handler for multiple values fields. +- #231453 Allow fields index their columns. Implemented for reference value column in node and user reference fields. Needs update.php. +- #536730 Enhance cells in multiple column tables with CSS classes related to the fields they contain. +- #521002 by mh86 - Support for optgroups in allowed values for select elements. +- Render field descriptions as cell titles when multigroup edit mode is configured with multiple column option. +- #244896 by stella, canaryMason - Add incremental classes for multiple value fields in views. +- #416154 Preparation for Content Multigroup integration with Views. +- #416154 by coltrane, skybow - Synchronized deltas Views integration: Filter on equal deltas in multigroups. +- #227129 by igor.ro - Expose "delta" column in multiple value fields to Views. +- #531662 by neochief - i18n support. Allow external modules to translate field labels, descriptions and allowed values list as typed in the field settings form. +- #531662 i18n support for fieldgroups. +- #558420 Accept trimmed titles in nodereference autocomplete validation to prevent title mismatch errors when title ends with space. +- #580156 Wrap multigroups with unique CSS classes in node view. +- #544542 Allow subgroup fieldsets in a multigroup to be collapsible. +- #596428 by NancyDru - Allow external modules alter the content type list. +- #670344 by dagmar: Make CCK compatible with both, views 2 and views 3. + +Bugfixes: +- Multigroups were defaulting to 'required' when created, should default to 'not required'. +- #766734 Restructure multigroup field layout to be consistent with other fieldgroups, and use + content_get_nested_elements() to grab the right values no matter how the fields are structured. +- #739490 by foripepe: Token integration - fix notices during token generation +- #986612 by Dave Reid: Token integration - fix variable name clash (harmless in normal cases) +- #435520 by yched, sun: Fix text fields rendered as 'n/a' in some cases +- #728472 by Darren Oh : Ensure the module's preprocess functions run first +- #894880 by yched: fix notices in check_plain() when rendering empty 'plain text' values +- #705512 by cha0s, roderick: 'add more' button - fix PHP5.3 compatibility +- #736440 by yched, dhthwy: fix memory leaks on long running migration scripts (e.g. migrate.module) +- #887742 by yched: fix notices in _content_get_formatter() in some Views +- #470470 by neilnz, use iLIKE for postgres selects. +- #769592 by vkareh, add default values to nodeapi. +- #714762 by Robbert, make diff module integration PHP5 compliant. +- Add sanitization to nodereference formatters. +- #482774 Update breaks when CCK is disabled. +- #479994 by quicksketch: fix "add more' button with devel.module's query logging (multigroup). +- #499696 by DeFr - Noderefernce / Userreference: fix Views mode when the view has exposed filters. +- #498924 #multiple FAPI attribute is used for a radios and checkboxes in content export forms. +- #409144 Review extra elements for node edit form provided by core modules. +- #361473 CCK fieldgroup panels doesn't respect CCK field privacy settings. +- #515984 Multiple field delta ORDER BY incorrect. +- #414298 by Michelle, merlinofchaos - Follow up to remove fieldgroup.panels.inc (it was moved to panels/content_types). +- #522112 by hefox, prevent malformed condition for vid IN () in views handler for multiple values fields. +- #505278 by Michelle, merlinofchaos - Provide backward compatibility with previous method to build Panels 3 subtype names for fields. +- #523864 Minor coding style issues in Panels 3 relationships implementations. +- #83787 Fix PHP warning: array_merge(): Argument #1 is not an array in content_multigroup.node_form.inc on line 432. +- #481568 by merlinofchaos - Empty property error when attempting to save a user reference in Panels. +- #510396 by yched - Use field/type definition to render fields in views. +- #526500 Fix maximum number of deltas in multigroups with explicit number of repeats defined. +- #393020 by auth - Fieldgroup data is lost when importing to module provided content type with group info for existing fields. +- #538872 Diff does not respect field permissions. +- #522558 Date field jumps delta when date field is emptied or fieldset is deleted. +- #416134 Userreference, impossible to filter allowed values by blocked users. Requires update.php. +- Minor fix in multigroup. Remove the check for module_exists('fieldgroup'), which is something implicit. +- Minor fix in multigroup. Remove unused argument $form_id from invocation to worker functions in hook_form_alter(). +- #545942 warning: array_filter() [function.array-filter]: The first argument should be an array in userreference_update_6002(). +- #521002 Fix validation errors when using optgroups in allowed values for select elements. +- #550252 by GuyPaddock - content_db_index_exists produces SQL errors when creating node reference fields for MySQL 4 (related to #231453). +- #551280 by xurizaemon - Typo fix for "this field cannot hold more that 3 values" error message. +- #558744 by chellomere - Fix one of the swedish translation strings to be correct, and much clearer. +- #562260 by przadka - content_db_index_exists() has wrong syntax for PostgreSQL. +- #567168 by jcmarco - Checkbox required not defined for on/off widgets. +- #568430 by Jody Lynn - Bad @see in content-field.tpl.php. +- #572672 by Jan van Diepen - Remove redundant (and bad) inclusion of node/content_types.inc in content_copy_import_form_submit(). +- #585048 Setting "All users" in "User status that can be referenced" option reverts to "Blocked users". +- #589306 warning: Invalid argument supplied for foreach() in includes/panels/content_types/content_field.inc on line 166. +- #551188 "Add another item" for FileField widgets causes Invalid argument supplied for foreach() in content.node_form.inc on line 456. +- #605152 by pokurek - Missing number formatter fr_2. +- #618910 Multigroup filter fails when using relationship. +- #538458 Do not allow to change the widget type for fields in multigroups when the change is not compatible. +- #604830 by mattyoung - 32 characters limit on field and group identifiers in "Manage fields" screen. +- #464030 by eojthebrave - Typo in content_copy.module help. +- #577590 Add a visual indication to the fieldgroup title if the multigroup is required. +- #577590 Prevent validation errors when multigroup is not required and no subgroup of fields if filled in. +- #577580 Multigroup fields with empty values loose their ordering when saved via node_save. +- #614292 by DeFr - Fix CCK Reference fields based on views broken by recent change in views_plugin_style Views 2.7 (#502348). +- #625768 CCK 6.x-2.6, got fatal error on update.php while running userreference_update_6002(). +- #640488 warning: max() [function.max]: Array must contain at least one element in content.module on line 1044. +- #649106 by thekevinday: Fix content_copy_form_alter(), where $form_state argument is not passed by reference. +- #669800 Do not enforce required attribute for multiple value fields the current user is not allowed to edit. +- Fix validation of required fields and multigroups. +- SA-CONTRIB-2010-088 follow up fix for nodereference_autocomplete_access() and content_access(). + +Changes: +- #421116 Move content.js to js/content.admin.js for consistency with location of new js files. diff --git a/sites/all/modules/cck/DEVELOPER.txt b/sites/all/modules/cck/DEVELOPER.txt new file mode 100644 index 0000000..42fa089 --- /dev/null +++ b/sites/all/modules/cck/DEVELOPER.txt @@ -0,0 +1,5 @@ + +DEVELOPER DOCUMENTATION +UPDATING FROM 5.x TO 6.x + +See http://drupal.org/node/191796 for a guide to updating field modules. diff --git a/sites/all/modules/cck/LICENSE.txt b/sites/all/modules/cck/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/cck/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/cck/README.txt b/sites/all/modules/cck/README.txt new file mode 100644 index 0000000..17e1c28 --- /dev/null +++ b/sites/all/modules/cck/README.txt @@ -0,0 +1,47 @@ + +Content Construction Kit +------------------------ + +NOTE: Install the advanced_help module (http://drupal.org/project/advanced_help) +to access more help (writing still in progress...) + +To install, place the entire CCK folder into your modules directory. +Go to Administer -> Site building -> Modules and enable the Content module and one or +more field type modules: + +- text.module +- number.module +- userreference.module +- nodereference.module + +Now go to Administer -> Content management -> Content types. Create a new +content type and edit it to add some fields. Then test by creating +a new node of your new type using the Create content menu link. + +The included optionswidget.module provides radio and check box selectors +for the various field types. + +The included fieldgroup.module allows you to group fields together +in fieldsets to help organize them. + +A comprehensive guide to using CCK is available as a CCK Handbook +at http://drupal.org/node/101723. + +Known incompatibilitie +---------------------- + +The Devel Themer module that ships with Devel is known to mess with CCK admin pages. +As a general rule, Devel Themer should only be switched on intermittently when doing +theme work on a specific page, and switched off immediately after that, for it adds +massive processing overhead. + +Maintainers +----------- +The Content Construction Kit was originally developped by: +John Van Dyk +Jonathan Chaffer + +Current maintainers: +Karen Stevenson +Yves Chedemois + diff --git a/sites/all/modules/cck/UPGRADE.txt b/sites/all/modules/cck/UPGRADE.txt new file mode 100644 index 0000000..76ee4bd --- /dev/null +++ b/sites/all/modules/cck/UPGRADE.txt @@ -0,0 +1,86 @@ + +================================================================ +UPDATING FROM VERSION 4.7 to 6.x +================================================================ + +THERE IS NO DIRECT UPGRADE PATH FROM 4.7 TO 6.x!! FIRST UPGRADE +YOUR DATABASE FROM 4.7 TO THE LATEST 5.x VERSION, THEN UPGRADE +TO 6.x. + +ALWAYS BACKUP YOUR DATABASE BEFORE UPGRADING! + +1) While 4.7 is still installed, upload the latest 4.7 version of + all the CCK files, go to update.php, and run all possible + updates for the Content module and all field modules. + +2) Install Drupal version 5. Once it is running, upload and install + the latest 5.x versions of all CCK modules, go to update.php and + run all possible updates. + +Jump to the instructions for updating from version 5.x to 6.x. + +================================================================ +UPDATING FROM VERSION 5.x to 6.x +================================================================ + +YOU MUST RUN ALL POSSIBLE UPDATES TO YOUR DATABASE IN 5.x USING +THE LATEST 5.x CODE, BEFORE UPGRADING FROM 5.x to 6.x. + +ALWAYS BACKUP YOUR DATABASE BEFORE UPGRADING! + +1) Before upgrading to 6.x, upload the latest 5.x versions of all + CCK modules, go to update.php and run all possible updates. + +2) Disable all CCK modules and remove them from the modules folder + before upgrading. + +3) Install Drupal version 6. Leave all contributed modules out of + the modules folder until core modules are up and running. + Set your administration theme to a core theme like Garland until + everything has been updated to help ensure you don't encounter + theme-related problems accessing the administration area. + +4) Once core is running, upload and install the latest 6.x versions + of ONLY CCK CORE FILES (the ones in the tarball on the CCK + project page). Enable them, then go to update.php and run all + possible updates. DO NOT add any other CCK modules to the + modules folder until the core CCK files are updated and working + correctly. + +5) After updating CCK core modules, you may get messages saying that + some updates failed and that you need to run update.php again. + If you get messages like that, keep re-running update.php until + you get no more messages. + +6) Once the core CCK modules are updated and working correctly, + add other CCK modules to the modules folder, enable them, + and run update.php. For best results, do this one module at a + time so you can tell immediately if any of them create problems + without letting those problems interfere with other updates. + + +Your database should now be ready to run in CCK version 6.x + +================================================================ + +The 4.7 to 5.x steps are necessary because of significant changes +in the database structure between the 4.7 and 6.x versions. These changes +make it extremely difficult to create an automatic upgrade path that +will work reliably in every possible situation. + +The extra steps in the 5.x to 6.x upgrade are because all modules +in the modules folder are automatically updated in Drupal 6, even if they +are not enabled. That means that modules that rely on core CCK may try +to run their updates even if core CCK is not enabled, and contributed +CCK modules that have broken updates will have their broken updates +run even if they are not enabled. + +A number of updates are dependent on other updates and won't work +until previous updates are finished or specific modules are enabled, +so you may get messages that other modules need to be enabled or that +update.php needs to be re-run, and you need to follow those instructions +until all the updates complete. + +Taking extra time during the upgrade by leaving modules out of the +modules folder altogether until you are ready to enable and update them +should reduce or eliminate update problems. \ No newline at end of file diff --git a/sites/all/modules/cck/content.info b/sites/all/modules/cck/content.info new file mode 100644 index 0000000..171b151 --- /dev/null +++ b/sites/all/modules/cck/content.info @@ -0,0 +1,10 @@ +name = Content +description = Allows administrators to define new content types. +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/content.install b/sites/all/modules/cck/content.install new file mode 100644 index 0000000..86d1d2a --- /dev/null +++ b/sites/all/modules/cck/content.install @@ -0,0 +1,620 @@ + $t('CCK - No Views integration'), + 'description' => $t("CCK integration with Views module requires Views 6.x-2.0-rc2 or greater."), + 'severity' => REQUIREMENT_ERROR, + ); + } + return $requirements; +} + +/** + * 'Safe' version of content_types() to use in updates and installs. + * + * Can't safely use content_fields() or content_types() in an update to get + * a fields array, especially without knowing what field modules are enabled, + * or the current state of the database and cache, so create a fields array + * from database info that is limited to fields from modules that are + * currently enabled. + */ +function content_types_install() { + drupal_load('module', 'content'); + module_load_include('inc', 'content', '/includes/content.crud'); + $module_field_types = $module_widgets = array(); + foreach (module_list() as $module) { + if ($field_type = module_invoke($module, 'field_info')) { + $module_field_types[$module] = $field_type; + } + if ($widget_type = module_invoke($module, 'widget_info')) { + $module_widgets[$module] = $widget_type; + } + } + $fields = array(); + $db_result = db_query("SELECT * FROM {". content_instance_tablename() ."} nfi ". + " LEFT JOIN {". content_field_tablename() ."} nf ON nf.field_name = nfi.field_name"); + while ($row = db_fetch_array($db_result)) { + $field = array_merge($row, unserialize($row['global_settings'])); + unset($field['global_settings']); + + // There may be module data available for currently disabled modules, + // or missing module data for currently enabled modules, so start over + // to get only field info for enabled modules. + unset($field['module']); + unset($field['widget_module']); + // 'columns' is a reserved word in MySQL4, so our column is named 'db_columns'. + $field['columns'] = isset($field['db_columns']) ? $field['db_columns'] : array(); + unset($field['db_columns']); + + foreach ($module_field_types as $module => $types) { + foreach ($types as $type_name => $type) { + if ($field['type'] == $type_name) { + $field['module'] = $module; + } + } + } + foreach ($module_widgets as $module => $types) { + foreach ($types as $type_name => $type) { + if ($field['widget_type'] == $type_name) { + $field['widget_module'] = $module; + } + } + } + if (!empty($field['module']) && !empty($field['widget_module'])) { + $field['widget_settings'] = unserialize($field['widget_settings']); + $field['display_settings'] = unserialize($field['display_settings']); + $field['columns'] = (array) module_invoke($field['module'], 'field_settings', 'database columns', $field); + $field = content_field_instance_expand($field); + $fields[$field['type_name']][$field['field_name']] = $field; + } + } + return $fields; +} + +/** + * Implementation of hook_install(). + */ +function content_install() { + variable_set('content_schema_version', 6009); + drupal_install_schema('content'); +} + + +/** + * Implementation of hook_uninstall(). + */ +function content_uninstall() { + drupal_uninstall_schema('content'); + // The variable is used during the uninstall process, + // so we removed it at the very end. + variable_del('content_schema_version'); + // Remove extra weights. + foreach (node_get_types('names') as $type_name) { + variable_del("content_extra_weights_$type_name"); + } +} + +/** + * Implementation of hook_enable(). + */ +function content_enable() { + // Make sure old data is emptied out of the caches, since it + // may no longer be valid since the module was last enabled, + // especially if not all the same field modules are enabled + // as before. Especially needed during updates. + cache_clear_all('*', 'cache_content', TRUE); + content_clear_type_cache(TRUE); +} + +/** + * Implementation of hook_disable(). + */ +function content_disable() { + // Make sure old data is emptied out of the caches, since it + // may no longer be valid when the module is re-enabled. + cache_clear_all('*', 'cache_content', TRUE); + content_clear_type_cache(TRUE); +} + +/** + * Implementation of hook_schema. + */ +function content_schema() { + + // Static (meta) tables. + + $schema['content_node_field'] = array( + 'fields' => array( + 'field_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'type' => array('type' => 'varchar', 'length' => 127, 'not null' => TRUE, 'default' => ''), + 'global_settings' => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE, 'serialize' => TRUE), + 'required' => array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0), + 'multiple' => array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0), + 'db_storage' => array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 1), + 'module' => array('type' => 'varchar', 'length' => 127, 'not null' => TRUE, 'default' => ''), + 'db_columns' => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE, 'serialize' => TRUE), + 'active' => array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0), + 'locked' => array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0), + ), + 'primary key' => array('field_name'), + ); + $schema['content_node_field_instance'] = array( + 'fields' => array( + 'field_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'type_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + 'label' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'widget_type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'widget_settings' => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE, 'serialize' => TRUE), + 'display_settings' => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE, 'serialize' => TRUE), + 'description' => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE), + 'widget_module' => array('type' => 'varchar', 'length' => 127, 'not null' => TRUE, 'default' => ''), + 'widget_active' => array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0), + ), + 'primary key' => array('field_name', 'type_name'), + ); + $schema['cache_content'] = drupal_get_schema_unprocessed('system', 'cache'); + + // When the module is first installed, the remaining code in the schema + // will create errors, since these tables have not yet been created. + // We don't need to create data tables on initial installation anyway + // since no fields have been created yet, so just return with this much + // of the schema. + + if (!db_table_exists('content_node_field') || !db_table_exists('content_node_field_instance')) { + return $schema; + } + + // Dynamic (data) tables. + + drupal_load('module', 'content'); + + // We can't use many helper functions here, like content_fields() or + // content_types() or we risk creating a fatal loop from circular + // logic when they call other functions that use this schema, so create + // the schema directly from a fresh query of the database. + + // content_table_schema() and content_database_info() have no + // circular logic and are safe to use here. + + $db_result = db_query("SELECT * FROM {". content_instance_tablename() ."} nfi ". + " LEFT JOIN {". content_field_tablename() ."} nf ON nf.field_name = nfi.field_name WHERE nf.active = 1 AND nfi.widget_active = 1"); + while ($field = db_fetch_array($db_result)) { + // 'columns' is a reserved word in MySQL4, so our db column is named 'db_columns'. + $field['columns'] = unserialize($field['db_columns']); + unset($field['db_columns']); + + $content_table = _content_tablename($field['type_name'], CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + $field_table = _content_tablename($field['field_name'], CONTENT_DB_STORAGE_PER_FIELD); + + + // We always add a 'per content type' table for each content type that + // has fields. + if (!isset($schema[$content_table])) { + $schema[$content_table] = content_table_schema(); + } + + $base_schema = content_table_schema($field); + if ($field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD) { + // Per-field storage: add the 'per field' table if needed. + if (!isset($schema[$field_table])) { + $schema[$field_table] = $base_schema; + } + } + else { + // Per-type storage: merge the information for the field + // in the existing table. + $schema[$content_table]['fields'] = array_merge($schema[$content_table]['fields'], $base_schema['fields']); + $schema[$content_table]['content fields'] = array_merge($schema[$content_table]['content fields'], $base_schema['content fields']); + } + } + return $schema; +} + +function content_update_last_removed() { + return 1008; +} + +/** + * Helper function for module updates : + * - checks no updates are pending for content.module + * - checks content module and the module being updated are both enabled. + * + * @param $module + * The name of the module being updated. + */ +function content_check_update($module = NULL) { + $ret = array(); + // Check that modules are enabled before running their updates. + if (!module_exists('content') || ($module && !module_exists($module))) { + drupal_set_message(t("Updates for CCK-related modules are not run until the modules are enabled on the administer modules page. When you enable them, you'll need to return to update.php and run the remaining updates.", array('@admin-modules-path' => url('admin/build/modules'), '@update-php' => base_path() .'update.php?op=selection')), 'warning', FALSE); + // The content module is not enabled, nothing else can happen. + if ($module && !module_exists('content') && module_exists($module)) { + $query_message = t('!module.module has updates but cannot be updated because content.module is not enabled.
If and when content.module is enabled, you will need to re-run the update script. You will continue to see this message until the module is enabled and updates are run.', array('!module' => $module)); + } + // The requested module is not enabled, which may be intentional. + // Just let the user know there are updates to be processed if enabled later. + else { + $query_message = t('!module.module has updates and is available in the modules folder but is not enabled.
If and when it is enabled, you will need to re-run the update script. You will continue to see this message until the module is enabled and updates are run.', array('!module' => $module ? $module : 'content')); + } + $ret['#abort'] = array('success' => FALSE, 'query' => $query_message); + return $ret; + } + // Check that content.module is up-to-date before running field module updates. + if ($module && (drupal_get_installed_schema_version('content', TRUE) < max(drupal_get_schema_versions('content')))) { + drupal_set_message(t('Some updates are still pending. Please return to update.php and run the remaining updates.', array('@update-php' => base_path() .'update.php?op=selection')), 'warning', FALSE); + $ret['#abort'] = array('success' => FALSE, 'query' => t('Some updates are still pending.
Please re-run the update script.')); + return $ret; + } + // If everything is OK and updates are not aborted, make sure + // content_associate_fields() gets run. With all the complexity of + // the dependent updates, it can get missed when an update is aborted. + // It won't hurt anything to do this more than once in order to be sure + // it doesn't get skipped. Without this step, we can end up with + // field modules that are enabled and updated, but not marked as active + // in the content_node_field table. + if ($module and module_exists($module)) { + content_associate_fields($module); + } +} + +/** + * Add module name to fields table to make it easier to identify the fields to delete when a module + * is uninstalled. + * + * Needed because the value drops out of content_info() when module is disabled, so there + * is no other way to find the associated fields. + */ +function content_update_6000() { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + + drupal_load('module', 'content'); + if (db_column_exists(content_field_tablename(), 'active')) { + return $ret; + } + db_add_field($ret, content_field_tablename(), 'module', array('type' => 'varchar', 'length' => 127, 'not null' => TRUE, 'default' => '')); + db_add_field($ret, content_field_tablename(), 'db_columns', array('type' => 'text', 'size' => 'medium', 'not null' => TRUE, 'initial' => '')); + db_add_field($ret, content_field_tablename(), 'active', array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0)); + db_add_field($ret, content_instance_tablename(), 'widget_module', array('type' => 'varchar', 'length' => 127, 'not null' => TRUE, 'default' => '')); + db_add_field($ret, content_instance_tablename(), 'widget_active', array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0)); + + // This will update the table for any modules enabled at this time. + foreach (module_list() as $module) { + content_associate_fields($module); + } + + // Fix the cache_content schema + if (db_table_exists('cache_content')) { + db_drop_table($ret, 'cache_content'); + } + db_create_table($ret, 'cache_content', drupal_get_schema_unprocessed('system', 'cache')); + variable_set('content_schema_version', 6000); + + // The cache table had to be used to store data until this update ran, + // so clear cache out now that we're switching back to the cache_content table. + $ret[] = update_sql('DELETE FROM {cache}'); + + return $ret; +} + +/** + * Rename node_field and node_field_instance tables. + * + * This is a carryover from when the data tables were renamed, + * postponed so we wouldn't create any more havoc than necessary + * until a major version change. + * + * Using 'content_node_field' instead of 'content_field' + * to avoid conflicts with field tables that will be prefixed + * with 'content_field'. + */ +function content_update_6001() { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + drupal_load('module', 'content'); + if (db_table_exists('content_node_field')) { + return $ret; + } + db_rename_table($ret, 'node_field', 'content_node_field'); + db_rename_table($ret, 'node_field_instance', 'content_node_field_instance'); + variable_set('content_schema_version', 6001); + content_clear_type_cache(TRUE); + return $ret; +} + +/** + * Get rid of automatic per content tables for content types that have no fields. + * Switching to adding those tables only when needed. + */ +function content_update_6002() { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + + drupal_load('module', 'content'); + $db_types = content_types_install(); + $field_types = array(); + + $result = db_query("SELECT DISTINCT type_name FROM {". content_instance_tablename() ."}"); + while ($type = db_fetch_array($result)) { + $field_types[] = $type['type_name']; + } + + foreach ($db_types as $content_type => $content_info) { + if (!in_array($content_type, $field_types)) { + $table = _content_tablename($content_type, CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + if (db_table_exists($table)) { + db_drop_table($ret, $table); + } + } + } + variable_set('content_schema_version', 6002); + content_clear_type_cache(TRUE); + return $ret; +} + +/** + * 'db_columns' column 1st got introduced as 'columns', which is forbidden in MySQL 4. + * This update function will only be useful for early D6 testers... + */ +function content_update_6003() { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + if (db_column_exists('content_node_field', 'columns')) { + db_change_field($ret, 'content_node_field', 'columns', 'db_columns', array('type' => 'text', 'size' => 'medium', 'not null' => TRUE)); + } + variable_set('content_schema_version', 6003); + return $ret; +} + +/** + * Index the 'nid' column on data tables to optimize node deletion. + * Large tables might deserve a multipass update. + */ +function content_update_6004(&$sandbox) { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + + // Do nothing if the indexes were already created by D5's content_update_1009. + if (variable_get('content_update_1009', FALSE)) { + return $ret; + } + + // Gather list of tables. + if (!isset($sandbox['tables'])) { + drupal_load('module', 'content'); + $sandbox['tables'] = array(); + $result = db_query('SELECT * FROM {'. content_instance_tablename() .'} nfi '. + ' LEFT JOIN {'. content_field_tablename() .'} nf ON nf.field_name = nfi.field_name'); + while ($field = db_fetch_array($result)) { + if ($field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD) { + $table = _content_tablename($field['field_name'], CONTENT_DB_STORAGE_PER_FIELD); + } + else { + $table = _content_tablename($field['type_name'], CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + } + $sandbox['tables'][$table] = $table; + } + $sandbox['count'] = count($sandbox['tables']); + } + + // One pass : add index on one table. + if ($table = array_shift($sandbox['tables'])) { + db_add_index($ret, $table, 'nid', array('nid')); + } + + if ($sandbox['count']) { + $ret['#finished'] = 1 - count($sandbox['tables']) / $sandbox['count']; + } + variable_set('content_schema_version', 6004); + return $ret; +} + +/** + * Add 'locked' property for fields. + */ +function content_update_6005() { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + drupal_load('module', 'content'); + db_add_field($ret, content_field_tablename(), 'locked', array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0)); + variable_set('content_schema_version', 6005); + return $ret; +} + +/** + * Make sure the 'locked' column is NOT NULL (error in previous content_update_6005(). + */ +function content_update_6006() { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + drupal_load('module', 'content'); + db_change_field($ret, content_field_tablename(), 'locked', 'locked', array('type' => 'int', 'size' => 'tiny', 'not null' => TRUE, 'default' => 0)); + variable_set('content_schema_version', 6006); + return $ret; +} + +/** + * Dummy update function to make sure the theme registry and css / JS aggregated files + * are updated. + */ +function content_update_6007() { + if ($abort = content_check_update()) { + return $abort; + } + + variable_set('content_schema_version', 6007); + return array(); +} + +/** + * Dummy update function to make sure schema version gets updated. + */ +function content_update_6008() { + if ($abort = content_check_update()) { + return $abort; + } + + variable_set('content_schema_version', 6008); + return array(); +} + +/** + * Add the 'exclude from $content' display setting to all existing field instances. + */ +function content_update_6009() { + if ($abort = content_check_update()) { + return $abort; + } + + $ret = array(); + $result = db_query("SELECT * FROM {content_node_field_instance}"); + while ($type = db_fetch_array($result)) { + $new_settings = array(); + $display_settings = unserialize($type['display_settings']); + if (!empty($display_settings)) { + foreach ($display_settings as $key => $val) { + $new_settings[$key] = $val; + if ($key !== 'label' && is_array($val)) { + $new_settings[$key]['exclude'] = 0; + } + } + } + else { + $new_settings = array( + 'label' => array('format' => 'above'), + 'full' => array('format' => 'default', 'exclude' => 0), + 'teaser' => array('format' => 'default', 'exclude' => 0), + ); + } + db_query("UPDATE {content_node_field_instance} SET display_settings='%s' WHERE field_name='%s' AND type_name='%s'", serialize($new_settings), $type['field_name'], $type['type_name']); + } + variable_set('content_schema_version', 6009); + return $ret; +} + +/** + * Fix multiple serialization caused by per-field to per-type migration. + * See http://drupal.org/node/407446. + */ +function content_update_6010(&$sandbox) { + if ($abort = content_check_update()) { + return $abort; + } + $ret = array(); + + drupal_load('module', 'content'); + + // Gather list of tables and columns that need to be updated. + if (!isset($sandbox['tables'])) { + $sandbox['tables'] = array(); + $fields = content_fields(); + foreach ($fields as $name => $field) { + $db_info = content_database_info($field); + foreach ($db_info['columns'] as $column => $attributes) { + if (isset($attributes['serialize']) && $attributes['serialize']) { + $sandbox['tables'][$db_info['table']]['table'] = $db_info['table']; + $sandbox['tables'][$db_info['table']]['columns'][] = $attributes['column']; + $sandbox['tables'][$db_info['table']]['multiple'] = $field['multiple']; + } + } + } + $sandbox['count'] = count($sandbox['tables']); + $sandbox['current_vid'] = 0; + $sandbox['current_delta'] = 0; + } + + // Number of rows to fix in one pass. + $limit = 500; + // Start correcting data. + if ($table_info = array_shift($sandbox['tables'])) { + $table = $table_info['table']; + $columns = $table_info['columns']; + + if ($table_info['multiple']) { + $query = "SELECT * FROM {" . $table . "} WHERE (vid = %d AND delta > %d) OR (vid > %d) ORDER BY vid ASC, delta ASC"; + $args = array($sandbox['current_vid'], $sandbox['current_delta'], $sandbox['current_vid']); + } + else { + $query = "SELECT * FROM {" . $table . "} WHERE vid > %d ORDER BY vid ASC"; + $args = array($sandbox['current_vid']); + } + $result = db_query_range($query, $args, 0, $limit); + $count = 0; + while ($row = db_fetch_array($result)) { + $update_query = $update_args = array(); + foreach ($columns as $column) { + $data = $row[$column]; + // No need to do anything if the data is NULL. + if (!empty($data)) { + // Unserialize until we get something that is not a string + while (is_string($data)) { + $unserialized = @unserialize($data); + if ($unserialized !== FALSE) { + $data = $unserialized; + } + else { + // TODO : test with a serialized string, just in case... + break; + } + } + // Re-serialize once. + $data = serialize($data); + // If we end up with something different than what we started with, update. + if ($data !== $row[$column]) { + $update_query[] = "$column = '%s'"; + $update_args[] = $data; + } + } + } + if ($update_query) { + $update_args[] = $row['vid']; + db_query("UPDATE {" . $table . "} SET ". implode(', ', $update_query) ." WHERE vid = %d", $update_args); + } + $sandbox['current_vid'] = $row['vid']; + $sandbox['current_delta'] = isset($row['delta']) ? $row['delta'] : 0; + $count++; + } + if ($count == $limit) { + // Add the table back into the list of tables to be processed if rows remain. + array_unshift($sandbox['tables'], $table_info); + } + else { + // Done with this table: reset vid and delta markers. + $sandbox['current_vid'] = 0; + $sandbox['current_delta'] = 0; + $ret[] = array('success' => TRUE, 'query' => "Fixed serialized values in table $table"); + } + } + + if ($sandbox['count']) { + $ret['#finished'] = 1 - count($sandbox['tables']) / $sandbox['count']; + } + return $ret; +} \ No newline at end of file diff --git a/sites/all/modules/cck/content.module b/sites/all/modules/cck/content.module new file mode 100644 index 0000000..d12efa8 --- /dev/null +++ b/sites/all/modules/cck/content.module @@ -0,0 +1,2901 @@ +'. t('The content module, a required component of the Content Construction Kit (CCK), allows administrators to associate custom fields with content types. In Drupal, content types are used to define the characteristics of a post, including the title and description of the fields displayed on its add and edit pages. Using the content module (and the other helper modules included in CCK), custom fields beyond the default "Title" and "Body" may be added. CCK features are accessible through tabs on the content types administration page. (See the node module help page for more information about content types.)', array('@content-types' => url('admin/content/types'), '@node-help' => url('admin/help/node'))) .'

'; + $output .= '

'. t('When adding a custom field to a content type, you determine its type (whether it will contain text, numbers, or references to other objects) and how it will be displayed (either as a text field or area, a select box, checkbox, radio button, or autocompleting field). A field may have multiple values (i.e., a "person" may have multiple e-mail addresses) or a single value (i.e., an "employee" has a single employee identification number). As you add and edit fields, CCK automatically adjusts the structure of the database as necessary. CCK also provides a number of other features, including intelligent caching for your custom data, an import and export facility for content type definitions, and integration with other contributed modules.') .'

'; + $output .= '

'. t('Custom field types are provided by a set of optional modules included with CCK (each module provides a different type). The modules page allows you to enable or disable CCK components. A default installation of CCK includes:', array('@modules' => url('admin/build/modules'))) .'

'; + $output .= '
    '; + $output .= '
  • '. t('number, which adds numeric field types, in integer, decimal or floating point form. You may define a set of allowed inputs, or specify an allowable range of values. A variety of common formats for displaying numeric data are available.') .'
  • '; + $output .= '
  • '. t("text, which adds text field types. A text field may contain plain text only, or optionally, may use Drupal's input format filters to securely manage rich text input. Text input fields may be either a single line (text field), multiple lines (text area), or for greater input control, a select box, checkbox, or radio buttons. If desired, CCK can validate the input to a set of allowed values.") .'
  • '; + $output .= '
  • '. t('nodereference, which creates custom references between Drupal nodes. By adding a nodereference field and two different content types, for instance, you can easily create complex parent/child relationships between data (multiple "employee" nodes may contain a nodereference field linking to an "employer" node).') .'
  • '; + $output .= '
  • '. t('userreference, which creates custom references to your sites\' user accounts. By adding a userreference field, you can create complex relationships between your site\'s users and posts. To track user involvement in a post beyond Drupal\'s standard Authored by field, for instance, add a userreference field named "Edited by" to a content type to store a link to an editor\'s user account page.') .'
  • '; + $output .= '
  • '. t('fieldgroup, which creates collapsible fieldsets to hold a group of related fields. A fieldset may either be open or closed by default. The order of your fieldsets, and the order of fields within a fieldset, is managed via a drag-and-drop interface provided by content module.') .'
  • '; + $output .= '
'; + $output .= '

'. t('For more information, see the online handbook entry for CCK or the CCK project page.', array('@handbook-cck' => 'http://drupal.org/handbook/modules/cck', '@project-cck' => 'http://drupal.org/project/cck')) .'

'; + return $output; + } +} + +/** + * Implementation of hook_flush_caches. + */ +function content_flush_caches() { + return array(content_cache_tablename()); +} + +/** + * Implementation of hook_init(). + */ +function content_init() { + drupal_add_css(drupal_get_path('module', 'content') .'/theme/content-module.css'); + if (module_exists('diff') && !function_exists('content_diff')) { + module_load_include('inc', 'content', 'includes/content.diff'); + } +} + +/** + * Implementation of hook_perm(). + */ +function content_perm() { + return array('Use PHP input for field settings (dangerous - grant with care)'); +} + +/** + * Implementation of hook_menu_alter(). + */ +function content_menu_alter(&$items) { + // Customize the content types page with our own callback + $items['admin/content/types']['page callback'] = 'content_types_overview'; + $items['admin/content/types']['file'] = 'content.admin.inc'; + $items['admin/content/types']['file path'] = drupal_get_path('module', 'content') .'/includes'; +} + +/** + * Implementation of hook_menu(). + */ +function content_menu() { + $items = array(); + $items['admin/content/types/fields'] = array( + 'title' => 'Fields', + 'page callback' => 'content_fields_list', + 'access arguments' => array('administer content types'), + 'file' => 'includes/content.admin.inc', + 'type' => MENU_LOCAL_TASK, + ); + // Callback for AHAH add more buttons. + $items['content/js_add_more'] = array( + 'page callback' => 'content_add_more_js', + 'access arguments' => array('access content'), + 'file' => 'includes/content.node_form.inc', + 'type' => MENU_CALLBACK, + ); + + // Make sure this doesn't fire until content_types is working, + // and tables are updated, needed to avoid errors on initial installation. + if (!defined('MAINTENANCE_MODE') && variable_get('content_schema_version', -1) >= 6007) { + foreach (node_get_types() as $type) { + $type_name = $type->type; + $content_type = content_types($type_name); + $type_url_str = $content_type['url_str']; + $items['admin/content/node-type/'. $type_url_str .'/fields'] = array( + 'title' => 'Manage fields', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('content_field_overview_form', $type_name), + 'access arguments' => array('administer content types'), + 'file' => 'includes/content.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + ); + $items['admin/content/node-type/'. $type_url_str .'/display'] = array( + 'title' => 'Display fields', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('content_display_overview_form', $type_name), + 'access arguments' => array('administer content types'), + 'file' => 'includes/content.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + ); + $contexts = content_build_modes('_tabs'); + foreach ($contexts as $key => $tab) { + $items['admin/content/node-type/'. $type_url_str .'/display/'. $key] = array( + 'title' => $tab['title'], + 'page arguments' => array('content_display_overview_form', $type_name, $key), + 'access arguments' => array('administer content types'), + 'type' => $key == 'basic' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK, + 'weight' => $key == 'basic' ? 0 : 1, + ); + } + // Cast as an array in case this is called before any fields have + // been added, like when a new content type is created. + foreach ((array) $content_type['fields'] as $field) { + $field_name = $field['field_name']; + $items['admin/content/node-type/'. $type_url_str .'/fields/'. $field_name] = array( + 'title' => $field['widget']['label'], + 'page callback' => 'drupal_get_form', + 'page arguments' => array('content_field_edit_form', $type_name, $field_name), + 'access arguments' => array('administer content types'), + 'file' => 'includes/content.admin.inc', + 'type' => MENU_LOCAL_TASK, + ); + $items['admin/content/node-type/'. $type_url_str .'/fields/'. $field_name .'/remove'] = array( + 'title' => 'Remove field', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('content_field_remove_form', $type_name, $field_name), + 'access arguments' => array('administer content types'), + 'file' => 'includes/content.admin.inc', + 'type' => MENU_CALLBACK, + ); + } + } + } + return $items; +} + +/** + * Hook elements(). + * + * Used to add multiple value processing, validation, and themes. + * + * FAPI callbacks can be declared here, and the element will be + * passed to those callbacks. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + */ +function content_elements() { + return array( + 'content_multiple_values' => array(), + 'content_field' => array(), + ); +} + +/** + * Implementation of hook_theme(). + */ +function content_theme() { + $path = drupal_get_path('module', 'content') .'/theme'; + module_load_include('inc', 'content', 'theme/theme'); + + return array( + 'content_field' => array( + 'template' => 'content-field', + 'arguments' => array('element' => NULL), + 'path' => $path, + ), + 'content_overview_links' => array( + 'arguments' => array(), + ), + 'content_field_overview_form' => array( + 'template' => 'content-admin-field-overview-form', + 'file' => 'theme.inc', + 'path' => $path, + 'arguments' => array('form' => NULL), + ), + 'content_display_overview_form' => array( + 'template' => 'content-admin-display-overview-form', + 'file' => 'theme.inc', + 'path' => $path, + 'arguments' => array('form' => NULL), + ), + 'content_exclude' => array( + 'arguments' => array('content' => NULL, 'object' => array(), 'context' => NULL), + ), + 'content_view_multiple_field' => array( + 'arguments' => array('items' => NULL, 'field' => NULL, 'data' => NULL), + ), + 'content_multiple_values' => array( + 'arguments' => array('element' => NULL), + ), + ); +} + +/** + * Implementation of hook_views_api(). + */ +function content_views_api() { + return array( + 'api' => 2, + 'path' => drupal_get_path('module', 'content') . '/includes/views', + ); +} + +/** + * Implementation of hook_ctools_plugin_directory(). + */ +function content_ctools_plugin_directory($module, $plugin) { + if ($module == 'ctools' && $plugin == 'content_types') { + return 'includes/panels/' . $plugin; + } +} + +/** + * Load data for a node type's fields. + * Implementation of hook_nodeapi 'load' op. + * + * When loading one of the content.module nodes, we need to let each field handle + * its own loading. This can make for a number of queries in some cases, so we + * cache the loaded object structure and invalidate it during the update process. + */ +function content_load(&$node) { + $cid = 'content:'. $node->nid .':'. $node->vid; + if ($cached = cache_get($cid, content_cache_tablename())) { + foreach ($cached->data as $key => $value) { + $node->$key = $value; + } + } + else { + $default_additions = _content_field_invoke_default('load', $node); + if ($default_additions) { + foreach ($default_additions as $key => $value) { + $node->$key = $value; + } + } + $additions = _content_field_invoke('load', $node); + if ($additions) { + foreach ($additions as $key => $value) { + $node->$key = $value; + $default_additions[$key] = $value; + } + } + cache_set($cid, $default_additions, content_cache_tablename()); + } +} + +/** + * Implementation of hook_nodeapi 'validate' op. + * + */ +function content_validate(&$node, $form = NULL) { + _content_field_invoke_default('validate', $node, $form); + _content_field_invoke('validate', $node, $form); +} + +/** + * Implementation of hook_nodeapi 'presave' op. + * + */ +function content_presave(&$node) { + _content_field_invoke('presave', $node); + _content_field_invoke_default('presave', $node); +} + +/** + * Implementation of hook_nodeapi 'insert' op. + * + * Insert node type fields. + */ +function content_insert(&$node) { + _content_field_invoke('insert', $node); + _content_field_invoke_default('insert', $node); +} + +/** + * Implementation of hook_nodeapi 'update' op. + * + * Update node type fields. + */ +function content_update(&$node) { + _content_field_invoke('update', $node); + _content_field_invoke_default('update', $node); + cache_clear_all('content:'. $node->nid .':'. $node->vid, content_cache_tablename()); +} + +/** + * Implementation of hook_nodeapi 'delete' op. + * + * Delete node type fields. + */ +function content_delete(&$node) { + _content_field_invoke('delete', $node); + _content_field_invoke_default('delete', $node); + cache_clear_all('content:'. $node->nid .':', content_cache_tablename(), TRUE); +} + +/** + * Implementation of hook_nodeapi 'delete_revision' op. + * + * Delete node type fields for a revision. + */ +function content_delete_revision(&$node) { + _content_field_invoke('delete revision', $node); + _content_field_invoke_default('delete revision', $node); + cache_clear_all('content:'. $node->nid .':'. $node->vid, content_cache_tablename()); +} + +/** + * Implementation of hook_nodeapi 'view' op. + * + * Generate field render arrays. + */ +function content_view(&$node, $teaser = FALSE, $page = FALSE) { + // Let field modules sanitize their data for output. + _content_field_invoke('sanitize', $node, $teaser, $page); + + // Merge fields. + $additions = _content_field_invoke_default('view', $node, $teaser, $page); + $old = isset($node->content) ? (array) $node->content : array(); + $node->content = array_merge($old, $additions); +} + +/** + * Render a single field, fully themed with label and multiple values. + * + * To be used by third-party code (Views, Panels...) that needs to output + * an isolated field. Do *not* use inside node templates, use the + * $FIELD_NAME_rendered variables instead. + * + * By default, the field is displayed using the settings defined for the + * 'full node' or 'teaser' contexts (depending on the value of the $teaser param). + * Set $node->build_mode to a different value to use a different context. + * + * Different settings can be specified by adjusting $field['display_settings']. + * + * @param $field + * The field definition. + * @param $node + * The node containing the field to display. Can be a 'pseudo-node', containing + * at least 'type', 'nid', 'vid', and the field data. + * @param $teaser + * @param $page + * Similar to hook_nodeapi('view') + * @return + * The themed output for the field. + */ +function content_view_field($field, $node, $teaser = FALSE, $page = FALSE) { + $output = ''; + if (isset($node->$field['field_name'])) { + $items = $node->$field['field_name']; + + // Use 'full'/'teaser' if not specified otherwise. + $node->build_mode = isset($node->build_mode) ? $node->build_mode : NODE_BUILD_NORMAL; + + // One-field equivalent to _content_field_invoke('sanitize'). + $field_types = _content_field_types(); + $module = $field_types[$field['type']]['module']; + $function = $module .'_field'; + if (function_exists($function)) { + $function('sanitize', $node, $field, $items, $teaser, $page); + $node->$field['field_name'] = $items; + } + + $view = content_field('view', $node, $field, $items, $teaser, $page); + // content_field('view') adds a wrapper to handle variables and 'excluded' + // fields for node templates. We bypass it and render the actual field. + $output = drupal_render($view[$field['field_name']]['field']); + } + return $output; +} + +/** + * Implementation of hook_nodeapi 'alter' op. + * + * Add back the formatted values in the 'view' element for all fields, + * so that node templates can use it. + */ +function content_alter(&$node, $teaser = FALSE, $page = FALSE) { + _content_field_invoke_default('alter', $node, $teaser, $page); +} + +/** + * Implementation of hook_nodeapi 'prepare translation' op. + * + * Generate field render arrays. + */ +function content_prepare_translation(&$node) { + $default_additions = _content_field_invoke_default('prepare translation', $node); + $additions = _content_field_invoke('prepare translation', $node); + // Merge module additions after the default ones to enable overriding + // of field values. + $node = (object) array_merge((array) $node, $default_additions, $additions); +} + +/** + * Implementation of hook_nodeapi(). + */ +function content_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { + // Prevent against invalid 'nodes' built by broken 3rd party code. + if (isset($node->type)) { + $type = content_types($node->type); + // Save cycles if the type has no CCK fields. + if (!empty($type['fields'])) { + $callback = 'content_'. str_replace(' ', '_', $op); + if (function_exists($callback)) { + $callback($node, $a3, $a4); + } + } + + // Special case for 'view' op, we want to adjust weights of non-cck fields + // even if there are no actual fields for this type. + if ($op == 'view') { + $node->content['#pre_render'][] = 'content_alter_extra_weights'; + $node->content['#content_extra_fields'] = $type['extra']; + } + } +} + +/** + * Implementation of hook_form_alter(). + */ +function content_form_alter(&$form, $form_state, $form_id) { + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + $type = content_types($form['#node']->type); + if (!empty($type['fields'])) { + module_load_include('inc', 'content', 'includes/content.node_form'); + // Merge field widgets. + $form = array_merge($form, content_form($form, $form_state)); + } + $form['#pre_render'][] = 'content_alter_extra_weights'; + $form['#content_extra_fields'] = $type['extra']; + } +} + +/** + * Pre-render callback to adjust weights of non-CCK fields. + */ +function content_alter_extra_weights($elements) { + if (isset($elements['#content_extra_fields'])) { + foreach ($elements['#content_extra_fields'] as $key => $value) { + // Some core 'fields' use a different key in node forms and in 'view' + // render arrays. Check we're not on a form first. + if (!isset($elements['#build_id']) && isset($value['view']) && isset($elements[$value['view']])) { + $elements[$value['view']]['#weight'] = $value['weight']; + } + elseif (isset($elements[$key])) { + $elements[$key]['#weight'] = $value['weight']; + } + } + } + return $elements; +} + +/** + * Proxy function to call content_add_more_submit(), because it might not be + * included yet when the form is processed and invokes the callback. + */ +function content_add_more_submit_proxy($form, &$form_state) { + module_load_include('inc', 'content', 'includes/content.node_form'); + content_add_more_submit($form, $form_state); +} + +/** + * Proxy function to call content_multiple_value_after_build(), because it might + * not be included yet when the form is processed and invokes the callback. + */ +function content_multiple_value_after_build_proxy($elements, &$form_state) { + module_load_include('inc', 'content', 'includes/content.node_form'); + return content_multiple_value_after_build($elements, $form_state); +} + +/** + * Theme an individual form element. + * + * Combine multiple values into a table with drag-n-drop reordering. + */ +function theme_content_multiple_values($element) { + $field_name = $element['#field_name']; + $field = content_fields($field_name); + $output = ''; + + if ($field['multiple'] >= 1) { + $table_id = $element['#field_name'] .'_values'; + $order_class = $element['#field_name'] .'-delta-order'; + $required = !empty($element['#required']) ? '*' : ''; + + $header = array( + array( + 'data' => t('!title: !required', array('!title' => $element['#title'], '!required' => $required)), + 'colspan' => 2 + ), + array('data' => t('Order'), 'class' => 'content-multiple-weight-header'), + ); + if ($field['multiple'] == 1) { + $header[] = array('data' => ''. t('Remove') .'', 'class' => 'content-multiple-remove-header'); + } + $rows = array(); + + // Sort items according to '_weight' (needed when the form comes back after + // preview or failed validation) + $items = array(); + foreach (element_children($element) as $key) { + if ($key !== $element['#field_name'] .'_add_more') { + $items[$element[$key]['#delta']] = &$element[$key]; + } + } + uasort($items, '_content_sort_items_value_helper'); + + // Add the items as table rows. + foreach ($items as $delta => $item) { + $item['_weight']['#attributes']['class'] = $order_class; + $delta_element = drupal_render($item['_weight']); + if ($field['multiple'] == 1) { + $remove_element = drupal_render($item['_remove']); + } + $cells = array( + array('data' => '', 'class' => 'content-multiple-drag'), + drupal_render($item), + array('data' => $delta_element, 'class' => 'delta-order'), + ); + $row_class = 'draggable'; + if ($field['multiple'] == 1) { + if (!empty($item['_remove']['#default_value'])) { + $row_class .= ' content-multiple-removed-row'; + } + $cells[] = array('data' => $remove_element, 'class' => 'content-multiple-remove-cell'); + } + $rows[] = array('data' => $cells, 'class' => $row_class); + } + + $output .= theme('table', $header, $rows, array('id' => $table_id, 'class' => 'content-multiple-table')); + $output .= $element['#description'] ? '
'. $element['#description'] .'
' : ''; + $output .= drupal_render($element[$element['#field_name'] .'_add_more']); + + drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class); + drupal_add_js(drupal_get_path('module', 'content') .'/js/content.node_form.js'); + } + else { + foreach (element_children($element) as $key) { + $output .= drupal_render($element[$key]); + } + } + + return $output; +} + +/** + * Modules notify Content module when uninstalled, disabled, etc. + * + * @param string $op + * the module operation: uninstall, install, enable, disable + * @param string $module + * the name of the affected module. + * @TODO + * figure out exactly what needs to be done by content module when + * field modules are installed, uninstalled, enabled or disabled. + */ +function content_notify($op, $module) { + switch ($op) { + case 'install': + content_clear_type_cache(); + break; + case 'uninstall': + module_load_include('inc', 'content', 'includes/content.crud'); + content_module_delete($module); + break; + case 'enable': + content_associate_fields($module); + content_clear_type_cache(); + break; + case 'disable': + // When CCK modules are disabled before content module's update is run + // to add the active column, we can't do this. + if (variable_get('content_schema_version', -1) < 6007) { + return FALSE; + } + db_query("UPDATE {". content_field_tablename() ."} SET active=0 WHERE module='%s'", $module); + db_query("UPDATE {". content_instance_tablename() ."} SET widget_active=0 WHERE widget_module='%s'", $module); + content_clear_type_cache(TRUE); + break; + } +} + +/** + * Allows a module to update the database for fields and columns it controls. + * + * @param string $module + * The name of the module to update on. + */ +function content_associate_fields($module) { + // When CCK modules are enabled before content module's update is run, + // to add module and active columns, we can't do this. + if (variable_get('content_schema_version', -1) < 6007) { + return FALSE; + } + $module_fields = module_invoke($module, 'field_info'); + if ($module_fields) { + foreach ($module_fields as $name => $field_info) { + watchdog('content', 'Updating field type %type with module %module.', array('%type' => $name, '%module' => $module)); + db_query("UPDATE {". content_field_tablename() ."} SET module = '%s', active = %d WHERE type = '%s'", $module, 1, $name); + } + } + $module_widgets = module_invoke($module, 'widget_info'); + if ($module_widgets) { + foreach ($module_widgets as $name => $widget_info) { + watchdog('content', 'Updating widget type %type with module %module.', array('%type' => $name, '%module' => $module)); + db_query("UPDATE {". content_instance_tablename() ."} SET widget_module = '%s', widget_active = %d WHERE widget_type = '%s'", $module, 1, $name); + } + } + // This is called from updates and installs, so get the install-safe + // version of a fields array. + $fields_set = array(); + module_load_include('install', 'content'); + $types = content_types_install(); + foreach ($types as $type_name => $fields) { + foreach ($fields as $field) { + if ($field['module'] == $module && !in_array($field['field_name'], $fields_set)) { + $columns = (array) module_invoke($field['module'], 'field_settings', 'database columns', $field); + db_query("UPDATE {". content_field_tablename() ."} SET db_columns = '%s' WHERE field_name = '%s'", serialize($columns), $field['field_name']); + $fields_set[] = $field['field_name']; + } + } + } +} + +/** + * Implementation of hook_field(). Handles common field housekeeping. + * + * This implementation is special, as content.module does not define any field + * types. Instead, this function gets called after the type-specific hook, and + * takes care of default stuff common to all field types. + * + * Db-storage ops ('load', 'insert', 'update', 'delete', 'delete revisions') + * are not executed field by field, and are thus handled separately in + * content_storage. + * + * The 'view' operation constructs the $node in a way that you can use + * drupal_render() to display the formatted output for an individual field. + * i.e. print drupal_render($node->countent['field_foo']); + * + * The code now supports both single value formatters, which theme an + * individual item value as has been done in previous version of CCK, + * and multiple value formatters, which theme all values for the field + * in a single theme. The multiple value formatters could be used, for + * instance, to plot field values on a single map or display them + * in a graph. Single value formatters are the default, multiple value + * formatters can be designated as such in formatter_info(). + * + * The node array will look like: + * $node->content['field_foo']['wrapper'] = array( + * '#type' => 'content_field', + * '#title' => 'label' + * '#field_name' => 'field_name', + * '#node' => $node, + * // Value of the $teaser param of hook_nodeapi('view'). + * '#teaser' => $teaser, + * // Value of the $page param of hook_nodeapi('view'). + * '#page' => $page, + * // The curent rendering context ('teaser', 'full', NODE_BUILD_SEARCH_INDEX...). + * '#context' => $context, + * 'items' => + * 0 => array( + * '#item' => $items[0], + * // Only for 'single-value' formatters + * '#theme' => $theme, + * '#field_name' => 'field_name', + * '#type_name' => $node->type, + * '#formatter' => $formatter_name, + * '#node' => $node, + * '#delta' => 0, + * ), + * 1 => array( + * '#item' => $items[1], + * // Only for 'single-value' formatters + * '#theme' => $theme, + * '#field_name' => 'field_name', + * '#type_name' => $node->type, + * '#formatter' => $formatter_name, + * '#node' => $node, + * '#delta' => 1, + * ), + * // Only for 'multiple-value' formatters + * '#theme' => $theme, + * '#field_name' => 'field_name', + * '#type_name' => $node->type, + * '#formatter' => $formatter_name, + * ), + * ); + */ +function content_field($op, &$node, $field, &$items, $teaser, $page, $wrappers = NULL) { + switch ($op) { + case 'validate': + // If the field is configured for multiple values and these are handled + // by content module, we need to filter out items flagged for removal and + // count non-empty items to enforce field requirement settings. + if ($field['multiple'] >= 1 && content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + module_load_include('inc', 'content', 'includes/content.node_form'); + // Note that the $teaser argument for nodeapi('validate') is the $form. + content_multiple_value_nodeapi_validate($node, $field, $items, $teaser); + } + break; + + case 'presave': + if (!empty($node->devel_generate)) { + include_once('./'. drupal_get_path('module', 'content') .'/includes/content.devel.inc'); + content_generate_fields($node, $field); + $items = $node->{$field['field_name']}; + } + + // Manual node_save calls might not have all fields filled in. + // On node insert, we need to make sure all tables get at least an empty + // record, or subsequent edits, using drupal_write_record() in update mode, + // won't insert any data. + // Missing fields on node update are handled in content_storage(). + if (empty($items) && !isset($node->nid)) { + foreach (array_keys($field['columns']) as $column) { + $items[0][$column] = NULL; + } + $node->$field['field_name'] = $items; + } + + // If there was an AHAH add more button in this field, don't save it. + // TODO: is it still needed ? + unset($items[$field['field_name'] .'_add_more']); + + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + // Reorder items to account for drag-n-drop reordering. + $items = _content_sort_items($field, $items); + } + + // Filter out items flagged for removal. + $items = content_set_empty($field, $items); + + break; + + case 'view': + $addition = array(); + + // Previewed nodes bypass the 'presave' op, so we need to do some massaging. + if ($node->build_mode == NODE_BUILD_PREVIEW) { + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + // Reorder items to account for drag-n-drop reordering. + $items = _content_sort_items($field, $items); + } + + // Filter out items flagged for removal. + $items = content_set_empty($field, $items); + } + + // NODE_BUILD_NORMAL is 0, and ('whatever' == 0) is TRUE, so we need a ===. + if ($node->build_mode === NODE_BUILD_NORMAL || $node->build_mode == NODE_BUILD_PREVIEW) { + $context = $teaser ? 'teaser' : 'full'; + } + else { + $context = $node->build_mode; + } + // The field may be missing info for $contexts added by modules + // enabled after the field was last edited. + $formatter_name = isset($field['display_settings'][$context]) && isset($field['display_settings'][$context]['format']) ? $field['display_settings'][$context]['format'] : 'default'; + if ($formatter = _content_get_formatter($formatter_name, $field['type'])) { + $theme = $formatter['module'] .'_formatter_'. $formatter_name; + $single = (content_handle('formatter', 'multiple values', $formatter) == CONTENT_HANDLE_CORE); + + $label_display = isset($field['display_settings']['label']['format']) ? $field['display_settings']['label']['format'] : 'above'; + // Do not include field labels when indexing content. + if ($context == NODE_BUILD_SEARCH_INDEX) { + $label_display = 'hidden'; + } + + $element = array( + '#type' => 'content_field', + '#title' => check_plain(t($field['widget']['label'])), + '#field_name' => $field['field_name'], + '#access' => $formatter_name != 'hidden' && content_access('view', $field, NULL, $node), + '#label_display' => $label_display, + '#node' => $node, + '#teaser' => $teaser, + '#page' => $page, + '#context' => $context, + '#single' => $single, + 'items' => array(), + ); + + // Fill-in items. + foreach (array_keys($items) as $weight => $delta) { + $element['items'][$delta] = array( + '#item' => $items[$delta], + '#weight' => $weight, + ); + } + + // Append formatter information either on each item ('single-value' formatter) + // or at the upper 'items' level ('multiple-value' formatter) + $format_info = array( + '#theme' => $theme, + '#field_name' => $field['field_name'], + '#type_name' => $node->type, + '#formatter' => $formatter_name, + '#node' => $node, + ); + if ($single) { + foreach ($items as $delta => $item) { + $element['items'][$delta] += $format_info; + $element['items'][$delta]['#item']['#delta'] = $delta; + } + } + else { + $element['items'] += $format_info; + } + + // The wrapper lets us get the themed output for the whole field + // to populate the $FIELD_NAME_rendered variable for node templates, + // and hide it from the $content variable if needed. + // See 'preprocess_node' op and theme_content_field_wrapper()? + $wrapper = array( + 'field' => $element, + '#weight' => $field['widget']['weight'], + '#post_render' => array('content_field_wrapper_post_render'), + '#field_name' => $field['field_name'], + '#type_name' => $node->type, + '#context' => $context, + ); + + $addition = array($field['field_name'] => $wrapper); + } + return $addition; + + case 'alter': + // Add back the formatted values in the 'view' element, + // so that tokens and node templates can use it. + // Note: Doing this in 'preprocess_node' breaks token integration. + + // The location of the field's rendered output depends on whether the + // field is in a fieldgroup or not. _content_field_invoke_default() takes + // care of pre-searching all fields and providing the wrappers. In case of + // direct calls, search the wrappers ourselves. + if (is_null($wrappers)) { + $wrappers = content_get_nested_elements($node->content, $field['field_name']); + } + + foreach ($wrappers as $wrapper) { + $element = $wrapper['field']; + // '#single' is not set if the field is hidden or inaccessible. + if (isset($element['#single'])) { + if (!empty($element['#single'])) { + // Single value formatter. + foreach (element_children($element['items']) as $delta) { + // '#children' is not set if the field is empty. + $items[$delta]['view'] = isset($element['items'][$delta]['#children']) ? $element['items'][$delta]['#children'] : ''; + } + } + elseif (isset($element['items']['#children'])) { + // Multiple values formatter. + $items[0]['view'] = $element['items']['#children']; + } + } + else { + // Hidden or inaccessible field. + $items[0]['view'] = ''; + } + } + break; + + case 'preprocess_node': + // Add $FIELD_NAME_rendered variables. + $addition = array($field['field_name'] .'_rendered' => ''); + + // The location of the field's rendered output depends on whether the + // field is in a fieldgroup or not. _content_field_invoke_default() takes + // care of pre-searching all fields and providing the wrappers. In case of + // direct calls, search the wrappers ourselves. + if (is_null($wrappers)) { + $wrappers = content_get_nested_elements($node->content, $field['field_name']); + } + foreach ($wrappers as $wrapper) { + // '#children' is not set if the field is empty. + $addition[$field['field_name'] .'_rendered'] .= isset($wrapper['#children']) ? $wrapper['#children'] : ''; + } + return $addition; + + case 'prepare translation': + $addition = array(); + if (isset($node->translation_source->$field['field_name'])) { + $addition[$field['field_name']] = $node->translation_source->$field['field_name']; + } + return $addition; + } +} + +/** + * Helper function to filter out items flagged for removal. + * + * On order to keep marker rows in the database, the function ensures + * that the right number of 'all columns NULL' values is kept. + * + * @param array $field + * @param array $items + * @return array + * returns filtered and adjusted item array + */ +function content_set_empty($field, $items) { + // Prepare an empty item. + $empty = array(); + foreach (array_keys($field['columns']) as $column) { + $empty[$column] = NULL; + } + + // Filter out items flagged for removal. + $filtered = array(); + $function = $field['module'] .'_content_is_empty'; + foreach ((array) $items as $delta => $item) { + if (empty($item['_remove'])) { + $filtered[] = ($function($item, $field) ? $empty : $item); + } + } + + // Make sure we store the right number of 'empty' values. + $pad = $field['multiple'] > 1 ? $field['multiple'] : 1; + $filtered = array_pad($filtered, $pad, $empty); + + return $filtered; +} + +/** + * Helper function to sort items in a field according to + * user drag-n-drop reordering. + */ +function _content_sort_items($field, $items) { + if ($field['multiple'] >= 1 && isset($items[0]['_weight'])) { + usort($items, '_content_sort_items_helper'); + foreach ($items as $delta => $item) { + if (is_array($item) && isset($item['_weight'])) { + unset($items[$delta]['_weight']); + } + } + } + return $items; +} + +/** + * Sort function for items order. + * (copied form element_sort(), which acts on #weight keys) + */ +function _content_sort_items_helper($a, $b) { + $a_weight = (is_array($a) && isset($a['_weight'])) ? $a['_weight'] : 0; + $b_weight = (is_array($b) && isset($b['_weight'])) ? $b['_weight'] : 0; + if ($a_weight == $b_weight) { + return 0; + } + return ($a_weight < $b_weight) ? -1 : 1; +} + +/** + * Same as above, using ['_weight']['#value'] + */ +function _content_sort_items_value_helper($a, $b) { + $a_weight = (is_array($a) && isset($a['_weight']['#value'])) ? $a['_weight']['#value'] : 0; + $b_weight = (is_array($b) && isset($b['_weight']['#value'])) ? $b['_weight']['#value'] : 0; + if ($a_weight == $b_weight) { + return 0; + } + return ($a_weight < $b_weight) ? -1 : 1; +} + +/** + * Handle storage ops for _content_field_invoke_default(). + */ +function content_storage($op, $node) { + // Don't try this before content module's update is run to add + // the active and module columns. + if (variable_get('content_schema_version', -1) < 6007) { + return FALSE; + } + + $type_name = $node->type; + $type = content_types($type_name); + + switch ($op) { + case 'load': + // OPTIMIZE: load all non multiple fields in a single JOIN query ? + // warning: 61-join limit in MySQL ? + $additions = array(); + // For each table used by this content type, + foreach ($type['tables'] as $table) { + $schema = drupal_get_schema($table); + // The per-type table might not have any fields actually stored in it. + if (!$schema['content fields']) { + continue; + } + $query = 'SELECT * FROM {'. $table .'} WHERE vid = %d'; + + // If we're loading a table for a multiple field, + // we fetch all rows (values) ordered by delta, + // else we only fetch one row. + $result = isset($schema['fields']['delta']) ? db_query($query .' ORDER BY delta', $node->vid) : db_query_range($query, $node->vid, 0, 1); + + // For each table row, populate the fields. + while ($row = db_fetch_array($result)) { + // For each field stored in the table, add the field item. + foreach ($schema['content fields'] as $field_name) { + $item = array(); + $field = content_fields($field_name, $type_name); + $db_info = content_database_info($field); + // For each column declared by the field, populate the item. + foreach ($db_info['columns'] as $column => $attributes) { + $item[$column] = $row[$attributes['column']]; + } + + // Add the item to the field values for the node. + if (!isset($additions[$field_name])) { + $additions[$field_name] = array(); + } + + // Preserve deltas when loading items from database. + if (isset($row['delta'])) { + // Make sure multiple value fields have consecutive deltas. + if ($row['delta'] > 0 && !isset($additions[$field_name][$row['delta']-1])) { + $empty = array(); + foreach (array_keys($db_info['columns']) as $column) { + $empty[$column] = NULL; + } + $next_delta = !empty($additions[$field_name]) ? (max(array_keys($additions[$field_name])) + 1) : 0; + for ($delta = $next_delta; $delta < $row['delta']; $delta++) { + if (!isset($additions[$field_name][$delta])) { + $additions[$field_name][$delta] = $empty; + } + } + } + $additions[$field_name][$row['delta']] = $item; + } + else { + $additions[$field_name][] = $item; + } + } + } + } + return $additions; + + case 'insert': + case 'update': + foreach ($type['tables'] as $table) { + $schema = drupal_get_schema($table); + $record = array(); + foreach ($schema['content fields'] as $field_name) { + if (isset($node->$field_name)) { + $field = content_fields($field_name, $type_name); + // Multiple fields need specific handling, we'll deal with them later on. + if ($field['multiple']) { + continue; + } + $db_info = content_database_info($field); + foreach ($db_info['columns'] as $column => $attributes) { + $record[$attributes['column']] = $node->{$field_name}[0][$column]; + } + } + } + // $record might be empty because + // - the table stores a multiple field : + // we do nothing, this is handled later on + // - this is the per-type table and no field is actually stored in it : + // we still store the nid and vid + if (count($record) || empty($schema['content fields'])) { + $record['nid'] = $node->nid; + $record['vid'] = $node->vid; + // Can't rely on the insert/update op of the node to decide if this + // is an insert or an update, a node or revision may have existed + // before any fields were created, so there may not be an entry here. + + // TODO - should we auto create an entry for all existing nodes when + // fields are added to content types -- either a NULL value + // or the default value? May need to offer the user an option of + // how to handle that. + if (db_result(db_query("SELECT COUNT(*) FROM {". $table ."} WHERE vid = %d", $node->vid))) { + content_write_record($table, $record, array('vid')); + } + else { + content_write_record($table, $record); + } + } + } + + // Handle multiple fields. + foreach ($type['fields'] as $field) { + if ($field['multiple'] && isset($node->$field['field_name'])) { + $db_info = content_database_info($field); + // Delete and insert, rather than update, in case a value was added. + if ($op == 'update') { + db_query('DELETE FROM {'. $db_info['table'] .'} WHERE vid = %d', $node->vid); + } + // Collect records for non-empty items. + $function = $field['module'] .'_content_is_empty'; + $records = array(); + foreach ($node->$field['field_name'] as $delta => $item) { + if (!$function($item, $field)) { + $record = array(); + foreach ($db_info['columns'] as $column => $attributes) { + $record[$attributes['column']] = $item[$column]; + } + $record['nid'] = $node->nid; + $record['vid'] = $node->vid; + $record['delta'] = $delta; + $records[] = $record; + } + } + // If there was no non-empty item, insert delta 0 with NULL values. + if (empty($records)) { + $record = array(); + foreach ($db_info['columns'] as $column => $attributes) { + $record[$attributes['column']] = NULL; + } + $record['nid'] = $node->nid; + $record['vid'] = $node->vid; + $record['delta'] = 0; + $records[] = $record; + } + // Insert the collected records for this field into database. + foreach ($records as $record) { + content_write_record($db_info['table'], $record); + } + } + } + break; + + case 'delete': + foreach ($type['tables'] as $table) { + db_query('DELETE FROM {'. $table .'} WHERE nid = %d', $node->nid); + } + break; + + case 'delete revision': + foreach ($type['tables'] as $table) { + db_query('DELETE FROM {'. $table .'} WHERE vid = %d', $node->vid); + } + break; + } +} + +/** + * Save a record to the database based upon the schema. + * + * Directly copied from core's drupal_write_record, which can't update a + * column to NULL. See http://drupal.org/node/227677 and + * http://drupal.org/node/226264 for more details about that problem. + * + * TODO - get rid of this function and change references back to + * drupal_write_record() if the patch gets into core. Will need a method + * of protecting people on older versions, though. + * + * Default values are filled in for missing items, and 'serial' (auto increment) + * types are filled in with IDs. + * + * @param $table + * The name of the table; this must exist in schema API. + * @param $object + * The object to write. This is a reference, as defaults according to + * the schema may be filled in on the object, as well as ID on the serial + * type(s). Both array an object types may be passed. + * @param $update + * If this is an update, specify the primary keys' field names. It is the + * caller's responsibility to know if a record for this object already + * exists in the database. If there is only 1 key, you may pass a simple string. + * @return + * Failure to write a record will return FALSE. Otherwise SAVED_NEW or + * SAVED_UPDATED is returned depending on the operation performed. The + * $object parameter contains values for any serial fields defined by + * the $table. For example, $object->nid will be populated after inserting + * a new node. + */ +function content_write_record($table, &$object, $update = array()) { + // Standardize $update to an array. + if (is_string($update)) { + $update = array($update); + } + + // Convert to an object if needed. + if (is_array($object)) { + $object = (object) $object; + $array = TRUE; + } + else { + $array = FALSE; + } + + $schema = drupal_get_schema($table); + if (empty($schema)) { + return FALSE; + } + + $fields = $defs = $values = $serials = $placeholders = array(); + + // Go through our schema, build SQL, and when inserting, fill in defaults for + // fields that are not set. + foreach ($schema['fields'] as $field => $info) { + // Special case -- skip serial types if we are updating. + if ($info['type'] == 'serial' && count($update)) { + continue; + } + + // For inserts, populate defaults from Schema if not already provided + if (!isset($object->$field) && !count($update) && isset($info['default'])) { + $object->$field = $info['default']; + } + + // Track serial fields so we can helpfully populate them after the query. + if ($info['type'] == 'serial') { + $serials[] = $field; + // Ignore values for serials when inserting data. Unsupported. + unset($object->$field); + } + + // Build arrays for the fields, placeholders, and values in our query. + if (isset($object->$field) || array_key_exists($field, $object)) { + $fields[] = $field; + if (isset($object->$field)) { + $placeholders[] = db_type_placeholder($info['type']); + + if (empty($info['serialize'])) { + $values[] = $object->$field; + } + else { + $values[] = serialize($object->$field); + } + } + else { + $placeholders[] = 'NULL'; + } + } + } + + // Build the SQL. + $query = ''; + if (!count($update)) { + $query = "INSERT INTO {". $table ."} (". implode(', ', $fields) .') VALUES ('. implode(', ', $placeholders) .')'; + $return = SAVED_NEW; + } + else { + $query = ''; + foreach ($fields as $id => $field) { + if ($query) { + $query .= ', '; + } + $query .= $field .' = '. $placeholders[$id]; + } + + foreach ($update as $key) { + $conditions[] = "$key = ". db_type_placeholder($schema['fields'][$key]['type']); + $values[] = $object->$key; + } + + $query = "UPDATE {". $table ."} SET $query WHERE ". implode(' AND ', $conditions); + $return = SAVED_UPDATED; + } + + // Execute the SQL. + if (db_query($query, $values)) { + if ($serials) { + // Get last insert ids and fill them in. + foreach ($serials as $field) { + $object->$field = db_last_insert_id($table, $field); + } + } + + // If we began with an array, convert back so we don't surprise the caller. + if ($array) { + $object = (array) $object; + } + + return $return; + } + + return FALSE; +} + +/** + * Invoke a field hook. + * + * For each operation, both this function and _content_field_invoke_default() are + * called so that the default database handling can occur. + */ +function _content_field_invoke($op, &$node, $teaser = NULL, $page = NULL) { + $type_name = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type); + $type = content_types($type_name); + $field_types = _content_field_types(); + + $return = array(); + foreach ($type['fields'] as $field) { + $items = isset($node->$field['field_name']) ? $node->$field['field_name'] : array(); + + // Make sure AHAH 'add more' button isn't sent to the fields for processing. + unset($items[$field['field_name'] .'_add_more']); + + $module = $field_types[$field['type']]['module']; + $function = $module .'_field'; + if (function_exists($function)) { + $result = $function($op, $node, $field, $items, $teaser, $page); + if (is_array($result)) { + $return = array_merge($return, $result); + } + else if (isset($result)) { + $return[] = $result; + } + } + // test for values in $items in case modules added items on insert + if (isset($node->$field['field_name']) || count($items)) { + $node->$field['field_name'] = $items; + } + } + return $return; +} + +/** + * Invoke content.module's version of a field hook. + */ +function _content_field_invoke_default($op, &$node, $teaser = NULL, $page = NULL) { + $type_name = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type); + $type = content_types($type_name); + $field_types = _content_field_types(); + + $return = array(); + // The operations involving database queries are better off handled by table + // rather than by field. + if (in_array($op, array('load', 'insert', 'update', 'delete', 'delete revision'))) { + return content_storage($op, $node); + } + else { + // The 'alter' and 'preprocess_node' ops need the location of each field + // within the $node->content array. Search once for all fields rather than + // letting content_field() search for each field individually. + if (in_array($op, array('alter', 'preprocess_node'))) { + $all_wrappers = content_get_nested_elements($node->content, array_keys($type['fields'])); + } + foreach ($type['fields'] as $field) { + $items = isset($node->$field['field_name']) ? $node->$field['field_name'] : array(); + $wrappers = isset($all_wrappers) ? $all_wrappers[$field['field_name']] : NULL; + $result = content_field($op, $node, $field, $items, $teaser, $page, $wrappers); + if (is_array($result)) { + $return = array_merge($return, $result); + } + else if (isset($result)) { + $return[] = $result; + } + if (isset($node->$field['field_name'])) { + $node->$field['field_name'] = $items; + } + } + } + return $return; +} + +/** + * Return a list of all content types. + * + * @param $content_type_name + * If set, return information on just this type. + * + * Do some type checking and set up empty arrays for missing + * info to avoid foreach errors elsewhere in the code. + */ +function content_types($type_name = NULL) { + // handle type name with either an underscore or a dash + $type_name = !empty($type_name) ? str_replace('-', '_', $type_name) : NULL; + + $info = _content_type_info(); + if (isset($info['content types'])) { + if (!isset($type_name)) { + return $info['content types']; + } + if (isset($info['content types'][$type_name])) { + return $info['content types'][$type_name]; + } + } + return array('tables' => array(), 'fields' => array(), 'extra' => array()); +} + +/** + * Return a list of all fields. + * + * @param $field_name + * If not empty, return information on just this field. + * @param $content_type_name + * If not empty, return information of the field within the context of this content + * type. + * + * Be sure to check empty() instead of isset() on field_name and + * content_type_name to avoid bad results when the value is set + * but empty, as sometimes happens in the formatter. + */ +function content_fields($field_name = NULL, $content_type_name = NULL) { + $info = _content_type_info(); + if (isset($info['fields'])) { + if (empty($field_name)) { + return $info['fields']; + } + if (isset($info['fields'][$field_name])) { + if (empty($content_type_name)) { + return $info['fields'][$field_name]; + } + if (isset($info['content types'][$content_type_name]['fields'][$field_name])) { + return $info['content types'][$content_type_name]['fields'][$field_name]; + } + } + } +} + +/** + * Return a list of field types. + */ +function _content_field_types() { + $info = _content_type_info(); + return isset($info['field types']) ? $info['field types'] : array(); +} + +/** + * Return a list of widget types. + */ +function _content_widget_types() { + $info = _content_type_info(); + return isset($info['widget types']) ? $info['widget types'] : array(); +} + +/** + * Return the formatter description corresponding to a formatter name, + * defaulting to 'default' if none is found. + */ +function _content_get_formatter($formatter_name, $field_type) { + $field_types = _content_field_types(); + $formatters = $field_types[$field_type]['formatters']; + + if (!isset($formatters[$formatter_name]) && $formatter_name != 'hidden') { + // This might happen when the selected formatter has been renamed in the + // module, or if the module has been disabled since then. + $formatter_name = 'default'; + } + + return isset($formatters[$formatter_name]) ? $formatters[$formatter_name] : FALSE; +} + +/** + * Collate all information on content types, fields, and related structures. + * + * @param $reset + * If TRUE, clear the cache and fetch the information from the database again. + */ +function _content_type_info($reset = FALSE) { + global $language; + static $info; + + if ($reset || !isset($info)) { + // Make sure this function doesn't run until the tables have been created, + // For instance: when first enabled and called from content_menu(), + // or when uninstalled and some subsequent field module uninstall + // attempts to refresh the data. + + // Don't try this before content module's update is run + // to add module and active columns to the table. + if (variable_get('content_schema_version', -1) < 6007) { + return array(); + } + + if (!$reset && $cached = cache_get('content_type_info:'. $language->language, content_cache_tablename())) { + $info = $cached->data; + } + else { + $info = array( + 'field types' => array(), + 'widget types' => array(), + 'fields' => array(), + 'content types' => array(), + ); + + // Populate field types. + foreach (module_list() as $module) { + $module_field_types = module_invoke($module, 'field_info'); + if ($module_field_types) { + foreach ($module_field_types as $name => $field_info) { + // Truncate names to match the value that is stored in the database. + $db_name = substr($name, 0, 32); + $info['field types'][$db_name] = $field_info; + $info['field types'][$db_name]['module'] = $module; + $info['field types'][$db_name]['formatters'] = array(); + } + } + } + + // Populate widget types and formatters for known field types. + foreach (module_list() as $module) { + if ($module_widgets = module_invoke($module, 'widget_info')) { + foreach ($module_widgets as $name => $widget_info) { + // Truncate names to match the value that is stored in the database. + $db_name = substr($name, 0, 32); + $info['widget types'][$db_name] = $widget_info; + $info['widget types'][$db_name]['module'] = $module; + // Replace field types with db_compatible version of known field types. + $info['widget types'][$db_name]['field types'] = array(); + foreach ($widget_info['field types'] as $field_type) { + $field_type_db_name = substr($field_type, 0, 32); + if (isset($info['field types'][$field_type_db_name])) { + $info['widget types'][$db_name]['field types'][] = $field_type_db_name; + } + } + } + } + + if ($module_formatters = module_invoke($module, 'field_formatter_info')) { + foreach ($module_formatters as $name => $formatter_info) { + foreach ($formatter_info['field types'] as $field_type) { + // Truncate names to match the value that is stored in the database. + $db_name = substr($field_type, 0, 32); + if (isset($info['field types'][$db_name])) { + $info['field types'][$db_name]['formatters'][$name] = $formatter_info; + $info['field types'][$db_name]['formatters'][$name]['module'] = $module; + } + } + } + } + } + + // Populate actual field instances. + module_load_include('inc', 'content', 'includes/content.crud'); + foreach (node_get_types('types', NULL, TRUE) as $type_name => $data) { + $type = (array) $data; + $type['url_str'] = str_replace('_', '-', $type['type']); + $type['fields'] = array(); + $type['tables'] = array(); + if ($fields = content_field_instance_read(array('type_name' => $type_name))) { + foreach ($fields as $field) { + $db_info = content_database_info($field); + $type['tables'][$db_info['table']] = $db_info['table']; + + // Allow external modules to translate field strings. + $field_strings = array( + 'widget_label' => $field['widget']['label'], + 'widget_description' => $field['widget']['description'], + ); + drupal_alter('content_field_strings', $field_strings, $field['type_name'], $field['field_name']); + $field['widget']['label'] = $field_strings['widget_label']; + $field['widget']['description'] = $field_strings['widget_description']; + + $type['fields'][$field['field_name']] = $field; + // This means that content_fields($field_name) (no type name) + // returns the last instance loaded. + $info['fields'][$field['field_name']] = $field; + } + // Make sure the per-type table is added, even if no field is actually + // stored in it. + $table = _content_tablename($type['type'], CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + $type['tables'][$table] = $table; + } + + // Gather information about non-CCK 'fields'. + $extra = module_invoke_all('content_extra_fields', $type_name); + drupal_alter('content_extra_fields', $extra, $type_name); + // Add saved weights. + foreach (variable_get('content_extra_weights_'. $type_name, array()) as $key => $value) { + // Some stored entries might not exist anymore, for instance if uploads + // have been disabled, or vocabularies removed... + if (isset($extra[$key])) { + $extra[$key]['weight'] = $value; + } + } + $type['extra'] = $extra; + + $info['content types'][$type_name] = $type; + } + + cache_set('content_type_info:'. $language->language, $info, content_cache_tablename()); + } + } + return $info; +} + +/** + * Implementation of hook_node_type() + * React to change in node types + */ +function content_node_type($op, $info) { + switch ($op) { + case 'insert': + module_load_include('inc', 'content', 'includes/content.crud'); + content_type_create($info); + break; + case 'update': + module_load_include('inc', 'content', 'includes/content.crud'); + content_type_update($info); + break; + case 'delete': + module_load_include('inc', 'content', 'includes/content.crud'); + content_type_delete($info); + break; + } +} + +/** + * Clear the cache of content_types; called in several places when content + * information is changed. + */ +function content_clear_type_cache($rebuild_schema = FALSE) { + cache_clear_all('*', content_cache_tablename(), TRUE); + _content_type_info(TRUE); + + // Refresh the schema to pick up new information. + if ($rebuild_schema) { + $schema = drupal_get_schema(NULL, TRUE); + } + + if (module_exists('views')) { + // Needed because this can be called from .install files + module_load_include('module', 'views'); + views_invalidate_cache(); + } +} + +/** + * Retrieve the database storage location(s) for a field. + * + * TODO: add a word about why it's not included in the global _content_type_info array. + * + * @param $field + * The field whose database information is requested. + * @return + * An array with the keys: + * "table": The name of the database table where the field data is stored. + * "columns": An array of columns stored for this field. Each is a collection + * of information returned from hook_field_settings('database columns'), + * with the addition of a "column" attribute which holds the name of the + * database column that stores the data. + */ +function content_database_info($field) { + $db_info = array(); + if ($field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD) { + $db_info['table'] = _content_tablename($field['field_name'], CONTENT_DB_STORAGE_PER_FIELD); + } + else { + $db_info['table'] = _content_tablename($field['type_name'], CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + } + + $db_info['columns'] = (array) $field['columns']; + // Generate column names for this field from generic column names. + foreach ($db_info['columns'] as $column_name => $attributes) { + $db_info['columns'][$column_name]['column'] = $field['field_name'] .'_'. $column_name; + } + + return $db_info; +} + +/** + * Helper function for identifying the storage type for a field. + */ +function content_storage_type($field) { + if ($field['multiple'] > 0) { + return CONTENT_DB_STORAGE_PER_FIELD; + } + else { + module_load_include('inc', 'content', 'includes/content.crud'); + $instances = content_field_instance_read(array('field_name' => $field['field_name'])); + if (count($instances) > 1) { + return CONTENT_DB_STORAGE_PER_FIELD; + } + } + return CONTENT_DB_STORAGE_PER_CONTENT_TYPE; +} + +/** + * Manipulate a 2D array to reverse rows and columns. + * + * The default data storage for fields is delta first, column names second. + * This is sometimes inconvenient for field modules, so this function can be + * used to present the data in an alternate format. + * + * @param $array + * The array to be transposed. It must be at least two-dimensional, and + * the subarrays must all have the same keys or behavior is undefined. + * @return + * The transposed array. + */ +function content_transpose_array_rows_cols($array) { + $result = array(); + if (is_array($array)) { + foreach ($array as $key1 => $value1) { + if (is_array($value1)) { + foreach ($value1 as $key2 => $value2) { + if (!isset($result[$key2])) { + $result[$key2] = array(); + } + $result[$key2][$key1] = $value2; + } + } + } + } + return $result; +} + +/** + * Helper function to flatten an array of allowed values. + * + * @param $array + * A single or multidimensional array. + * @return + * A flattened array. + */ +function content_array_flatten($array) { + $result = array(); + if (is_array($array)) { + foreach ($array as $key => $value) { + if (is_array($value)) { + $result += content_array_flatten($value); + } + else { + $result[$key] = $value; + } + } + } + return $result; +} + +/** + * Create an array of the allowed values for this field. + * + * Used by number and text fields, expects to find either + * PHP code that will return the correct value, or a string + * with keys and labels separated with '|' and with each + * new value on its own line. + * + * @param $field + * The field whose allowed values are requested. + * @param $flatten + * Optional. Use TRUE to return a flattened array (default). + * FALSE can be used to support optgroups for select widgets + * when allowed values list is generated using PHP code. + */ +function content_allowed_values($field, $flatten = TRUE) { + static $allowed_values; + + $cid = $field['field_name'] .':'. ($flatten ? '1' : '0'); + if (isset($allowed_values[$cid])) { + return $allowed_values[$cid]; + } + + $allowed_values[$cid] = array(); + + if (isset($field['allowed_values_php'])) { + ob_start(); + $result = eval($field['allowed_values_php']); + if (is_array($result)) { + if ($flatten) { + $result = content_array_flatten($result); + } + $allowed_values[$cid] = $result; + } + ob_end_clean(); + } + + if (empty($allowed_values[$cid]) && isset($field['allowed_values'])) { + $list = explode("\n", $field['allowed_values']); + $list = array_map('trim', $list); + $list = array_filter($list, 'strlen'); + foreach ($list as $opt) { + // Sanitize the user input with a permissive filter. + $opt = content_filter_xss($opt); + if (strpos($opt, '|') !== FALSE) { + list($key, $value) = explode('|', $opt); + $allowed_values[$cid][$key] = (isset($value) && $value !=='') ? $value : $key; + } + else { + $allowed_values[$cid][$opt] = $opt; + } + } + // Allow external modules to translate allowed values list. + drupal_alter('content_allowed_values', $allowed_values[$cid], $field); + } + return $allowed_values[$cid]; +} + +/** + * Filter out HTML from allowed values array while leaving entities unencoded. + * + * @see content_allowed_values() + * @see optionwidgets_select_process() + * @see content_handler_filter_many_to_one::allowed_values() + */ +function content_allowed_values_filter_html(&$options) { + foreach ($options as $key => $opt) { + if (is_array($opt)) { + content_allowed_values_filter_html($options[$key]); + } + else { + $options[$key] = html_entity_decode(strip_tags($opt), ENT_QUOTES); + } + } +} + +/** + * Like filter_xss_admin(), but with a shorter list of allowed tags. + * + * Used for items entered by administrators, like field descriptions, + * allowed values, where some (mainly inline) mark-up may be desired + * (so check_plain() is not acceptable). + */ +function content_filter_xss($string) { + return filter_xss($string, _content_filter_xss_allowed_tags()); +} + +/** + * List of tags allowed by content_filter_xss(). + */ +function _content_filter_xss_allowed_tags() { + return array('a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins', 'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img'); +} + +/** + * Human-readable list of allowed tags, for display in help texts. + */ +function _content_filter_xss_display_allowed_tags() { + return '<'. implode('> <', _content_filter_xss_allowed_tags()) .'>'; +} + +/** + * Format a field item for display. + * + * Used to display a field's values outside the context of the $node, as + * when fields are displayed in Views, or to display a field in a template + * using a different formatter than the one set up on the Display Fields tab + * for the node's context. + * + * @param $field + * Either a field array or the name of the field. + * @param $item + * The field item(s) to be formatted (such as $node->field_foo[0], + * or $node->field_foo if the formatter handles multiple values itself) + * @param $formatter_name + * The name of the formatter to use. + * @param $node + * Optionally, the containing node object for context purposes and + * field-instance options. + * + * @return + * A string containing the contents of the field item(s) sanitized for display. + * It will have been passed through the necessary check_plain() or check_markup() + * functions as necessary. + */ +function content_format($field, $item, $formatter_name = 'default', $node = NULL) { + if (!is_array($field)) { + $field = content_fields($field); + } + + if (content_access('view', $field, NULL, $node) && $formatter = _content_get_formatter($formatter_name, $field['type'])) { + $theme = $formatter['module'] .'_formatter_'. $formatter_name; + + $element = array( + '#theme' => $theme, + '#field_name' => $field['field_name'], + '#type_name' => isset($node->type) ? $node->type :'', + '#formatter' => $formatter_name, + '#node' => $node, + '#delta' => isset($item['#delta']) ? $item['#delta'] : NULL, + ); + + if (content_handle('formatter', 'multiple values', $formatter) == CONTENT_HANDLE_CORE) { + // Single value formatter. + + // hook_field('sanitize') expects an array of items, so we build one. + $items = array($item); + $function = $field['module'] .'_field'; + if (function_exists($function)) { + $function('sanitize', $node, $field, $items, FALSE, FALSE); + } + + $element['#item'] = $items[0]; + } + else { + // Multiple values formatter. + $items = $item; + $function = $field['module'] .'_field'; + if (function_exists($function)) { + $function('sanitize', $node, $field, $items, FALSE, FALSE); + } + + foreach ($items as $delta => $item) { + $element[$delta] = array( + '#item' => $item, + '#weight' => $delta, + ); + } + } + + return theme($theme, $element); + } +} + +/** + * Registry of available node build modes. + * + * @param $selector + * Determines what information should be returned. + * @return + * Depending on the value of the $selector parameter: + * - NULL: a flat list of all available build modes. + * The other two options are mainly used internally by CCK's UI: + * - '_tabs': the list of tabs to be shown on the 'Display fields' screens. + * - a string tab id: the build modes in this tab. + */ +function content_build_modes($selector = NULL) { + static $info; + + if (!isset($info)) { + $data = array(); + foreach (module_implements('content_build_modes') as $module) { + $function = $module .'_content_build_modes'; + $data = array_merge($data, (array) $function()); + } + $flat = array(); + foreach ($data as $tab) { + // Use the + operator to preserve numeric indexes (core build modes). + $flat += (array) $tab['build modes']; + } + $info = array('tabs' => $data, 'build modes' => $flat); + } + + if ($selector === '_tabs') { + return $info['tabs']; + } + elseif (isset($selector) && isset($info['tabs'][$selector])) { + return isset($info['tabs'][$selector]) ? $info['tabs'][$selector]['build modes'] : array(); + } + else { + return $info['build modes']; + } +} + +/** + * Implementations of hook_content_build_modes + * on behalf of core modules. + * + * @return + * An array describing the build modes used by the module. + * They are grouped by secondary tabs on CCK's 'Display fields' screens. + * + * Expected format: + * array( + * // The first level keys (tab1_url, tab2_url) will be used to generate + * // the url of the tab: admin/content/node-type/[type_name]/display/[tab1_url] + * // A module can add its render modes to a tab defined by another module. + * // In this case, there's no need to provide a 'title' for this tab. + * 'tab1_url' => array( + * 'title' => t('The human-readable title of the tab'), + * 'build modes' => array( + * // The keys of the 'context' array are the values used in $node->build_mode. + * 'mymodule_mode1' => array( + * 'title' => t('The human-readable name of the build mode'), + * // The 'views style' property determines if the render mode should be + * // available as an option in Views' 'node' row style (not implemented yet). + * 'views style' => TRUE, + * ), + * 'mymodule_mode2' => array( + * 'title' => t('Mode 2'), + * 'views style' => TRUE, + * ), + * ), + * ), + * 'tab2_url' => array( + * // ... + * ), + * ); + */ +function node_content_build_modes() { + return array( + 'basic' => array( + 'title' => t('Basic'), + 'build modes' => array( + 'teaser' => array( + 'title' => t('Teaser'), + 'views style' => TRUE, + ), + 'full' => array( + 'title' => t('Full node'), + 'views style' => TRUE, + ), + ), + ), + 'rss' => array( + 'title' => t('RSS'), + 'build modes' => array( + NODE_BUILD_RSS => array( + 'title' => t('RSS'), + 'views style' => FALSE, + ), + ), + ), + ); +} +function search_content_build_modes() { + return array( + 'search' => array( + 'title' => t('Search'), + 'build modes' => array( + NODE_BUILD_SEARCH_INDEX => array( + 'title' => t('Search Index'), + 'views style' => FALSE, + ), + NODE_BUILD_SEARCH_RESULT => array( + 'title' => t('Search Result'), + 'views style' => FALSE, + ), + ), + ), + ); +} +function book_content_build_modes() { + return array( + 'print' => array( + 'title' => t('Print'), + 'build modes' => array( + NODE_BUILD_PRINT => array( + 'title' => t('Print'), + 'views style' => TRUE, + ), + ), + ), + ); +} + +/** + * Generate a table name for a field or a content type. + * + * @param $name + * The name of the content type or content field + * @param $storage + * CONTENT_DB_STORAGE_PER_FIELD or CONTENT_DB_STORAGE_PER_CONTENT_TYPE + * @return + * A string containing the generated name for the database table + */ +function _content_tablename($name, $storage, $version = NULL) { + if (is_null($version)) { + $version = variable_get('content_schema_version', 0); + } + + if ($version < 1003) { + $version = 0; + } + else { + $version = 1003; + } + + $name = str_replace('-', '_', $name); + switch ("$version-$storage") { + case '0-'. CONTENT_DB_STORAGE_PER_CONTENT_TYPE : + return "node_$name"; + case '0-'. CONTENT_DB_STORAGE_PER_FIELD : + return "node_data_$name"; + case '1003-'. CONTENT_DB_STORAGE_PER_CONTENT_TYPE : + return "content_type_$name"; + case '1003-'. CONTENT_DB_STORAGE_PER_FIELD : + return "content_$name"; + } +} + +/** + * Generate table name for the content field table. + * + * Needed because the table name changes depending on version. + * Using 'content_node_field' instead of 'content_field' + * to avoid conflicts with field tables that will be prefixed + * with 'content_field'. + */ +function content_field_tablename($version = NULL) { + if (is_null($version)) { + $version = variable_get('content_schema_version', 0); + } + return $version < 6001 ? 'node_field' : 'content_node_field'; +} + +/** + * Generate table name for the content field instance table. + * + * Needed because the table name changes depending on version. + */ +function content_instance_tablename($version = NULL) { + if (is_null($version)) { + $version = variable_get('content_schema_version', 0); + } + return $version < 6001 ? 'node_field_instance' : 'content_node_field_instance'; +} + +/** + * Generate table name for the content cache table. + * + * Needed because the table name changes depending on version. Because of + * a new database column, the content_cache table will be unusable until + * update 6000 runs, so the cache table will be used instead. + */ +function content_cache_tablename() { + if (variable_get('content_schema_version', -1) < 6000) { + return 'cache'; + } + else { + return 'cache_content'; + } +} + +/** + * A basic schema used by all field and type tables. + * + * This will only add the columns relevant for the specified field. + * Leave $field['columns'] empty to get only the base schema, + * otherwise the function will return the whole thing. + */ +function content_table_schema($field = NULL) { + $schema = array( + 'fields' => array( + 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0) + ), + 'primary key' => array('vid'), + 'indexes' => array( + 'nid' => array('nid'), + ), + ); + + // Add delta column if needed. + if (!empty($field['multiple'])) { + $schema['fields']['delta'] = array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0); + $schema['primary key'][] = 'delta'; + } + $schema['content fields'] = array(); + + // Add field columns column if needed. + // This function is called from install files where it is not safe + // to use content_fields() or content_database_info(), so we + // just used the column values stored in the $field. + // We also need the schema to include fields from disabled modules + // or there will be no way to delete those fields. + + if (!empty($field['columns'])) { + foreach ($field['columns'] as $column => $attributes) { + $column_name = $field['field_name'] .'_'. $column; + if (isset($attributes['index']) && $attributes['index']) { + $schema['indexes'][$column_name] = array($column_name); + unset($attributes['index']); + } + unset($attributes['column']); + unset($attributes['sortable']); + $schema['fields'][$column_name] = $attributes; + } + $schema['content fields'][] = $field['field_name']; + } + return $schema; +} + +/** + * Checks if an index exists. + * + * @todo: May we remove this funcion when implemented by Drupal core itself? + * @link http://drupal.org/node/360854 + * @link http://dev.mysql.com/doc/refman/5.0/en/extended-show.html + * + * @param $table + * Name of the table. + * @param $name + * Name of the index. + * @return + * TRUE if the table exists. Otherwise FALSE. + */ +function content_db_index_exists($table, $name) { + global $db_type; + if ($db_type == 'mysql' || $db_type == 'mysqli') { + if (version_compare(db_version(), '5.0.3') < 0) { + // Earlier versions of MySQL don't support a WHERE clause for SHOW. + $result = db_query('SHOW INDEX FROM {'. $table .'}'); + while ($row = db_fetch_array($result)) { + if ($row['Key_name'] == $name) { + return TRUE; + } + } + return FALSE; + } + return (bool)db_result(db_query("SHOW INDEX FROM {". $table ."} WHERE key_name = '$name'")); + } + elseif ($db_type == 'pgsql') { + // Note that the index names in Schema API for PostgreSQL are prefixed by + // the table name and suffixed by '_idx'. + return (bool)db_result(db_query("SELECT COUNT(indexname) FROM pg_indexes WHERE indexname = '{". $table ."}_{$name}_idx'")); + } + return FALSE; +} + +/** + * Helper function for determining the behavior of a field or a widget + * with respect to a given operation. (currently used for field 'view', + * and widget 'default values' and 'multiple values') + * + * @param $entity + * 'field' or 'widget' + * @param $op + * the name of the operation ('view', 'default value'...) + * @param $field + * The field array, including widget info. + * @return + * CONTENT_CALLBACK_NONE - do nothing for this operation + * CONTENT_CALLBACK_CUSTOM - use the module's callback function. + * CONTENT_CALLBACK_DEFAULT - use content module default behavior + * + */ +function content_callback($entity, $op, $field) { + switch ($entity) { + case 'field': + $info = module_invoke($field['module'], "field_info"); + return isset($info[$field['type']]['callbacks'][$op]) ? $info[$field['type']]['callbacks'][$op] : CONTENT_CALLBACK_DEFAULT; + + case 'widget': + $info = module_invoke($field['widget']['module'], "widget_info"); + return isset($info[$field['widget']['type']]['callbacks'][$op]) ? $info[$field['widget']['type']]['callbacks'][$op] : CONTENT_CALLBACK_DEFAULT; + } +} + +/** + * Helper function for determining the handling of a field, widget or + * formatter with respect to a given operation. + * + * Currently used for widgets and formatters 'multiple values'. + * + * @param $entity + * 'field', 'widget' or 'formatter' + * @param $op + * the name of the operation ('default values'...) + * @param $object + * - if $entity is 'field' or 'widget': the field array, + * including widget info. + * - if $entity is 'formater': the formatter array. + * @return + * CONTENT_HANDLE_CORE - the content module handles this operation. + * CONTENT_HANDLE_MODULE - the implementing module handles this operation. + */ +function content_handle($entity, $op, $object) { + switch ($entity) { + case 'field': + $info = module_invoke($object['module'], "field_info"); + return isset($info[$object['type']][$op]) ? $info[$object['type']][$op] : CONTENT_HANDLE_CORE; + + case 'widget': + $info = module_invoke($object['widget']['module'], "widget_info"); + return isset($info[$object['widget']['type']][$op]) ? $info[$object['widget']['type']][$op] : CONTENT_HANDLE_CORE; + + case 'formatter': + // Much simpler, formatters arrays *are* the 'formatter_info' itself. + // We let content_handle deal with them only for code consistency. + return isset($object[$op]) ? $object[$op] : CONTENT_HANDLE_CORE; + } +} + +/** + * Helper function to return the correct default value for a field. + * + * @param $node + * The node. + * @param $field + * The field array. + * @param $items + * The value of the field in the node. + * @return + * The default value for that field. + */ +function content_default_value(&$form, &$form_state, $field, $delta) { + $widget_types = _content_widget_types(); + $module = $widget_types[$field['widget']['type']]['module']; + + $default_value = array(); + if (!empty($field['widget']['default_value_php'])) { + ob_start(); + $result = eval($field['widget']['default_value_php']); + ob_end_clean(); + if (is_array($result)) { + $default_value = $result; + } + } + elseif (!empty($field['widget']['default_value'])) { + $default_value = $field['widget']['default_value']; + } + return (array) $default_value; +} + +/** + * Determine whether the user has access to a given field. + * + * @param $op + * The operation to be performed. Possible values: + * - "edit" + * - "view" + * @param $field + * The field on which the operation is to be performed. + * @param $account + * (optional) The account to check, if not given use currently logged in user. + * @param $node + * (optional) The node on which the operation is to be performed. + * @return + * TRUE if the operation is allowed; + * FALSE if the operation is denied. + */ +function content_access($op, $field, $account = NULL, $node = NULL) { + global $user; + + if (is_null($account)) { + $account = $user; + } + // Check for valid field data. + if (!isset($field['field_name'])) { + return FALSE; + } + $access = module_invoke_all('field_access', $op, $field, $account, $node); + foreach ($access as $value) { + if ($value === FALSE) { + return FALSE; + } + } + return TRUE; +} + + /** + * Hide specified fields from the $content variable in node templates. + */ +function content_field_wrapper_post_render($content, $element) { + $field = content_fields($element['#field_name'], $element['#type_name']); + if (theme('content_exclude', $content, $field, $element['#context'])) { + return ''; + } + return $content; +} + + +/** + * 'Theme' function for a field's addition to $content. + * + * Adapts the all-inclusive $content variable in node templates to allow + * some field content to be excluded. This is a theme function, so it can be + * overridden in different themes to produce different results. + * + * The html for individual fields and groups are available in the + * $FIELD_NAME_rendered and $GROUP_NAME_rendered variables. + * + * This allows more flexibility in node templates : you can use custom markup + * around a few specific fields, and print the rest of the node with $content. + * + * @param $content + * The themed content for this field or group. + * + * @param $object + * The field or group array for this item. + * $object['#type_name'] holds the content type. + * $object['#field_name'] holds the field name (if a field). + * $object['#group_name'] holds the group name (if a group). + * $object['display_settings'] holds the display settings + * for all contexts, in an array like: + * $object['display_settings'] => array( + * 'full' => array( + * 'format' => 'default', + * 'exclude' => 0, + * ), + * 'teaser' => array( + * 'format' => 'default', + * 'exclude' => 1, + * ), + * ); + * + * @param $context + * The context for which the node is being rendered. + * Can be one of the following values : + * - 'teaser' + * - 'full' + * - NODE_BUILD_SEARCH_INDEX + * - NODE_BUILD_SEARCH_RESULT + * - NODE_BUILD_RSS + * - NODE_BUILD_PRINT + * - ... any other custom build mode exposed by 3rd party modules using + * hook_content_build_modes(). + * + * @return + * Whether or not content is to be added to $content in this context. + * Uses the value of the 'Exclude' checkbox for this field + * as set on the Manage fields screen. + */ +function theme_content_exclude($content, $object, $context) { + // The field may be missing info for $contexts added by modules + // enabled after the field was last edited. + if (empty($object['display_settings']) + || empty($object['display_settings'][$context]) + || !is_array($object['display_settings'][$context]) + || empty($object['display_settings'][$context]['exclude'])) { + return FALSE; + } + else { + return TRUE; + } +} + +/** + * Theme preprocess function for field.tpl.php. + * + * The $variables array contains the following arguments: + * - $node + * - $field + * - $items + * - $teaser + * - $page + * + * @see field.tpl.php + * + * TODO : this should live in theme/theme.inc, but then the preprocessor + * doesn't get called when the theme overrides the template. Bug in theme layer ? + */ +function template_preprocess_content_field(&$variables) { + $element = $variables['element']; + $field = content_fields($element['#field_name'], $element['#node']->type); + + $variables['node'] = $element['#node']; + $variables['field'] = $field; + $variables['items'] = array(); + + if ($element['#single']) { + // Single value formatter. + foreach (element_children($element['items']) as $delta) { + $variables['items'][$delta] = $element['items'][$delta]['#item']; + // Use isset() to avoid undefined index message on #children when field values are empty. + $variables['items'][$delta]['view'] = isset($element['items'][$delta]['#children']) ? $element['items'][$delta]['#children'] : ''; + } + } + else { + // Multiple values formatter. + // We display the 'all items' output as $items[0], as if it was the + // output of a single valued field. + // Raw values are still exposed for all items. + foreach (element_children($element['items']) as $delta) { + $variables['items'][$delta] = $element['items'][$delta]['#item']; + } + $variables['items'][0]['view'] = $element['items']['#children']; + } + + $variables['teaser'] = $element['#teaser']; + $variables['page'] = $element['#page']; + + $field_empty = TRUE; + + foreach ($variables['items'] as $delta => $item) { + if (!isset($item['view']) || (empty($item['view']) && (string)$item['view'] !== '0')) { + $variables['items'][$delta]['empty'] = TRUE; + } + else { + $field_empty = FALSE; + $variables['items'][$delta]['empty'] = FALSE; + } + } + + $additions = array( + 'field_type' => $field['type'], + 'field_name' => $field['field_name'], + 'field_type_css' => strtr($field['type'], '_', '-'), + 'field_name_css' => strtr($field['field_name'], '_', '-'), + 'label' => check_plain(t($field['widget']['label'])), + 'label_display' => $element['#label_display'], + 'field_empty' => $field_empty, + 'template_files' => array( + 'content-field', + 'content-field-'. $element['#field_name'], + 'content-field-'. $element['#node']->type, + 'content-field-'. $element['#field_name'] .'-'. $element['#node']->type, + ), + ); + $variables = array_merge($variables, $additions); +} + +/** + * Theme preprocess function for node. + * + * - Adds $FIELD_NAME_rendered variables + * containing the themed output for the whole field. + * - Adds the formatted values in the 'view' key of the items. + */ +function content_preprocess_node(&$vars) { + $additions = _content_field_invoke_default('preprocess_node', $vars['node']); + $vars = array_merge($vars, $additions); +} + +/** + * Debugging using hook_content_fieldapi. + * + * @TODO remove later + * + * @param $op + * @param $field + */ +function content_content_fieldapi($op, $field) { + if (module_exists('devel')) { + //dsm($op); + //dsm($field); + } +} + +/** + * Implementation of hook_content_extra_fields. + * + * Informations for non-CCK 'node fields' defined in core. + */ +function content_content_extra_fields($type_name) { + $type = node_get_types('type', $type_name); + $extra = array(); + + if ($type->has_title) { + $extra['title'] = array( + 'label' => $type->title_label, + 'description' => t('Node module form.'), + 'weight' => -5 + ); + } + if ($type->has_body) { + $extra['body_field'] = array( + 'label' => $type->body_label, + 'description' => t('Node module form.'), + 'weight' => 0, + 'view' => 'body' + ); + } + $extra['revision_information'] = array( + 'label' => t('Revision information'), + 'description' => t('Node module form.'), + 'weight' => 20 + ); + $extra['author'] = array( + 'label' => t('Authoring information'), + 'description' => t('Node module form.'), + 'weight' => 20, + ); + $extra['options'] = array( + 'label' => t('Publishing options'), + 'description' => t('Node module form.'), + 'weight' => 25, + ); + if (module_exists('comment')) { + $extra['comment_settings'] = array( + 'label' => t('Comment settings'), + 'description' => t('Comment module form.'), + 'weight' => 30 + ); + } + if (module_exists('locale') && variable_get("language_content_type_$type_name", 0)) { + $extra['language'] = array( + 'label' => t('Language'), + 'description' => t('Locale module form.'), + 'weight' => 0 + ); + } + if (module_exists('translation') && translation_supported_type($type_name)) { + $extra['translation'] = array( + 'label' => t('Translation settings'), + 'description' => t('Translation module form.'), + 'weight' => 30 + ); + } + if (module_exists('menu')) { + $extra['menu'] = array( + 'label' => t('Menu settings'), + 'description' => t('Menu module form.'), + 'weight' => -2 + ); + } + if (module_exists('taxonomy') && taxonomy_get_vocabularies($type_name)) { + $extra['taxonomy'] = array( + 'label' => t('Taxonomy'), + 'description' => t('Taxonomy module form.'), + 'weight' => -3 + ); + } + if (module_exists('book')) { + $extra['book'] = array( + 'label' => t('Book'), + 'description' => t('Book module form.'), + 'weight' => 10 + ); + } + if (module_exists('path')) { + $extra['path'] = array( + 'label' => t('Path settings'), + 'description' => t('Path module form.'), + 'weight' => 30 + ); + } + if ($type_name == 'poll' && module_exists('poll')) { + $extra['title'] = array( + 'label' => t('Poll title'), + 'description' => t('Poll module title.'), + 'weight' => -5 + ); + $extra['choice_wrapper'] = array( + 'label' => t('Poll choices'), + 'description' => t('Poll module choices.'), + 'weight' => -4 + ); + $extra['settings'] = array( + 'label' => t('Poll settings'), + 'description' => t('Poll module settings.'), + 'weight' => -3 + ); + } + if (module_exists('upload') && variable_get("upload_$type_name", TRUE)) { + $extra['attachments'] = array( + 'label' => t('File attachments'), + 'description' => t('Upload module form.'), + 'weight' => 30, + 'view' => 'files' + ); + } + + return $extra; +} + +/** + * Retrieve the user-defined weight for non-CCK node 'fields'. + * + * CCK's 'Manage fields' page lets users reorder node fields, including non-CCK + * items (body, taxonomy, other hook_nodeapi-added elements by contrib modules...). + * Contrib modules that want to have their 'fields' supported need to expose + * them with hook_content_extra_fields, and use this function to retrieve the + * user-defined weight. + * + * @param $type_name + * The content type name. + * @param $pseudo_field_name + * The name of the 'field'. + * @return + * The weight for the 'field', respecting the user settings stored + * by content.module. + */ +function content_extra_field_weight($type_name, $pseudo_field_name) { + $type = content_types($type_name); + + // If we don't have the requested item, this may be because the cached + // information for 'extra' fields hasn't been refreshed yet. + if (!isset($type['extra'][$pseudo_field_name])) { + content_clear_type_cache(); + $type = content_types($type_name); + } + + if (isset($type['extra'][$pseudo_field_name])) { + return $type['extra'][$pseudo_field_name]['weight']; + } +} + +/** + * Find max delta value actually in use for a field. + * + * Helper function to do things like tell when we should prevent a + * change in multiple value settings that would result in data loss, + * or know if content actually exists for a field. + * + * @param $field_name + * The field name to examine. + * @param $type_name + * If provided, search only for existing data in that type, + * otherwise search for all instances of field data in all types. + * @return + * NULL if field is not in use, or the maximum delta value in use. + * + * TODO + * Go back to the field settings validation and use this function + * to prevent (or confirm) changes in multiple values that + * would destroy data. + * + * Fields with only NULL data will show up as being in use. + * Do we want to eliminate them from the results? + */ +function content_max_delta($field_name, $type_name = NULL) { + $fields = content_fields(); + $field = $fields[$field_name]; + + // Non-multiple value fields don't use the delta column, + // but could exist in multiple databases. If any value + // exists in any examined table, the max delta will be zero. + if (empty($field['multiple'])) { + $content_types = content_types(); + foreach ($content_types as $content_type) { + if (empty($type_name) || $content_type['type'] == $type_name) { + foreach ($content_type['fields'] as $field) { + $db_info = content_database_info($field); + if (db_result(db_query("SELECT COUNT(*) FROM {". $db_info['table'] ."}")) >= 1) { + return 0; + } + } + } + } + } + // Multiple value fields always share the same table and use the delta. + // If we want to find delta values for a particular type, we join + // in the node table to limit the type. + else { + $db_info = content_database_info($field); + if (!empty($type_name)) { + $delta = db_result(db_query("SELECT MAX(delta) FROM {". $db_info['table'] ."} f LEFT JOIN {node} n ON f.vid = n.vid WHERE n.type = '%s'", $type_name)); + } + else { + $delta = db_result(db_query("SELECT MAX(delta) FROM {". $db_info['table'] ."}")); + } + if ($delta >= 0) { + return $delta; + } + } + // If we got this far, there is no data for this field. + return NULL; +} + +/** + * Helper function to identify inactive fields. + */ +function content_inactive_fields($type_name = NULL) { + module_load_include('inc', 'content', 'includes/content.crud'); + if (!empty($type_name)) { + $param = array('type_name' => $type_name); + $inactive = array($type_name => array()); + } + else { + $param = array(); + $inactive = array(); + } + $all = content_field_instance_read($param, TRUE); + $active = array_keys(content_fields()); + foreach ($all as $field) { + if (!in_array($field['field_name'], $active)) { + $inactive[$field['type_name']][$field['field_name']] = content_field_instance_expand($field); + } + } + if (!empty($type_name)) { + return $inactive[$type_name]; + } + return $inactive; +} + +/** + * Helper function to identify inactive instances. + * This will be the same results as content_inactive_fields(), + * EXCEPT that his function will return inactive instances even + * if the fields have other (shared) instances that are still active. + */ +function content_inactive_instances($type_name = NULL) { + module_load_include('inc', 'content', 'includes/content.crud'); + if (!empty($type_name)) { + $param = array('type_name' => $type_name); + $inactive = array($type_name => array()); + } + else { + $param = array(); + $inactive = array(); + } + $all = content_field_instance_read($param, TRUE); + foreach ($all as $field) { + $inactive[$field['type_name']][$field['field_name']] = content_field_instance_expand($field); + } + if (!empty($type_name)) { + return $inactive[$type_name]; + } + return $inactive; +} + +/** + * Return the nested form elements for a field by name. + * This can be used either to retrieve the entire sub-element + * for a field by name, no matter how deeply nested it is within + * fieldgroups or multigroups, or to find the multiple value + * sub-elements within a field element by name (i.e. 'value' or + * 'nid'). You can also use this function to see if an item exists + * in a form (the return will be an empty array if it does not exist). + * + * A field/group will generally only exist once in a form but the + * function can also be used to locate all the 'value' elements + * within a multiple value field if you pass the multiple value field + * as the $form argument; + * + * For example, for a field named field_custom, the following will + * pluck out the form element for this field from the node form, + * no matter how deeply it is nested within fieldgroups or fieldsets: + * + * $element = array_shift(content_get_nested_elements($node_form, 'field_custom')); + * + * You can prefix the function with '&' to retrieve the element by + * reference to alter it directly: + * + * $elements = &content_get_nested_elements($form, 'field_custom'); + * foreach ($elements as $element) { + * $element['#after_build'][] = 'my_field_afterbuild'; + * } + * + * During the #after_build you could then do something like the + * following to alter each individual part of a multiple value field: + * + * $sub_elements = &content_get_nested_elements($element, 'value', 'all'); + * foreach ($sub_elements as $sub_element) { + * $sub_element['#element_validate'][] = 'custom_validation'; + * } + * + * @param $form + * The form array to search. + * @param $field_name + * The key of the form elements to return (can be a field name, a group name, + * or a sub-field), or an array of names. + * @return + * An array of all matching form elements, returned by reference. + */ +function &content_get_nested_elements(&$form, $field_name) { + // Handle single-field and multi-fields calls. + if (is_array($field_name)) { + $single = FALSE; + $names = drupal_map_assoc($field_name); + } + else { + $single = TRUE; + $names = array($field_name => $field_name); + } + + // Initialize the results array (array_fill_keys() is only available in PHP + // 5.2+). + $results = array(); + foreach ($names as $name) { + $results[$name] = array(); + } + + // Search for the elements within the $form. Start at the top-level, + // sub-elements will be added as we walk the form. + $queue = array(&$form); + while ($current = &_content_shift($queue)) { + foreach (element_children($current) as $key) { + // If this is one of the elements we are looking for, push it in the + // results. + if (isset($names[$key])) { + $results[$key][] = &$current[$key]; + } + // Else push the element in our search queue. + elseif (is_array($current[$key]) && !empty($current[$key])) { + $queue[] = &$current[$key]; + } + } + } + + return $single ? $results[$field_name] : $results; +} + +/** + * Returns the first element of an array by reference. + * + * This is conceptually equivalent to PHP built-in array_shift, but the + * element is returned by reference to allow the caller to modify it. + * + * @param $array + * The array whose first element's should be returned. + * @return mixed + * A reference to the variable that was the first array element. + */ +function &_content_shift(&$array) { + reset($array); + $k = key($array); + $v = &$array[$k]; + unset($array[$k]); + return $v; +} + +/** + * Helper function to set a value in a form element, + * no matter how deeply nested it is in the form. + * + * @param $form + * The form array to search. + * @param $field_name + * The name or key of the form elements to return. Can be a field name, a group name, or a sub-field. + * @param $value + * The value to set the element to. Should be an array that matches the part of the form that is being altered. + * @return + * TRUE or FALSE, depending on whether the element was discovered and set. + */ +function content_set_nested_elements(&$form, $field_name, $value) { + $success = FALSE; + $elements = &content_get_nested_elements($form, $field_name); + if (!empty($elements)) { + foreach ($elements as &$element) { + $element = $value; + $success = TRUE; + } + } + return $success; +} diff --git a/sites/all/modules/cck/help/add-existing-field.html b/sites/all/modules/cck/help/add-existing-field.html new file mode 100644 index 0000000..fbfba0e --- /dev/null +++ b/sites/all/modules/cck/help/add-existing-field.html @@ -0,0 +1,46 @@ +

Using a field across several content type can be handy if a piece of data +is relevant for several content types. A typical use case is a 'Phone number' +field, used in both 'Restaurant' and 'Hotel' content types, even if hotels +and restaurants are different enough to deserve their own specific set of +fields and thus their own dedicated content types.

+ +

When a field has been added to more than one content type, we also say it is +"shared", and that it as "several instances".

+ +

At the bottom of the Manage fields page for a content type, +you'll find this:

+ + + +

In order to add a new instance of an existing field to a content type, you +need to provide the following information:

+
+
Label:
+
+ A human-readable name for the field. It will be used in input forms and + on displayed content.
+ All characters are allowed, including spaces, accentuated or non-european + characters.
+
+ +
Field:
+
+ The field to be shared.
+ A field cannot appear more than once in each content type. Thus, only + fields that are not already present in the current content type will be + proposed as "shareable". If none, the Add existing field + option is not available on the Manage fields page for + this content type.
+ Selecting a field automatically populates the Label and + Widget values with the ones used by the previous field + instance, but you can change them if needed before submitting the form. +
+ +
Widget:
+
+ The form element that will be used to input data for this field on + content forms : text input, select list, etc...
+ Each field type has its own list of available widgets. When selecting a + field to share, the list of widgets you can select is automatically updated. +
+
\ No newline at end of file diff --git a/sites/all/modules/cck/help/add-existing-field.png b/sites/all/modules/cck/help/add-existing-field.png new file mode 100644 index 0000000..4b12665 Binary files /dev/null and b/sites/all/modules/cck/help/add-existing-field.png differ diff --git a/sites/all/modules/cck/help/add-new-field.html b/sites/all/modules/cck/help/add-new-field.html new file mode 100644 index 0000000..974902e --- /dev/null +++ b/sites/all/modules/cck/help/add-new-field.html @@ -0,0 +1,57 @@ +

At the bottom of the Manage fields page for a content type, +you'll find this:

+ + + +

In order to add a new field to a content type, you need to provide the +following information:

+
+
Label:
+
+ A human-readable name for the field. It will be used in input forms and + on displayed content.
+ All characters are allowed, including spaces, accentuated or non-european + characters. +
+ +
Field name:
+
+ A machine-readable name for the field. It is used internally to identify + the field and handle database storage. When doing custom theming, it is + this identifier that you'll use to refer to that field.
+ Important: The field name cannot be changed once the field has been + created.
+ Allowed characters: a-z (unaccentuated), 0-9 and the underscore (_).
+ The length of the field name cannot exceed 32 characters (including the + 'field_' prefix that gets added automatically - that is, 26 free characters)
+
+ +
Field type:
+
+ The type of data to be stored in that field.
+ Important: The field type cannot be changed once the field has been + created.
+ The available field types depend on the modules you have enabled on your site. CCK comes with 6 + basic field types : +
    +
  • Text
  • +
  • Integer
  • +
  • Float
  • +
  • Decimal
  • +
  • Node reference
  • +
  • User reference
  • +
+ Additional modules can be downloaded to handle other field types such as + dates, files, images... Look at the + CCK project page + and the complete list of CCK-related modules. +
+ +
Widget:
+
+ The form element that will be used to input data for this field on + content forms : text input, select list, etc...
+ Each field type has its own list of available widgets. When selecting a + field type, the list of widgets you can select is automatically updated. +
+
\ No newline at end of file diff --git a/sites/all/modules/cck/help/add-new-field.png b/sites/all/modules/cck/help/add-new-field.png new file mode 100644 index 0000000..a729044 Binary files /dev/null and b/sites/all/modules/cck/help/add-new-field.png differ diff --git a/sites/all/modules/cck/help/add-new-group.html b/sites/all/modules/cck/help/add-new-group.html new file mode 100644 index 0000000..fde5f7e --- /dev/null +++ b/sites/all/modules/cck/help/add-new-group.html @@ -0,0 +1,39 @@ +

Field groups are used to visually gather several fields that are associated +by some sort of logic, for instance several text fields that hold the different +parts of an 'Address'. On input forms and displayed content, the corresponding +fields are enclosed inside an HTML fieldset.

+ + + + +

At the bottom of the Manage fields page for a content type, +you'll find this:

+ + + +

In order to add a new group to a content type, you need to provide the +following information:

+
+
Label:
+
+ A human-readable name for the group. It will be used in input forms and + on displayed content.
+ All characters are allowed, including spaces, accentuated or non-european + characters.
+
+ +
Group name:
+
+ A machine-readable name for the group. It is used internally to identify + the group. When doing custom theming, it is this identifier that you'll use + to refer to that group.
+ Important: The group name cannot be changed once the group has been + created.
+ Allowed characters: a-z (unaccentuated), 0-9 and the underscore (_).
+ The length of the group name cannot exceed 32 characters (including the + 'group_' prefix that gets added automatically - that is, 26 free characters)
+
+
+ +

Once a group has been created, you can define what fields it will enclose by +rearranging fields and groups.

\ No newline at end of file diff --git a/sites/all/modules/cck/help/add-new-group.png b/sites/all/modules/cck/help/add-new-group.png new file mode 100644 index 0000000..cedbe44 Binary files /dev/null and b/sites/all/modules/cck/help/add-new-group.png differ diff --git a/sites/all/modules/cck/help/add-new.png b/sites/all/modules/cck/help/add-new.png new file mode 100644 index 0000000..bda9a22 Binary files /dev/null and b/sites/all/modules/cck/help/add-new.png differ diff --git a/sites/all/modules/cck/help/add.html b/sites/all/modules/cck/help/add.html new file mode 100644 index 0000000..0ac0350 --- /dev/null +++ b/sites/all/modules/cck/help/add.html @@ -0,0 +1,15 @@ +

The form elements at the bottom of the Manage fields page +let you add fields and groups to your content types.

+ + + +

(The Add existing field row is +displayed only if there are fields available in other content types. +The Add new group rows is displayed only if Fieldgroup module +is enabled.)

+

Your fields and groups will be created after you click the Save +button at the bottom of the page. In subsequent pages you will be presented +with the settings form for each field you added.

+ +

You will find more details on the required informations in the +following pages:

\ No newline at end of file diff --git a/sites/all/modules/cck/help/content.help.ini b/sites/all/modules/cck/help/content.help.ini new file mode 100644 index 0000000..b303cbc --- /dev/null +++ b/sites/all/modules/cck/help/content.help.ini @@ -0,0 +1,59 @@ + +[advanced help settings] +name = CCK +index name = "CCK (Content Construction Kit)" + +[fields] +title = Fields and Widgets +weight = -10 + +[manage-fields] +title = 'Manage fields' tab + +[add] +title = Add fields and groups +parent = manage-fields +weight = 1 + +[add-new-field] +title = Add a new field +parent = add +weight = 1 + +[add-existing-field] +title = Add an existing field : share a field across content types +parent = add +weight = 2 + +[add-new-group] +title = Add a new group +parent = add +weight = 3 + +[rearrange] +title = Rearrange fields and groups +parent = manage-fields +weight = 2 + +[remove] +title = Remove fields and groups +parent = manage-fields +weight = 3 + +[theme] +title = Theming CCK data in nodes + +[theme-node-templates] +title = Node templates +parent = theme +weight = 1 + +[theme-field-templates] +title = Field templates +parent = theme +weight = 2 + +[theme-formatters] +title = Formatter theme functions +parent = theme +weight = 3 diff --git a/sites/all/modules/cck/help/drag-groups.png b/sites/all/modules/cck/help/drag-groups.png new file mode 100644 index 0000000..8d13dc8 Binary files /dev/null and b/sites/all/modules/cck/help/drag-groups.png differ diff --git a/sites/all/modules/cck/help/drag-new.png b/sites/all/modules/cck/help/drag-new.png new file mode 100644 index 0000000..05c2c69 Binary files /dev/null and b/sites/all/modules/cck/help/drag-new.png differ diff --git a/sites/all/modules/cck/help/draggable.png b/sites/all/modules/cck/help/draggable.png new file mode 100644 index 0000000..3ab0bb8 Binary files /dev/null and b/sites/all/modules/cck/help/draggable.png differ diff --git a/sites/all/modules/cck/help/fields.html b/sites/all/modules/cck/help/fields.html new file mode 100644 index 0000000..789fa5f --- /dev/null +++ b/sites/all/modules/cck/help/fields.html @@ -0,0 +1 @@ +

The Content Construction Kit (CCK) is composed of numerous field and widget modules that can be used to add fields to any content type.

\ No newline at end of file diff --git a/sites/all/modules/cck/help/group-node-display.png b/sites/all/modules/cck/help/group-node-display.png new file mode 100644 index 0000000..78ce35b Binary files /dev/null and b/sites/all/modules/cck/help/group-node-display.png differ diff --git a/sites/all/modules/cck/help/group-node-edit-form.png b/sites/all/modules/cck/help/group-node-edit-form.png new file mode 100644 index 0000000..831dd76 Binary files /dev/null and b/sites/all/modules/cck/help/group-node-edit-form.png differ diff --git a/sites/all/modules/cck/help/manage-fields.html b/sites/all/modules/cck/help/manage-fields.html new file mode 100644 index 0000000..5c5396f --- /dev/null +++ b/sites/all/modules/cck/help/manage-fields.html @@ -0,0 +1,3 @@ +

This page lets you manage the CCK fields in your content type : add fields and +groups, rearrange them, access their configuration pages, remove them from the +content type.

\ No newline at end of file diff --git a/sites/all/modules/cck/help/rearrange.html b/sites/all/modules/cck/help/rearrange.html new file mode 100644 index 0000000..e5084aa --- /dev/null +++ b/sites/all/modules/cck/help/rearrange.html @@ -0,0 +1,24 @@ +

To change the order of fields, grab a drag-and-drop handle + and drag the field to a new location in the list +(grab a handle by clicking and holding the mouse while hovering over a handle +icon). Remember that your changes will not be saved until you click the +Save button at the bottom of the page.

+ +

The order you define will be used both on input forms (when creating or +editing a post), and on content display (teasers, content page, RSS items...)

+

You can also change the order of non-CCK 'fields' like Title +or File attachments. Depending on the 'field', this will +affect input forms and/or content display (some of those 'fields' are not +displayed in both contexts).

+

If your content type has groups (requires the Fieldgroup module), you can +move a field inside a group by dragging it below the row of the group, and +then slightly to the right before dropping it. Note that groups can also be +reordered, but can currently not be nested inside other groups.

+ + + +

When adding a field or a group, you can drag them directly to the +intended spot in the list of fields and groups that are already present in your +content type, before clicking Save:

+ + \ No newline at end of file diff --git a/sites/all/modules/cck/help/remove.html b/sites/all/modules/cck/help/remove.html new file mode 100644 index 0000000..5fcd157 --- /dev/null +++ b/sites/all/modules/cck/help/remove.html @@ -0,0 +1,16 @@ +

Removing a field

+ +

When you remove a field from a content type, the data it holds are +permanently erased. You will be asked to confirm this action

+ +

You will have to manually update your Views, pathauto settings, etc... if +needed.

+ +

Note : if the field is shared across several content types, removing it from +one content type does not affect the data for the other content +types.

+ +

Removing a group

+ +

Removing a group from a content type does not remove the fields +it contains, and therefore erases no field data.

\ No newline at end of file diff --git a/sites/all/modules/cck/help/theme-field-templates.html b/sites/all/modules/cck/help/theme-field-templates.html new file mode 100644 index 0000000..085e909 --- /dev/null +++ b/sites/all/modules/cck/help/theme-field-templates.html @@ -0,0 +1,75 @@ +

Field-level theming determines how the values of a given field are +displayed. The resulting output ends up in the $content +and $<FIELD_NAME>_rendered variables in the node +templates.

+ +

Template files

+ +

In order to customize field themeing:

+ +
    +
  • + Copy the content-field.tpl.php template file into + your theme's root folder (please keep the contents of the + cck/theme folder untouched. For the same reason, + need to copy the file instead of just moving it). +
  • +
  • + Edit that copy to your liking. See the comments in + cck/theme/content/content-field.tpl.php for a list + of all variables available in this template. +
  • +
+ +

Template suggestions

+ +

In addition, the theme layer will also look for field-specific variants +(suggestions), in the following order of precedence:

+ +
+
content-field-<FIELD_NAME>-<CONTENT_TYPE_NAME>.tpl.php
+
+ ex: content-field-field_myfield-story.tpl.php - + If present, will be used to theme the 'field_myfield' field when displaying + a 'story' node. +
+ +
content-field-<CONTENT_TYPE_NAME>.tpl.php
+
+ ex: content-field-story.tpl.php - If present, + will be used to theme all fields of 'story' nodes. +
+ +
content-field-<FIELD_NAME>.tpl.php
+
+ ex: content-field-field_myfield.tpl.php - + If present, will be used to theme all 'field_myfield' field in all the + content types it appears in. +
+ +
content-field.tpl.php
+
+ If none of the above is present, the base template will be used. +
+
+ +Important: +
    +
  • + Suggestions work only if the theme also has the base template file. + If your theme has content-field-*.tpl.php files, + it must also have a content-field.tpl.php file. +
  • +
  • + Whenever you add new template files in your theme, you need to + rebuild the theme registry, or the theme engine won't see them.
    + You can do that by :
    + - visiting the Administer modules page
    + - or using Devel module's + 'clear cache' link. +
  • +
+ +

See the Working with template suggestions +section of the Theme guide for Drupal 6 +for more informations about templates and template suggestions.

\ No newline at end of file diff --git a/sites/all/modules/cck/help/theme-formatters.html b/sites/all/modules/cck/help/theme-formatters.html new file mode 100644 index 0000000..50bf89a --- /dev/null +++ b/sites/all/modules/cck/help/theme-formatters.html @@ -0,0 +1,13 @@ +

Formatters are used to turn the raw data for a single field value into html. +The Display Fields tab lets you chose which formatter you want to use +for each of your fields.

+ +

In CCK 2.0 for Drupal 6, all formatters now go through the theme layer. +Therefore, overriding a formatter's theme is another way you can alter how your +values are displayed (whereas changing content-field.tpl.php +lets you change the html that "wraps" the values).

+ +

Most formatters come as theme functions, but some might use templates instead. +Either way, you can override them using the usual Drupal 6 theme override +practices. For more informations, see the Theme guide for Drupal 6, +and more specifically the Overriding themable output section.

\ No newline at end of file diff --git a/sites/all/modules/cck/help/theme-node-templates.html b/sites/all/modules/cck/help/theme-node-templates.html new file mode 100644 index 0000000..27944ba --- /dev/null +++ b/sites/all/modules/cck/help/theme-node-templates.html @@ -0,0 +1,130 @@ +

Template files

+ +

All themes usually come with a default node.tpl.php +template. Drupal core lets you use the following variant (suggestion):

+ +
+
node-<CONTENT_TYPE_NAME>.tpl.php
+
+ ex: node-story.tpl.php - If present, will be used + to theme a 'story' node. +
+
+ +

Important: whenever you add new template files in your theme, you +need to rebuild the theme registry, or the theme engine won't see them.
+You can do that by :
+- visiting the Administer modules page
+- or using Devel module's +'clear cache' link.

+ +

Template variables

+ +

CCK makes the following variables available in your theme's node templates:

+ +
+
$<FIELD_NAME>_rendered
+
+ Contains the rendered html for the field, including the label and all the + field's values, with the settings defined on the Display fields tab. +
+ +
$<GROUP_NAME>_rendered
+
+ Contains the rendered html for the fieldgroup (if any), including the label + and all the group's fields, with the settings defined on the Display + fields tab.
+ This variable therefore includes the html contained in all the + $<FIELD_NAME>_rendered variables for the + group's fields. +
+ +
$FIELD_NAME
+
+ Contains the raw values of the fields, in the usual array-format used + internally by CCK. What you find in there depends on the field type.
+ Each value also contains a 'view' element, that + holds the ready-to-display value as rendered by the formatter. For instance: +
+array(
+  0 => array(
+    'nid' => 5,
+    'view' => '<a href="node/5">Title of node 5</a>',
+  ),
+);
+ Raw data are not sanitized for output, it is therefore not + advised to use them directly. Use the 'view' + value, or run the values through content_format(). +
+
+ +

Excluding fields from the $content variable

+ +

By default, the $content variable used in node +templates contains the rendered html for the whole node : CCK fields and +fieldgroups, but also body, file attachments, fivestar widgets, ...

+ +

If for some fields you want to use the more fine-grained variables described +above, you might want to use the Exclude checkboxes on the Display +fields screen, so that the output of those fields is excluded from the +$content variable.

+ +

You can then customize the display and layout of some CCK fields or groups +using the $<FIELD_NAME>_rendered / +$<GROUP_NAME>_rendered variables, and trust +$content to display 'the rest' without getting +duplicate information.

+ +
Advanced trick
+

The Exclude checkboxes affect all active themes. On sites with multiple +themes, however, the list of fields to exclude from $content +might need to be different across the themes, depending on how their respective +node templates are structured.

+ +

A theme can bypass those settings by overriding the theme_content_exclude() +function to specify the list of fields to exclude for this theme (see the +PHPDoc of the function for more information).

+ + +

Special case : nodes in nodereference fields

+ +

In addition to the above, the following suggestions will be looked for +in priority for nodes that are displayed as values of a nodereference field using +the 'teaser' or 'full node' formatters:

+ +
+
node-nodereference-<REFERRING_FIELD_NAME>-<TYPE_NAME>.tpl.php
+
+ ex: node-nodereference-field_noderef-story.tpl.php - + If present, will be used to theme a 'story' node when refererenced in the + 'field_noderef' field. +
+ +
node-nodereference-<TYPE_NAME>.tpl.php
+
+ ex: node-nodereference-story.tpl.php - If present, + will be used to theme a 'story' node when refererenced in any nodereference + field. +
+ +
node-nodereference-<REFERRING_FIELD_NAME>.tpl.php
+
+ ex: node-nodereference-field_noderef.tpl.php - If + present, will be used to a node refererenced in the 'field_noderef' field. +
+ +
node-nodereference.tpl.php
+
+ If present, will be used to theme nodes referenced in nodereference fields. +
+
+ +

The following additional variables are available in templates for referenced nodes:

+ +
+
$referring_field
+
The nodereference field that references the current node.
+ +
$referring_node
+
The node referencing the current node.
+
\ No newline at end of file diff --git a/sites/all/modules/cck/help/theme.html b/sites/all/modules/cck/help/theme.html new file mode 100644 index 0000000..08b7329 --- /dev/null +++ b/sites/all/modules/cck/help/theme.html @@ -0,0 +1,9 @@ +

Note: these instructions assume you are familiar with the basic concepts +of Drupal 6 theming. For more informations, see the Theme guide for Drupal 6, +and more specifically the Overriding themable output +section.

+ +

There are 3 levels where you can customize how the data in CCK fields +is displayed in nodes:

+ + diff --git a/sites/all/modules/cck/help/theme.png b/sites/all/modules/cck/help/theme.png new file mode 100644 index 0000000..d928100 Binary files /dev/null and b/sites/all/modules/cck/help/theme.png differ diff --git a/sites/all/modules/cck/images/remove.png b/sites/all/modules/cck/images/remove.png new file mode 100644 index 0000000..ca48c0c Binary files /dev/null and b/sites/all/modules/cck/images/remove.png differ diff --git a/sites/all/modules/cck/includes/content.admin.inc b/sites/all/modules/cck/includes/content.admin.inc new file mode 100644 index 0000000..e22c744 --- /dev/null +++ b/sites/all/modules/cck/includes/content.admin.inc @@ -0,0 +1,1959 @@ + t('Operations'), 'colspan' => '4'),); + $rows = array(); + + foreach ($names as $key => $name) { + $type = $types[$key]; + if (node_hook($type, 'form')) { + $type_url_str = str_replace('_', '-', $type->type); + $row = array( + check_plain($name), + check_plain($type->type), + ); + // Make the description smaller + $row[] = array('data' => filter_xss_admin($type->description), 'class' => 'description'); + // Set the edit column. + $row[] = array('data' => l(t('edit'), 'admin/content/node-type/'. $type_url_str)); + // Set links for managing fields. + // TODO: a hook to allow other content modules to add more stuff? + $row[] = array('data' => l(t('manage fields'), 'admin/content/node-type/'. $type_url_str .'/fields')); + // Set the delete column. + if ($type->custom) { + $row[] = array('data' => l(t('delete'), 'admin/content/node-type/'. $type_url_str .'/delete')); + } + else { + $row[] = array('data' => ''); + } + + $rows[] = $row; + } + } + + // Allow external modules alter the table headers and rows. + foreach (module_implements('content_types_overview_alter') as $module) { + $function = $module .'_content_types_overview_alter'; + $function($header, $rows); + } + + if (empty($rows)) { + $rows[] = array(array('data' => t('No content types available.'), 'colspan' => '7', 'class' => 'message')); + } + + return theme('table', $header, $rows) .theme('content_overview_links'); +} + +function theme_content_overview_links() { + return ''; +} + +/** + * Menu callback; lists all defined fields for quick reference. + */ +function content_fields_list() { + $fields = content_fields(); + $field_types = _content_field_types(); + + // Sort fields by field name. + ksort($fields); + + $header = array(t('Field name'), t('Field type'), t('Used in')); + $rows = array(); + foreach ($fields as $field) { + $row = array(); + $row[] = $field['locked'] ? t('@field_name (Locked)', array('@field_name' => $field['field_name'])) : $field['field_name']; + $row[] = t($field_types[$field['type']]['label']); + + $types = array(); + $result = db_query("SELECT nt.name, nt.type FROM {". content_instance_tablename() ."} nfi ". + "LEFT JOIN {node_type} nt ON nt.type = nfi.type_name ". + "WHERE nfi.field_name = '%s' ". + // Keep disabled modules out of table. + "AND nfi.widget_active = 1 ". + "ORDER BY nt.name ASC", $field['field_name']); + while ($type = db_fetch_array($result)) { + $content_type = content_types($type['type']); + $types[] = l($type['name'], 'admin/content/node-type/'. $content_type['url_str'] .'/fields'); + } + $row[] = implode(', ', $types); + + $rows[] = array('data' => $row, 'class' => $field['locked'] ? 'menu-disabled' : ''); + } + if (empty($rows)) { + $output = t('No fields have been defined for any content type yet.'); + } + else { + $output = theme('table', $header, $rows); + } + return $output; +} + +/** + * Helper function to display a message about inactive fields. + */ +function content_inactive_message($type_name) { + $inactive_fields = content_inactive_fields($type_name); + if (!empty($inactive_fields)) { + $field_types = _content_field_types(); + $widget_types = _content_widget_types($type_name); + drupal_set_message(t('This content type has inactive fields. Inactive fields are not included in lists of available fields until their modules are enabled.'), 'error'); + foreach ($inactive_fields as $field_name => $field) { + drupal_set_message(t('!field (!field_name) is an inactive !field_type field that uses a !widget_type widget.', array( + '!field' => $field['widget']['label'], + '!field_name' => $field['field_name'], + '!field_type' => array_key_exists($field['type'], $field_types) ? $field_types[$field['type']]['label'] : $field['type'], + '!widget_type' => array_key_exists($field['widget']['type'], $widget_types) ? $widget_types[$field['widget']['type']]['label'] : $field['widget']['type'], + ))); + } + } +} + +/** + * Menu callback; listing of fields for a content type. + * + * Allows fields to be reordered and nested in fieldgroups using + * JS drag-n-drop. Non-CCK form elements can also be moved around. + */ +function content_field_overview_form(&$form_state, $type_name) { + + content_inactive_message($type_name); + + // When displaying the form, make sure the list of fields + // is up-to-date. + if (empty($form_state['post'])) { + content_clear_type_cache(); + } + + // Gather type information. + $type = content_types($type_name); + $fields = $type['fields']; + $field_types = _content_field_types(); + + $extra = $type['extra']; + $groups = $group_options = $group_types = array(); + if (module_exists('fieldgroup')) { + $groups = fieldgroup_groups($type['type']); + $group_types = fieldgroup_types(); + $plain_tree = _fieldgroup_plain_tree($groups); + $group_options = _fieldgroup_groups_label($type['type']); + // Add the ability to group under the newly created row. + $group_options['_add_new_group'] = '_add_new_group'; + } + + // Store the default weights as we meet them, to be able to put the + //'add new' rows after them. + $weights = array(); + + $form = array( + '#tree' => TRUE, + '#type_name' => $type['type'], + '#fields' => array_keys($fields), + '#groups' => array_keys($groups), + '#extra' => array_keys($extra), + '#field_rows' => array(), + '#group_rows' => array(), + ); + + // Fields. + foreach ($fields as $name => $field) { + $weight = $field['widget']['weight']; + $form[$name] = array( + 'label' => array('#value' => check_plain($field['widget']['label'])), + 'field_name' => array('#value' => $field['field_name']), + 'type' => array('#value' => t($field_types[$field['type']]['label'])), + 'configure' => array('#value' => l(t('Configure'), 'admin/content/node-type/'. $type['url_str'] .'/fields/'. $field['field_name'])), + 'remove' => array('#value' => l(t('Remove'), 'admin/content/node-type/'. $type['url_str'] .'/fields/'. $field['field_name'] .'/remove')), + 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), + 'parent' => array('#type' => 'select', '#options' => $group_options, '#default_value' => ''), + 'prev_parent' => array('#type' => 'hidden', '#value' => ''), + 'hidden_name' => array('#type' => 'hidden', '#default_value' => $field['field_name']), + '#leaf' => TRUE, + '#row_type' => 'field', + 'field' => array('#type' => 'value', '#value' => $field), + ); + if ($field['locked']) { + $form[$name]['configure'] = array('#value' => t('Locked')); + $form[$name]['remove'] = array(); + $form[$name]['#disabled_row'] = TRUE; + } + $form['#field_rows'][] = $name; + $weights[] = $weight; + } + + // Groups. + foreach ($groups as $name => $group) { + $current_group_options = $plain_tree; + unset($current_group_options[$name]); + $weight = $group['weight']; + $form[$name] = array( + 'label' => array('#value' => check_plain($group['label'])), + 'group_name' => array('#value' => $group['group_name']), + 'group_type' => array('#value' => t($group_types[$group['group_type']])), + 'configure' => array('#value' => l(t('Configure'), 'admin/content/node-type/'. $type['url_str'] .'/groups/'. $group['group_name'])), + 'remove' => array('#value' => l(t('Remove'), 'admin/content/node-type/'. $type['url_str'] .'/groups/'. $group['group_name'] .'/remove')), + 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), + 'parent' => array('#type' => 'select', '#options' => $current_group_options, '#default_value' => ''), + 'prev_parent' => array('#type' => 'hidden', '#value' => ''), + 'hidden_name' => array('#type' => 'hidden', '#default_value' => $group['group_name']), + '#row_type' => 'group', + 'group' => array('#type' => 'value', '#value' => $group), + ); + // Adjust child fields rows. + if (isset($group['fields'])) { + foreach ($group['fields'] as $field_name => $field) { + $form[$field_name]['parent']['#default_value'] = $name; + $form[$field_name]['prev_parent']['#value'] = $name; + } + } + // Adjust child group rows + $form[$name]['parent']['#default_value'] = $group['parent']; + $form[$name]['prev_parent']['#value'] = $group['parent']; + + $form['#group_rows'][] = $name; + $weights[] = $weight; + } + + // Non-CCK 'fields'. + foreach ($extra as $name => $label) { + $weight = $extra[$name]['weight']; + $form[$name] = array( + 'label' => array('#value' => check_plain(t($extra[$name]['label']))), + 'description' => array('#value' => isset($extra[$name]['description']) ? $extra[$name]['description'] : ''), + 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), + 'parent' => array('#type' => 'hidden', '#default_value' => ''), + 'configure' => array('#value' => isset($extra[$name]['configure']) ? $extra[$name]['configure'] : ''), + 'remove' => array('#value' => isset($extra[$name]['remove']) ? $extra[$name]['remove'] : ''), + 'hidden_name' => array('#type' => 'hidden', '#default_value' => $name), + '#leaf' => TRUE, + '#root' => TRUE, + '#disabled_row' => TRUE, + '#row_type' => 'extra', + ); + $form['#field_rows'][] = $name; + $weights[] = $weight; + } + + // Additional row : add new field. + $weight = max($weights) + 1; + $field_type_options = content_field_type_options(); + $widget_type_options = content_widget_type_options(NULL, TRUE); + if ($field_type_options && $widget_type_options) { + array_unshift($field_type_options, t('- Select a field type -')); + array_unshift($widget_type_options, t('- Select a widget -')); + $name = '_add_new_field'; + $form[$name] = array( + 'label' => array( + '#type' => 'textfield', + '#size' => 15, + '#description' => t('Label'), + ), + 'field_name' => array( + '#type' => 'textfield', + // This field should stay LTR even for RTL languages. + '#field_prefix' => 'field_', + '#field_suffix' => '‎', + '#attributes' => array('dir'=>'ltr'), + '#size' => 15, + // Field names are limited to 32 characters including the 'field_' + // prefix which is 6 characters long. + '#maxlength' => 26, + '#description' => t('Field name (a-z, 0-9, _)'), + ), + 'type' => array( + '#type' => 'select', + '#options' => $field_type_options, + '#description' => theme('advanced_help_topic', 'content', 'fields') . t('Type of data to store.'), + ), + 'widget_type' => array( + '#type' => 'select', + '#options' => $widget_type_options, + '#description' => t('Form element to edit the data.'), + ), + 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), + 'parent' => array('#type' => 'select', '#options' => $group_options, '#default_value' => ''), + 'hidden_name' => array('#type' => 'hidden', '#default_value' => $name), + '#leaf' => TRUE, + '#add_new' => TRUE, + '#row_type' => 'add_new_field', + ); + $form['#field_rows'][] = $name; + } + + // Additional row : add existing field. + $existing_field_options = content_existing_field_options($type_name); + if ($existing_field_options && $widget_type_options) { + $weight++; + array_unshift($existing_field_options, t('- Select an existing field -')); + $name = '_add_existing_field'; + $form[$name] = array( + 'label' => array( + '#type' => 'textfield', + '#size' => 15, + '#description' => t('Label'), + ), + 'field_name' => array( + '#type' => 'select', + '#options' => $existing_field_options, + '#description' => t('Field to share'), + ), + 'widget_type' => array( + '#type' => 'select', + '#options' => $widget_type_options, + '#description' => t('Form element to edit the data.'), + ), + 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), + 'parent' => array('#type' => 'select', '#options' => $group_options, '#default_value' => ''), + 'hidden_name' => array('#type' => 'hidden', '#default_value' => $name), + '#leaf' => TRUE, + '#add_new' => TRUE, + '#row_type' => 'add_existing_field', + ); + $form['#field_rows'][] = $name; + } + + // Additional row : add new group. + if (!empty($group_types)) { + $current_group_options = $group_options; + $options = fieldgroup_types(); + unset($current_group_options['_add_new_group']); + $weight++; + $name = '_add_new_group'; + $form[$name] = array( + 'label' => array( + '#type' => 'textfield', + '#size' => 15, + '#description' => t('Label'), + ), + 'group_name' => array( + '#type' => 'textfield', + // This field should stay LTR even for RTL languages. + '#field_prefix' => 'group_', + '#field_suffix' => '‎', + '#attributes' => array('dir'=>'ltr'), + '#size' => 15, + // Group names are limited to 32 characters including the 'group_' + // prefix which is 6 characters long. + '#maxlength' => 26, + '#description' => t('Group name (a-z, 0-9, _)'), + ), + 'group_option' => array( + '#type' => 'hidden', + '#value' => '', + ), + 'group_type' => array( + '#type' => 'hidden', + '#value' => 'standard', + ), + 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), + 'parent' => array('#type' => 'select', '#options' => $current_group_options, '#default_value' => ''), + 'prev_parent' => array('#type' => 'hidden', '#value' => ''), + 'hidden_name' => array('#type' => 'hidden', '#default_value' => $name), + '#add_new' => TRUE, + '#row_type' => 'add_new_group', + ); + if (count($group_types) > 1) { + $form[$name]['group_type'] = array( + '#type' => 'select', + '#description' => t('Type of group.'), + '#options' => $group_types, + '#default_value' => 'standard', + ); + } + $form['#group_rows'][] = $name; + } + + $form['submit'] = array('#type' => 'submit', '#value' => t('Save')); + return $form; +} + +function content_field_overview_form_validate($form, &$form_state) { + _content_field_overview_form_validate_add_new($form, $form_state); + _content_field_overview_form_validate_add_existing($form, $form_state); +} + +/** + * Helper function for content_field_overview_form_validate. + * + * Validate the 'add new field' row. + */ +function _content_field_overview_form_validate_add_new($form, &$form_state) { + $field = $form_state['values']['_add_new_field']; + + // Validate if any information was provided in the 'add new field' row. + if (array_filter(array($field['label'], $field['field_name'], $field['type'], $field['widget_type']))) { + // No label. + if (!$field['label']) { + form_set_error('_add_new_field][label', t('Add new field: you need to provide a label.')); + } + + // No field name. + if (!$field['field_name']) { + form_set_error('_add_new_field][field_name', t('Add new field: you need to provide a field name.')); + } + // Field name validation. + else { + $field_name = $field['field_name']; + + // Add the 'field_' prefix. + if (substr($field_name, 0, 6) != 'field_') { + $field_name = 'field_'. $field_name; + form_set_value($form['_add_new_field']['field_name'], $field_name, $form_state); + } + + // Invalid field name. + if (!preg_match('!^field_[a-z0-9_]+$!', $field_name)) { + form_set_error('_add_new_field][field_name', t('Add new field: the field name %field_name is invalid. The name must include only lowercase unaccentuated letters, numbers, and underscores.', array('%field_name' => $field_name))); + } + if (strlen($field_name) > 32) { + form_set_error('_add_new_field][field_name', t('Add new field: the field name %field_name is too long. The name is limited to 32 characters, including the \'field_\' prefix.', array('%field_name' => $field_name))); + } + // A field named 'field_instance' would cause a tablename clash with {content_field_instance} + if ($field_name == 'field_instance') { + form_set_error('_add_new_field][field_name', t("Add new field: the name 'field_instance' is a reserved name.")); + } + + // Field name already exists. + // We need to check inactive fields as well, so we can't use content_fields(). + module_load_include('inc', 'content', 'includes/content.crud'); + $fields = content_field_instance_read(array(), TRUE); + $used = FALSE; + foreach ($fields as $existing_field) { + $used |= ($existing_field['field_name'] == $field_name); + } + if ($used) { + form_set_error('_add_new_field][field_name', t('Add new field: the field name %field_name already exists.', array('%field_name' => $field_name))); + } + } + + // No field type. + if (!$field['type']) { + form_set_error('_add_new_field][type', t('Add new field: you need to select a field type.')); + } + + // No widget type. + if (!$field['widget_type']) { + form_set_error('_add_new_field][widget_type', t('Add new field: you need to select a widget.')); + } + // Wrong widget type. + elseif ($field['type']) { + $widget_types = content_widget_type_options($field['type']); + if (!isset($widget_types[$field['widget_type']])) { + form_set_error('_add_new_field][widget_type', t('Add new field: invalid widget.')); + } + } + } +} + +/** + * Helper function for content_field_overview_form_validate. + * + * Validate the 'add existing field' row. + */ +function _content_field_overview_form_validate_add_existing($form, &$form_state) { + // The form element might be absent if no existing fields can be added to + // this content type + if (isset($form_state['values']['_add_existing_field'])) { + $field = $form_state['values']['_add_existing_field']; + + // Validate if any information was provided in the 'add existing field' row. + if (array_filter(array($field['label'], $field['field_name'], $field['widget_type']))) { + // No label. + if (!$field['label']) { + form_set_error('_add_existing_field][label', t('Add existing field: you need to provide a label.')); + } + + // No existing field. + if (!$field['field_name']) { + form_set_error('_add_existing_field][field_name', t('Add existing field: you need to select a field.')); + } + + // No widget type. + if (!$field['widget_type']) { + form_set_error('_add_existing_field][widget_type', t('Add existing field: you need to select a widget.')); + } + // Wrong widget type. + elseif ($field['field_name'] && ($existing_field = content_fields($field['field_name']))) { + $widget_types = content_widget_type_options($existing_field['type']); + if (!isset($widget_types[$field['widget_type']])) { + form_set_error('_add_existing_field][widget_type', t('Add existing field: invalid widget.')); + } + } + } + } +} + +function content_field_overview_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + + $type_name = $form['#type_name']; + $type = content_types($type_name); + + // Update field weights. + $extra = array(); + foreach ($form_values as $key => $values) { + // Groups are handled in fieldgroup_content_overview_form_submit(). + if (in_array($key, $form['#fields'])) { + db_query("UPDATE {". content_instance_tablename() ."} SET weight = %d WHERE type_name = '%s' AND field_name = '%s'", + $values['weight'], $type_name, $key); + } + elseif (in_array($key, $form['#extra'])) { + $extra[$key] = $values['weight']; + } + } + + if ($extra) { + variable_set('content_extra_weights_'. $type_name, $extra); + } + else { + variable_del('content_extra_weights_'. $type_name); + } + + content_clear_type_cache(); + + $destinations = array(); + + // Create new field. + if (!empty($form_values['_add_new_field']['field_name'])) { + $field = $form_values['_add_new_field']; + $field['type_name'] = $type_name; + + module_load_include('inc', 'content', 'includes/content.crud'); + if (content_field_instance_create($field)) { + // Store new field information for fieldgroup submit handler. + $form_state['fields_added']['_add_new_field'] = $field['field_name']; + $destinations[] = 'admin/content/node-type/'. $type['url_str'] .'/fields/'. $field['field_name']; + } + else { + drupal_set_message(t('There was a problem creating field %label.', array( + '%label' => $field['label']))); + } + } + + // Add existing field. + if (!empty($form_values['_add_existing_field']['field_name'])) { + $field = $form_values['_add_existing_field']; + $field['type_name'] = $type_name; + $existing_field = content_fields($field['field_name']); + + if ($existing_field['locked']) { + drupal_set_message(t('The field %label cannot be added to a content type because it is locked.', array('%label' => $field['field_name']))); + } + else { + module_load_include('inc', 'content', 'includes/content.crud'); + if (content_field_instance_create($field)) { + // Store new field information for fieldgroup submit handler. + $form_state['fields_added']['_add_existing_field'] = $field['field_name']; + $destinations[] = 'admin/content/node-type/'. $type['url_str'] .'/fields/'. $field['field_name']; + } + else { + drupal_set_message(t('There was a problem adding field %label.', array('%label' => $field['field_name']))); + } + } + } + + if ($destinations) { + $destinations[] = urldecode(substr(drupal_get_destination(), 12)); + unset($_REQUEST['destination']); + $form_state['redirect'] = content_get_destinations($destinations); + } + +} + +/** + * Menu callback; presents a listing of fields display settings for a content type. + * + * Form includes form widgets to select which fields appear for teaser, full node + * and how the field labels should be rendered. + */ +function content_display_overview_form(&$form_state, $type_name, $contexts_selector = 'basic') { + content_inactive_message($type_name); + + // Gather type information. + $type = content_types($type_name); + $field_types = _content_field_types(); + $fields = $type['fields']; + + $groups = array(); + if (module_exists('fieldgroup')) { + $groups = fieldgroup_groups($type['type']); + } + $contexts = content_build_modes($contexts_selector); + + $form = array( + '#tree' => TRUE, + '#type_name' => $type['type'], + '#fields' => array_keys($fields), + '#groups' => array_keys($groups), + '#contexts' => $contexts_selector, + ); + + if (empty($fields)) { + drupal_set_message(t('There are no fields configured for this content type. You can add new fields on the Manage fields page.', array( + '@link' => url('admin/content/node-type/'. $type['url_str'] .'/fields'))), 'warning'); + return $form; + } + + // Fields. + $label_options = array( + 'above' => t('Above'), + 'inline' => t('Inline'), + 'hidden' => t(''), + ); + foreach ($fields as $name => $field) { + $field_type = $field_types[$field['type']]; + $defaults = $field['display_settings']; + $weight = $field['widget']['weight']; + + $form[$name] = array( + 'human_name' => array('#value' => check_plain($field['widget']['label'])), + 'weight' => array('#type' => 'value', '#value' => $weight), + 'parent' => array('#type' => 'value', '#value' => ''), + ); + + // Label + if ($contexts_selector == 'basic') { + $form[$name]['label']['format'] = array( + '#type' => 'select', + '#options' => $label_options, + '#default_value' => isset($defaults['label']['format']) ? $defaults['label']['format'] : 'above', + ); + } + + // Formatters. + $options = array(); + foreach ($field_type['formatters'] as $formatter_name => $formatter_info) { + $options[$formatter_name] = $formatter_info['label']; + } + $options['hidden'] = t(''); + + foreach ($contexts as $key => $value) { + $form[$name][$key]['format'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => isset($defaults[$key]['format']) ? $defaults[$key]['format'] : 'default', + ); + // exclude from $content + $form[$name][$key]['exclude'] = array( + '#type' => 'checkbox', + '#options' => array(0 => t('Include'), 1 => t('Exclude')), + '#default_value' => isset($defaults[$key]['exclude']) ? $defaults[$key]['exclude'] : 0, + ); + } + } + + // Groups. + $label_options = array( + 'above' => t('Above'), + 'hidden' => t(''), + ); + $options = array( + 'no_style' => t('no styling'), + 'simple' => t('simple'), + 'fieldset' => t('fieldset'), + 'fieldset_collapsible' => t('fieldset - collapsible'), + 'fieldset_collapsed' => t('fieldset - collapsed'), + 'hidden' => t(''), + ); + foreach ($groups as $name => $group) { + $defaults = $group['settings']['display']; + $weight = $group['weight']; + + $form[$name] = array( + 'human_name' => array('#value' => check_plain($group['label'])), + 'weight' => array('#type' => 'value', '#value' => $weight), + 'parent' => array('#type' => 'value', '#value' => ''), + ); + if ($contexts_selector == 'basic') { + $form[$name]['label'] = array( + '#type' => 'select', + '#options' => $label_options, + '#default_value' => isset($defaults['label']) ? $defaults['label'] : 'above', + ); + } + foreach ($contexts as $key => $title) { + $form[$name][$key]['format'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => isset($defaults[$key]['format']) ? $defaults[$key]['format'] : 'fieldset', + ); + // exclude in $content + $form[$name][$key]['exclude'] = array( + '#type' => 'checkbox', + '#options' => array(0 => t('Include'), 1 => t('Exclude')), + '#default_value' => isset($defaults[$key]['exclude']) ? $defaults[$key]['exclude'] : 0, + ); + } + foreach ($group['fields'] as $field_name => $field) { + $form[$field_name]['parent']['#value'] = $name; + } + $form[$name]['parent']['#value'] = $group['parent']; + $form[$name]['group']['#value']['depth'] = $group['depth']; + } + + $form['submit'] = array('#type' => 'submit', '#value' => t('Save')); + return $form; +} + +/** + * Submit handler for the display overview form. + */ +function content_display_overview_form_submit($form, &$form_state) { + module_load_include('inc', 'content', 'includes/content.crud'); + $form_values = $form_state['values']; + foreach ($form_values as $key => $values) { + // Groups are handled in fieldgroup_display_overview_form_submit(). + if (in_array($key, $form['#fields'])) { + $field = content_fields($key, $form['#type_name']); + // We have some numeric keys here, so we can't use array_merge. + $field['display_settings'] = $values + $field['display_settings']; + content_field_instance_update($field, FALSE); + } + } + + // Clear caches and rebuild menu. + content_clear_type_cache(TRUE); + menu_rebuild(); + + drupal_set_message(t('Your settings have been saved.')); +} + +/** + * Return an array of field_type options. + */ +function content_field_type_options() { + static $options; + + if (!isset($options)) { + $options = array(); + $field_types = _content_field_types(); + $field_type_options = array(); + foreach ($field_types as $field_type_name => $field_type) { + // skip field types which have no widget types. + if (content_widget_type_options($field_type_name)) { + $options[$field_type_name] = t($field_type['label']); + } + } + asort($options); + } + return $options; +} + +/** + * Return an array of widget type options for a field type. + * + * If no field type is provided, returns a nested array of + * all widget types, keyed by field type human name + */ +function content_widget_type_options($field_type = NULL, $by_label = FALSE) { + static $options; + + if (!isset($options)) { + $options = array(); + foreach (_content_widget_types() as $widget_type_name => $widget_type) { + foreach ($widget_type['field types'] as $widget_field_type) { + $options[$widget_field_type][$widget_type_name] = t($widget_type['label']); + } + } + } + + if ($field_type) { + return !empty($options[$field_type]) ? $options[$field_type] : array(); + } + elseif ($by_label) { + $field_types = _content_field_types(); + $options_by_label = array(); + foreach ($options as $field_type => $widgets) { + $options_by_label[t($field_types[$field_type]['label'])] = $widgets; + } + return $options_by_label; + } + else { + return $options; + } +} + +/** + * Return an array of existing field to be added to a node type. + */ +function content_existing_field_options($type_name) { + $type = content_types($type_name); + $fields = content_fields(); + $field_types = _content_field_types(); + + $options = array(); + foreach ($fields as $field) { + if (!isset($type['fields'][$field['field_name']]) && !$field['locked']) { + $field_type = $field_types[$field['type']]; + $text = t('@type: @field (@label)', array('@type' => t($field_type['label']), '@label' => t($field['widget']['label']), '@field' => $field['field_name'])); + $options[$field['field_name']] = (drupal_strlen($text) > 80) ? truncate_utf8($text, 77) . '...' : $text; + } + } + // Sort the list by type, then by field name, then by label. + asort($options); + + return $options; +} + +/** + * A form element for selecting field, widget, and label. + */ +function content_field_basic_form(&$form_state, $form_values) { + module_load_include('inc', 'content', 'includes/content.crud'); + + $type_name = $form_values['type_name']; + $type = content_types($form_values['type_name']); + $field_name = $form_values['field_name']; + $field_type = $form_values['type']; + $label = $form_values['label']; + + $form = array(); + + $form['basic'] = array( + '#type' => 'fieldset', + '#title' => t('Edit basic information'), + ); + $form['basic']['field_name'] = array( + '#title' => t('Field name'), + '#type' => 'textfield', + '#value' => $field_name, + '#description' => t("The machine-readable name of the field. This name cannot be changed."), + '#disabled' => TRUE, + ); + $form['basic']['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#default_value' => $label, + '#required' => TRUE, + '#description' => t('A human-readable name to be used as the label for this field in the %type content type.', array('%type' => $type['name'])), + ); + $form['basic']['type'] = array( + '#type' => 'select', + '#title' => t('Field type'), + '#options' => content_field_type_options(), + '#default_value' => $field_type, + '#description' => t('The type of data you would like to store in the database with this field. This option cannot be changed.'), + '#disabled' => TRUE, + ); + $form['basic']['widget_type'] = array( + '#type' => 'select', + '#title' => t('Widget type'), + '#required' => TRUE, + '#options' => content_widget_type_options($field_type), + '#default_value' => $form_values['widget_type'], + '#description' => t('The type of form element you would like to present to the user when creating this field in the %type content type.', array('%type' => $type['name'])), + ); + + $form['type_name'] = array( + '#type' => 'value', + '#value' => $type_name, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Continue'), + ); + + $form['#validate'] = array(); + $form['#submit'] = array('content_field_basic_form_submit'); + + return $form; +} + +/** + * Create a new field for a content type. + */ +function content_field_basic_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + + $label = $form_values['label']; + + // Set the right module information + $field_types = _content_field_types(); + $widget_types = _content_widget_types(); + $form_values['module'] = $field_types[$form_values['type']]['module']; + $form_values['widget_module'] = $widget_types[$form_values['widget_type']]['module']; + + // Make sure we retain previous values and only over-write changed values. + module_load_include('inc', 'content', 'includes/content.crud'); + $instances = content_field_instance_read(array('field_name' => $form_values['field_name'], 'type_name' => $form_values['type_name'])); + $field = array_merge(content_field_instance_collapse($instances[0]), $form_values); + if (content_field_instance_update($field)) { + drupal_set_message(t('Updated basic settings for field %label.', array( + '%label' => $label))); + } + else { + drupal_set_message(t('There was a problem updating the basic settings for field %label.', array( + '%label' => $label))); + } + + $type = content_types($form_values['type_name']); + $form_state['redirect'] = 'admin/content/node-type/'. $type['url_str'] .'/fields/'. $form_values['field_name']; + $form_state['rebuild'] = FALSE; +} + +/** + * Menu callback; present a form for removing a field from a content type. + */ +function content_field_remove_form(&$form_state, $type_name, $field_name) { + $type = content_types($type_name); + $field = $type['fields'][$field_name]; + + $form = array(); + $form['type_name'] = array( + '#type' => 'value', + '#value' => $type_name, + ); + $form['field_name'] = array( + '#type' => 'value', + '#value' => $field_name, + ); + + $output = confirm_form($form, + t('Are you sure you want to remove the field %field?', array('%field' => $field['widget']['label'])), + 'admin/content/node-type/'. $type['url_str'] .'/fields', + t('If you have any content left in this field, it will be lost. This action cannot be undone.'), + t('Remove'), t('Cancel'), + 'confirm' + ); + + if ($field['locked']) { + unset($output['actions']['submit']); + $output['description']['#value'] = t('This field is locked and cannot be removed.'); + } + + return $output; +} + +/** + * Remove a field from a content type. + */ +function content_field_remove_form_submit($form, &$form_state) { + module_load_include('inc', 'content', 'includes/content.crud'); + $form_values = $form_state['values']; + + $type = content_types($form_values['type_name']); + $field = $type['fields'][$form_values['field_name']]; + if ($field['locked']) { + return; + } + + if ($type && $field && $form_values['confirm']) { + if (content_field_instance_delete($form_values['field_name'], $form_values['type_name'])) { + drupal_set_message(t('Removed field %field from %type.', array( + '%field' => $field['widget']['label'], + '%type' => $type['name']))); + } + else { + drupal_set_message(t('There was a problem deleting %field from %type.', array( + '%field' => $field['widget']['label'], + '%type' => $type['name']))); + } + $form_state['redirect'] = 'admin/content/node-type/'. $type['url_str'] .'/fields'; + } +} + +/** + * Menu callback; presents the field editing page. + */ +function content_field_edit_form(&$form_state, $type_name, $field_name) { + $output = ''; + $type = content_types($type_name); + $field = $type['fields'][$field_name]; + + if ($field['locked']) { + $output = array(); + $output['locked'] = array( + '#value' => t('The field %field is locked and cannot be edited.', array('%field' => $field['widget']['label'])), + ); + return $output; + } + + $field_types = _content_field_types(); + $field_type = $field_types[$field['type']]; + $widget_types = _content_widget_types(); + $widget_type = $widget_types[$field['widget']['type']]; + + $title = isset($field['widget']['label']) ? $field['widget']['label'] : $field['field_name']; + drupal_set_title(check_plain($title)); + + // See if we need to change the widget type or label. + if (isset($form_state['change_basic'])) { + module_load_include('inc', 'content', 'includes/content.crud'); + $field_values = content_field_instance_collapse($field); + return content_field_basic_form($form_state, $field_values); + } + + $add_new_sequence = isset($_REQUEST['destinations']); + + // Remove menu tabs when we are in an 'add new' sequence. + if ($add_new_sequence) { + menu_set_item(NULL, menu_get_item('node')); + } + + $form = array(); + $form['#field'] = $field; + $form['#type'] = $type; + + // Basic iformation : hide when we are in an 'add new' sequence. + $form['basic'] = array( + '#type' => 'fieldset', + '#title' => t('%type basic information', array('%type' => $type['name'])), + '#access' => !$add_new_sequence, + ); + $form['basic']['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#value' => $field['widget']['label'], + '#disabled' => TRUE, + ); + $form['basic']['field_name'] = array( + '#type' => 'hidden', + '#title' => t('Field name'), + '#value' => $field['field_name'], + '#disabled' => TRUE, + ); + $form['basic']['type'] = array( + '#type' => 'hidden', + '#title' => t('Field type'), + '#value' => $field['type'], + '#disabled' => TRUE, + ); + $widget_options = content_widget_type_options($field['type']); + $form['basic']['widget_type'] = array( + '#type' => 'select', + '#title' => t('Widget type'), + '#options' => $widget_options, + '#default_value' => $field['widget']['type'] ? $field['widget']['type'] : key($widget_options), + '#disabled' => TRUE, + ); + $form['basic']['change'] = array( + '#type' => 'submit', + '#value' => t('Change basic information'), + '#submit' => array('content_field_edit_form_submit_update_basic'), + ); + + $form['widget'] = array( + '#type' => 'fieldset', + '#title' => t('%type settings', array('%type' => $type['name'])), + '#description' => t('These settings apply only to the %field field as it appears in the %type content type.', array( + '%field' => $field['widget']['label'], + '%type' => $type['name'])), + ); + $form['widget']['weight'] = array( + '#type' => 'hidden', + '#default_value' => $field['widget']['weight'], + ); + + $additions = (array) module_invoke($widget_type['module'], 'widget_settings', 'form', $field['widget']); + drupal_alter('widget_settings', $additions, 'form', $field['widget']); + $form['widget'] = array_merge($form['widget'], $additions); + + $form['widget']['description'] = array( + '#type' => 'textarea', + '#title' => t('Help text'), + '#default_value' => $field['widget']['description'], + '#rows' => 5, + '#description' => t('Instructions to present to the user below this field on the editing form.
Allowed HTML tags: @tags', array('@tags' => _content_filter_xss_display_allowed_tags())), + '#required' => FALSE, + ); + + // Add handling for default value if not provided by field. + if (content_callback('widget', 'default value', $field) == CONTENT_CALLBACK_DEFAULT) { + + // Store the original default value for use in programmed forms. + // Set '#default_value' instead of '#value' so programmed values + // can override whatever we set here. + $default_value = isset($field['widget']['default_value']) ? $field['widget']['default_value'] : array(); + $default_value_php = isset($field['widget']['default_value_php']) ? $field['widget']['default_value_php'] : ''; + $form['widget']['default_value'] = array( + '#type' => 'value', + '#default_value' => $default_value, + ); + $form['widget']['default_value_php'] = array( + '#type' => 'value', + '#default_value' => $default_value_php, + ); + + // We can't tell at the time we build the form if this is a programmed + // form or not, so we always end up adding the default value widget + // even if we won't use it. + $form['widget']['default_value_fieldset'] = array( + '#type' => 'fieldset', + '#title' => t('Default value'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + // Default value widget. + $widget_form = array('#node' => (object) array('type' => $type_name)); + $widget_form_state = array('values' => array($field['field_name'] => $default_value)); + // Make sure the default value is not a required field. + $widget_field = $field; + $widget_field['required'] = FALSE; + module_load_include('inc', 'content', 'includes/content.node_form'); + $form_element = content_field_form($widget_form, $widget_form_state, $widget_field, 0); + $form['widget']['default_value_fieldset']['default_value_widget'] = $form_element; + $form['widget']['default_value_fieldset']['default_value_widget']['#tree'] = TRUE; + // Set up form info that the default value widget will need to find in the form. + $form['#field_info'] = array($widget_field['field_name'] => $widget_field); + + // Advanced: PHP code. + $form['widget']['default_value_fieldset']['advanced_options'] = array( + '#type' => 'fieldset', + '#title' => t('PHP code'), + '#collapsible' => TRUE, + '#collapsed' => empty($field['widget']['default_value_php']), + ); + + if (user_access('Use PHP input for field settings (dangerous - grant with care)')) { + $db_info = content_database_info($field); + $columns = array_keys($db_info['columns']); + foreach ($columns as $key => $column) { + $columns[$key] = t("'@column' => value for @column", array('@column' => $column)); + } + $sample = t("return array(\n 0 => array(@columns),\n // You'll usually want to stop here. Provide more values\n // if you want your 'default value' to be multi-valued:\n 1 => array(@columns),\n 2 => ...\n);", array('@columns' => implode(', ', $columns))); + + $form['widget']['default_value_fieldset']['advanced_options']['default_value_php'] = array( + '#type' => 'textarea', + '#title' => t('Code'), + '#default_value' => isset($field['widget']['default_value_php']) ? $field['widget']['default_value_php'] : '', + '#rows' => 6, + '#tree' => TRUE, + '#description' => t('Advanced usage only: PHP code that returns a default value. Should not include <?php ?> delimiters. If this field is filled out, the value returned by this code will override any value specified above. Expected format:
!sample
To figure out the expected format, you can use the devel load tab provided by devel module on a %type content page.', array( + '!sample' => $sample, + '@link_devel' => 'http://www.drupal.org/project/devel', + '%type' => $type_name)), + ); + } + else { + $form['widget']['default_value_fieldset']['advanced_options']['markup_default_value_php'] = array( + '#type' => 'item', + '#title' => t('Code'), + '#value' => !empty($field['widget']['default_value_php']) ? ''. check_plain($field['widget']['default_value_php']) .'' : t('<none>'), + '#description' => empty($field['widget']['default_value_php']) ? t("You're not allowed to input PHP code.") : t('This PHP code was set by an administrator and will override any value specified above.'), + ); + } + } + + $form['field'] = array( + '#type' => 'fieldset', + '#title' => t('Global settings'), + '#description' => t('These settings apply to the %field field in every content type in which it appears.', array('%field' => $field['widget']['label'])), + ); + $form['field']['required'] = array( + '#type' => 'checkbox', + '#title' => t('Required'), + '#default_value' => $field['required'], + ); + $description = t('Maximum number of values users can enter for this field.'); + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + $description .= '
'. t("'Unlimited' will provide an 'Add more' button so the users can add as many values as they like."); + } + $description .= '
'. t('Warning! Changing this setting after data has been created could result in the loss of data!') .''; + $form['field']['multiple'] = array( + '#type' => 'select', + '#title' => t('Number of values'), + '#options' => array(1 => t('Unlimited'), 0 => 1) + drupal_map_assoc(range(2, 10)), + '#default_value' => $field['multiple'], + '#description' => $description, + ); + + $form['field']['previous_field'] = array( + '#type' => 'hidden', + '#value' => serialize($field), + ); + + $additions = (array) module_invoke($field_type['module'], 'field_settings', 'form', $field); + drupal_alter('field_settings', $additions, 'form', $field); + $form['field'] = array_merge($form['field'], $additions); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save field settings'), + ); + $form['type_name'] = array( + '#type' => 'value', + '#value' => $type_name, + ); + $form['field_name'] = array( + '#type' => 'value', + '#value' => $field_name, + ); + $form['type'] = array( + '#type' => 'value', + '#value' => $field['type'], + ); + $form['module'] = array( + '#type' => 'value', + '#value' => $field['module'], + ); + $form['widget']['label'] = array( + '#type' => 'value', + '#value' => $field['widget']['label'], + ); + $form['widget_module'] = array( + '#type' => 'value', + '#value' => $field['widget']['module'], + ); + $form['columns'] = array( + '#type' => 'value', + '#value' => $field['columns'], + ); + return $form; +} + +/** + * Validate a field's settings. + */ +function content_field_edit_form_validate($form, &$form_state) { + $form_values = $form_state['values']; + if (isset($form_state['change_basic']) || $form_values['op'] == t('Change basic information')) { + return; + } + + module_load_include('inc', 'content', 'includes/content.crud'); + $previous_field = unserialize($form_values['previous_field']); + $field = content_field_instance_expand($form_values); + $field['db_storage'] = content_storage_type($field); + + $field_types = _content_field_types(); + $field_type = $field_types[$field['type']]; + $widget_types = _content_widget_types(); + $widget_type = $widget_types[$field['widget']['type']]; + + if ($dropped_data = content_alter_db_analyze($previous_field, $field)) { + // @TODO + // This is a change that might result in loss of data. + // Add a confirmation form here. + // dsm($dropped_data); + } + + module_invoke($widget_type['module'], 'widget_settings', 'validate', array_merge($field, $form_values)); + module_invoke($field_type['module'], 'field_settings', 'validate', array_merge($field, $form_values)); + + // If content.module is handling the default value, + // validate the result using the field validation. + if (content_callback('widget', 'default value', $field) == CONTENT_CALLBACK_DEFAULT) { + + // If this is a programmed form, get rid of the default value widget, + // we have the default values already. + if ($form['#programmed']) { + form_set_value(array('#parents' => array('default_value_widget')), NULL, $form_state); + return; + } + + if (isset($form_values['default_value_php']) && + ($php = trim($form_values['default_value_php']))) { + $error = FALSE; + ob_start(); + $return = eval($php); + ob_end_clean(); + if (!is_array($return)) { + $error = TRUE; + } + else { + foreach ($return as $item) { + if (!is_array($item)) { + $error = TRUE; + break; + } + } + } + if ($error) { + $db_info = content_database_info($field); + $columns = array_keys($db_info['columns']); + foreach ($columns as $key => $column) { + $columns[$key] = t("'@column' => value for @column", array('@column' => $column)); + } + $sample = t("return array(\n 0 => array(@columns),\n // You'll usually want to stop here. Provide more values\n // if you want your 'default value' to be multi-valued:\n 1 => array(@columns),\n 2 => ...\n);", array('@columns' => implode(', ', $columns))); + + form_set_error('default_value_php', t('The default value PHP code returned an incorrect value.
Expected format:
!sample
Returned value: @value', array( + '!sample' => $sample, + '@value' => print_r($return, TRUE)))); + return; + } + else { + $default_value = $return; + $is_code = TRUE; + form_set_value(array('#parents' => array('default_value_php')), $php, $form_state); + form_set_value(array('#parents' => array('default_value')), array(), $form_state); + } + } + elseif (!empty($form_values['default_value_widget'])) { + // Fields that handle their own multiple values may use an expected + // value as the top-level key, so just pop off the top element. + $key = array_shift(array_keys($form_values['default_value_widget'])); + $default_value = $form_values['default_value_widget'][$key]; + $is_code = FALSE; + form_set_value(array('#parents' => array('default_value_php')), '', $form_state); + form_set_value(array('#parents' => array('default_value')), $default_value, $form_state); + } + if (isset($default_value)) { + $node = array(); + $node[$form_values['field_name']] = $default_value; + $field['required'] = FALSE; + $field_function = $field_type['module'] .'_field'; + + $errors_before = form_get_errors(); + + // Widget now does its own validation, should be no need + // to add anything for widget validation here. + if (function_exists($field_function)) { + $field_function('validate', $node, $field, $default_value, $form, NULL); + } + // The field validation routine won't set an error on the right field, + // so set it here. + $errors_after = form_get_errors(); + if (count($errors_after) > count($errors_before)) { + if (trim($form_values['default_value_php'])) { + form_set_error('default_value_php', t("The PHP code for 'default value' returned @value, which is invalid.", array( + '@value' => print_r($default_value, TRUE)))); + } + else { + form_set_error('default_value', t('The default value is invalid.')); + } + } + } + } +} + +/** + * Button submit handler. + */ +function content_field_edit_form_submit_update_basic($form, &$form_state) { + $form_state['change_basic'] = TRUE; + $form_state['rebuild'] = TRUE; +} + +/** + * Save a field's settings after editing. + */ +function content_field_edit_form_submit($form, &$form_state) { + module_load_include('inc', 'content', 'includes/content.crud'); + $form_values = $form_state['values']; + content_field_instance_update($form_values); + + $destinations = !empty($_REQUEST['destinations']) ? $_REQUEST['destinations'] : array(); + // Remove any external URLs. + $destinations = array_diff($destinations, array_filter($destinations, 'menu_path_is_external')); + if ($destinations) { + drupal_set_message(t('Added field %label.', array('%label' => $form_values['label']))); + $form_state['redirect'] = content_get_destinations($destinations); + } + else { + drupal_set_message(t('Saved field %label.', array('%label' => $form_values['label']))); + $type = content_types($form_values['type_name']); + $form_state['redirect'] = 'admin/content/node-type/'. $type['url_str'] .'/fields'; + } +} + +/** + * Helper function to handle multipage redirects. + */ +function content_get_destinations($destinations) { + $query = array(); + $path = array_shift($destinations); + if ($destinations) { + $query['destinations'] = $destinations; + } + return array($path, $query); +} + +/** + * Content Schema Alter + * + * Alter the database schema. + * + * TODO figure out an API-safe way to use batching to update the nodes that + * will be affected by this change so the node_save() hooks will fire. + * + */ +function content_alter_schema($previous_field, $new_field) { + content_alter_db($previous_field, $new_field); +} + +/** + * Schema Alter Analyze + * + * Analyze if changes will remove columns or delta values, thus losing data. + * Do this so we can delete the data and fire the necessary hooks, before + * we actually alter the schema. + */ +function content_alter_db_analyze($previous_field, $new_field) { + $dropped = array(); + // There is no loss of data if there was no previous data. + if (empty($previous_field)) { + return $dropped; + } + + // Analyze possible data loss from changes in storage type. + if (!empty($previous_field) && !empty($new_field)) { + // Changing from multiple to not multiple data, will cause loss of all + // values greater than zero. + if ($previous_field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD && + $new_field['db_storage'] == CONTENT_DB_STORAGE_PER_CONTENT_TYPE) { + $dropped['delta'] = 0; + } + // Changing from one multiple value to another will cause loss of all + // values for deltas greater than or equal to the new multiple value. + elseif (isset($previous_field['multiple']) && isset($new_field['multiple'])) { + if ($previous_field['multiple'] > $new_field['multiple'] && + $new_field['multiple'] > 1) { + $dropped['delta'] = $new_field['multiple']; + } + } + } + + // Analyze possible data loss from changes in field columns. + $previous_schema = !empty($previous_field) ? content_table_schema($previous_field) : array('fields' => array()); + $new_schema = !empty($new_field) ? content_table_schema($new_field) : array('fields' => array()); + $dropped_columns = array_diff(array_keys($previous_schema['fields']), array_keys($new_schema['fields'])); + if ($dropped_columns) { + $dropped['columns'] = $dropped_columns; + } +// if (empty($new_schema['fields'])) { +// // No new columns, will lose all columns for a field. +// foreach ($previous_schema['fields'] as $column => $attributes) { +// $dropped['columns'][] = $column; +// } +// } +// else { +// // Check both old and new columns to see if we are deleting some columns for a field. +// foreach ($previous_schema['fields'] as $column => $attributes) { +// if (!isset($new_schema['fields'][$column])) { +// $dropped['columns'][] = $column; +// } +// } +// } + + return $dropped; +} + +/** + * Perform adds, alters, and drops as needed to synchronize the database with + * new field definitions. + */ +function content_alter_db($previous_field, $new_field) { + $ret = array(); + + // One or the other of these must be valid. + if (empty($previous_field) && empty($new_field)) { + return $ret; + } + + // Gather relevant information : schema, table name... + $previous_schema = !empty($previous_field) ? content_table_schema($previous_field) : array(); + $new_schema = !empty($new_field) ? content_table_schema($new_field) : array(); + if (!empty($previous_field)) { + $previous_db_info = content_database_info($previous_field); + $previous_table = $previous_db_info['table']; + } + if (!empty($new_field)) { + $new_db_info = content_database_info($new_field); + $new_table = $new_db_info['table']; + } + + // Deletion of a field instance: drop relevant columns and tables and return. + if (empty($new_field)) { + if ($previous_field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD) { + db_drop_table($ret, $previous_table); + } + else { + foreach ($previous_schema['fields'] as $column => $attributes) { + if (!in_array($column, array('nid', 'vid', 'delta'))) { + db_drop_field($ret, $previous_table, $column); + } + } + } + content_alter_db_cleanup(); + return $ret; + } + + // Check that content types that have fields do have a per-type table. + if (!empty($new_field)) { + $base_tablename = _content_tablename($new_field['type_name'], CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + if (!db_table_exists($base_tablename)) { + db_create_table($ret, $base_tablename, content_table_schema()); + } + } + + // Create new table and columns, if not already created. + if (!db_table_exists($new_table)) { + db_create_table($ret, $new_table, $new_schema); + } + else { + // Or add fields and/or indexes to an existing table. + foreach ($new_schema['fields'] as $column => $attributes) { + if (!in_array($column, array('nid', 'vid', 'delta'))) { + // Create the column if it does not exist. + if (!db_column_exists($new_table, $column)) { + db_add_field($ret, $new_table, $column, $attributes); + } + // Create the index if requested to, and it does not exist. + if (isset($new_schema['indexes'][$column]) && !content_db_index_exists($new_table, $column)) { + db_add_index($ret, $new_table, $column, $new_schema['indexes'][$column]); + } + } + } + } + + // If this is a new field, we're done. + if (empty($previous_field)) { + content_alter_db_cleanup(); + return $ret; + } + + // If the previous table doesn't exist, we're done. + // Could happen if someone tries to run a schema update from an + // content.install update function more than once. + if (!db_table_exists($previous_table)) { + content_alter_db_cleanup(); + return $ret; + } + + // If changing data from one schema to another, see if changes require that + // we drop multiple values or migrate data from one storage type to another. + $migrate_columns = array_intersect_assoc($new_schema['fields'], $previous_schema['fields']); + unset($migrate_columns['nid'], $migrate_columns['vid'], $migrate_columns['delta']); + + // If we're going from one multiple value a smaller one or to single, + // drop all delta values higher than the new maximum delta value. + // Not needed if the new multiple is unlimited or if the new table is the content table. + if ($new_table != $base_tablename && $new_field['multiple'] < $previous_field['multiple'] && $new_field['multiple'] != 1) { + db_query("DELETE FROM {". $new_table ."} WHERE delta >= ". max(1, $new_field['multiple'])); + } + + // If going from multiple to non-multiple, make sure the field tables have + // the right database structure to accept migrated data. + if ($new_field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD) { + if ($previous_field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD && count($previous_schema['fields'])) { + // Already using per-field storage; change multiplicity if needed. + if ($previous_field['multiple'] > 0 && $new_field['multiple'] == 0) { + db_drop_field($ret, $new_table, 'delta'); + db_drop_primary_key($ret, $new_table); + db_add_primary_key($ret, $new_table, array('vid')); + } + else if ($previous_field['multiple'] == 0 && $new_field['multiple'] > 0) { + db_add_field($ret, $new_table, 'delta', array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0)); + db_drop_primary_key($ret, $new_table); + db_add_primary_key($ret, $new_table, array('vid', 'delta')); + } + } + } + + // Migrate data from per-content-type storage. + if ($previous_field['db_storage'] == CONTENT_DB_STORAGE_PER_CONTENT_TYPE && + $new_field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD) { + $columns = array_keys($migrate_columns); + if ($new_field['multiple']) { + db_query('INSERT INTO {'. $new_table .'} (vid, nid, delta, '. implode(', ', $columns) .') '. + ' SELECT vid, nid, 0, '. implode(', ', $columns) .' FROM {'. $previous_table .'}'); + } + else { + db_query('INSERT INTO {'. $new_table .'} (vid, nid, '. implode(', ', $columns) .') '. + ' SELECT vid, nid, '. implode(', ', $columns) .' FROM {'. $previous_table .'}'); + } + foreach ($columns as $column_name) { + db_drop_field($ret, $previous_table, $column_name); + } + } + + // Migrate data from per-field storage, and drop per-field table. + if ($previous_field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD && + $new_field['db_storage'] == CONTENT_DB_STORAGE_PER_CONTENT_TYPE) { + // In order to be able to use drupal_write_record, we need to + // rebuild the schema now. + content_alter_db_cleanup(); + if ($previous_field['multiple']) { + $result = db_query("SELECT * FROM {". $previous_table ."} c JOIN {node} n ON c.nid = n.nid WHERE delta = 0 AND n.type = '%s'", $new_field['type_name']); + } + else { + $result = db_query("SELECT * FROM {". $previous_table ."} c JOIN {node} n ON c.nid = n.nid WHERE n.type = '%s'", $new_field['type_name']); + } + $record = array(); + while ($data = db_fetch_array($result)) { + $record['nid'] = $data['nid']; + $record['vid'] = $data['vid']; + if ($previous_field['multiple']) { + $record['delta'] = $data['delta']; + } + foreach ($migrate_columns as $column => $attributes) { + if (is_null($data[$column])) { + $record[$column] = NULL; + } + else { + $record[$column] = $data[$column]; + // Prevent double serializtion in drupal_write_record. + if (isset($attributes['serialize']) && $attributes['serialize']) { + $record[$column] = unserialize($record[$column]); + } + } + } + if (db_result(db_query('SELECT COUNT(*) FROM {'. $new_table . + '} WHERE vid = %d AND nid = %d', $data['vid'], $data['nid']))) { + $keys = $new_field['multiple'] ? array('vid', 'delta') : array('vid'); + drupal_write_record($new_table, $record, $keys); + } + else { + drupal_write_record($new_table, $record); + } + } + db_drop_table($ret, $previous_table); + } + + // Change modified columns that don't involve storage changes. + foreach ($new_schema['fields'] as $column => $attributes) { + if (isset($previous_schema['fields'][$column]) && + $previous_field['db_storage'] == $new_field['db_storage']) { + if ($attributes != $previous_schema['fields'][$column]) { + if (!in_array($column, array('nid', 'vid', 'delta'))) { + db_change_field($ret, $new_table, $column, $column, $attributes); + } + } + } + } + + // Remove obsolete columns. + foreach ($previous_schema['fields'] as $column => $attributes) { + if (!isset($new_schema['fields'][$column])) { + if (!in_array($column, array('nid', 'vid', 'delta'))) { + db_drop_field($ret, $previous_table, $column); + } + } + } + + // TODO: debugging stuff - should be removed + if (module_exists('devel')) { + //dsm($ret); + } + return $ret; +} + +/** + * Helper function for handling cleanup operations when schema changes are made. + */ +function content_alter_db_cleanup() { + // Rebuild the whole database schema. + // TODO: this could be optimized. We don't need to rebuild in *every case*... + // Or do we? This affects the schema and menu and may have unfortunate + // delayed effects if we don't clear everything out at this point. + content_clear_type_cache(TRUE); +} + +/** + * Helper function to order fields and groups when theming (preprocessing) + * overview forms. + * + * The $form is passed by reference because we assign depths as parenting + * relationships are sorted out. + */ +function _content_overview_order(&$form, $field_rows, $group_rows) { + // Put weight and parenting values into a $dummy render structure + // and let drupal_render figure out the corresponding row order. + $dummy = array(); + // Group rows: account for weight. + if (module_exists('fieldgroup')) { + foreach ($group_rows as $name) { + $dummy[$name] = array('#weight' => $form[$name]['weight']['#value'], '#value' => $name .' '); + } + } + // Field rows : account for weight and parenting. + foreach ($field_rows as $name) { + $dummy[$name] = array('#weight' => $form[$name]['weight']['#value'], '#value' => $name .' '); + if (module_exists('fieldgroup')) { + if ($parent = $form[$name]['parent']['#value']) { + $form[$name]['#depth'] = 1; + $dummy[$parent][$name] = $dummy[$name]; + unset($dummy[$name]); + } + } + } + // Nested fieldgroup + if (module_exists('fieldgroup')) { + // readjust the depth and parenting of fieldgroup + $nested = array(); + foreach ($group_rows as $name) { + if (empty($form[$name]['#depth'])) $form[$name]['#depth'] = 0; + + // Skip top level groups, only nested groups need adjustment. + if ($parent = $form[$name]['parent']['#value']) { + $form[$name]['#depth'] = $form[$parent]['#depth'] + 1; + if (array_key_exists($parent, $nested)){ + $nested[$parent][$name] = $dummy[$name]; + $nested[$name] = & $nested[$parent][$name]; + } + else { + $dummy[$parent][$name] = $dummy[$name]; + $nested[$name] = & $dummy[$parent][$name]; + } + unset($dummy[$name]); + } + } + // readjust the depth + foreach ($field_rows as $name) { + if ($parent = $form[$name]['parent']['#value']) { + $form[$name]['#depth'] = $form[$parent]['#depth'] + 1; + } + } + } + + return $dummy ? explode(' ', trim(drupal_render($dummy))) : array(); +} + +/** + * Batching process for changing the field schema, + * running each affected node through node_save() first, to + * fire all hooks. + * + * TODO This is just a placeholder for now because batching can't be safely + * used with API hooks. Need to come back and figure out how to incorporate + * this and get it working properly when the fields are altered via the API. + */ +function content_alter_fields($previous_field, $new_field) { + // See what values need to be updated in the field data. + $mask = content_alter_db_mask($previous_field, $new_field); + + // We use batch processing to prevent timeout when updating a large number + // of nodes. If there is no previous data to adjust, we can just go straight + // to altering the schema, otherwise use batch processing to update + // the database one node at a time, then update the schema. + if (empty($mask)) { + return content_alter_db($previous_field, $new_field); + } + $updates = array( + 'mask' => $mask['mask'], + 'alt_mask' => $mask['alt_mask'], + 'delta' => $mask['delta'], + ); + $batch = array( + 'operations' => array( + array('content_field_batch_update', array($previous_field['field_name'] => $updates)), + array('content_alter_db', array($previous_field, $new_field)) + ), + 'finished' => '_content_alter_fields_finished', + 'title' => t('Processing'), + 'error_message' => t('The update has encountered an error.'), + 'file' => './'. drupal_get_path('module', 'content') .'/includes/content.admin.inc', + ); + batch_set($batch); + if (!empty($url)) { + batch_process($url, $url); + } +} + +/** + * Content Replace Fields 'finished' callback. + */ +function _content_alter_fields_finished($success, $results, $operations) { + if ($success) { + drupal_set_message(t('The database has been altered and data has been migrated or deleted.')); + } + else { + drupal_set_message(t('An error occurred and database alteration did not complete.'), 'error'); + $message = format_plural(count($results), '1 item successfully processed:', '@count items successfully processed:'); + $message .= theme('item_list', $results); + drupal_set_message($message); + } +} + +/** + * Create a mask for the column data that should be deleted in each field. + * + * This is a bit tricky. We could theoretically have some columns + * that should be set to empty and others with valid info that should + * not be emptied out. But if delta values > X are to be wiped out, they + * need to wipe out even columns that still have values. And the NULL + * values in these columns after the alteration may be enough to make + * the item 'empty', as defined by hook_content_is_empty(), even if + * some columns still have values, so all these things need to be tested. + */ +function content_alter_db_mask($previous_field, $new_field) { + // Get an array of column values that will be dropped by this + // schema change and create a mask to feed to content_batch_update. + + $dropped = content_alter_db_analyze($previous_field, $new_field); + if (empty($dropped)) { + return array(); + } + $mask = array('mask' => array()); + foreach (array_keys($previous_field['columns']) as $column_name) { + // The basic mask will empty the dropped columns. + if (isset($dropped['columns']) && in_array($column_name, $dropped['columns'])) { + $mask['mask'][$column_name] = NULL; + } + // Over the delta we'll empty all columns. + if (isset($dropped['delta'])) { + $mask['alt_mask'][$column_name] = NULL; + } + } + if (isset($dropped['delta'])) { + $mask['delta'] = $dropped['delta']; + } + return $mask; +} + +/** + * Content Field Batch Update Operation + * + * Find all nodes that contain a field and update their values. + * + * @param $updates + * an array like: + * 'field_name' => array( + * 'mask' => array() + * // Keyed array of column names and replacement values for use + * // below delta, or for all values if no delta is supplied. + * 'alt_mask' => array() + * // Optional, keyed array of column names and replacement values for use + * // at or above delta, if a delta is supplied. + * 'delta' => # + * // Optional, the number to use as the delta value where you switch from + * // one mask to the other. + * ), + */ +function content_field_batch_update($updates, &$context) { + if (empty($field)) { + $context['finished'] = 1; + return; + } + $field_name = $updates['field_name']; + $field = content_fields($field_name); + + if (!isset($context['sandbox']['progress'])) { + $db_info = content_database_info($field); + + // Might run into non-existent tables when cleaning up a corrupted + // database, like some of the old content storage changes in the + // .install files. + if (!db_table_exists($db_info['table'])) { + return $context['finished'] = 1; + } + $nodes = array(); + $result = db_query("SELECT nid FROM {". $db_info['table'] ."}"); + while ($node = db_fetch_array($result)) { + $nodes[] = $node['nid']; + } + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = count($nodes); + $context['sandbox']['nodes'] = $nodes; + } + + // Process nodes by groups of 5. + $count = min(5, count($context['sandbox']['nodes'])); + + for ($i = 1; $i <= $count; $i++) { + // For each nid, load the node, empty the column values + // or the whole field, and re-save it. + $nid = array_shift($context['sandbox']['nodes']); + $node = content_field_replace($nid, array($updates)); + + // Store result for post-processing in the finished callback. + $context['results'][] = l($node->title, 'node/'. $node->nid); + + // Update our progress information. + $context['sandbox']['progress']++; + $context['message'] = t('Processing %title', array('%title' => $node->title)); + } + + // Inform the batch engine that we are not finished, + // and provide an estimation of the completion level we reached. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } +} + +/** + * Content Field Replace + * + * Replace field values in a node from an array of update values. + * + * Supply an array of one or more fields and masks of field column values + * to be replaced into field values, one mask for basic values and an optional + * different mask for values in field items equal to or higher than a + * specified delta. + * + * The masks should contain only the column values to be substituted in. + * The supplied values will be merged into the existing values to replace + * only the values in the mask, leaving all other values unchanged. + * + * The ability to set different masks starting at a delta allows the + * possibility of setting values above a certain delta to NULL prior + * to altering the database schema. + * + * @param $nid + * @param $updates + * an array like: + * 'field_name' => array( + * 'mask' => array() + * // Keyed array of column names and replacement values for use + * // below delta, or for all values if no delta is supplied. + * 'alt_mask' => array() + * // Optional, keyed array of column names and replacement values for use + * // at or above delta, if a delta is supplied. + * 'delta' => # + * // Optional, the number to use as the delta value where you switch from + * // one mask to the other. + * ), + */ +function content_field_replace($nid, $updates) { + $node = node_load($nid, NULL, TRUE); + foreach ($updates as $field_name => $update) { + $items = isset($node->$field_name) ? $node->$field_name : array(); + foreach ($items as $delta => $value) { + $field_mask = (isset($update['delta']) && isset($update['alt_mask']) && $delta >= $update['delta']) ? $update['alt_mask'] : $mask['mask']; + // Merge the mask into the field values to do the replacements. + $items[$delta] = array_merge($items[$delta], $field_mask); + } + // Test if the new values will make items qualify as empty. + $items = content_set_empty($field, $items); + $node->$field_name = $items; + } + node_save($node); + return $node; +} + +/** + * Helper form element validator : integer. + */ +function _element_validate_integer($element, &$form_state) { + $value = $element['#value']; + if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) { + form_error($element, t('%name must be an integer.', array('%name' => $element['#title']))); + } +} + +/** + * Helper form element validator : integer > 0. + */ +function _element_validate_integer_positive($element, &$form_state) { + $value = $element['#value']; + if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value <= 0)) { + form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title']))); + } +} + +/** + * Helper form element validator : number. + */ +function _element_validate_number($element, &$form_state) { + $value = $element['#value']; + if ($value != '' && !is_numeric($value)) { + form_error($element, t('%name must be a number.', array('%name' => $element['#title']))); + } +} diff --git a/sites/all/modules/cck/includes/content.crud.inc b/sites/all/modules/cck/includes/content.crud.inc new file mode 100644 index 0000000..4e9178c --- /dev/null +++ b/sites/all/modules/cck/includes/content.crud.inc @@ -0,0 +1,694 @@ + widget nested array that is used + * by the content module, or in flattened $form_values arrays, by + * converting flattened arrays to the nested format. + * + * A hook_content_fieldapi() is available for each field instance action, + * and each hook receives the nested field => widget array as an argument. + * + * The hook_content_fieldapi() $ops include: + * + * - create instance + * - read instance + * - update instance + * - delete instance + * + * Another function, content_module_delete($module) will clean up + * after a module that has been deleted by removing all data and + * settings information that was created by that module. + */ + +/** + * Create an array of default values for a field type. + */ +function content_field_default_values($field_type) { + $field_types = _content_field_types(); + $module = $field_types[$field_type]['module']; + + $field = array( + 'module' => $module, + 'type' => $field_type, + 'active' => 0, + ); + + if (module_exists($module)) { + $field['active'] = 1; + } + + $field['columns'] = (array) module_invoke($module, 'field_settings', 'database columns', $field); + // Ensure columns always default to NULL values. + foreach ($field['columns'] as $column_name => $column) { + $field['columns'][$column_name]['not null'] = FALSE; + } + + $field['required'] = 0; + $field['multiple'] = 0; + $field['db_storage'] = CONTENT_DB_STORAGE_PER_CONTENT_TYPE; + + // Make sure field settings all have an index in the array. + $setting_names = (array) module_invoke($module, 'field_settings', 'save', $field); + drupal_alter('field_settings', $setting_names, 'save', $field); + foreach ($setting_names as $setting) { + $field[$setting] = NULL; + } + return $field; +} + +/** + * Create an array of default values for a field instance. + */ +function content_instance_default_values($field_name, $type_name, $widget_type) { + $widget_types = _content_widget_types(); + $module = $widget_types[$widget_type]['module']; + + $widget = array( + 'field_name' => $field_name, + 'type_name' => $type_name, + 'weight' => 0, + 'label' => $field_name, + 'description' => '', + 'widget_type' => $widget_type, + 'widget_module' => $module, + 'display_settings' => array(), + 'widget_settings' => array(), + ); + + if (module_exists($module)) { + $widget['widget_active'] = 1; + } + + $settings_names = array_merge(array('label'), array_keys(content_build_modes())); + $widget['display_settings'] = array(); + foreach ($settings_names as $name) { + $widget['display_settings'][$name]['format'] = ($name == 'label') ? 'above' : 'default'; + $widget['display_settings'][$name]['exclude'] = 0; + } + + // Make sure widget settings all have an index in the array. + $settings_names = (array) module_invoke($module, 'widget_settings', 'save', $widget); + drupal_alter('widget_settings', $settings_names, 'save', $widget); + $widget['widget_settings'] = array(); + foreach ($settings_names as $name) { + $widget['widget_settings'][$name] = NULL; + } + return $widget; +} + +/** + * Expand field info to create field => widget info. + */ +function content_field_instance_expand($field) { + if (isset($field['widget'])) { + return $field; + } + $field['widget'] = !empty($field['widget_settings']) ? $field['widget_settings'] : array(); + $field['widget']['label'] = !empty($field['label']) ? $field['label'] : $field['field_name']; + $field['widget']['weight'] = !empty($field['weight']) ? $field['weight'] : 0; + $field['widget']['description'] = !empty($field['description']) ? $field['description'] : ''; + + if (!empty($field['widget_type'])) { + $field['widget']['type'] = $field['widget_type']; + $widget_types = _content_widget_types(); + $field['widget']['module'] = isset($widget_types[$field['widget_type']]['module']) ? $widget_types[$field['widget_type']]['module'] : $field['widget_module']; + } + elseif (!empty($field['widget_module'])) { + $field['widget']['module'] = $field['widget_module']; + } + + unset($field['widget_type']); + unset($field['weight']); + unset($field['label']); + unset($field['description']); + unset($field['widget_module']); + unset($field['widget_settings']); + + // If content.module is handling the default value, + // initialize $widget_settings with default values, + if (isset($field['default_value']) && isset($field['default_value_php']) && + content_callback('widget', 'default value', $field) == CONTENT_CALLBACK_DEFAULT) { + $field['widget']['default_value'] = !empty($field['default_value']) ? $field['default_value'] : NULL; + $field['widget']['default_value_php'] = !empty($field['default_value_php']) ? $field['default_value_php'] : NULL; + unset($field['default_value']); + unset($field['default_value_php']); + } + return $field; +} + +/** + * Collapse field info from field => widget to flattened form values. + */ +function content_field_instance_collapse($field) { + if (!isset($field['widget'])) { + return $field; + } + $field['widget_settings'] = !empty($field['widget']) ? $field['widget'] : array(); + $field['widget_type'] = !empty($field['widget']['type']) ? $field['widget']['type'] : ''; + $field['weight'] = !empty($field['widget']['weight']) ? $field['widget']['weight'] : 0; + $field['label'] = !empty($field['widget']['label']) ? $field['widget']['label'] : $field['field_name']; + $field['description'] = !empty($field['widget']['description']) ? $field['widget']['description'] : ''; + $field['type_name'] = !empty($field['type_name']) ? $field['type_name'] : ''; + + if (!empty($field['widget']['module'])) { + $widget_module = $field['widget']['module']; + } + elseif (!empty($field['widget']['type'])) { + $widget_types = _content_widget_types(); + $widget_module = $widget_types[$field['widget']['type']]['module']; + } + else { + $widget_module = ''; + } + $field['widget_module'] = $widget_module; + unset($field['widget_settings']['type']); + unset($field['widget_settings']['weight']); + unset($field['widget_settings']['label']); + unset($field['widget_settings']['description']); + unset($field['widget_settings']['module']); + unset($field['widget']); + return $field; +} + +/** + * Create a new field instance. + * + * @param $field + * An array of properties to create the field with, input either in + * the field => widget format used by the content module or as an + * array of form values. + * + * Required values: + * - field_name, the name of the field to be created + * - type_name, the content type of the instance to be created + * + * If there is no prior instance to create this from, we also need: + * - type, the type of field to create + * - widget_type, the type of widget to use + * @param $rebuild + * TRUE to clear content type caches and rebuild menu (default). + * FALSE allows the caller to process several fields at a time quickly, but then + * the caller is reponsible to clear content type caches and rebuild menu as soon + * as all fields have been processed. For example: + * @code + * // Create several fields at a time. + * foreach ($fields as $field) { + * content_field_instance_create($field, FALSE); + * } + * // Clear caches and rebuild menu. + * content_clear_type_cache(TRUE); + * menu_rebuild(); + * @endcode + * @see content_clear_type_cache() + * @see menu_rebuild() + */ +function content_field_instance_create($field, $rebuild = TRUE) { + include_once('./'. drupal_get_path('module', 'content') .'/includes/content.admin.inc'); + + $form_values = $field; + $field = content_field_instance_expand($field); + + // If there are prior instances, fill out missing values from the prior values, + // otherwise get missing values from default values. + $prior_instances = content_field_instance_read(array('field_name' => $field['field_name'])); + if (!empty($prior_instances) && is_array($prior_instances)) { + $prev_field = content_field_instance_expand($prior_instances[0]); + + // Weight, label, and description may have been forced into the $field + // by content_field_instance_expand(). If there is a previous instance to + // get these values from and there was no value supplied originally, use + // the previous value. + $field['widget']['weight'] = isset($form_values['weight']) ? $form_values['weight'] : $prev_field['widget']['weight']; + $field['widget']['label'] = isset($form_values['label']) ? $form_values['label'] : $prev_field['widget']['label']; + $field['widget']['description'] = isset($form_values['description']) ? $form_values['description'] : $prev_field['widget']['description']; + } + else { + $prev_field = array('widget' => array()); + } + + // If we have a field type, we can build default values for this field type. + $default_values = array('widget' => array()); + if (isset($field['type'])) { + $default_values = content_field_default_values($field['type']); + $default_instance_values = content_instance_default_values($field['field_name'], $field['type_name'], $field['widget']['type']); + $default_values = content_field_instance_expand(array_merge($default_values, $default_instance_values)); + } + + // Merge default values, previous values, and current values to create + // a complete field array. + $widget = array_merge($default_values['widget'], $prev_field['widget'], $field['widget']); + $field = array_merge($default_values, $prev_field, $field); + $field['widget'] = $widget; + + // Make sure we know what module to invoke for field info. + if (empty($field['module']) && !empty($field['type'])) { + $field_types = _content_field_types(); + $field['module'] = $field_types[$field['type']]['module']; + } + + // The storage type may need to be updated. + $field['db_storage'] = content_storage_type($field); + + // Get a fresh copy of the column information whenever a field is created. + $field['columns'] = (array) module_invoke($field['module'], 'field_settings', 'database columns', $field); + + if (empty($prev_field['widget']) || $prior_instances < 1) { + // If this is the first instance, create the field. + $field['db_storage'] = $field['multiple'] > 0 ? CONTENT_DB_STORAGE_PER_FIELD : CONTENT_DB_STORAGE_PER_CONTENT_TYPE; + _content_field_write($field, 'create'); + } + elseif (!empty($prev_field['widget']) && $prev_field['db_storage'] == CONTENT_DB_STORAGE_PER_CONTENT_TYPE && count($prior_instances) > 0) { + // If the database storage has changed, update the field and previous instances. + $field['db_storage'] = CONTENT_DB_STORAGE_PER_FIELD; + + foreach ($prior_instances as $instance) { + $new_instance = $instance; + $new_instance['db_storage'] = CONTENT_DB_STORAGE_PER_FIELD; + + // Invoke hook_content_fieldapi(). + module_invoke_all('content_fieldapi', 'update instance', $new_instance); + + content_alter_schema($instance, $new_instance); + } + } + + // Invoke hook_content_fieldapi(). + module_invoke_all('content_fieldapi', 'create instance', $field); + + // Update the field and the instance with the latest values. + _content_field_write($field, 'update'); + _content_field_instance_write($field, 'create'); + + content_alter_schema(array(), $field); + + if ($rebuild) { + content_clear_type_cache(TRUE); + menu_rebuild(); + } + + return $field; +} + +/** + * Update an existing field instance. + * + * @param $field + * An array of properties to update the field with, input either in + * the field => widget format used by the content module or as an + * array of form values. + * @param $rebuild + * TRUE to clear content type caches and rebuild menu (default). + * FALSE allows the caller to process several fields at a time quickly, but then + * the caller is reponsible to clear content type caches and rebuild menu as soon + * as all fields have been processed. For example: + * @code + * // Update several fields at a time. + * foreach ($fields as $field) { + * content_field_instance_update($field, FALSE); + * } + * // Clear caches and rebuild menu. + * content_clear_type_cache(TRUE); + * menu_rebuild(); + * @endcode + * @see content_clear_type_cache() + * @see menu_rebuild() + */ +function content_field_instance_update($field, $rebuild = TRUE) { + include_once('./'. drupal_get_path('module', 'content') .'/includes/content.admin.inc'); + + // Ensure the field description is in the 'expanded' form. + $field = content_field_instance_expand($field); + + // Get the previous value from the table. + $previous = content_field_instance_read(array('field_name' => $field['field_name'], 'type_name' => $field['type_name'])); + $prev_field = array_pop($previous); + + // Create a complete field array by merging the previous and current values, + // letting the current values overwrite the previous ones. + $widget = array_merge($prev_field['widget'], $field['widget']); + $field = array_merge($prev_field, $field); + $field['widget'] = $widget; + + // Make sure we know what module to invoke for field info. + if (empty($field['module']) && !empty($field['type'])) { + $field_types = _content_field_types(); + $field['module'] = $field_types[$field['type']]['module']; + } + + // The storage type may need to be updated. + $field['db_storage'] = content_storage_type($field); + + // Changes in field values may affect columns, or column + // information may have changed, get a fresh copy. + $field['columns'] = (array) module_invoke($field['module'], 'field_settings', 'database columns', $field); + + // If the database storage has changed, update the field and previous instances. + $prior_instances = content_field_instance_read(array('field_name' => $field['field_name'])); + + if ($prev_field['db_storage'] == CONTENT_DB_STORAGE_PER_CONTENT_TYPE && count($prior_instances) > 1) { + // Update the field's data storage. + $field['db_storage'] = CONTENT_DB_STORAGE_PER_FIELD; + + // Update the schema for prior instances to adapt to the change in db storage. + foreach ($prior_instances as $instance) { + if ($instance['type_name'] != $field['type_name']) { + $new_instance = $instance; + $new_instance['db_storage'] = CONTENT_DB_STORAGE_PER_FIELD; + + // Invoke hook_content_fieldapi(). + module_invoke_all('content_fieldapi', 'update instance', $new_instance); + + content_alter_schema($instance, $new_instance); + } + } + } + + // Invoke hook_content_fieldapi(). + module_invoke_all('content_fieldapi', 'update instance', $field); + + // Update the field and the instance with the latest values. + _content_field_write($field, 'update'); + _content_field_instance_write($field, 'update'); + + content_alter_schema($prev_field, $field); + + if ($rebuild) { + content_clear_type_cache(TRUE); + + // The label is in the menu tree, so we need a menu rebuild + // if the label changes. + if ($prev_field['widget']['label'] != $field['widget']['label']) { + menu_rebuild(); + } + } + + return $field; +} + +/** + * Write a field record. + * + * @param $field + * The field array to process. + */ +function _content_field_write($field, $op = 'update') { + // Rearrange the data to create the global_settings array. + $field['global_settings'] = array(); + $setting_names = (array) module_invoke($field['module'], 'field_settings', 'save', $field); + drupal_alter('field_settings', $setting_names, 'save', $field); + + foreach ($setting_names as $setting) { + // Unlike _content_field_instance_write() and 'widget_settings', 'global_settings' + // is never preexisting, so we take no particular precautions here. + $field['global_settings'][$setting] = isset($field[$setting]) ? $field[$setting] : ''; + unset($field[$setting]); + } + // 'columns' is a reserved word in MySQL4, so our column is named 'db_columns'. + $field['db_columns'] = $field['columns']; + + switch ($op) { + case 'create': + drupal_write_record(content_field_tablename(), $field); + break; + case 'update': + drupal_write_record(content_field_tablename(), $field, 'field_name'); + break; + } + unset($field['db_columns']); + return $field; +} + +/** + * Write a field instance record. + * + * @param $field + * The field array to process. + */ +function _content_field_instance_write($field, $op = 'update') { + // Collapse the field => widget format, so that the values to be saved by + // drupal_write_record are on top-level. + $field = content_field_instance_collapse($field); + + // Rearrange the data to create the widget_settings array. + $setting_names = (array) module_invoke($field['widget_module'], 'widget_settings', 'save', $field); + drupal_alter('widget_settings', $setting_names, 'save', $field); + foreach ($setting_names as $setting) { + // In some cases (when the updated $field was originally read from + // the db, as opposed to gathered from the values of a form), the values + // are already in the right place, we take care to not wipe them. + if (isset($field[$setting])) { + $field['widget_settings'][$setting] = $field[$setting]; + unset($field[$setting]); + } + } + + switch ($op) { + case 'create': + drupal_write_record(content_instance_tablename(), $field); + break; + case 'update': + drupal_write_record(content_instance_tablename(), $field, array('field_name', 'type_name')); + break; + } + return $field; +} + +/** + * Load a field instance. + * + * @param $param + * An array of properties to use in selecting a field instance. Valid keys: + * - 'type_name' - The name of the content type in which the instance exists. + * - 'field_name' - The name of the field whose instance is to be loaded. + * if NULL, all instances will be returned. + * @param $include_inactive + * TRUE will return field instances that are 'inactive', because their field + * module or widget module is currently disabled. + * @return + * The field arrays. + */ +function content_field_instance_read($param = NULL, $include_inactive = FALSE) { + $cond = array(); + $args = array(); + if (is_array($param)) { + // Turn the conditions into a query. + foreach ($param as $key => $value) { + $cond[] = 'nfi.'. db_escape_string($key) ." = '%s'"; + $args[] = $value; + } + } + if (!$include_inactive) { + $cond[] = 'nf.active = 1'; + $cond[] = 'nfi.widget_active = 1'; + } + $where = $cond ? ' WHERE '. implode(' AND ', $cond) : ''; + + $db_result = db_query("SELECT * FROM {". content_instance_tablename() ."} nfi ". + " JOIN {". content_field_tablename() ."} nf ON nfi.field_name = nf.field_name ". + "$where ORDER BY nfi.weight ASC, nfi.label ASC", $args); + + $fields = array(); + while ($instance = db_fetch_array($db_result)) { + // Unserialize arrays. + foreach (array('widget_settings', 'display_settings', 'global_settings', 'db_columns') as $key) { + $instance[$key] = (!empty($instance[$key])) ? (array) unserialize($instance[$key]) : array(); + } + // 'columns' is a reserved word in MySQL4, so our column is named 'db_columns'. + $instance['columns'] = $instance['db_columns']; + unset($instance['db_columns']); + + // Unfold 'global_settings'. + foreach ($instance['global_settings'] as $key => $value) { + $instance[$key] = $value; + } + unset($instance['global_settings']); + + // Put the field in the $field => 'widget' structure that is used + // all around content.module. + $field = content_field_instance_expand($instance); + + // Invoke hook_content_fieldapi(). + module_invoke_all('content_fieldapi', 'read instance', $field); + $fields[] = $field; + } + return $fields; +} + +/** + * Delete an existing field instance. + * + * @param $field_name + * The field name to delete. + * @param $type_name + * The content type where the field instance is going to be deleted. + * @param $rebuild + * TRUE to clear content type caches and rebuild menu (default). + * FALSE allows the caller to process several fields at a time quickly, but then + * the caller is reponsible to clear content type caches and rebuild menu as soon + * as all fields have been processed. For example: + * @code + * // Delete several fields at a time. + * foreach ($fields as $field) { + * content_field_instance_delete($field['field_name'], $type_name, FALSE); + * } + * // Clear caches and rebuild menu. + * content_clear_type_cache(TRUE); + * menu_rebuild(); + * @endcode + * @see content_clear_type_cache() + * @see menu_rebuild() + */ +function content_field_instance_delete($field_name, $type_name, $rebuild = TRUE) { + include_once('./'. drupal_get_path('module', 'content') .'/includes/content.admin.inc'); + + // Get the previous field value. + $field = array_pop(content_field_instance_read(array('field_name' => $field_name, 'type_name' => $type_name))); + + // Invoke hook_content_fieldapi(). + module_invoke_all('content_fieldapi', 'delete instance', $field); + + db_query("DELETE FROM {". content_instance_tablename() . + "} WHERE field_name = '%s' AND type_name = '%s'", $field['field_name'], $field['type_name']); + + // If no instances remain, delete the field entirely. + $instances = content_field_instance_read(array('field_name' => $field_name)); + if (sizeof($instances) < 1) { + db_query("DELETE FROM {". content_field_tablename() ."} WHERE field_name = '%s'", $field['field_name']); + content_alter_schema($field, array()); + } + // If only one instance remains, we may need to change the database + // representation for this field. + elseif (sizeof($instances) == 1 && !($field['multiple'])) { + // Multiple-valued fields are always stored per-field-type. + $instance = $instances[0]; + $new_instance = $instance; + $new_instance['db_storage'] = CONTENT_DB_STORAGE_PER_CONTENT_TYPE; + _content_field_write($new_instance, 'update'); + + content_alter_schema($instance, $new_instance); + } + + // If the deleted instance was the last field for the content type, + // we drop the per-type table. We also consider possibly inactive fields. + if (!content_field_instance_read(array('type_name' => $field['type_name']), TRUE)) { + $base_tablename = _content_tablename($field['type_name'], CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + if (db_table_exists($base_tablename)) { + db_drop_table($ret, $base_tablename); + } + } + + if ($rebuild) { + content_clear_type_cache(TRUE); + menu_rebuild(); + } + + return $field; +} + +/** + * Delete all data related to a module. + * + * @param string $module + */ +function content_module_delete($module) { + // Delete the field data. + // If content module has been uninstalled first, all tables + // have already been dropped, and running that code will raise errors. + if (db_table_exists(content_instance_tablename())) { + $results = db_query("SELECT field_name, type_name FROM {". content_instance_tablename() ."} WHERE widget_module = '%s'", $module); + while ($field = db_fetch_array($results)) { + content_field_instance_delete($field['field_name'], $field['type_name'], FALSE); + } + // Force the caches and static arrays to update to the new info. + content_clear_type_cache(TRUE); + menu_rebuild(); + } +} + +/** + * Make changes needed when a content type is created. + * + * @param $info + * value supplied by hook_node_type() + * + * node_get_types() is still missing the new type at this point due to + * a static caching bug. We ask it to rebuild its cache so that + * content_clear_type_cache() can do its job properly. + */ +function content_type_create($info) { + node_get_types(NULL, NULL, TRUE); + content_clear_type_cache(TRUE); +} + +/** + * Make changes needed when an existing content type is updated. + * + * @param $info + * value supplied by hook_node_type() + */ +function content_type_update($info) { + if (!empty($info->old_type) && $info->old_type != $info->type) { + // Rename the content type in all fields that use changed content type. + db_query("UPDATE {". content_instance_tablename() ."} SET type_name='%s' WHERE type_name='%s'", array($info->type, $info->old_type)); + + // Rename the content fields table to match new content type name. + $old_type = content_types($info->old_type); + $old_name = _content_tablename($old_type['type'], CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + $new_name = _content_tablename($info->type, CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + if (db_table_exists($old_name)) { + $ret = array(); + db_rename_table($ret, $old_name, $new_name); + watchdog('content', 'Content fields table %old_name has been renamed to %new_name and field instances have been updated.', array( + '%old_name' => $old_name, '%new_name' => $new_name)); + } + + // Rename the variable storing weights for non-CCK fields. + if ($extra = variable_get('content_extra_weights_'. $info->old_type, array())) { + variable_set('content_extra_weights_'. $info->type, $extra); + variable_del('content_extra_weights_'. $info->old_type); + } + } + + // Reset all content type info. + // Menu needs to be rebuilt as well, but node types need to be rebuilt first. + // node_type_form_submit() takes care of this. + content_clear_type_cache(TRUE); +} + +/** + * Make changes needed when a content type is deleted. + * + * @param $info + * value supplied by hook_node_type() + * + * TODO should we skip doing this entirely since core leaves the + * nodes in the database as orphans and wait until the nodes are + * deleted to respond? + * + */ +function content_type_delete($info) { + // Don't delete data for content-types defined by disabled modules. + if (!empty($info->disabled)) { + return; + } + + // TODO : What about inactive fields ? + // Currently, content_field_instance_delete doesn't work on those... + $fields = content_field_instance_read(array('type_name' => $info->type)); + foreach ($fields as $field) { + content_field_instance_delete($field['field_name'], $info->type, FALSE); + } + $table = _content_tablename($info->type, CONTENT_DB_STORAGE_PER_CONTENT_TYPE); + if (db_table_exists($table)) { + $ret = array(); + db_drop_table($ret, $table); + watchdog('content', 'The content fields table %name has been deleted.', array('%name' => $table)); + } + // Menu needs to be rebuilt as well, but node types need to be rebuilt first. + // node_type_form_submit() takes care of this. + content_clear_type_cache(TRUE); +} diff --git a/sites/all/modules/cck/includes/content.devel.inc b/sites/all/modules/cck/includes/content.devel.inc new file mode 100644 index 0000000..c70a5a2 --- /dev/null +++ b/sites/all/modules/cck/includes/content.devel.inc @@ -0,0 +1,217 @@ +type; + $type = content_types($type_name); + $field_types = _content_field_types(); + + if (!empty($type['fields'])) { + foreach ($type['fields'] as $field) { + $node_field = array(); + // If module handles own multiples, then only call its hook once. + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_MODULE) { + $max = 0; + } + else { + switch ($field['multiple']) { + case 0: + $max = 0; + break; + case 1: + $max = rand(0, 3); //just an arbitrary number for 'unlimited' + break; + default: + $max = $field['multiple']; + break; + } + } + for ($i = 0; $i <= $max; $i++) { + $module = $field_types[$field['type']]['module']; + $function = $module .'_content_generate'; + if (function_exists($function)) { + $result = $function($node, $field); // $items, $teaser, $page + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_MODULE) { + // Fields that handle their own multiples will add their own deltas. + $node_field = $result; + } + else { + // When multiples are handled by the content module, add a delta for each result. + $node_field[$i] = $result; + } + } + } + $node->{$field['field_name']} = $node_field; + } + } +} + +/** + * A simple function to return multiple values for fields that use + * custom multiple value widgets but don't need any other special multiple + * values handling. This will call the field generation function + * a random number of times and compile the results into a node array. + */ +function content_devel_multiple($function, $node, $field) { + $node_field = array(); + if (function_exists($function)) { + switch ($field['multiple']) { + case 0: + $max = 0; + break; + case 1: + $max = rand(0, 3); //just an arbitrary number for 'unlimited' + break; + default: + $max = $field['multiple']; + break; + } + for ($i = 0; $i <= $max; $i++) { + $node_field[$i] = $function($node, $field); + } + } + return $node_field; +} + +if (module_exists('text')) { + function text_content_generate($node, $field) { + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_MODULE) { + return content_devel_multiple('_text_content_generate', $node, $field); + } + else { + return _text_content_generate($node, $field); + } + } + + function _text_content_generate($node, $field) { + $node_field = array(); + if ($field['widget']['type'] == 'text_textarea') { + $format = $field['text_processing'] ? rand(0, 3) : 0; + $node_field['value'] = devel_create_content($format); + $node_field['format'] = $format; + } + else { + $allowed_values = content_allowed_values($field); + if (!empty($allowed_values)) { + // Just pick one of the specified allowed values. + $node_field['value'] = array_rand($allowed_values); + } + else { + // Generate a value that respects max_length. + if (empty($field['max_length'])) { + $field['max_length'] = 12; + } + $node_field['value'] = user_password($field['max_length']); + } + } + return $node_field; + } +} + +if (module_exists('number')) { + function number_content_generate($node, $field) { + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_MODULE) { + return content_devel_multiple('_number_content_generate', $node, $field); + } + else { + return _number_content_generate($node, $field); + } + } + + function _number_content_generate($node, $field) { + $node_field = array(); + $allowed_values = content_allowed_values($field); + if (!empty($allowed_values)) { + // Just pick one of the specified allowed values. + $node_field['value'] = array_rand($allowed_values); + } + else { + $min = is_numeric($field['min']) ? $field['min'] : 0; + switch ($field['type']) { + case 'number_integer': + $max = is_numeric($field['max']) ? $field['max'] : 10000; + $decimal = 0; + $scale = 0; + break; + + case 'number_decimal': + $precision = is_numeric($field['precision']) ? $field['precision'] : 10; + $scale = is_numeric($field['scale']) ? $field['scale'] : 2; + $max = is_numeric($field['max']) ? $field['max'] : pow(10, ($precision - $scale)); + $decimal = rand(0, (10 * $scale)) / 100; + break; + + case 'number_float': + $precision = rand(10, 32); + $scale = rand(0, 2); + $decimal = rand(0, (10 * $scale)) / 100; + $max = is_numeric($field['max']) ? $field['max'] : pow(10, ($precision - $scale)); + break; + } + $node_field['value'] = round((rand($min, $max) + $decimal), $scale); + } + return $node_field; + } +} + +if (module_exists('nodereference')) { + function nodereference_content_generate($node, $field) { + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_MODULE) { + return content_devel_multiple('_nodereference_content_generate', $node, $field); + } + else { + return _nodereference_content_generate($node, $field); + } + } + + function _nodereference_content_generate($node, $field) { + $node_field = array(); + $allowed_values = nodereference_allowed_values($field); + unset($allowed_values[0]); + if (!empty($allowed_values)) { + // Just pick one of the specified allowed values. + $node_field['nid'] = array_rand($allowed_values); + } + return $node_field; + } +} + +if (module_exists('userreference')) { + function userreference_content_generate($node, $field) { + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_MODULE) { + return content_devel_multiple('_userreference_content_generate', $node, $field); + } + else { + return _userreference_content_generate($node, $field); + } + } + + function _userreference_content_generate($node, $field) { + $node_field = array(); + $allowed_values = userreference_allowed_values($field); + if (isset($allowed_values['none'])) { + unset($allowed_values['none']); + } + if (!empty($allowed_values)) { + // Just pick one of the specified allowed values. + $node_field['uid'] = array_rand($allowed_values); + } + return $node_field; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/content.diff.inc b/sites/all/modules/cck/includes/content.diff.inc new file mode 100644 index 0000000..2b6243d --- /dev/null +++ b/sites/all/modules/cck/includes/content.diff.inc @@ -0,0 +1,127 @@ +type)) { + $type = content_types($new_node->type); + $field_types = _content_field_types(); + foreach ($type['fields'] as $field) { + // Ignore fields the current user is not allowed to view. + if (!content_access('view', $field, NULL, $new_node)) { + continue; + } + $function = $field_types[$field['type']]['module'] . '_content_diff_values'; + $function = function_exists($function) ? $function : 'content_content_diff_values'; + $old_values = array(); + $new_values = array(); + if (isset($old_node->$field['field_name'])) { + $old_values = $function($old_node, $field, $old_node->$field['field_name']); + } + if (isset($new_node->$field['field_name'])) { + $new_values = $function($new_node, $field, $new_node->$field['field_name']); + } + if ($old_values || $new_values) { + $result[$field['field_name']] = array( + '#name' => $field['widget']['label'], + '#old' => $old_values, + '#new' => $new_values, + '#weight' => $field['widget']['weight'], + '#format' => array( + 'show_header' => FALSE, + ), + ); + } + } + } + return $result; +} + +/** + * Default 'implementation' of hook_content_diff_values. + * + * Note that diff.module takes care of running check_plain on the output. + */ +function content_content_diff_values($node, $field, $items) { + $return = array(); + foreach ($items as $item) { + foreach (explode("\n", $item['value']) as $i) { + $return[] = $i; + } + } + return $return; +} + +if (module_exists('userreference')) { + /** + * Implementation of hook_content_diff_values. + */ + function userreference_content_diff_values($node, $field, $items) { + static $titles = array(); + // Gather ids. + $ids = array(); + foreach ($items as $item) { + if ($item['uid'] && is_numeric($item['uid'])) { + $ids[] = $item['uid']; + } + } + // Fetch titles we don't know yet. + $queried_ids = array_diff($ids, array_keys($titles)); + if ($queried_ids) { + $result = db_query('SELECT uid, name FROM {users} WHERE uid IN ('. db_placeholders($queried_ids) .')', $queried_ids); + while ($row = db_fetch_array($result)) { + $titles[$row['uid']] = $row['name']; + } + } + // Return result. + $return = array(); + foreach ($items as $item) { + if ($item['uid'] && isset($titles[$item['uid']])) { + $return[] = $titles[$item['uid']]; + } + } + return $return; + } +} + +if (module_exists('nodereference')) { + /** + * Implementation of hook_content_diff_values. + */ + function nodereference_content_diff_values($node, $field, $items) { + static $titles = array(); + // Gather ids. + $ids = array(); + foreach ($items as $item) { + if ($item['nid'] && is_numeric($item['nid'])) { + $ids[] = $item['nid']; + } + } + // Fetch titles we don't know yet. + $queried_ids = array_diff($ids, array_keys($titles)); + if ($queried_ids) { + $result = db_query('SELECT nid, title FROM {node} WHERE nid IN ('. db_placeholders($queried_ids) .')', $queried_ids); + while ($row = db_fetch_array($result)) { + $titles[$row['nid']] = $row['title']; + } + } + // Return result. + $return = array(); + foreach ($items as $item) { + if ($item['nid'] && isset($titles[$item['nid']])) { + $return[] = $titles[$item['nid']]; + } + } + return $return; + } +} diff --git a/sites/all/modules/cck/includes/content.node_form.inc b/sites/all/modules/cck/includes/content.node_form.inc new file mode 100644 index 0000000..7f5b166 --- /dev/null +++ b/sites/all/modules/cck/includes/content.node_form.inc @@ -0,0 +1,520 @@ + $field) { + $form['#field_info'][$field['field_name']] = $field; + $form += (array) content_field_form($form, $form_state, $field); + } + return $form; +} + +/** + * Create a separate form element for each field. + * + * // TODO: $count param ? not used anymore ? + * Hook_widget() picks up two new values, $count and $delta, to help + * widgets know what information to return since multiple values are + * sometimes controlled by the content module. + * + * @param $form + * the form to add this field element to + * @param $form_state + * the form_state for the above form + * @param $field + * the field array to use to create the form element + * @param $get_delta + * use to get only a specific delta value of a multiple value field, otherwise + * function will return the entire $field form element. + */ +function content_field_form(&$form, &$form_state, $field, $get_delta = NULL) { + $form['#cache'] = FALSE; + $node = $form['#node']; + $addition = array(); + $form_element = array(); + $field_name = $field['field_name']; + + $items = array(); + + // TODO: is the "if (function_exists($function)) {" needed ? + // defining the $function here makes it unclear where it is actually called + $function = $field['widget']['module'] .'_widget'; + if (function_exists($function)) { + // Prepare the values to be filled in the widget. + // We look in the following places: + // - Form submitted values + // - Node values (when editing an existing node), or pre-filled values (when + // creating a new node translation) + // - Default values set for the field (when creating a new node). + if (!empty($form_state['values'][$field['field_name']])) { + $items = $form_state['values'][$field['field_name']]; + // If there was an AHAH add more button in this field, don't save it. + unset($items[$field['field_name'] .'_add_more']); + } + elseif (!empty($node->$field['field_name'])) { + $items = $node->$field['field_name']; + } + elseif (empty($node->nid)) { + if (content_callback('widget', 'default value', $field) != CONTENT_CALLBACK_NONE) { + // If a module wants to insert custom default values here, + // it should provide a hook_default_value() function to call, + // otherwise the content module's content_default_value() function + // will be used. + $callback = content_callback('widget', 'default value', $field) == CONTENT_CALLBACK_CUSTOM ? $field['widget']['module'] .'_default_value' : 'content_default_value'; + if (function_exists($callback)) { + $items = $callback($form, $form_state, $field, 0); + } + } + } + + // See if access to this form element is restricted, + // if so, skip widget processing and just set the value. + $access = content_access('edit', $field, NULL, $node); + if (!$access) { + $addition[$field_name] = array( + '#access' => $access, + '#type' => 'value', + '#value' => $items, + ); + return $addition; + } + + // If content module handles multiple values for this form element, + // and not selecting an individual $delta, process the multiple value form. + if (!isset($get_delta) && content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + $form_element = content_multiple_value_form($form, $form_state, $field, $items); + } + // If the widget is handling multiple values (e.g optionwidgets), + // or selecting an individual element, just get a single form + // element and make it the $delta value. + else { + $delta = isset($get_delta) ? $get_delta : 0; + if ($element = $function($form, $form_state, $field, $items, $delta)) { + $title = check_plain(t($field['widget']['label'])); + $description = content_filter_xss(t($field['widget']['description'])); + $defaults = array( + '#required' => $get_delta > 0 ? FALSE : $field['required'], + '#columns' => array_keys($field['columns']), + '#title' => $title, + '#description' => $description, + '#delta' => $delta, + '#field_name' => $field['field_name'], + '#type_name' => $field['type_name'], + ); + // If we're processing a specific delta value for a field where the + // content module handles multiples, set the delta in the result. + // For fields that handle their own processing, we can't make assumptions + // about how the field is structured, just merge in the returned value. + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + $form_element[$delta] = array_merge($element, $defaults); + } + else { + $form_element = array_merge($element, $defaults); + } + } + } + + // Field name is needed at top level as well as the individual elements + // so the multiple values or other field level theme or processing can find it. + if ($form_element) { + $defaults = array( + '#field_name' => $field['field_name'], + '#tree' => TRUE, + '#weight' => $field['widget']['weight'], + '#access' => $access, + // TODO: what's the need for #count ? does not seem to be used anywhere ? + '#count' => count($form_element), + ); + $addition[$field['field_name']] = array_merge($form_element, $defaults); + } + } + return $addition; +} + +/** + * Special handling to create form elements for multiple values. + * + * Handles generic features for multiple fields: + * - number of widgets + * - AHAH-'add more' button + * - drag-n-drop value reordering + */ +function content_multiple_value_form(&$form, &$form_state, $field, $items) { + $field_name = $field['field_name']; + + switch ($field['multiple']) { + case 0: + $deltas = array(0); + $max = 0; + break; + + case 1: + $deltas = array_keys($items); + $current_item_count = max(1, (isset($form_state['item_count'][$field_name]) ? $form_state['item_count'][$field_name] : count($deltas))); + $max = (!empty($deltas) ? max($deltas) : -1); + while (count($deltas) < $current_item_count) { + $max++; + $deltas[] = $max; + } + break; + + default: + $max = $field['multiple'] - 1; + $deltas = range(0, $max); + break; + } + + $title = check_plain(t($field['widget']['label'])); + $description = content_filter_xss(t($field['widget']['description'])); + + $form_element = array( + '#theme' => 'content_multiple_values', + '#title' => $title, + '#required' => $field['required'], + '#description' => $description, + ); + $function = $field['widget']['module'] .'_widget'; + + foreach ($deltas as $delta) { + if ($element = $function($form, $form_state, $field, $items, $delta)) { + $defaults = array( + '#title' => ($field['multiple'] >= 1) ? '' : $title, + '#description' => ($field['multiple'] >= 1) ? '' : $description, + '#required' => ($field['multiple'] == 0 ? $field['required'] : FALSE), + '#weight' => $delta, + '#delta' => $delta, + '#columns' => array_keys($field['columns']), + '#field_name' => $field_name, + '#type_name' => $field['type_name'], + ); + + // Add an input field for the delta (drag-n-drop reordering), which will + // be hidden by tabledrag js behavior. + if ($field['multiple'] >= 1) { + // We name the element '_weight' to avoid clashing with column names + // defined by field modules. + $element['_weight'] = array( + '#type' => 'weight', + '#delta' => $max, // this 'delta' is the 'weight' element's property + '#default_value' => isset($items[$delta]['_weight']) ? $items[$delta]['_weight'] : $delta, + '#weight' => 100, + ); + } + + // Add a checkbox to allow users remove a single delta item. + // See content_set_empty() and theme_content_multiple_values(). + if ($field['multiple'] == 1) { + // We name the element '_remove' to avoid clashing with column names + // defined by field modules. + $element['_remove'] = array( + '#type' => 'checkbox', + '#attributes' => array('class' => 'content-multiple-remove-checkbox'), + '#default_value' => isset($items[$delta]['_remove']) ? $items[$delta]['_remove'] : 0, + ); + } + + $form_element[$delta] = array_merge($element, $defaults); + } + } + + // Add an #after_build callback to prevent validation of fields that are + // flagged for removal and enforce field requirement settings. + if ($field['multiple'] >= 1) { + $form_element['#after_build'] = array('content_multiple_value_after_build_proxy'); + } + + // Add AHAH add more button, if not working with a programmed form. + if ($field['multiple'] == 1 && empty($form['#programmed'])) { + // Make sure the form is cached so ahah can work. + $form['#cache'] = TRUE; + $content_type = content_types($field['type_name']); + $field_name_css = str_replace('_', '-', $field_name); + + $form_element[$field_name .'_add_more'] = array( + '#type' => 'submit', + '#name' => $field_name .'_add_more', + '#value' => t('Add another item'), + '#weight' => $field['widget']['weight'] + $max + 1, + // Submit callback for disabled JavaScript. drupal_get_form() might get + // the form from the cache, so we can't rely on content_form_alter() + // including this file. Therefore, call a proxy function to do this. + '#submit' => array('content_add_more_submit_proxy'), + '#ahah' => array( + 'path' => 'content/js_add_more/'. $content_type['url_str'] .'/'. $field_name, + 'wrapper' => $field_name_css .'-items', + 'method' => 'replace', + 'effect' => 'fade', + ), + // When JS is disabled, the content_add_more_submit handler will find + // the relevant field using these entries. + '#field_name' => $field_name, + '#type_name' => $field['type_name'], + ); + + // Add wrappers for the fields and 'more' button. + $form_element['#prefix'] = '
'; + $form_element['#suffix'] = '
'; + $form_element[$field_name .'_add_more']['#prefix'] = '
'; + $form_element[$field_name .'_add_more']['#suffix'] = '
'; + } + + return $form_element; +} + +/** + * After build callback for multiple value fields. + */ +function content_multiple_value_after_build($elements, &$form_state) { + $items_map = array(); + + foreach (element_children($elements) as $delta) { + // Find delta items for this field when the form if being processed for validation. + if (isset($elements[$delta]) && $elements[$delta] && is_numeric($delta) && !empty($elements[$delta]['#needs_validation'])) { + + // Find items that have been flagged for removal. + if (isset($elements[$delta]['_remove']) && !empty($elements[$delta]['_remove']['#value'])) { + + // Update the value in the #post attribute of the elements. + $post = &$elements[$delta]['#post']; + foreach ($elements[$delta]['#parents'] as $name) { + $post = &$post[$name]; + } + $post = array('_weight' => $elements[$delta]['_weight']['#value'], '_remove' => 1); + + // Alter the value of this element and children recursively. + content_multiple_value_after_build_recursive($elements[$delta], $elements[$delta]['#post']); + + $items_map[$delta] = TRUE; + } + else { + $items_map[$delta] = FALSE; + } + } + } + + // If the multiple values field is required, then make sure there's at + // least one item not flagged for removal. This is necessary to point + // the user to the correct form element when the validation error is + // issued from content_multiple_value_nodeapi_validate(). + $items_count = count($items_map); + if (!empty($elements['#required']) && $items_count > 0) { + // If the number of removed items is equal to the total number of + // items, then we'll reset the '_remove' flag of the first item, and + // that will be used to point the user when the required field error + // is issued by content_multiple_value_nodeapi_validate(). + if ($items_count == count(array_filter($items_map))) { + $delta = key($items_map); + if (isset($elements[$delta]['_remove'])) { + $elements[$delta]['_remove']['#value'] = 0; + } + } + } + + return $elements; +} + +/** + * Helper function to deal with items flagged for removal recursively. + */ +function content_multiple_value_after_build_recursive(&$elements, $post) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key] && !in_array($key, array('_weight', '_remove', '_error_element'))) { + // Recurse through all children elements. + content_multiple_value_after_build_recursive($elements[$key], $post); + } + } + + // Remove values for items flagged for removal. + if (isset($elements['#value'])) { + $elements['#value'] = NULL; + form_set_value($elements, NULL, $form_state); + $elements['#post'] = $post; + } +} + +/** + * Implementation of nodeapi('validate') for multiple value fields + * managed by content module itself. + */ +function content_multiple_value_nodeapi_validate(&$node, $field, &$items, $form) { + $field_name = $field['field_name']; + + // Getting the field structure from the form allows other modules alter + // field properties such as the required attribute. + $field = $form['#field_info'][$field_name]; + + // Get rid of the add more items element. + unset($items[$field_name .'_add_more']); + + // Reorder items to account for drag-n-drop reordering. + $items = _content_sort_items($field, $items); + + // Create a copy of the items before filtering those that are flagged + // for removal. We need this copy later to obtain the error element. + $items_copy = $items; + + // Filter out items flagged for removal. + $items = content_set_empty($field, $items); + + // Enforce field requirement settings. + if ($field['required'] && empty($node->_content_ignore_required_fields[$field_name]) && content_access('edit', $field, NULL, $node)) { + // Count non-empty items. + $count = 0; + $function = $field['module'] .'_content_is_empty'; + foreach ($items as $item) { + if (!$function($item, $field)) { + $count++; + } + } + // The field is required so we expect at least one non-empty item. + if ($count == 0) { + // Try to guess the element path in the form from the first item that + // is not flagged for removal. Defaults to first item. + $error_element_index = 0; + foreach ($items_copy as $index => $item) { + if (empty($item['_remove'])) { + $error_element_index = $index; + break; + } + } + $error_element = isset($items_copy[$error_element_index]) && is_array($items_copy[$error_element_index]) && isset($items_copy[$error_element_index]['_error_element']) ? $items_copy[$error_element_index]['_error_element'] : ''; + form_set_error($error_element, t('%name field is required.', array('%name' => t($field['widget']['label'])))); + } + } +} + +/** + * Submit handler to add more choices to a content form. This handler is used when + * JavaScript is not available. It makes changes to the form state and the + * entire form is rebuilt during the page reload. + */ +function content_add_more_submit($form, &$form_state) { + // Set the form to rebuild and run submit handlers. + node_form_submit_build_node($form, $form_state); + $field_name = $form_state['clicked_button']['#field_name']; + $type_name = $form_state['clicked_button']['#type_name']; + + // Make the changes we want to the form state. + if ($form_state['values'][$field_name][$field_name .'_add_more']) { + $form_state['item_count'][$field_name] = count($form_state['values'][$field_name]); + } +} + +/** + * Menu callback for AHAH addition of new empty widgets. + */ +function content_add_more_js($type_name_url, $field_name) { + $content_type = content_types($type_name_url); + $type = content_types($type_name_url); + $field = content_fields($field_name, $type['type']); + + if (($field['multiple'] != 1) || empty($_POST['form_build_id'])) { + // Invalid request. + drupal_json(array('data' => '')); + exit; + } + + // Retrieve the cached form. + $form_state = array('submitted' => FALSE); + $form_build_id = $_POST['form_build_id']; + $form = form_get_cache($form_build_id, $form_state); + if (!$form) { + // Invalid form_build_id. + drupal_json(array('data' => '')); + exit; + } + + // We don't simply return a new empty widget to append to existing ones, because + // - ahah.js won't simply let us add a new row to a table + // - attaching the 'draggable' behavior won't be easy + // So we resort to rebuilding the whole table of widgets including the existing ones, + // which makes us jump through a few hoops. + + // The form that we get from the cache is unbuilt. We need to build it so that + // _value callbacks can be executed and $form_state['values'] populated. + // We only want to affect $form_state['values'], not the $form itself + // (built forms aren't supposed to enter the cache) nor the rest of $form_data, + // so we use copies of $form and $form_data. + $form_copy = $form; + $form_state_copy = $form_state; + $form_copy['#post'] = array(); + form_builder($_POST['form_id'], $form_copy, $form_state_copy); + // Just grab the data we need. + $form_state['values'] = $form_state_copy['values']; + // Reset cached ids, so that they don't affect the actual form we output. + form_clean_id(NULL, TRUE); + + // Sort the $form_state['values'] we just built *and* the incoming $_POST data + // according to d-n-d reordering. + unset($form_state['values'][$field_name][$field['field_name'] .'_add_more']); + foreach ($_POST[$field_name] as $delta => $item) { + $form_state['values'][$field_name][$delta]['_weight'] = $item['_weight']; + $form_state['values'][$field_name][$delta]['_remove'] = isset($item['_remove']) ? $item['_remove'] : 0; + } + $form_state['values'][$field_name] = _content_sort_items($field, $form_state['values'][$field_name]); + $_POST[$field_name] = _content_sort_items($field, $_POST[$field_name]); + + // Build our new form element for the whole field, asking for one more element. + $delta = max(array_keys($_POST[$field_name])) + 1; + $form_state['item_count'] = array($field_name => count($_POST[$field_name]) + 1); + $form_element = content_field_form($form, $form_state, $field); + // Let other modules alter it. + // We pass an empty array as hook_form_alter's usual 'form_state' parameter, + // instead of $form_atate (for reasons we may never remember). + // However, this argument is still expected to be passed by-reference + // (and PHP5.3 will throw an error if it isn't.) This leads to: + $data = &$form_element; + $empty_form_state = array(); + $data['__drupal_alter_by_ref'] = array(&$empty_form_state); + drupal_alter('form', $data, 'content_add_more_js'); + + // Add the new element at the right place in the (original, unbuilt) form. + $success = content_set_nested_elements($form, $field_name, $form_element[$field_name]); + + // Save the new definition of the form. + $form_state['values'] = array(); + form_set_cache($form_build_id, $form, $form_state); + + // Build the new form against the incoming $_POST values so that we can + // render the new element. + $_POST[$field_name][$delta]['_weight'] = $delta; + $form_state = array('submitted' => FALSE); + $form += array( + '#post' => $_POST, + '#programmed' => FALSE, + ); + $form = form_builder($_POST['form_id'], $form, $form_state); + + // Render the new output. + $field_form = array_shift(content_get_nested_elements($form, $field_name)); + + // We add a div around the new content to receive the ahah effect. + $field_form[$delta]['#prefix'] = '
'. (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : ''); + $field_form[$delta]['#suffix'] = (isset($field_form[$delta]['#suffix']) ? $field_form[$delta]['#suffix'] : '') .'
'; + // Prevent duplicate wrapper. + unset($field_form['#prefix'], $field_form['#suffix']); + + // If a newly inserted widget contains AHAH behaviors, they normally won't + // work because AHAH doesn't know about those - it just attaches to the exact + // form elements that were initially specified in the Drupal.settings object. + // The new ones didn't exist then, so we need to update Drupal.settings + // by ourselves in order to let AHAH know about those new form elements. + $javascript = drupal_add_js(NULL, NULL); + $output_js = isset($javascript['setting']) ? '' : ''; + + $output = theme('status_messages') . drupal_render($field_form) . $output_js; + + // Using drupal_json() breaks filefield's file upload, because the jQuery + // Form plugin handles file uploads in a way that is not compatible with + // 'text/javascript' response type. + $GLOBALS['devel_shutdown'] = FALSE; + print drupal_to_js(array('status' => TRUE, 'data' => $output)); + exit; +} diff --git a/sites/all/modules/cck/includes/content.rules.inc b/sites/all/modules/cck/includes/content.rules.inc new file mode 100644 index 0000000..abf0f6a --- /dev/null +++ b/sites/all/modules/cck/includes/content.rules.inc @@ -0,0 +1,347 @@ + t('Populate a field'), + 'arguments' => array( + 'node' => array( + 'type' => 'node', + 'label' => t('Content'), + ), + ), + 'eval input' => array('code'), + 'help' => t('You should make sure that the used field exists in the given content type.'), + 'module' => 'CCK', + ); + return $info; +} + +/** + * Action: populate a field. + */ +function content_rules_action_populate_field($node, $settings, $element, &$state) { + // Get information about the field. + $field = content_fields($settings['field_name'], $node->type); + $value = _content_rules_get_field_value($settings, $state); + + if (!empty($field) && is_array($value)) { + $node->$settings['field_name'] = $value; + return array('node' => $node); + } +} + + +/** + * Action "populate a field" configuration form. + * This is a multistep form! + */ +function content_rules_action_populate_field_form($settings, &$form, &$form_state) { + $settings += array('field_name' => '', 'code' => '', 'value' => NULL); + if (empty($settings['field_name'])) { + $form['settings']['field_name'] = array( + '#type' => 'select', + '#title' => t('Field'), + '#options' => content_rules_get_field_names_by_type(), + '#default_value' => $settings['field_name'], + '#description' => t('Select the machine-name of the field.'), + '#required' => TRUE, + ); + // Hide some form elements in the first step. + $form['negate']['#access'] = FALSE; + $form['input_help']['#access'] = FALSE; + $form['weight']['#access'] = FALSE; + + // Replace the usual submit handlers with a own handler. + $form['submit']['#submit'] = array('content_rules_action_populate_field_form_step_submit'); + $form['submit']['#value'] = t('Continue'); + } + else { + // Show the fields form here. + module_load_include('inc', 'content', 'includes/content.node_form'); + $field = content_fields($settings['field_name']); + + $form['#node'] = (object)array('type' => '', $settings['field_name'] => $settings['value']); + $form['#field_info'][$field['field_name']] = $field; + // We can't put it into $form['settings'] as this would break AHAH callbacks + $form += (array) content_field_form($form, $form_state, $field); + $form[ $settings['field_name'] ]['#weight'] = 4; + + unset($form['#cache']); + + // Advanced: PHP code. + $form['advanced_options'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced: Specify the fields value with PHP code'), + '#collapsible' => TRUE, + '#collapsed' => empty($settings['code']), + '#weight' => 5, + ); + + $db_info = content_database_info($field); + $columns = array_keys($db_info['columns']); + foreach ($columns as $key => $column) { + $columns[$key] = t("'@column' => value for @column", array('@column' => $column)); + } + $sample = t("return array(\n 0 => array(@columns),\n // You'll usually want to stop here. Provide more values\n // if you want your 'default value' to be multi-valued:\n 1 => array(@columns),\n 2 => ...\n);", array('@columns' => implode(', ', $columns))); + + $form['advanced_options']['code'] = array( + '#type' => 'textarea', + '#title' => t('Code'), + '#default_value' => $settings['code'], + '#rows' => 6, + '#description' => t('Advanced usage only: PHP code that returns the value to set. Should not include <?php ?> delimiters. If this field is filled out, the value returned by this code will override any value specified above. Expected format:
!sample
Using devel.module\'s \'devel load\' tab on a content page might help you figure out the expected format.', array( + '!sample' => $sample, + '@link_devel' => 'http://www.drupal.org/project/devel', + )), + ); + + // Add this file to be included when the form is built by rules + // as it's needed by CCKs add more button. + // See rules_after_build_include_files(). + $form['#includes'][] = './'. drupal_get_path('module', 'node') .'/node.pages.inc'; + } +} + +function content_rules_action_populate_field_form_step_submit($form, &$form_state) { + $form_state['element']['#settings']['field_name'] = $form_state['values']['settings']['field_name']; +} + +/** + * Validate the chosen value or php code. + */ +function content_rules_action_populate_field_validate($form, &$form_state) { + if (!isset($form_state['element']['#settings']['field_name'])) { + //Just validate the last step. + return; + } + + if (isset($form_state['values']['code']) && ($php = $form_state['values']['code'])) { + if (strpos($php, 'return') === FALSE) { + form_set_error('code', t('You have to return the default value in the expected format.')); + } + } + else { + // Validate the field. + $settings = $form_state['element']['#settings']; + $field = content_fields($settings['field_name']); + $field_types = _content_field_types(); + $function = $field_types[$field['type']]['module'] .'_field'; + if (function_exists($function)) { + $form['#node'] = (object)array('type' => '', $settings['field_name'] => $form_state['values'][$settings['field_name']]); + $items = isset($form['#node']->$field['field_name']) ? $form['#node']->$field['field_name'] : array(); + + //Make sure AHAH 'add more' button isn't sent to the fields + // for processing. + unset($items[$field['field_name'] .'_add_more']); + + $function('validate', $form['#node'], $field, $items, $form, NULL); + content_field('validate', $form['#node'], $field, $items, $form, NULL); + } + } +} + +function content_rules_action_populate_field_submit(&$settings, $form, &$form_state) { + // Take over field values and filter out private properties added by CCK + $settings['value'] = array_filter($form_state['values'][$settings['field_name']], 'is_array'); + + foreach ($settings['value'] as $key => $data) { + foreach (array_filter(array_keys($data)) as $col) { + if ($col[0] == '_') { + unset($settings['value'][$key][$col]); + } + } + if ($key && count(array_filter($settings['value'][$key])) == 0) { + // For multi-valued fields don't check for any additional empty values. + unset($settings['value'][$key]); + } + } + + $settings['code'] = $form_state['values']['code']; + + if (function_exists('rules_action_custom_php_submit')) { + // Support adding variables to the php code, if php module is present. + rules_action_custom_php_submit($settings, $form, $form_state); + } + + // Add all values to the input evaluator, so that textfields / textares can + // make use of it. + $names = array('code'); + + foreach ($settings['value'] as $key => $data) { + foreach (array_filter($data, 'is_string') as $col => $value) { + $names[] = "value|$key|$col"; + } + } + $form_state['element']['#info']['eval input'] = $names; +} + + +/** + * Label callback: Improve the label of the action. + */ +function content_rules_action_populate_field_label($settings, $argument_labels) { + return t("Populate @node's field '@field'", array('@field' => $settings['field_name']) + $argument_labels); +} + +function workflow_ng_action_populate_field_upgrade(&$element) { + $element['#name'] = 'content_rules_action_populate_field'; + $element['#settings']['code'] = $element['#settings']['default_value_php']; + $element['#settings'][$element['#settings']['field_name']] = array(); + unset($element['#settings']['default_value_php']); +} + + +/** + * Implementation of hook_rules_condition_info(). + */ +function content_rules_condition_info() { + $info = array(); + $info['content_rules_field_has_value'] = array( + 'label' => t('Field has value'), + 'arguments' => array( + 'node' => array('type' => 'node', 'label' => t('Content')), + ), + 'eval input' => array('code'), + 'help' => t('You should make sure that the used field exists in the given content type. The condition returns TRUE, if the selected field has the given value.'), + 'module' => 'CCK', + ); + $info['content_rules_field_changed'] = array( + 'label' => t('Field has changed'), + 'arguments' => array( + 'node' => array('type' => 'node', 'label' => t('Content containing changes')), + 'node_unchanged' => array('type' => 'node', 'label' => t('Content not containing changes')), + ), + 'help' => t('You should make sure that the used field exists in the given content type.'), + 'module' => 'CCK', + ); + return $info; +} + +/** + * Condition: Check the value of a field. + */ +function content_rules_field_has_value($node, $settings) { + // Get information about the field. + $field = content_fields($settings['field_name'], $node->type); + $value = _content_rules_get_field_value($settings, $state); + + if (empty($field) || !is_array($value)) { + return FALSE; + } + + return _content_rules_field_has_value($node->$settings['field_name'], $value); +} + +/** + * Use the same configuration form as the "populate field" action. + */ +function content_rules_field_has_value_form($settings, &$form, &$form_state) { + content_rules_action_populate_field_form($settings, $form, $form_state); +} +function content_rules_field_has_value_validate($form, &$form_state) { + content_rules_action_populate_field_validate($form, $form_state); +} +function content_rules_field_has_value_submit(&$settings, $form, &$form_state) { + content_rules_action_populate_field_submit($settings, $form, $form_state); +} + +function content_rules_field_has_value_label($settings, $argument_labels) { + return t("@node's field '@field' has value", array('@field' => $settings['field_name']) + $argument_labels); +} + +/** + * Condition: Check if the field has changed. + */ +function content_rules_field_changed($node1, $node2, $settings) { + // Get information about the field. + $field = content_fields($settings['field_name'], $node1->type); + + return !empty($field) && !_content_rules_field_has_value($node1->$settings['field_name'], $node2->$settings['field_name']); +} + +function content_rules_field_changed_form($settings, &$form, &$form_state) { + $settings += array('field_name' => ''); + $form['settings']['field_name'] = array( + '#type' => 'select', + '#title' => t('Field'), + '#options' => content_rules_get_field_names_by_type(), + '#default_value' => $settings['field_name'], + '#description' => t('Select the machine-name of the field to look at.'), + '#required' => TRUE, + ); +} + +function content_rules_field_changed_label($settings, $argument_labels) { + return t("@node's field '@field' has been changed", array('@field' => $settings['field_name']) + $argument_labels); +} + + +/** + * Returns the fields of a given field type only. + * Suitable for using it with #options. + */ +function content_rules_get_field_names_by_type($type = NULL) { + $fields = array(); + foreach (content_fields() as $field) { + if (!isset($type) || $field['type'] == $type) { + $fields[$field['field_name']] = $field['field_name']; + } + } + asort($fields); + return $fields; +} + +function _content_rules_get_field_value($settings, &$state) { + if ($settings['code']) { + if (function_exists('rules_input_evaluator_php_apply')) { + // Support adding variables to the php code, if php module is present. + $value = rules_input_evaluator_php_apply($settings['code'], $settings['vars'], $state, FALSE); + } + else { + ob_start(); + $value = eval($settings['code']); + ob_end_clean(); + } + } + else { + $value = $settings['value']; + } + return $value; +} + +/** + * Checks whether both field values match in a robust way. + * + * It returns TRUE, only if the number of multiple values matches and + * each property of the cck field's value is the same in the node. + * + * @param $node_value The value present in the node. + * @param $value The value to check for. + */ +function _content_rules_field_has_value($node_value, $value) { + if (count($value) != count($node_value)) { + return FALSE; + } + // Loop over multiple fields + foreach ($value as $delta => $sub_value) { + // Check if all properties of the value are there in the node value too + if (is_array($sub_value) && is_array($node_value[$delta])) { + if (count(array_diff_assoc($sub_value, $node_value[$delta])) != 0) { + return FALSE; + } + } + elseif ($sub_value !== $node_value[$delta]) { + return FALSE; + } + } + return TRUE; +} diff --git a/sites/all/modules/cck/includes/content.token.inc b/sites/all/modules/cck/includes/content.token.inc new file mode 100644 index 0000000..9e4d4a0 --- /dev/null +++ b/sites/all/modules/cck/includes/content.token.inc @@ -0,0 +1,76 @@ + array( + 'title' => t('Token'), + 'build modes' => array( + 'token' => array( + 'title' => t('Token'), + 'views style' => FALSE, + ), + ), + ), + ); +} + +// Two helper functions that generate appropriate tokens for CCK-added fields. +function content_token_values($type, $object = NULL, $options = array()) { + $tokens = array(); + if ($type == 'node') { + // Prevent against invalid 'nodes' built by broken 3rd party code. + if (isset($object->type)) { + // Let PHP free the $node object when we are done. Working directly on the + // incoming $object causes memory leak issues on long-running scripts such + // as migrations. See http://drupal.org/node/736440. + $node = drupal_clone($object); + $content_type = content_types($node->type); + $node->build_mode = 'token'; + $node->content = array(); + content_view($node); + // The formatted values will only be known after the content has been rendered. + drupal_render($node->content); + content_alter($node); + + $field_types = _content_field_types(); + foreach ($content_type['fields'] as $field_name => $field) { + $items = isset($node->{$field_name}) ? $node->{$field_name} : array(); + $function = $field_types[$field['type']]['module'] . '_token_values'; + if (!empty($items) && function_exists($function)) { + $token_values = (array) $function('field', $items, $options); + foreach ($token_values as $token => $value) { + $tokens[$field_name .'-'. $token] = $value; + } + } + } + } + } + return $tokens; +} + +function content_token_list($type = 'all') { + if ($type == 'node' || $type == 'all') { + $list = array(); + $field_types = _content_field_types(); + + foreach (content_fields() as $field) { + $sub_list = array(); + $function = $field_types[$field['type']]['module'] . '_token_list'; + if (function_exists($function)) { + $sub_list = $function('field'); + foreach ($sub_list as $category => $token_list) { + foreach ($token_list as $token => $description) { + $list['CCK '. $category][$field['field_name'] .'-'. $token] = $description; + } + } + } + } + + return $list; + } +} diff --git a/sites/all/modules/cck/includes/panels/content_types/content_field.inc b/sites/all/modules/cck/includes/panels/content_types/content_field.inc new file mode 100644 index 0000000..72c1d35 --- /dev/null +++ b/sites/all/modules/cck/includes/panels/content_types/content_field.inc @@ -0,0 +1,214 @@ + t('Content field'), + 'defaults' => array('label' => '', 'formatter' => ''), + 'content type' => 'content_content_field_content_type_content_type', + ); +} + +/** + * Return all field content types available. + */ +function content_content_field_content_type_content_types() { + // This will hold all the individual field content types. + $types = array(); + + // Get all fields on the site. + $field_types = _content_field_types(); + + foreach (content_types() as $type_name => $type) { + foreach ($type['fields'] as $field_name => $field) { + if (!isset($types[$field_name])) { + $types[$field_name] = array( + 'category' => t('Node'), + 'icon' => 'icon_cck_field.png', + 'title' => t('Field: @widget_label (@field_name) - @field_type', array( + '@widget_label' => t($field['widget']['label']), + '@field_name' => $field_name, + '@field_type' => t($field_types[$field['type']]['label']), + )), + 'description' => t('Field on the referenced node.'), + 'types' => array(), + ); + if (isset($field_types[$field['type']]['content_icon'])) { + $types[$field_name]['icon'] = $field_types[$field['type']]['content_icon']; + } + } + $types[$field_name]['types'][$type_name] = $type['name']; + } + } + + // Create the required context for each field related to the content types. + foreach ($types as $field_name => $field_content_type) { + $types[$field_name]['required context'] = new ctools_context_required(t('Node'), 'node', array( + 'type' => array_keys($types[$field_name]['types']), + )); + unset($types[$field_name]['types']); + } + + return $types; +} + +/** + * Just one subtype. + * + * Ordinarily this function is meant to get just one subtype. However, we are + * using it to deal with the fact that we have changed the subtype names. This + * lets us translate the name properly. + */ +function content_content_field_content_type_content_type($subtype) { + // Previous versions of CCK included the content type as part of the subtype. + // This allows those to continue to sort of work, at least during render. + if (strpos($subtype, ':') !== FALSE) { + list($content_type, $subtype) = explode(':', $subtype, 2); + } + + $types = content_content_field_content_type_content_types(); + if (isset($types[$subtype])) { + return $types[$subtype]; + } +} + +/** + * Output function for the 'field' content type. + */ +function content_content_field_content_type_render($subtype, $conf, $panel_args, $context) { + // Previous versions of CCK included the content type as part of the subtype. + // This allows those to continue to sort of work, at least during render. + if (strpos($subtype, ':') !== FALSE) { + list($content_type, $subtype) = explode(':', $subtype, 2); + } + + if (is_array($context)) { + $context = array_pop($context); + } + // If we do not have a node, then we cannot generate output. + if (!isset($context->data)) { + return; + } + $node = drupal_clone($context->data); + + // Extract the node type from the node in context, the field name from the + // panels content type subtype, and get the content field structure. + $field_name = $subtype; + $field = content_fields($field_name, $node->type); + + // Get the formatter that was selected in the settings dialog. + $formatter = $conf['formatter']; + + // Check view access to the field. + if (!content_access('view', $field, NULL, $node)) { + return; + } + + // Force panel settings into the field's display settings. + $field['display_settings']['label']['format'] = $conf['label'] == 'normal' || !empty($conf['override_title']) ? 'hidden' : $conf['label']; + $field['display_settings']['full']['format'] = $formatter; + $node->build_mode = NODE_BUILD_NORMAL; + // TODO : allow panel-specific template suggestions for content-field.tpl.php ? + + $output = content_view_field($field, $node); + + $block = new stdClass(); + $block->module = 'content'; + $block->delta = $field_name; + if ($conf['label'] == 'normal') { + $block->title = t($field['widget']['label']); + } + $block->content = $output; + + return $block; +} + +/** + * Returns a settings form for the custom type. + */ +function content_content_field_content_type_edit_form(&$form, &$form_state) { + $conf = $form_state['conf']; + + $form['label'] = array( + '#type' => 'select', + '#title' => t('Field label'), + '#default_value' => isset($conf['label']) ? $conf['label'] : '', + '#options' => array( + 'normal' => t('Block title'), + 'above' => t('Above'), + 'inline' => t('Inline'), + ), + '#description' => t('Configure how the label is going to be displayed. This option takes no effect when "Override title" option is enabled, the specified block title is displayed instead.'), + ); + + // Extract the field name from the panels content type subtype. + $field_name = $form_state['subtype_name']; + + // Previous versions of CCK included the content type as part of the subtype. + // This allows those to continue to sort of work. + if (strpos($field_name, ':') !== FALSE) { + list($content_type, $field_name) = explode(':', $field_name, 2); + } + + // Get all the information about our field. + $field = content_fields($field_name); + + // Get information about all the field types on the site. + $field_types = _content_field_types(); + + // Get the information about the type that our field is. + $type_info = $field_types[$field['type']]; + + // Put the possible formatters for our type into an array. + $options = array(); + foreach ($type_info['formatters'] as $formatter_name => $formatter) { + $options[$formatter_name] = $formatter['label']; + } + + $form['formatter'] = array( + '#type' => 'select', + '#title' => t('Field formatter'), + '#default_value' => isset($conf['formatter']) ? $conf['formatter'] : 'default', + '#options' => $options, + '#description' => t('Select a formatter.'), + '#required' => TRUE, + ); +} + +function content_content_field_content_type_edit_form_submit(&$form, &$form_state) { + // Copy everything from our defaults. + foreach (array_keys($form_state['plugin']['defaults']) as $key) { + $form_state['conf'][$key] = $form_state['values'][$key]; + } +} + +/** + * Admin title for field content type. + */ +function content_content_field_content_type_admin_title($subtype, $conf, $context) { + // Previous versions of CCK included the content type as part of the subtype. + // This allows those to continue to sort of work, at least during render. + if (strpos($subtype, ':') !== FALSE) { + list($content_type, $subtype) = explode(':', $subtype, 2); + } + + // Get all fields on the site. + $field_types = _content_field_types(); + + // Get all the information about our field. + $field = content_fields($subtype); + + return t('"@s" field: @widget_label (@field_name) - @field_type', array( + '@s' => $context->identifier, + '@widget_label' => t($field['widget']['label']), + '@field_name' => $subtype, + '@field_type' => t($field_types[$field['type']]['label']), + )); +} diff --git a/sites/all/modules/cck/includes/panels/content_types/icon_cck_field.png b/sites/all/modules/cck/includes/panels/content_types/icon_cck_field.png new file mode 100644 index 0000000..d33aae5 Binary files /dev/null and b/sites/all/modules/cck/includes/panels/content_types/icon_cck_field.png differ diff --git a/sites/all/modules/cck/includes/views/content.views.inc b/sites/all/modules/cck/includes/views/content.views.inc new file mode 100644 index 0000000..bc6be2c --- /dev/null +++ b/sites/all/modules/cck/includes/views/content.views.inc @@ -0,0 +1,374 @@ + array( + 'path' => drupal_get_path('module', 'content') . '/includes/views/handlers', + ), + 'handlers' => array( + + // argument handlers + 'content_handler_argument' => array( + 'parent' => 'views_handler_argument', + ), + 'content_handler_argument_string' => array( + 'parent' => 'views_handler_argument_string', + ), + 'content_handler_argument_numeric' => array( + 'parent' => 'views_handler_argument_numeric', + ), + 'content_handler_argument_reference' => array( + 'parent' => 'content_handler_argument_numeric', + ), + 'content_handler_argument_many_to_one' => array( + 'parent' => 'views_handler_argument_many_to_one', + ), + + // field handlers + 'content_handler_field' => array( + 'parent' => 'views_handler_field_node', + ), + 'content_handler_field_multiple' => array( + 'parent' => 'content_handler_field', + ), + + // filter handlers + 'content_handler_filter_string' => array( + 'parent' => 'views_handler_filter_string', + ), + 'content_handler_filter_numeric' => array( + 'parent' => 'views_handler_filter_numeric', + ), + 'content_handler_filter_float' => array( + 'parent' => 'views_handler_filter_float', + ), + 'content_handler_filter_many_to_one' => array( + 'parent' => 'views_handler_filter_many_to_one', + ), + + // relationship handlers + 'content_handler_relationship' => array( + 'parent' => 'views_handler_relationship', + ), + + // sort handlers + 'content_handler_sort' => array( + 'parent' => 'views_handler_sort', + ), + ), + ); +} + +/** + * Implementation of hook_views_plugins. + * + * Defines some plugins used by the Views modes for + * nodereference and userreference. + */ +function content_views_plugins() { + $plugins = array( + 'module' => 'content', // This just tells our themes are elsewhere. + 'display' => array( + 'content_simple' => array( + 'path' => drupal_get_path('module', 'content') . '/includes/views/handlers', + // Those strings are not translated for now. + // We'll need to change that if / when we remove 'no ui' + 'title' => 'Simple', // TODO: better name ? (currently not displayed anyway) + 'help' => 'Destination-agnostic display. Mostly useful for programmatic views.', + 'handler' => 'content_plugin_display_simple', + 'no ui' => TRUE, // Programmatic use only. + 'uses hook menu' => FALSE, + 'use ajax' => FALSE, + 'use pager' => FALSE, + 'accept attachments' => FALSE, + ), + 'content_references' => array( + 'path' => drupal_get_path('module', 'content') . '/includes/views/handlers', + // Those strings are not translated for now. + // We'll need to change that if / when we remove 'no ui' + 'title' => 'Simple - for reference fields', // TODO: better name ? (currently not displayed anyway) + 'help' => 'Destination-agnostic display. Mostly useful for programmatic views.', + 'parent' => 'content_simple', + 'handler' => 'content_plugin_display_references', + 'no ui' => TRUE, // Programmatic use only. + 'uses hook menu' => FALSE, + 'use ajax' => FALSE, + 'use pager' => FALSE, + 'accept attachments' => FALSE, + ), + ), + 'style' => array( + 'content_php_array_autocomplete' => array( + 'path' => drupal_get_path('module', 'content') . '/includes/views/handlers', + // Those strings are not translated for now. + // We'll need to change that if / when we remove 'no ui' + 'title' => 'Results array (with title)', + 'help' => 'Returns the view as a PHP array of names + rendered rows.', + 'handler' => 'content_plugin_style_php_array_ac', + 'no ui' => TRUE, // Programmatic use only. + 'uses row plugin' => TRUE, + 'uses fields' => TRUE, + 'type' => 'content_simple', + 'even empty' => TRUE, + ), + ), + ); + return $plugins; +} + +/** + * Implementation of hook_views_data(). + * + * Exposes all fields to the views system. + */ +function content_views_data() { + $data = array(); + foreach (content_fields() as $field) { + $module = $field['module']; + $result = (array) module_invoke($module, 'field_settings', 'views data', $field); + drupal_alter('field_settings', $result, 'views data', $field); + if (empty($result)) { + $result = content_views_field_views_data($field); + } + if (is_array($result)) { + $data = array_merge($data, $result); + } + } + return $data; +} + + +function content_views_field_views_data($field) { + $field_types = _content_field_types(); + + // Check the field module is available. + // TODO: is this really how we should do it ? + if (isset($field_types[$field['type']])) { + $db_info = content_database_info($field); + + // Field modules that do not store data in the database + // should not create views data tables. + if (empty($db_info['columns'])) { + return; + } + + $table_alias = content_views_tablename($field); + + $types = array(); + foreach (content_types() as $type) { + if (isset($type['fields'][$field['field_name']])) { + // TODO : run check_plain here instead of on the imploded string below ? + $types[] = $type['name']; + } + } + + $data = array(); + $data['table']['group'] = t('Content'); + $data['table']['join']['node'] = array( + 'table' => $db_info['table'], + 'left_field' => 'vid', + 'field' => 'vid', + ); + $data['table']['join']['node_revisions'] = array( + 'table' => $db_info['table'], + 'left_field' => 'vid', + 'field' => 'vid', + ); + + // Build the list of columns enabled for default views integration. + $db_columns = array(); + $additional_fields = array(); + foreach ($db_info['columns'] as $column => $attributes) { + // Select explicitly enabled field columns. + if (!empty($attributes['views'])) { + $db_columns[$column] = $attributes; + } + // Ensure all columns are retrieved. + $additional_fields[$attributes['column']] = $attributes['column']; + } + // Pick up the first column when none has been explicitly enabled + // (pre CCK 2.2 backwards compatibility). + if (empty($db_columns)) { + // Can't use array_slice(), it won't work in PHP4 for assoc array. + foreach ($db_info['columns'] as $column => $attributes) { + $db_columns[$column] = $attributes; + break; + } + } + $columns = array(); + $db_fields = array(); + $arguments = array(); + $filters = array(); + foreach ($db_columns as $column => $attributes) { + $columns[] = $column; + $db_fields[] = $attributes['column']; + $sorts[] = !empty($attributes['sortable']) ? TRUE : FALSE; + + // Identify likely filters and arguments for each column based on field type. + switch ($attributes['type']) { + case 'int': + case 'mediumint': + case 'tinyint': + case 'bigint': + case 'serial': + $filters[] = 'content_handler_filter_numeric'; + $arguments[] = 'content_handler_argument_numeric'; + break; + case 'numeric': + case 'float': + $filters[] = 'content_handler_filter_float'; + $arguments[] = 'content_handler_argument_numeric'; + break; + + case 'text': + case 'blob': + // TODO add markup handlers for these types + default: + $filters[] = 'content_handler_filter_string'; + $arguments[] = 'content_handler_argument_string'; + break; + } + } + + // Provide automatic filters, sorts, and arguments for each column, not just the first. + $db_fields_count = count($db_fields); + foreach ($db_fields as $i => $db_field) { + $label_truncated = truncate_utf8(t($field['widget']['label']), 10, TRUE); + if ($db_fields_count == 1) { + $title = t('@label (!name)', array('@label' => t($field['widget']['label']), '!name' => $field['field_name'])); + $title_short = check_plain($label_truncated); + } + else { + $title = t('@label (!name) - !column', array('@label' => t($field['widget']['label']), '!name' => $field['field_name'], '!column' => $columns[$i])); + $title_short = t('@label-truncated - !column', array('@label-truncated' => $label_truncated, '!column' => $columns[$i])); + } + + $data[$db_field] = array( + 'group' => t('Content'), + 'title' => $title, + 'title short' => $title_short, + 'help' => t($field_types[$field['type']]['label']) .' - '. t('Appears in: @types', array('@types' => implode(', ', $types))), + ); + if ($i == 0) { + $data[$db_field]['field'] = array( + 'title' => t('@label (!name)', array('@label' => t($field['widget']['label']), '!name' => $field['field_name'])), + 'title short' => check_plain($label_truncated), + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => 'content_handler_field_multiple', + 'click sortable' => $sorts[$i], + 'additional fields' => $additional_fields, + 'content_field_name' => $field['field_name'], + 'access callback' => 'content_access', + 'access arguments' => array('view', $field), + ); + } + $data[$db_field]['argument'] = array( + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => $arguments[$i], + 'additional fields' => $additional_fields, + 'content_field_name' => $field['field_name'], + 'empty field name' => t(''), + ); + $data[$db_field]['filter'] = array( + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => $filters[$i], + 'additional fields' => $additional_fields, + 'content_field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + if (!empty($sorts[$i])) { + $data[$db_field]['sort'] = array( + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => 'content_handler_sort', + 'additional fields' => $additional_fields, + 'content_field_name' => $field['field_name'], + ); + } + } + + // Expose additional delta column for multiple value fields. + if ($field['multiple']) { + $title = t('@label (!name) - delta', array('@label' => t($field['widget']['label']), '!name' => $field['field_name'])); + $title_short = t('@label-truncated - delta', array('@label-truncated' => $label_truncated)); + + $db_field = 'delta'; + $data[$db_field] = array( + 'group' => t('Content'), + 'title' => $title, + 'title short' => $title_short, + 'help' => t('Delta - Appears in: @types', array('@types' => implode(', ', $types))), + ); + $data[$db_field]['field'] = array( + 'title' => $title, + 'title short' => $title_short, + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + 'additional fields' => $additional_fields, + 'access callback' => 'content_access', + 'access arguments' => array('view', $field), + ); + $data[$db_field]['argument'] = array( + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => 'views_handler_argument_numeric', + 'additional fields' => $additional_fields, + 'empty field name' => t(''), + ); + $data[$db_field]['filter'] = array( + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => 'views_handler_filter_numeric', + 'additional fields' => $additional_fields, + 'allow empty' => TRUE, + ); + $data[$db_field]['sort'] = array( + 'field' => $db_field, + 'table' => $db_info['table'], + 'handler' => 'views_handler_sort', + 'additional fields' => $additional_fields, + ); + } + + return array($table_alias => $data); + } +} + +/** + * Helper function so it is possible to change the Views tablename + * in the future without re-writing code. + */ +function content_views_tablename($field) { + return 'node_data_'. $field['field_name']; +} + +function theme_content_view_multiple_field($items, $field, $values) { + $output = ''; + $i = 0; + foreach ($items as $item) { + if (!empty($item) || $item == '0') { + $output .= '
'. $item .'
'; + $i++; + } + } + return $output; +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/content.views_convert.inc b/sites/all/modules/cck/includes/views/content.views_convert.inc new file mode 100644 index 0000000..9ee21e7 --- /dev/null +++ b/sites/all/modules/cck/includes/views/content.views_convert.inc @@ -0,0 +1,71 @@ +add_item() won't produce + * the right results, usually needed to set field options or values. + */ +function content_views_convert($display, $type, &$view, $views_field) { + static $views_fields; + + if (empty($views_fields)) { + $views_fields = array(); + $types = content_types(); + foreach ($types as $ctype) { + foreach ($ctype['fields'] as $field) { + $module = $field['module']; + $result = (array) module_invoke($module, 'field_settings', 'views data', $field); + drupal_alter('field_settings', $result, 'views data', $field); + if (empty($result)) { + // The views field name had the column name appended, + // like field_name_value or field_username_uid. + $column = array_shift(array_keys($field['columns'])); + $views_fields[$field['field_name'] .'_'. $column] = $field; + } + } + } + } + + // Is this a field that CCK should handle? If not, return. + if (!in_array($views_field['field'], array_keys($views_fields))) { + return; + } + + // Now update values, options, etc. to those selected in the imported view. + switch ($type) { + case 'field': + $view->display[$display]->display_options['fields'][$views_field['field']]['format'] = $views_field['options']; + if ($views_field['handler'] == 'content_views_field_handler_group') { + $view->display[$display]->display_options['fields'][$views_field['field']]['multiple']['group'] = 1; + } + else { + $view->display[$display]->display_options['fields'][$views_field['field']]['multiple']['group'] = 0; + } + return; + + case 'filter': + // TODO + return; + + case 'exposed_filter': + // TODO + return; + + case 'argument': + // TODO + return; + + case 'sort': + // TODO + break; + + } + return; +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_argument.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_argument.inc new file mode 100644 index 0000000..6da1be3 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_argument.inc @@ -0,0 +1,17 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_argument_many_to_one.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_many_to_one.inc new file mode 100644 index 0000000..1fac5de --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_many_to_one.inc @@ -0,0 +1,47 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } + + function summary_name($data) { + $options = $this->allowed_values(); + $value = $data->{$this->name_alias}; + if (isset($options[$value])) { + $value = $options[$value]; + } + else { + $value = parent::summary_name($data); + } + + return $value; + } + + function title_query() { + $options = $this->allowed_values(); + $values = $this->value; + foreach ($values as $key => $value) { + if (isset($options[$value])) { + $values[$key] = $options[$value]; + } + } + return $values; + } + + function allowed_values() { + $field = $this->content_field; + $function = $field['module'] .'_allowed_values'; + $options = function_exists($function) ? $function($field) : content_allowed_values($field); + return (array) $options; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_argument_numeric.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_numeric.inc new file mode 100644 index 0000000..4403a21 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_numeric.inc @@ -0,0 +1,17 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_argument_reference.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_reference.inc new file mode 100644 index 0000000..e290254 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_reference.inc @@ -0,0 +1,24 @@ +value), '%d')); + + $table_data = views_fetch_data($this->name_table); + $table = array_shift($table_data['table']['join']); + + $result = db_query("SELECT $this->name_field AS title FROM {". $table['table'] ."} WHERE ". $table['field'] ." IN ($placeholders)", $this->value); + while ($row = db_fetch_object($result)) { + $titles[] = check_plain($row->title); + } + return $titles; + } +} diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_argument_string.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_string.inc new file mode 100644 index 0000000..3ad907b --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_argument_string.inc @@ -0,0 +1,17 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_field.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_field.inc new file mode 100644 index 0000000..a905dde --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_field.inc @@ -0,0 +1,228 @@ +content_field = content_fields($this->definition['content_field_name']); + } + + function init(&$view, $options) { + $field = $this->content_field; + parent::init($view, $options); + if ($field['multiple']) { + $this->additional_fields['delta'] = 'delta'; + } + // Make sure we grab enough information to build a pseudo-node with enough + // credentials at render-time. + $this->additional_fields['type'] = array('table' => 'node', 'field' => 'type'); + $this->additional_fields['nid'] = array('table' => 'node', 'field' => 'nid'); + $this->additional_fields['vid'] = array('table' => 'node', 'field' => 'vid'); + } + + function option_definition() { + $options = parent::option_definition(); + $field = $this->content_field; + + // Override views_handler_field_node's default label + $options['label'] = array('default' => '', 'translatable' => TRUE); + $options['label_type'] = array('default' => 'widget'); + $options['format'] = array('default' => 'default'); + + return $options; + } + + /** + * Provide formatter option. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + // TODO: do we want the 'link to node' checkbox ? + // That's usually formatters business... + + $field = $this->content_field; + $options = $this->options; + + $form['label_type'] = array( + '#title' => t('Label'), + '#type' => 'radios', + '#options' => array( + 'none' => t('None'), + 'widget' => t('Widget label (@label)', array('@label' => $field['widget']['label'])), + 'custom' => t('Custom'), + ), + '#default_value' => $options['label_type'], + '#weight' => 2, + ); + $form['label'] = array( + '#title' => t('Custom label'), + '#type' => 'textfield', + '#default_value' => $options['label'], + '#process' => array('views_process_dependency'), + '#dependency' => array('radio:options[label_type]' => array('custom')), + '#weight' => 3, + ); + + $field_types = _content_field_types(); + $formatters = array(); + if (is_array($field_types[$field['type']]['formatters'])) { + foreach ($field_types[$field['type']]['formatters'] as $name => $info) { + $formatters[$name] = $info['label']; + } + } + $form['format'] = array( + '#title' => t('Format'), + '#type' => 'select', + '#options' => $formatters, + '#required' => TRUE, + '#default_value' => $options['format'], + '#weight' => 4, + ); + } + + + /** + * Make sure some value is stored as a label. + * + * Don't use t(), since Views' views_handler_field already has + * $this->options['label'] marked as a translatable field. + * + * @see http://drupal.org/node/285470 + */ + function options_submit($form, &$form_state) { + switch ($form_state['values']['options']['label_type']) { + case 'none': + $form_state['values']['options']['label'] = ''; + break; + case 'widget': + $form_state['values']['options']['label'] = $this->content_field['widget']['label']; + break; + } + } + + /** + * @TODO + * Now that we save the label in the submit process above we could + * get rid of this function. Leave it here for now to be sure the + * label works for fields that haven't been updated since this + * change was made, since $this->options['label'] will be missing a + * value until it is updated in the view. + * + * Don't use t(), since Views' views_handler_field already has + * $this->options['label'] marked as a translatable field. + */ + function label() { + $field = $this->content_field; + switch ($this->options['label_type']) { + case 'none': + return ''; + case 'widget': + return $field['widget']['label']; + default: + return $this->options['label']; + } + } + + /** + * Return DIV or SPAN based upon the field's element type. + */ + function element_type($none_supported = FALSE, $default_empty = FALSE) { + // The 'element_type' property denotes Views 3.x ('semantic views' + // functionnality). If the property is set, and not set to '' ("default"), + // let the generic method handle the output. + if (isset($this->options['element_type']) && $this->options['element_type'] !== '') { + return parent::element_type($none_supported, $default_empty); + } + + if ($default_empty) { + return ''; + } + + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + // TODO Figure out exactly when to return a div or a . Any field + // that ever needs to be shown inline in Views UI. It needs to return + // a div for textareas to prevent wrapping a around a

. + // Earl says we need to be sure that other fields we don't know + // about won't end up wrapping a span around a block-level element. + if ($this->content_field['widget']['type'] == 'text_textarea') { + return 'div'; + } + else { + return 'span'; + } + } + + function options_validate($form, &$form_state) { } + + /** + * Provide text for the administrative summary + */ + function admin_summary() { + // Display the formatter name. + $field = $this->content_field; + $field_types = _content_field_types(); + if (isset($field_types[$field['type']]['formatters'][$this->options['format']])) { + return t($field_types[$field['type']]['formatters'][$this->options['format']]['label']); + } + } + + function render($values) { + // We're down to a single node here, so we can retrieve the actual field + // definition for the node type being considered. + $field = content_fields($this->content_field['field_name'], $values->{$this->aliases['type']}); + + // If the field does not appear in the node type, then we have no value + // to display, and can just return. + if (empty($field)) { + return ''; + } + + $options = $this->options; + $db_info = content_database_info($field); + + // Build a pseudo-node from the retrieved values. + $node = drupal_clone($values); + $node->type = $values->{$this->aliases['type']}; + $node->nid = $values->{$this->aliases['nid']}; + $node->vid = $values->{$this->aliases['vid']}; + // Some formatters need to behave differently depending on the build_mode + // (for instance: preview), so we provide one. + $node->build_mode = NODE_BUILD_NORMAL; + + $item = array(); + foreach ($db_info['columns'] as $column => $attributes) { + $item[$column] = $values->{$this->aliases[$attributes['column']]}; + } + + $item['#delta'] = $field['multiple'] ? $values->{$this->aliases['delta']} : 0; + + // Render items. + $formatter_name = $options['format']; + if ($formatter = _content_get_formatter($formatter_name, $field['type'])) { + if (content_handle('formatter', 'multiple values', $formatter) == CONTENT_HANDLE_CORE) { + // Single-value formatter. + $output = content_format($field, $item, $formatter_name, $node); + } + else { + // Multiple values formatter - we actually have only one value to display. + $output = content_format($field, array($item), $formatter_name, $node); + } + return $this->render_link($output, $values); + } + return ''; + } + +} diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_field_multiple.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_field_multiple.inc new file mode 100644 index 0000000..615182b --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_field_multiple.inc @@ -0,0 +1,321 @@ +content_field; + parent::init($view, $options); + + $this->defer_query = !empty($options['multiple']['group']) && $field['multiple']; + + if ($this->defer_query) { + // Grouped field: ditch the existing additional_fields (field columns + delta). + // In the main query we'll only need: + // - vid, which will be used to retrieve the actual values in pre_render, + // - node type and nid, which wil be used in the pseudo-node used when + // rendering. + $this->additional_fields = array( + 'type' => array('table' => 'node', 'field' => 'type'), + 'nid' => array('table' => 'node', 'field' => 'nid'), + ); + if ($view->base_table == 'node_revisions') { + $this->additional_fields['vid'] = array('table' => 'node_revisions', 'field' => 'vid'); + } + else { + $this->additional_fields['vid'] = array('table' => 'node', 'field' => 'vid'); + } + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['multiple'] = array( + 'contains' => array( + 'group' => array('default' => TRUE), + 'multiple_number' => array('default' => ''), + 'multiple_from' => array('default' => ''), + 'multiple_reversed' => array('default' => FALSE), + ), + ); + + return $options; + } + + /** + * Provide 'group multiple values' option. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $field = $this->content_field; + $options = $this->options; + + $form['multiple'] = array( + '#access' => $field['multiple'], + '#weight' => 1, + ); + $form['multiple']['group'] = array( + '#title' => t('Group multiple values'), + '#type' => 'checkbox', + '#default_value' => $options['multiple']['group'], + '#description' => t('If unchecked, each item in the field will create a new row, which may appear to cause duplicates. This setting is not compatible with click-sorting in table displays.'), + ); + // Make the string translatable by keeping it as a whole rather than + // translating prefix and suffix separately. + list($prefix, $suffix) = explode('@count', t('Show @count value(s)')); + $form['multiple']['multiple_number'] = array( + '#type' => 'textfield', + '#size' => 5, + '#field_prefix' => $prefix, + '#field_suffix' => $suffix, + '#default_value' => $options['multiple']['multiple_number'], + '#prefix' => '

', + '#process' => array('views_process_dependency'), + '#dependency' => array('edit-options-multiple-group' => array(TRUE)), + ); + list($prefix, $suffix) = explode('@count', t('starting from @count')); + $form['multiple']['multiple_from'] = array( + '#type' => 'textfield', + '#size' => 5, + '#field_prefix' => $prefix, + '#field_suffix' => $suffix, + '#default_value' => $options['multiple']['multiple_from'], + '#process' => array('views_process_dependency'), + '#dependency' => array('edit-options-multiple-group' => array(TRUE)), + '#description' => t('(first item is 0)'), + ); + $form['multiple']['multiple_reversed'] = array( + '#title' => t('Reversed'), + '#type' => 'checkbox', + '#default_value' => $options['multiple']['multiple_reversed'], + '#suffix' => '
', + '#process' => array('views_process_dependency'), + '#dependency' => array('edit-options-multiple-group' => array(TRUE)), + '#description' => t('(start from last values)'), + ); + } + + /** + * Determine if this field is click sortable. + */ + function click_sortable() { + $field = $this->content_field; + $options = $this->options; + + // Grouped fields are not click-sortable. + return !empty($this->definition['click sortable']) && !$this->defer_query; + } + + function query() { + // If this is not a grouped field, use the generic query(). + if (!$this->defer_query) { + return parent::query(); + } + + // Grouped field: do NOT call ensure_my_table, only add additional fields. + $this->add_additional_fields(); + $this->field_alias = $this->aliases['vid']; + } + + function pre_render($values) { + // If there are no values to render (displaying a summary, or query returned no results), + // or if this is not a grouped field, do nothing specific. + if (isset($this->view->build_info['summary']) || empty($values) || !$this->defer_query) { + return parent::pre_render($values); + } + + $field = $this->content_field; + $db_info = content_database_info($field); + $options = $this->options; + + // Build the list of vids to retrieve. + // TODO: try fetching from cache_content first ?? + $vids = array(); + $this->field_values = array(); + foreach ($values as $result) { + if (isset($result->{$this->field_alias})) { + $vids[] = $result->{$this->field_alias}; + } + } + + // It may happend that the multiple values field is related to a non + // required relation for which no node data related to the field being + // processed here is available. + if (empty($vids)) { + return parent::pre_render($values); + } + + // List columns to retrieve. + $alias = content_views_tablename($field); + // Prefix aliases with '_' to avoid clashing with field columns names. + $query_columns = array( + 'vid AS _vid', + "delta as _delta", + // nid is needed to generate the links for 'link to node' option. + 'nid AS _nid', + ); + // The actual field columns. + foreach ($db_info['columns'] as $column => $attributes) { + $query_columns[] = "$attributes[column] AS $column"; + } + $query = 'SELECT '. implode(', ', $query_columns) . + ' FROM {'. $db_info['table'] ."}". + " WHERE vid IN (". implode(',', $vids) .')'. + " ORDER BY _nid ASC, _delta ". ($options['multiple']['multiple_reversed'] ? 'DESC' : 'ASC'); + $result = db_query($query); + + while ($item = db_fetch_array($result)) { + // Clean up the $item from vid and delta. We keep nid for now. + $vid = $item['_vid']; + unset($item['_vid']); + $delta = !empty($item['_delta']) ? $item['_delta'] : 0; + $item['#delta'] = $item['_delta']; + unset($item['_delta']); + $this->field_values[$vid][$delta] = $item; + } + } + + /** + * Return DIV or SPAN based upon the field's element type. + * + * Fields rendered with the 'group multiple' option use
markers, + * and thus shouldn't be wrapped in a . + */ + function element_type($none_supported = FALSE, $default_empty = FALSE) { + // If this is not a grouped field, use the parent method. + if (!$this->defer_query) { + return parent::element_type($none_supported, $default_empty); + } + + // The 'element_type' property denotes Views 3.x ('semantic views' + // functionnality). If the property is set, and not set to '' ("default"), + // let the generic method handle the output. + if (isset($this->options['element_type']) && $this->options['element_type'] !== '') { + return parent::element_type($none_supported, $default_empty); + } + + if ($default_empty) { + return ''; + } + + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + return 'div'; + } + + function render($values) { + // If this is not a grouped field, use content_handler_field::render(). + if (!$this->defer_query) { + return parent::render($values); + } + + // We're down to a single node here, so we can retrieve the actual field + // definition for the node type being considered. + $field = content_fields($this->content_field['field_name'], $values->{$this->aliases['type']}); + + // If the field does not appear in the node type, then we have no value + // to display, and can just return. + if (empty($field)) { + return ''; + } + + $options = $this->options; + + $vid = $values->{$this->field_alias}; + if (isset($this->field_values[$vid])) { + // Gather items, respecting the 'Display n values starting from m' settings. + $count_skipped = 0; + $items = array(); + foreach ($this->field_values[$vid] as $item) { + if (empty($options['multiple']['multiple_from']) || ($count_skipped >= $options['multiple']['multiple_from'])) { + if (empty($options['multiple']['multiple_number']) || (count($items) < $options['multiple']['multiple_number'])) { + // Grab the nid - needed for render_link(). + $nid = $item['_nid']; + unset($item['_nid']); + $items[] = $item; + } + else { + break; + } + } + $count_skipped++; + } + + // Build a pseudo-node from the retrieved values. + $node = drupal_clone($values); + // content_format and formatters will need a 'type'. + $node->type = $values->{$this->aliases['type']}; + $node->nid = $values->{$this->aliases['nid']}; + $node->vid = $values->{$this->aliases['vid']}; + + // Some formatters need to behave differently depending on the build_mode + // (for instance: preview), so we provide one. + $node->build_mode = NODE_BUILD_NORMAL; + + // Render items. + $formatter_name = $options['format']; + if ($items && ($formatter = _content_get_formatter($formatter_name, $field['type']))) { + $rendered = array(); + if (content_handle('formatter', 'multiple values', $formatter) == CONTENT_HANDLE_CORE) { + // Single-value formatter. + foreach ($items as $item) { + $output = content_format($field, $item, $formatter_name, $node); + if (!empty($output)) { + $rendered[] = $this->render_link($output, (object) array('nid' => $nid)); + } + } + } + else { + // Multiple values formatter. + $output = content_format($field, $items, $formatter_name, $values); + if (!empty($output)) { + $rendered[] = $this->render_link($output, (object) array('nid' => $nid)); + } + } + + if (count($rendered) > 1) { + // TODO: could we use generic field display ? + return theme('content_view_multiple_field', $rendered, $field, $values); + } + elseif ($rendered) { + return $rendered[0]; + } + } + } + + return ''; + } + + function render_link($data, $values) { + if (!$this->defer_query) { + return parent::render_link($data, $values); + } + + if (!empty($this->options['link_to_node']) && $data !== NULL && $data !== '') { + if (method_exists('render_as_link', 'views_handler_field')) { + // Views 2.3+ + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/" . $values->{$this->aliases['nid']}; + } + else { + // Views up to 2.2 + return l($data, "node/" . $values->nid, array('html' => TRUE)); + } + } + else { + return $data; + } + } + +} diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_filter_float.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_float.inc new file mode 100644 index 0000000..567f7d1 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_float.inc @@ -0,0 +1,25 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_filter_many_to_one.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_many_to_one.inc new file mode 100644 index 0000000..5b165d6 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_many_to_one.inc @@ -0,0 +1,41 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + $field = $this->content_field; + $this->value_title = $field['widget']['label']; + } + + function get_value_options() { + $this->value_options = $this->allowed_values(); + } + + // Get allowed values from hook_allowed_values(), if any, + // or from content_allowed_values(); + function allowed_values() { + $field = $this->content_field; + $function = $field['module'] .'_allowed_values'; + if ($this->value_form_type == 'select') { + // Select elements accept multidimensional arrays to support optgroups. + $options = function_exists($function) ? $function($field) : content_allowed_values($field, FALSE); + // For selects, HTML should be filtered out and entities left unencoded. + // See content_allowed_values / content_filter_xss / filter_xss. + content_allowed_values_filter_html($options); + } + else { + $options = function_exists($function) ? $function($field) : content_allowed_values($field); + } + return (array) $options; + } + +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_filter_numeric.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_numeric.inc new file mode 100644 index 0000000..aa19b3b --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_numeric.inc @@ -0,0 +1,16 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_filter_string.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_string.inc new file mode 100644 index 0000000..ff40576 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_filter_string.inc @@ -0,0 +1,16 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_relationship.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_relationship.inc new file mode 100644 index 0000000..56c85f7 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_relationship.inc @@ -0,0 +1,72 @@ +content_field = content_fields($this->definition['content_field_name']); + } + + function option_definition() { + $options = parent::option_definition(); + $options['delta'] = array('default' => -1); + + return $options; + } + + /** + * Add a delta selector for multiple fields. + */ + function options_form(&$form, &$form_state) { + $field = $this->content_field; + parent::options_form($form, $form_state); + + // Only add the form gadget if the field is multiple. + if ($field['multiple']) { + $max_delta = $field['multiple']; + // 1 means unlimited. + if ($max_delta == 1) { + $max_delta = 10; + } + + $options = array('-1' => t('All')); + for ($i = 0; $i < $max_delta; $i++) { + $options[$i] = $i + 1; + } + $form['delta'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $this->options['delta'], + '#title' => t('Delta'), + '#description' => t('The delta allows you to select which item in a multiple value field to key the relationship off of. Select "1" to use the first item, "2" for the second item, and so on. If you select "All", each item in the field will create a new row, which may appear to cause duplicates.'), + ); + } + } + + function ensure_my_table() { + if (!isset($this->table_alias)) { + $join = $this->get_join(); + if (!isset($join->extra)) { + $join->extra = array(); + } + $delta = isset($this->options['delta']) ? $this->options['delta'] : -1; + if ($delta != -1) { + $join->extra[] = array( + 'field' => 'delta', + 'value' => $delta, + 'numeric' => TRUE, + ); + } + + $this->table_alias = $this->query->add_table($this->table, $this->relationship, $join); + } + return $this->table_alias; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_handler_sort.inc b/sites/all/modules/cck/includes/views/handlers/content_handler_sort.inc new file mode 100644 index 0000000..593ba36 --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_handler_sort.inc @@ -0,0 +1,73 @@ +content_field = content_fields($this->definition['content_field_name']); + $this->additional_fields = $this->definition['additional fields']; + } + + function option_definition() { + $options = parent::option_definition(); + $options['delta'] = array('default' => -1); + + return $options; + } + + /** + * Add a delta selector for multiple fields. + */ + function options_form(&$form, &$form_state) { + $field = $this->content_field; + parent::options_form($form, $form_state); + + // Only add the form gadget if the field is multiple. + if ($field['multiple']) { + $max_delta = $field['multiple']; + // 1 means unlimited. + if ($max_delta == 1) { + $max_delta = 10; + } + + $options = array('-1' => t('All')); + for ($i = 0; $i < $max_delta; $i++) { + $options[$i] = $i + 1; + } + $form['delta'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $this->options['delta'], + '#title' => t('Delta'), + '#description' => t('The delta allows you to select which item in a multiple value field will be used for sorting. Select "1" to use the first item, "2" for the second item, and so on. If you select "All", each item in the field will create a new row, which may appear to cause duplicates.'), + ); + } + } + + function ensure_my_table() { + if (!isset($this->table_alias)) { + $join = $this->get_join(); + if (!isset($join->extra)) { + $join->extra = array(); + } + $delta = isset($this->options['delta']) ? $this->options['delta'] : -1; + if ($delta != -1) { + $join->extra[] = array( + 'field' => 'delta', + 'value' => $delta, + 'numeric' => TRUE, + ); + } + + $this->table_alias = $this->query->ensure_table($this->table, $this->relationship, $join); + } + return $this->table_alias; + } +} \ No newline at end of file diff --git a/sites/all/modules/cck/includes/views/handlers/content_plugin_display_simple.inc b/sites/all/modules/cck/includes/views/handlers/content_plugin_display_simple.inc new file mode 100644 index 0000000..d65e5de --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_plugin_display_simple.inc @@ -0,0 +1,42 @@ +view->render($this->display->id); + } + + function render() { + return !empty($this->view->result) || !empty($this->view->style_plugin->definition['even empty']) ? $this->view->style_plugin->render($this->view->result) : ''; + } + + function uses_exposed() { + return FALSE; + } +} + +class content_plugin_display_references extends content_plugin_display_simple { + function query() { + $options = $this->get_option('content_options'); + + if ($options['string'] !== '') { + $like = $GLOBALS["db_type"] == 'pgsql' ? "ILIKE" : "LIKE"; + $match_clauses = array( + 'contains' => "$like '%%%s%%'", + 'equals' => "= '%s'", + 'starts_with' => "$like '%s%%'", + ); + $clause = isset($match_clauses[$options['match']]) ? $match_clauses[$options['match']] : $match_clauses['contains']; + $alias = $this->view->query->ensure_table($options['table']); + $this->view->query->add_where(NULL, "$alias.$options[field_string] $clause", $options['string']); + } + elseif ($options['ids']) { + $alias = $this->view->query->ensure_table($options['table']); + $this->view->query->add_where(NULL, "$alias.$options[field_id] IN (" . db_placeholders($options['ids']) . ')', $options['ids']); + } + } +} + diff --git a/sites/all/modules/cck/includes/views/handlers/content_plugin_style_php_array_ac.inc b/sites/all/modules/cck/includes/views/handlers/content_plugin_style_php_array_ac.inc new file mode 100644 index 0000000..12fdccd --- /dev/null +++ b/sites/all/modules/cck/includes/views/handlers/content_plugin_style_php_array_ac.inc @@ -0,0 +1,33 @@ +render_grouping($this->view->result, $this->options['grouping']); + + $base_field = $this->view->base_field; + $title_field = $this->display->display_options['content_title_field']; + $title_field_alias = $this->view->field[$title_field]->field_alias; + + // TODO : We don't display grouping info for now. + // Could be useful for select widget, though. + $this->view->row_index = 0; + foreach ($sets as $title => $records) { + foreach ($records as $label => $row) { + $results[$row->{$base_field}] = array( + 'title' => $row->{$title_field_alias}, + 'rendered' => $this->row_plugin->render($row), + ); + $this->view->row_index++; + } + } + unset($this->view->row_index); + return $results; + } +} diff --git a/sites/all/modules/cck/js/content.admin.js b/sites/all/modules/cck/js/content.admin.js new file mode 100644 index 0000000..ea1613c --- /dev/null +++ b/sites/all/modules/cck/js/content.admin.js @@ -0,0 +1,79 @@ + +Drupal.behaviors.cckManageFields = function(context) { + attachUpdateSelects(context); +}; + +function attachUpdateSelects(context) { + var widgetTypes = Drupal.settings.contentWidgetTypes; + var fields = Drupal.settings.contentFields; + + // Store the default text of widget selects. + $('#content-field-overview .content-widget-type-select', context).each(function() { + this.initialValue = this.options[0].text; + }); + + // 'Field type' select updates its 'Widget' select. + $('#content-field-overview .content-field-type-select', context).each(function() { + this.targetSelect = $('.content-widget-type-select', $(this).parents('tr').eq(0)); + + $(this).change(function() { + var selectedFieldType = this.options[this.selectedIndex].value; + var options = (selectedFieldType in widgetTypes) ? widgetTypes[selectedFieldType] : [ ]; + this.targetSelect.contentPopulateOptions(options); + }); + + // Trigger change on initial pageload to get the right widget options + // when field type comes pre-selected (on failed validation). + $(this).trigger('change'); + }); + + // 'Existing field' select updates its 'Widget' select and 'Label' textfield. + $('#content-field-overview .content-field-select', context).each(function() { + this.targetSelect = $('.content-widget-type-select', $(this).parents('tr').eq(0)); + this.targetTextfield = $('.content-label-textfield', $(this).parents('tr').eq(0)); + + $(this).change(function(e, updateText) { + var updateText = (typeof(updateText) == 'undefined') ? true : updateText; + var selectedField = this.options[this.selectedIndex].value; + var selectedFieldType = (selectedField in fields) ? fields[selectedField].type : null; + var selectedFieldWidget = (selectedField in fields) ? fields[selectedField].widget : null + var options = (selectedFieldType && (selectedFieldType in widgetTypes)) ? widgetTypes[selectedFieldType] : [ ]; + this.targetSelect.contentPopulateOptions(options, selectedFieldWidget); + + if (updateText) { + $(this.targetTextfield).attr('value', (selectedField in fields) ? fields[selectedField].label : ''); + } + }); + + // Trigger change on initial pageload to get the right widget options + // and label when field type comes pre-selected (on failed validation). + $(this).trigger('change', false); + }); +} + +jQuery.fn.contentPopulateOptions = function(options, selected) { + return this.each(function() { + var disabled = false; + if (options.length == 0) { + options = [this.initialValue]; + disabled = true; + } + + // If possible, keep the same widget selected when changing field type. + // This is based on textual value, since the internal value might be + // different (optionwidgets_buttons vs. nodereference_buttons). + var previousSelectedText = this.options[this.selectedIndex].text; + + var html = ''; + jQuery.each(options, function(value, text) { + // Figure out which value should be selected. The 'selected' param + // takes precedence. + var is_selected = ((typeof(selected) !== 'undefined' && value == selected) || (typeof(selected) == 'undefined' && text == previousSelectedText)); + html += ''; + }); + + $(this) + .html(html) + .attr('disabled', disabled ? 'disabled' : ''); + }); +} \ No newline at end of file diff --git a/sites/all/modules/cck/js/content.node_form.js b/sites/all/modules/cck/js/content.node_form.js new file mode 100644 index 0000000..326ad1d --- /dev/null +++ b/sites/all/modules/cck/js/content.node_form.js @@ -0,0 +1,254 @@ + +/** + * Private namespace for local methods. + */ +Drupal.contentRemoveButtons = Drupal.contentRemoveButtons || {}; + +/** + * Manipulation of content remove buttons. + * + * TableDrag objects for multiple value fields (and multigroups) are scanned + * to find 'remove' checkboxes. These checkboxes are hidden when javascript is + * enabled (using the Global CSS Killswitch, html.js, defined in drupal.js). + * A new 'remove' button is created here in place of these checkboxes aimed to + * provide a more user-friendly method to remove items. + */ +Drupal.behaviors.contentRemoveButtons = function(context) { + var self = Drupal.contentRemoveButtons; + + $('table.content-multiple-table', context).not('.content-remove-buttons-processed').addClass('content-remove-buttons-processed').each(function() { + var table = this, tableDrag = Drupal.tableDrag[$(table).attr('id')]; + + // Replace remove checkboxes with buttons. + $('input.content-multiple-remove-checkbox', table).each(function() { + var $checkbox = $(this), $row = $checkbox.parents('tr:first'); + var isRemoved = $checkbox.attr('checked'); + var $button = $(Drupal.theme('contentRemoveButton', tableDrag.getRemoveButtonTitle(isRemoved))); + + // Bind the onClick event to the remove button. + $button.bind('click', function(event) { + self.onClick($button, $checkbox, $row, tableDrag); + return false; + }); + + // Attach the new button to the DOM tree. + $checkbox.parent().append($button); + + // If the row is removed, then hide the contents of the cells and show + // the removed warning on the cell next to the drag'n'drop cell. + if (isRemoved) { + self.getCellWrappers($row).hide(); + self.showRemovedWarning($row, tableDrag); + + // FAPI not rendering the form on errors - case #1: + // If the form has been submitted and any error was found, FAPI will + // send back the same exact form that was submitted to show the error + // messages, but it will not invoke the rendering engine which is where + // we actually assign the removed class to the row, so we need to check + // this situation here and add the class if it is not present. + if (!$row.hasClass('content-multiple-removed-row')) { + $row.addClass('content-multiple-removed-row'); + } + } + else { + // FAPI not rendering the form on errors - case #2: + // Similar issue than #1, but this time caused when user removes an + // item, previews, FAPI renders the new form with the removed class, + // then user changes anything in the form that causes an error, and + // also restores the previously removed item. This time, FAPI will + // send the form validation error with the item not flagged for removal + // but having the removed class that was present when the form was + // rendered in the previous step. So we need to remove this class here, + // if present, because the item is not really flagged for removal. + if ($row.hasClass('content-multiple-removed-row')) { + $row.removeClass('content-multiple-removed-row'); + } + } + }); + }); +}; + +/** + * onClick handler for remove buttons. + * + * @param $button + * The jQuery object of the remove button. + * @param $checkbox + * The jQuery object of the remove checkbox. + * @param $row + * The jQuery object of the table row. + * @param tableDrag + * The tableDrag object where the row is. + */ +Drupal.contentRemoveButtons.onClick = function($button, $checkbox, $row, tableDrag) { + var self = Drupal.contentRemoveButtons; + + // Prevent the user from firing this event while another one is still being + // processed. This flag is (should be) restored at end of animations. + // Note that this technique is required because the browser may experience + // delays while performing the animation, for whatever reason, and if this + // process it fired more than once at the same time for the same row, then + // it may cause unexpected behavior because the state of the elements being + // manipulated would be unknown. + if ($row.animating) { + return; + } + $row.animating = true; + + // Toggle the state of the checkbox. + var isRemoved = !$checkbox.attr('checked'); + $checkbox.attr('checked', isRemoved); + + // Toggle the row class. + if (isRemoved) { + $row.addClass('content-multiple-removed-row'); + } + else { + $row.removeClass('content-multiple-removed-row'); + } + + // Toggle the button title. + $button.attr('title', tableDrag.getRemoveButtonTitle(isRemoved)); + + // Get the list of cell wrappers in this row. + var $cellWrappers = self.getCellWrappers($row); + + // If for whatever reason this row doesn't have cells with elements, + // then we are done, but we still need to reset the global busy flag + // and display the tableDrag changed warning. + if (!$cellWrappers.size()) { + tableDrag.displayChangedWarning(); + $row.animating = false; + return; + } + + // Toggle the visible state of the row cells. + $cellWrappers.each(function() { + var $cellWrapper = $(this); + + // Drop the removed warning during restore operation. + if (!isRemoved) { + self.hideRemovedWarning($row); + } + + // Toggle the visibility state of the contents of cells. + $cellWrapper.animate({opacity: (isRemoved ? 'hide' : 'show')}, 'fast', function() { + var $cell = $cellWrapper.parent(); + + // Show the removed warning during remove operation. + if (isRemoved && $cell.prev(':first').hasClass('content-multiple-drag')) { + self.showRemovedWarning($row, tableDrag); + } + + // Disable the busy flag when animation of last cell has finished. + if ($cell.next(':first').hasClass('delta-order')) { + tableDrag.displayChangedWarning(); + $row.animating = false; + } + }); + }); +}; + +/** + * Show the removed warning on the given row. + * + * @param $row + * The jQuery object of the table row. + * @param tableDrag + * The tableDrag object where the row is. + */ +Drupal.contentRemoveButtons.showRemovedWarning = function($row, tableDrag) { + $('.content-multiple-drag', $row).next(':first').append(Drupal.theme('contentRemovedWarning', tableDrag.getRemovedWarning())); +}; + +/** + * Hide the removed warning from the given row. + * + * @param $row + * The jQuery object of the table row. + */ +Drupal.contentRemoveButtons.hideRemovedWarning = function($row) { + if ($('.content-multiple-removed-warning', $row).size()) { + $('.content-multiple-removed-warning', $row).remove(); + } +}; + +/** + * Get cell wrappers for the given row. + * + * @param $row + * The jQuery object of the table row. + */ +Drupal.contentRemoveButtons.getCellWrappers = function($row) { + // Create cell wrappers if this row has not already been processed. + if (!$('.content-multiple-cell-content-wrapper', $row).size()) { + // Wrap the contents of all cells (except the drag'n'drop, weight and + // remove button cells) with a dummy block element. This operation makes + // animations faster because we just need to show/hide a single element + // per cell, and it also prevents from creating more than one warning + // element per row. + $row.children('td:not(.content-multiple-drag):not(.delta-order):not(.content-multiple-remove-cell)').each(function() { + var $cell = $(this); + $cell.wrapInner('
'); + }); + } + return $('.content-multiple-cell-content-wrapper', $row); +}; + +/** + * Display table change warning when appropriate. + */ +Drupal.tableDrag.prototype.displayChangedWarning = function() { + if (this.changed == false) { + $(Drupal.theme('tableDragChangedWarning')).insertAfter(this.table).hide().fadeIn('slow'); + this.changed = true; + } +}; + +/** + * Get the title of the remove button. + * + * This method is an extension of the tableDrag class. This means a separate + * module can override this method for a particular tableDrag instance. For + * example, the multigroup module can change the text to read 'Remove this + * group of items', another module could change it to 'Remove this image', + * and so on... + * To override this function: + * + * @code + * var tableId = $(table).attr('id'); + * Drupal.tableDrag[tableId].getRemoveButtonTitle = function(isRemoved) { + * return (isRemoved ? Drupal.t('Restore this foo') : Drupal.t('Remove this foo')); + * }; + * @endcode + * + * @param isRemoved + * A flag that indicates the state of the button. + */ +Drupal.tableDrag.prototype.getRemoveButtonTitle = function(isRemoved) { + return (isRemoved ? Drupal.t('Restore this item') : Drupal.t('Remove this item')); +}; + +/** + * Get the item removed warning. + * + * This method is an extension of the tableDrag class. It can be overridden by + * a separate module. See getRemoveButtonTitle() for further information. + */ +Drupal.tableDrag.prototype.getRemovedWarning = function() { + return Drupal.t('Removed'); +}; + +/** + * Theme the remove button. + */ +Drupal.theme.prototype.contentRemoveButton = function(title) { + return ''; +}; + +/** + * Theme the item removed warning. + */ +Drupal.theme.prototype.contentRemovedWarning = function(warning) { + return '
'+ warning +'
'; +}; diff --git a/sites/all/modules/cck/modules/content_copy/content_copy.info b/sites/all/modules/cck/modules/content_copy/content_copy.info new file mode 100644 index 0000000..b2be2eb --- /dev/null +++ b/sites/all/modules/cck/modules/content_copy/content_copy.info @@ -0,0 +1,11 @@ +name = Content Copy +description = Enables ability to import/export field definitions. +dependencies[] = content +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/content_copy/content_copy.module b/sites/all/modules/cck/modules/content_copy/content_copy.module new file mode 100644 index 0000000..16e7add --- /dev/null +++ b/sites/all/modules/cck/modules/content_copy/content_copy.module @@ -0,0 +1,648 @@ + 'Export', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('content_copy_export_form'), + 'access arguments' => array('administer content types'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 3, + ); + $items['admin/content/types/import'] = array( + 'title' => 'Import', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('content_copy_import_form'), + 'access arguments' => array('administer content types'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 4, + ); + return $items; +} + +/** + * Implementation of hook_theme(). + */ +function content_copy_theme() { + return array( + 'content_copy_export_form' => array( + 'template' => 'content_copy_export_form', + 'arguments' => array('form' => NULL), + ), + ); +} + +/** + * A form to export field definitions. + */ +function content_copy_export_form(&$form_state) { + include_once('./'. drupal_get_path('module', 'content') .'/includes/content.admin.inc'); + include_once('./'. drupal_get_path('module', 'node') .'/content_types.inc'); + + $form_values = isset($form_state['values']) ? $form_state['values'] : array(); + $step = isset($form_state['storage']['step']) ? $form_state['storage']['step'] + 1 : 1; + + $exportable_fields = array(); + $groups = array(); + + $type_name = isset($form_values['type_name']) ? $form_values['type_name'] : ''; + if ($type_name) { + $type = content_types($type_name); + $exportable_fields = content_copy_fields($type_name); + if (module_exists('fieldgroup')) { + $groups = fieldgroup_groups($type_name); + } + } + + // If a content type has been selected and there are no fields or groups to select, + // jump straight to export. + if ($step == 2 && !($groups) && !($exportable_fields)) { + $step = 3; + } + + $form['#step'] = $step; + $form['#prefix'] = t('This form will process a content type and one or more fields from that type and export the settings. The export created by this process can be copied and pasted as an import into the current or any other database. The import will add the fields to an existing content type or create a new content type that includes the selected fields.'); + + switch ($step) { + case 1: // Select a content type. + $types = content_copy_types(); + $form['type_name'] = array( + '#title' => t('Types'), + '#type' => 'radios', + '#options' => $types, + '#description' => t('Select the content type to export.'), + ); + break; + + case 2: // Select groups and fields. + $form['type_name'] = array( + '#type' => 'hidden', + '#value' => $type_name, + ); + + $form += array( + '#fields' => $exportable_fields, + '#groups' => array_keys($groups), + ); + + $fields_options = $groups_options = array(); + + // Fields. + foreach ($exportable_fields as $field_name) { + $field = content_fields($field_name, $type_name); + $fields_options[$field_name] = ''; + $weight = $field['widget']['weight']; + $form[$field_name] = array( + 'human_name' => array('#value' => check_plain($field['widget']['label'])), + 'field_name' => array('#value' => $field['field_name']), + 'type' => array('#value' => $field['type']), + 'weight' => array('#type' => 'value', '#value' => $weight), + 'parent' => array('#type' => 'value', '#value' => ''), + '#row_type' => 'field', + ); + } + $form['fields'] = array( + '#type' => 'checkboxes', + '#options' => $fields_options, + '#default_value' => array_keys($fields_options), + ); + + // Groups. + foreach ($groups as $name => $group) { + $groups_options[$name] = ''; + $weight = $group['weight']; + $form[$name] = array( + 'human_name' => array('#value' => check_plain($group['label'])), + 'group_name' => array('#value' => $group['group_name']), + 'weight' => array('#type' => 'value', '#value' => $weight), + '#row_type' => 'group', + ); + foreach ($group['fields'] as $field_name => $field) { + // Do nothing for non-exportable (inactive) fields. + if (isset($form[$field_name])) { + $form[$field_name]['parent']['#value'] = $name; + } + } + } + if ($groups) { + $form['groups'] = array( + '#type' => 'checkboxes', + '#options' => $groups_options, + '#default_value' => array_keys($groups_options), + ); + } + break; + + case 3: // Display the export macro. + $GLOBALS['content_copy']['count'] = 0; + $form['export'] = array( + '#title' => t('Export data'), + '#type' => 'textarea', + '#cols' => 60, + '#value' => content_copy_export($form_values), + '#rows' => max(40, $GLOBALS['content_copy']['count']), + '#description' => t('Copy the export text and paste it into another content type using the import function.'), + ); + // The calls to drupal_execute('content_field_edit_form') in + // content_copy_export() affect the page title, + drupal_set_title(t('Content types')); + break; + } + + if ($step < 3) { // Omit submit button on the textarea block to display the export data. + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Export'), + ); + } + + $form['step'] = array( + '#type' => 'value', + '#value' => $step, + ); + + return $form; +} + +function content_copy_export_form_submit($form, &$form_state) { + $form_state['rebuild'] = TRUE; + $form_state['storage']['step'] = $form_state['values']['step']; +} + + +/** + * Process the export, get field admin forms for all requested fields + * and save the form values as formatted text. + */ +function content_copy_export($form_values) { + // Set a global variable to tell when to intervene with form_alter(). + $GLOBALS['content_copy']['status'] = 'export'; + + // Get the content type info by submitting the content type form. + $node_state = array('values' => array('type_name' => $form_values['type_name'])); + module_load_include('inc', 'node', 'content_types'); + drupal_execute('node_type_form', $node_state, node_get_types('type', $form_values['type_name'])); + + module_load_include('inc', 'content', 'includes/content.admin'); + module_load_include('inc', 'content', 'includes/content.crud'); + + // Get an array of groups to export. + // Record a macro for each group by submitting the group edit form. + $groups = array(); + if (!empty($form_values['groups']) && module_exists('fieldgroup')) { + $groups = array_filter($form_values['groups']); + foreach ($groups as $group) { + $group_state = array('values' => array('group_name' => $group)); + drupal_execute('fieldgroup_group_edit_form', $group_state, $form_values['type_name'], $group, 'edit'); + } + } + + // Get an array of fields to export + // Record a macro for each field by submitting the field settings form. + // Omit fields from the export if their module is not currently installed + // otherwise the system will generate errors when the macro tries to execute their forms. + if (!empty($form_values['fields'])) { + $type = content_types($form_values['type_name']); + $fields = array_filter($form_values['fields']); + foreach ($fields as $field_name) { + $field = $type['fields'][$field_name]; + $field_types = _content_field_types(); + $field_module = $field_types[$field['type']]['module']; + $widget_types = _content_widget_types(); + $widget_module = $widget_types[$field['widget']['type']]['module']; + if (!empty($field_module) && module_exists($field_module) && !empty($widget_module) && module_exists($widget_module)) { + $field_state = array('values' => content_field_instance_collapse($field)); + $field_state['values']['op'] = t('Save field settings'); + if (module_exists('fieldgroup')) { + // Avoid undefined index error by always creating this. + $field_state['values']['group'] = ''; + $group_name = fieldgroup_get_group($form_values['type_name'], $field_name); + if (in_array($group_name, $groups)) { + $field_state['values']['group'] = $group_name; + } + } + drupal_execute('content_field_edit_form', $field_state, $form_values['type_name'], $field_name); + } + } + } + + // Convert the macro array into formatted text. + $output = content_copy_get_macro(); + + // Add weights of non-CCK fields. + if ($extra = variable_get('content_extra_weights_'. $form_values['type_name'], array())) { + $output .= "\$content['extra'] = ". var_export((array) $extra, TRUE) .";\n"; + } + + return $output; +} + +/** + * A form to import formatted text created with export. + * + * The macro can be filled from a file, if provided. + * Provide a type_name to force the fields to be added to a specific + * type, or leave out type_name to create a new content type. + * + * Example: + * // If Content Copy is enabled, offer an import link. + * if (module_exists('content_copy')) { + * $form['macro'] = array( + * '#type' => 'fieldset', + * '#title' => t('Create a content type'), + * '#description' => t('Follow this link to create automatically a content type and import preconfigured fields.'), + * '#collapsible' => TRUE, + * '#collapsed' => FALSE, + * ); + * $form['macro']['link'] = array( + * '#type' => 'markup', + * '#value' => l(t('import'), 'admin/content/types/import', array(), 'type_name=event¯o_file='. drupal_get_path('module', 'my_module') .'/my_content_type.txt'), + * ); + * } + */ +function content_copy_import_form(&$form_state, $type_name = '') { + include_once('./'. drupal_get_path('module', 'content') .'/includes/content.admin.inc'); + include_once('./'. drupal_get_path('module', 'node') .'/content_types.inc'); + + $form['#prefix'] = t('This form will import field definitions exported from another content type or another database.
Note that fields cannot be duplicated within the same content type, so imported fields will be added only if they do not already exist in the selected type.'); + $form['type_name'] = array( + '#type' => 'select', + '#options' => array('' => t('')) + content_copy_types(), + '#default_value' => $type_name, + '#title' => t('Content type'), + '#description' => t('Select the content type to import these fields into.
Select <Create> to create a new content type to contain the fields.'), + ); + $form['macro'] = array( + '#type' => 'textarea', + '#rows' => 40, + '#title' => t('Import data'), + '#required' => TRUE, + '#description' => t('Paste the text created by a content export into this field.'), + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Import'), + ); + // Read in a file if there is one and set it as the default macro value. + if (isset($_REQUEST['macro_file']) && $file = file_get_contents($_REQUEST['macro_file'])) { + $form['macro']['#default_value'] = $file; + if (isset($_REQUEST['type_name'])) { + $form['type_name']['#default_value'] = $_REQUEST['type_name']; + } + $form['#prefix'] .= '

'. t('A file has been pre-loaded for import.') .'

'; + } + return $form; +} + +/** + * Submit handler for import form. + * For each submitted field: + * 1) add new field to the database + * 2) execute the imported field macro to update the settings to the imported values + */ +function content_copy_import_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + + // Get the content type we are importing into. + $type_name = $form_values['type_name']; + $type_label = node_get_types('name', $type_name); + + $content = NULL; + // Convert the import formatted text back into a $content array. + // Return if errors generated or not an array. + // Use '@' to suppress errors about undefined constants in the macro. + @eval($form_values['macro']); + + // Preliminary error trapping, must have valid arrays to work with. + if (!isset($content) || !isset($content['type']) || !is_array($content) || !is_array($content['type'])) { + form_set_error('macro', t('The import data is not valid import text.')); + return; + } + + module_load_include('inc', 'content', 'includes/content.crud'); + + // Get all type and field info for this database. + $content_info = _content_type_info(); + + $imported_type = $content['type']; + $imported_type_name = $imported_type['type']; + $imported_type_label = $imported_type['name']; + + // It is allowed to import a type with no fields, + // so the fields array could be empty and must be cast as an array. + $imported_fields = isset($content['fields']) ? $content['fields'] : array(); + + // Perform more pre-import error trapping. + // If there are potential problems, exit without doing the import. + $not_enabled = array(); + + // The groups array could be empty and still valid, make sure to cast it as an array. + // If there are groups in the import, make sure the fieldgroup module is enabled. + $imported_groups = array(); + if (isset($content['groups']) && module_exists('fieldgroup')) { + $imported_groups = (array) $content['groups']; + } + elseif (isset($content['groups']) && is_array($content['groups'])) { + $not_enabled[] = 'fieldgroup'; + } + + // Make sure that all the field and widget modules in the import are enabled in this database. + foreach ($imported_fields as $import) { + $field = content_field_instance_collapse($import); + if (empty($field['module']) || empty($field['widget_module'])) { + $not_enabled[] = $field['field_name']; + } + else { + if (!module_exists($field['module'])) { + $not_enabled[] = $field['module']; + } + if (!module_exists($field['widget_module'])) { + $not_enabled[] = $field['widget_module']; + } + } + } + + // If any required module is not enabled, set an error message and exit. + if ($not_enabled) { + form_set_error('macro', t('The following modules must be enabled for this import to work: %modules.', array( + '%modules' => implode(', ', array_unique($not_enabled)) + ))); + } + + // Make sure the imported content type doesn't already exist in the database. + if ($form_values['type_name'] == '') { + if (in_array($imported_type_name, array_keys($content_info['content types']))) { + form_set_error('macro', t('The content type %type already exists in this database.', array( + '%type' => $imported_type_name + ))); + } + } + + if (form_get_errors()) { + drupal_set_message(t('Exiting. No import performed.'), 'error'); + return; + } + + // Create the content type, if requested. + if ($form_values['type_name'] == '') { + + $type = (object) $imported_type; + $values = $imported_type; + // Prevent a warning in node/content_types.inc + $type->has_title = TRUE; + $type_form_state = array('values' => $values); + + // There's no API for creating node types, we still have to use drupal_execute(). + drupal_execute('node_type_form', $type_form_state, $type); + + // Reset type and database values once new type has been added. + $type_name = $imported_type_name; + $type_label = node_get_types('name', $type_name); + content_clear_type_cache(); + $content_info = _content_type_info(); + + if (form_get_errors() || !isset($content_info['content types']) || !is_array($content_info['content types'][$type_name])) { + drupal_set_message(t('An error has occurred adding the content type %type.
Please check the errors displayed for more details.', array( + '%type' => $imported_type_name + ))); + return; + } + } + + // Create the groups for this type, if they don't already exist. + if (module_exists('fieldgroup') && $imported_groups) { + foreach ($imported_groups as $group) { + $group_name = $group['group_name']; + fieldgroup_save_group($type_name, $group); + } + // Reset the static variable in fieldgroup_groups() with new data. + fieldgroup_groups('', FALSE, TRUE); + } + + // Iterate through the field forms in the import and execute each. + $rebuild = FALSE; + foreach ($imported_fields as $field) { + + // Make sure the field doesn't already exist in the type. + // If so, do nothing, fields can't be duplicated within a content type. + $field_name = $field['field_name']; + + // Might need to overwrite the content type name if a new type was created. + $field['type_name'] = $type_name; + + if (!empty($field['field_name']) && isset($content_info['content types'][$type_name]['fields'][$field_name])) { + drupal_set_message(t('The imported field %field_label (%field_name) was not added to %type because that field already exists in %type.', array( + '%field_label' => $field['label'], '%field_name' => $field_name, '%type' => $type_label))); + } + else { + $field = content_field_instance_create($field, FALSE); + $rebuild = TRUE; + drupal_set_message(t('The field %field_label (%field_name) was added to the content type %type.', array( + '%field_label' => $field['widget']['label'], '%field_name' => $field_name, '%type' => $type_label))); + } + + // Fieldgroup module erases all group related data when a module that + // provides a content type is disabled, but CCK does not remove the fields. + // In this case, we should ensure group data related to fields is properly + // restored. Hence, we need to update field group data for newly imported + // field, but also for fields that already exist. + if (module_exists('fieldgroup') && isset($imported_groups)) { + fieldgroup_update_fields($field); + } + } + + // Clear caches and rebuild menu only if any field has been created. + if ($rebuild) { + content_clear_type_cache(TRUE); + menu_rebuild(); + } + + // Import weights of non-CCK fields. + if (isset($content['extra'])) { + variable_set('content_extra_weights_'. $type_name, $content['extra']); + } +} + +/** + * Implementation of hook_form_alter(). + * Intervene to run form through macro when doing export + */ +function content_copy_form_alter(&$form, $form_state, $form_id) { + $alter_forms = array('node_type_form', 'content_field_edit_form', 'fieldgroup_group_edit_form'); + if (isset($GLOBALS['content_copy']) && isset($GLOBALS['content_copy']['status']) && $GLOBALS['content_copy']['status'] == 'export' && in_array($form_id, $alter_forms)) { + $form['#submit'][] = 'content_copy_record_macro'; + } +} + +/** + * Get all the *active* fields for a content type. + */ +function content_copy_fields($type_name) { + $fields = array(); + if (!$type_name) { + return $fields; + } + $content_info = _content_type_info(); + foreach ($content_info['content types'][$type_name]['fields'] as $field_name => $field) { + // Omit fields from the export if their module is not currently installed + // otherwise the system will generate errors when the macro tries to execute their forms. + $field_types = _content_field_types(); + $field_module = $field_types[$field['type']]['module']; + $widget_types = _content_widget_types(); + $widget_module = $widget_types[$field['widget']['type']]['module']; + + if (!$field['locked'] && !empty($field_module) && module_exists($field_module) && !empty($widget_module) && module_exists($widget_module)) { + $fields[] = $field_name; + } + } + return $fields; +} + +/** + * Get all content types. + */ +function content_copy_types() { + $types = array(); + $content_info = _content_type_info(); + foreach ($content_info['content types'] as $type_name => $val) { + $types[$type_name] = check_plain($val['name']) .' ('. $type_name .')'; + } + return $types; +} + +/** + * A handler that stores the form submissions into a $GLOBALS array + */ +function content_copy_record_macro($form, &$form_state) { + $edit = $form_state['values']; + $subs = isset($GLOBALS['content_copy']['submissions']) ? $GLOBALS['content_copy']['submissions'] : array(); + + // Get the form values and store them in a $GLOBALS['content_copy']['submissions'] array. + // Update $GLOBALS['content_copy']['count'] with an approximation of the number of rows in this item. + // Count is used to approximate necessary size of textarea in form. + + $form_id = $form_state['values']['form_id']; + if (isset($edit['type_name']) || isset($edit['submit']) || isset($edit['delete']) || isset($edit['form_id'])) { + unset($edit['type_name'], $edit['submit'], $edit['delete'], $edit['form_id'], $edit['previous_field']); + } + switch ($form_id) { + case 'node_type_form': + $subs['type'] = $edit; + $GLOBALS['content_copy']['count'] += sizeof($edit) + 5; + break; + + case 'fieldgroup_group_edit_form': + $subs['groups'][] = $edit; + $GLOBALS['content_copy']['count'] += sizeof($edit) + 5; + break; + + default: + if (isset($edit['field_widget_type'])) { + $tmp = explode('-', $edit['field_widget_type']); + $field_name = $tmp[0]; + } + else { + $field_name = isset($edit['field_name']) ? $edit['field_name'] : ''; + } + + // The display settings are being fetched directly from the DB. During import, + // we'll re-insert the data directly as well. + // + $query = 'SELECT display_settings FROM {'. content_instance_tablename() .'} WHERE field_name = \'%s\''; + $row_info = db_fetch_array(db_query($query, $field_name)); + + // If an error occurs, notify the user. + if ($db_err = db_error()) { + drupal_set_message(t("An error occurred when exporting the 'display settings' data for the field %field_name.
The db error is: '%db_err'.", array( + '%field_name' => $field_name, + '%db_err' => $db_err + ))); + } + else { + // The db fetch occurred successfully, unserialize the data blob and + // insert it into a new "display_settings" field of the data. + if ($display_settings = unserialize($row_info['display_settings'])) { + $edit['display_settings'] = $display_settings; + } + } + $subs['fields'][] = $edit; + $GLOBALS['content_copy']['count'] += sizeof($edit) + 5; + break; + } + + $GLOBALS['content_copy']['submissions'] = $subs; +} + +/** + * @return a code representation of the recorded macro. + */ +function content_copy_get_macro() { + // Define the indexes for the evaluated code. + $string = ""; + if (array_key_exists('submissions', $GLOBALS['content_copy'])) { + foreach ($GLOBALS['content_copy']['submissions'] as $form_type => $form) { + $string .= "\$content['$form_type'] = ". var_export((array) $form, TRUE) .";\n"; + } + return $string; + } +} + +function template_preprocess_content_copy_export_form($vars) { + $form = &$vars['form']; + + if ($form['#step'] == 2) { + $order = _content_overview_order($form, $form['#fields'], $form['#groups']); + + $rows = array(); + foreach ($order as $key) { + $element = &$form[$key]; + $row = new stdClass(); + + $row->row_type = $element['#row_type']; + $checkbox_key = $element['#row_type'] == 'field' ? 'fields' : 'groups'; + $row->checkbox = drupal_render($form[$checkbox_key][$key]); + foreach (element_children($element) as $child) { + $row->{$child} = drupal_render($element[$child]); + } + $row->label_class = in_array($key, $form['#groups']) ? 'label-group' : 'label-field'; + $row->indentation = theme('indentation', isset($element['#depth']) ? $element['#depth'] : 0); + + $rows[] = $row; + } + $vars['rows'] = $rows; + } + + $vars['submit'] = drupal_render($form); +} \ No newline at end of file diff --git a/sites/all/modules/cck/modules/content_copy/content_copy_export_form.tpl.php b/sites/all/modules/cck/modules/content_copy/content_copy_export_form.tpl.php new file mode 100644 index 0000000..7125bca --- /dev/null +++ b/sites/all/modules/cck/modules/content_copy/content_copy_export_form.tpl.php @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + row_type): + case 'field': ?> + + + + + + + + + + + + +
checkbox; ?>indentation; ?>human_name; ?>field_name; ?>type; ?>checkbox; ?>indentation; ?>human_name; ?>group_name; ?>
+ + diff --git a/sites/all/modules/cck/modules/content_multigroup/README.txt b/sites/all/modules/cck/modules/content_multigroup/README.txt new file mode 100644 index 0000000..5a45791 --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/README.txt @@ -0,0 +1,99 @@ + +CONTENTS OF THIS FILE +===================== +- USING MULTIGROUPS +- FIELDS AND WIDGETS THAT WORK IN MULTIGROUPS +- VIEWS INTEGRATION +- TROUBLESHOOTING + + +USING MULTIGROUPS +================= + +The Multigroup group treats all included fields like a single field, keeping +the related delta values of all included fields synchronized. + +To use a Multigroup, create a new group, make it the 'Multigroup' type, set +the number of multiple values for all the fields in the Multigroup, and drag +into it the fields that should be included. + +All fields in the Multigroup will automatically get the group setting for +multiple values. On the node form, the group is rearranged to keep the delta +values for each field in a single drag 'n drop group, by transposing the +normal array(group_name => field_name => delta => value) into +array(group_name => delta => field_name => value). + +During validation and submission, the field values are restored to their +normal positions. + + +FIELDS AND WIDGETS THAT WORK IN MULTIGROUPS +=========================================== + +All fields that allow the Content module to handle their multiple values should +work here. Fields that handle their own multiple values will not be allowed +into Multigroups unless they implement hook_content_multigroup_allowed_widgets() +to add their widgets to the allowed widget list. Example: + + @code + function MODULE_content_multigroup_allowed_widgets() { + return array('WIDGET_NAME_1', 'WIDGET_NAME_2', ...); + } + @endcode + +All fields that allow the Content module to handle their multiple values +should work correctly when a field placed on a Multigroup is moved off, to a +normal field group, or to the top level of the form. Fields that handle their +own multiple values which may store different results in Multigroup and +standard groups should implement hook_content_multigroup_no_remove_widgets() +to add their widgets to the list of widgets that cannot be removed from +Multigroups. Example: + + @code + function MODULE_content_multigroup_no_remove_widgets() { + return array('WIDGET_NAME_1', 'WIDGET_NAME_2', ...); + } + @endcode + +The Content Taxonomy module [1] is an example where it implements the previous +hooks for a few widgets. + +[1] http://drupal.org/project/content_taxonomy + +If a simple array of widgets is not sufficient to test whether this action +will work, modules can implement hook_content_multigroup_allowed_in() +and hook_content_multigroup_allowed_out() to intervene. Both hooks should +return an array as in the following example: + + @code + function MODULE_content_multigroup_allowed_in() { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. Reason here...'), + ); + } + @endcode + +Custom code and modules that add fields to groups outside of the UI should +use content_multigroup_allowed_in() and content_multigroup_allowed_out() to +test whether fields are allowed in or out of a Multigroup. These functions +can be located in content_multigroup.admin.inc. + + +VIEWS INTEGRATION +================= + +For each multigroup, there is a new filter under "Content multigroup" category +in Views that provides a method to synchronize fields by delta. + + +TROUBLESHOOTING +=============== + +The most likely cause of problems with field modules not working in multigroup +is if they wipe out #element_validate with their own validation functions, or +they hard-code assumptions into submit or validation processes that the form +is structured in the usual field => delta => value order instead of allowing +for the possibility of a different structure. See Nodereference for an example +of a field that handles validation without making assumptions about the form +structure. diff --git a/sites/all/modules/cck/modules/content_multigroup/content_multigroup.admin.inc b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.admin.inc new file mode 100644 index 0000000..7e7ca58 --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.admin.inc @@ -0,0 +1,531 @@ + t('N/A'), + 1 => t('Unlimited'), + 0 => 1) + drupal_map_assoc(range(2, 10)); +} + +/** + * Validation for creating/moving fields and groups on the + * Manage Fields screen. + */ +function content_multigroup_field_overview_form_validate($form, &$form_state) { + $form_values = $form_state['values']; + $type_name = $form['#type_name']; + $fields = array(); + $groups = array(); + + $group = $form_values['_add_new_group']; + if (array_filter(array($group['label'], $group['group_name']))) { + $group['settings'] = field_group_default_settings($group['group_type']); + $validation = fieldgroup_validate_name($group, $form['#type_name']); + + // If there's something wrong with the new group, + // don't bother doing any more validation, further + // processing will be stopped by the fieldgroup module. + if (!empty($validation['errors'])) { + return; + } + $group['group_name'] = $validation['group_name']; + $new_group_name = $group['group_name']; + $groups['_add_new_group'] = $group; + } + + // See if we have fields moving into or out of a Multigroup. + // Set any fields to use the new name here so they will get processed + // correctly by the fieldgroup module when saved. + $group_rows = array(); + foreach ($form_values as $key => $values) { + if ($values['parent'] == '_add_new_group') { + $values['parent'] = $new_group_name; + $form_values[$key] = $values; + } + + if (!empty($form[$key]['#row_type']) && $form[$key]['#row_type'] == 'group') { + // Gather up info about all groups. + $group_name = $form_values[$key]['group']['group_name']; + $groups[$group_name] = $form_values[$key]['group']; + $group_rows[$group_name] = $group_name; + } + if (!empty($form[$key]['#row_type']) && $form[$key]['#row_type'] == 'field') { + if ($values['prev_parent'] != $values['parent']) { + // Gather up fields that have moved in or out of a group. + $fields[$key] = $form_values[$key]['field']; + } + } + } + + $rebuild = FALSE; + + // Test that a group was not moved into a multigroup, an invalid combination. + foreach ($groups as $key => $values) { + if (in_array($key, $group_rows)) { + $parent = $form_values[$key]['parent']; + $parent_info = !empty($parent) ? $form_values[$parent] : array(); + if (!empty($parent) && $parent_info['group']['group_type'] == 'multigroup') { + $error_message = t('You cannot place any kind of group inside a multigroup. The group @name was moved back to where it started.', array('@name' => $key)); + form_set_value($form[$key]['weight'], $form[$key]['weight']['#default_value'], $form_state); + form_set_value($form[$key]['parent'], $form[$key]['parent']['#default_value'], $form_state); + drupal_set_message($error_message, 'error'); + } + } + } + + // Synchronize the multiple value values for all fields in a group, they must be the same. + // Also ensure that fields moved into multigroups are fields that are allowed. + // In some cases, it may not be safe to move a field back out of a multigroup because + // it will behave differently elsewhere, so check that too. + foreach ($fields as $field_name => $field) { + $new_group = $form_values[$field_name]['parent']; + $old_group = $form_values[$field_name]['prev_parent']; + if (!empty($new_group) && isset($groups[$new_group]) && $groups[$new_group]['group_type'] == 'multigroup') { + $allowed_in = content_multigroup_allowed_in($field, $groups[$new_group]); + if (!$allowed_in['allowed']) { + form_set_error($field_name, $allowed_in['message']); + } + else { + if (!empty($allowed_in['message'])) { + drupal_set_message($allowed_in['message']); + } + module_load_include('inc', 'content', 'includes/content.crud'); + $content_type = content_types($type_name); + $group_multiple = $groups[$new_group]['settings']['multigroup']['multiple']; + $multiple_values = content_multigroup_multiple_values(); + $field = $content_type['fields'][$field_name]; + $field['multiple'] = $group_multiple; + $field = content_field_instance_collapse($field); + content_field_instance_update($field, FALSE); + $rebuild = TRUE; + drupal_set_message(t('The field %field has been updated to use %multiple values, to match the multiple value setting of the Multigroup %group.', array( + '%field' => $field['label'], '%multiple' => $multiple_values[$group_multiple], '%group' => $groups[$new_group]['label']))); + } + } + elseif (!empty($old_group) && isset($groups[$old_group]) && $groups[$old_group]['group_type'] == 'multigroup') { + $allowed_out = content_multigroup_allowed_out($field, $groups[$old_group]); + if (!$allowed_out['allowed']) { + form_set_error($field_name, $allowed_out['message']); + } + elseif (!empty($allowed_out['message'])) { + drupal_set_message($allowed_out['message']); + } + } + } + + // Clear caches and rebuild menu only if any field has been updated. + if ($rebuild) { + content_clear_type_cache(TRUE); + menu_rebuild(); + } +} + +/** + * Helper function for deciding if a field is + * allowed into a Multigroup. + */ +function content_multigroup_allowed_in($field, $group) { + if ($group['group_type'] != 'multigroup') { + return array('allowed' => TRUE, 'message' => ''); + } + + // We can't allow fields with more multiple values than the group has + // to be moved into it. + $max_existing = content_max_delta($field['field_name']); + $group_multiple = $group['settings']['multigroup']['multiple']; + $multiple_values = content_multigroup_multiple_values(); + if ($group_multiple != 1 && $max_existing > $group_multiple) { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. The field %field already has %multiple values in the database but the group %group only allows %group_max. Making this change would result in the loss of data.', array('%field' => $field['widget']['label'], '%multiple' => $max_existing, '%group' => $group['label'], '%group_max' => $multiple_values[$group_multiple])) + ); + } + + // Fields that handle their own multiple values may not have the same values + // in Multigroup fields and normal fields. We don't know if they will work or not. + + // Adding a hook here where widgets that handle their own multiple values + // that will work correctly in Multigroups can allow their fields in. + + if (content_handle('widget', 'multiple values', $field) != CONTENT_HANDLE_CORE) { + $allowed_widgets = array( + 'optionwidgets_select', + 'optionwidgets_buttons', + 'optionwidgets_onoff', + 'nodereference_buttons', + 'nodereference_select', + 'userreference_buttons', + 'userreference_select', + ); + $allowed_widgets = array_merge($allowed_widgets, module_invoke_all('content_multigroup_allowed_widgets')); + if (!in_array($field['widget']['type'], $allowed_widgets)) { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. The field %field handles multiple values differently than the Content module. Making this change could result in the loss of data.', array('%field' => $field['widget']['label'])) + ); + } + } + + // Allow other modules to intervene. + // Any failure will prevent this action. + foreach (module_implements('content_multigroup_allowed_in') as $module) { + $function = $module .'_content_multigroup_allowed_in'; + $result = $function($field, $group); + if ($result['allowed'] === FALSE) { + return array('allowed' => FALSE, 'message' => $result['message']); + } + } + + $message = t('You are moving the field %field into a Multigroup.', array('%field' => $field['widget']['label'])); + return array('allowed' => TRUE, 'message' => $message); +} + +/** + * Helper function for deciding if a field is + * allowed out of a Multigroup. + */ +function content_multigroup_allowed_out($field, $group) { + if ($group['group_type'] != 'multigroup') { + return array('allowed' => TRUE, 'message' => ''); + } + // Optionwidgets do not behave the same in a Multigroup field as out of it. + // In a Multigroup the same option can be selected multiple times, + // but that is not possible in a normal group. + + // Adding a hook here where widgets that handle their own multiple values + // can indicate their fields should not be removed from Multigroups. + + $max_existing = content_max_delta($field['field_name']); + $no_remove_widgets = array( + 'optionwidgets_select', + 'optionwidgets_buttons', + 'optionwidgets_onoff', + 'nodereference_buttons', + 'nodereference_select', + 'userreference_buttons', + 'userreference_select', + ); + $no_remove_widgets = array_merge($no_remove_widgets, module_invoke_all('content_multigroup_no_remove_widgets')); + if (in_array($field['widget']['type'], $no_remove_widgets) && $max_existing > 0) { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. The field %field already has data created and uses a widget that stores data differently in a Standard group than in a Multigroup. Making this change could result in the loss of data.', array('%field' => $field['widget']['label'])) + ); + } + + // Allow other modules to intervene. + // Any failure will prevent this action. + foreach (module_implements('content_multigroup_allowed_out') as $module) { + $function = $module .'_content_multigroup_allowed_out'; + $result = $function($field, $group); + if ($result['allowed'] === FALSE) { + return array('allowed' => FALSE, 'message' => $result['message']); + } + } + + $message = t('You are moving the field %field out of a Multigroup.', array('%field' => $field['widget']['label'])); + return array('allowed' => TRUE, 'message' => $message); +} + +/** + * Alter the basic field settings form. + * + * It should not be possible to choose a widget type that is not compatible + * with multigroups. + */ +function content_multigroup_field_basic_form(&$form, &$form_state) { + $field_name = $form['basic']['field_name']['#value']; + $type_name = $form['type_name']['#value']; + + // Ignore this field if it is not part of a field group. + if (!($group_name = fieldgroup_get_group($type_name, $field_name))) { + return; + } + + // Retrieve information about the group the field is in. + $groups = fieldgroup_groups($type_name); + $group = $groups[$group_name]; + + // Ignore this field if it is not part of a multigroup. + if ($group['group_type'] != 'multigroup') { + return; + } + + // Retrieve information about the field itself. + $field = content_fields($field_name, $type_name); + + // Check if the widget can be moved out of the multigroup. + $allowed_out = content_multigroup_allowed_out($field, $group); + if (!$allowed_out['allowed']) { + $form['basic']['widget_type']['#disabled'] = TRUE; + $form['basic']['widget_type']['#suffix'] = '
'. t('The widget type cannot be changed because the field %field already has data created and this widget stores data differently in a Standard group than in a Multigroup. Allowing this change could result in the loss of data.', array('%field' => $field['widget']['label'])) .'
'; + return; + } + + // Remove from the list of available widgets those that are not + // compatible with multigroups. + $widget_types = _content_widget_types(); + foreach (array_keys($form['basic']['widget_type']['#options']) as $widget_type) { + if ($field['widget']['type'] != $widget_type) { + $field_copy = $field; + $field_copy['widget']['type'] = $widget_type; + $field_copy['widget']['module'] = $widget_types[$widget_type]['module']; + $allowed_in = content_multigroup_allowed_in($field_copy, $group); + if (!$allowed_in['allowed']) { + unset($form['basic']['widget_type']['#options'][$widget_type]); + } + } + } +} + +/** + * Alter the "Display fields" form. + * + * Add an additional selector for setting multigroup field display format. + */ +function content_multigroup_display_overview_form(&$form, &$form_state) { + + $type_name = $form['#type_name']; + $contexts_selector = $form['#contexts']; + + // Gather type information. + $content_type = content_types($type_name); + + // The content module stops building the form if the type has no fields. + if (empty($content_type['fields'])) { + return; + } + + $groups = array(); + $groups = fieldgroup_groups($type_name); + $contexts = content_build_modes($contexts_selector); + $all_contexts = content_build_modes(); + + // Multigroups, extra values. + $label_options = array( + 'above' => t('Above'), + 'hidden' => t(''), + ); + $options = array( + 'simple' => t('Simple'), + 'fieldset' => t('Fieldset'), + 'fieldset_collapsible' => t('Fieldset - collapsible'), + 'fieldset_collapsed' => t('Fieldset - collapsed'), + 'hr' => t('Horizontal line'), + 'table-single' => t('Table - Single column'), + 'table-multiple' => t('Table - Multiple columns'), + 'ul' => t('Unordered List'), + ); + foreach ($groups as $group_name => $group) { + if ($group['group_type'] != 'multigroup') { + continue; + } + $subgroup_settings = isset($group['settings']['multigroup']['subgroup']) ? $group['settings']['multigroup']['subgroup'] : array(); + + $subgroup_name = $group_name .'_subgroup'; + $form['#fields'] = array_merge(array($subgroup_name), $form['#fields']); + $form[$subgroup_name] = array( + 'human_name' => array('#value' => t('[Subgroup format]')), + 'weight' => array('#type' => 'value', '#value' => -20), + 'parent' => array('#type' => 'value', '#value' => $group_name), + 'subgroup' => array('#type' => 'value', '#value' => 1), + ); + if ($contexts_selector == 'basic') { + $form[$subgroup_name]['label'] = array( + '#type' => 'select', + '#options' => $label_options, + '#default_value' => isset($subgroup_settings['label']) ? $subgroup_settings['label'] : 'above', + ); + } + + // Allow a format selection for contexts on the current tab. + // Store other contexts as hidden values so they don't get lost. + foreach ($all_contexts as $key => $title) { + if (array_key_exists($key, $contexts)) { + $form[$subgroup_name][$key]['format'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => isset($subgroup_settings[$key]['format']) ? $subgroup_settings[$key]['format'] : 'fieldset', + ); + $form[$subgroup_name][$key]['exclude'] = array('#type' => 'value', '#value' => 0); + } + else { + $form[$subgroup_name][$key]['format'] = array( + '#type' => 'hidden', + '#value' => isset($subgroup_settings[$key]['format']) ? $subgroup_settings[$key]['format'] : 'fieldset', + ); + $form[$subgroup_name][$key]['exclude'] = array('#type' => 'value', '#value' => 0); + + } + } + } + $form['#submit'] = array_merge(array('content_multigroup_display_overview_form_submit'), $form['#submit']); +} + +/** + * Submit handler for the display overview form. + * + * Do this in pre_save so we catch it before the content module + * tries to use our 'field'. + */ +function content_multigroup_display_overview_form_submit($form, &$form_state) { + $groups = fieldgroup_groups($form['#type_name']); + //$reset_cache = FALSE; + // Find any subgroups we inserted into the display fields form, + // save our settings, and remove them from $form_state. + foreach ($form_state['values'] as $key => $values) { + if (in_array($key, $form['#fields']) && !empty($values['parent']) && !empty($values['subgroup'])) { + $group_name = $values['parent']; + $group = $groups[$group_name]; + unset($values['subgroup'], $values['parent']); + + // We have some numeric keys here, so we can't use array_merge. + foreach ($values as $k => $v) { + $form_state['values'][$group_name]['settings']['multigroup']['subgroup'][$k] = $v; + } + + // Remove the subgroup from $form_state. + unset($form_state['values'][$key]); + } + } +} + +/** + * Alter the Fieldgroup edit form to add Multigroup settings. + */ +function content_multigroup_group_edit_form(&$form, &$form_state) { + $type_name = $form['#content_type']['type']; + $group_name = $form['group_name']['#default_value']; + + $content_type = content_types($type_name); + $groups = fieldgroup_groups($type_name); + $group = $groups[$group_name]; + + if ($group['group_type'] != 'multigroup') { + return; + } + + module_load_include('inc', 'content', 'includes/content.admin'); + module_load_include('inc', 'content', 'includes/content.crud'); + $form['group_type'] = array( + '#type' => 'hidden', + '#value' => $group['group_type'], + ); + $form['settings']['multigroup'] = array( + '#type' => 'fieldset', + '#title' => t('Multigroup settings'), + '#collapsed' => FALSE, + '#collapsible' => TRUE, + ); + if (isset($group['settings']['multigroup']['subgroup'])) { + // Preserve subgroup display settings. + $form['settings']['multigroup']['subgroup'] = array( + '#type' => 'value', + '#value' => $group['settings']['multigroup']['subgroup'], + ); + } + + $form['settings']['multigroup']['multiple-columns'] = array( + '#type' => 'checkbox', + '#title' => t('Multiple columns'), + '#default_value' => isset($group['settings']['multigroup']['multiple-columns']) ? $group['settings']['multigroup']['multiple-columns'] : 0, + '#description' => t('Enable this option to render each field on a separate column on the node edit form.'), + ); + + $form['settings']['multigroup']['required'] = array( + '#type' => 'checkbox', + '#title' => t('Required'), + '#default_value' => !empty($group['settings']['multigroup']['required']) ? $group['settings']['multigroup']['required'] : 0, + '#description' => t('Enable this option to require a minimum of one collection of fields in this Multigroup.'), + ); + + $description = t('Number of times to repeat the collection of Multigroup fields.') .' '; + $description .= t("'Unlimited' will provide an 'Add more' button so the users can add items as many times as they like.") .' '; + $description .= t('All fields in this group will automatically be set to allow this number of values.'); + + $group_multiple = isset($group['settings']['multigroup']['multiple']) ? $group['settings']['multigroup']['multiple'] : 1; + $form['settings']['multigroup']['multiple'] = array( + '#type' => 'select', + '#title' => t('Number of repeats'), + '#options' => content_multigroup_multiple_values(), + '#default_value' => $group_multiple, + '#description' => $description, + ); + + $form['settings']['multigroup']['labels'] = array( + '#type' => 'fieldset', + '#title' => t('Labels'), + '#description' => t("Labels for each subgroup of fields. Labels can be hidden or shown in various contexts using the 'Display fields' screen."), + ); + if ($group_multiple < 2) { + $group_multiple = 0; + } + for ($i = 0; $i < 10; $i++) { + $form['settings']['multigroup']['labels'][$i] = array( + '#type' => 'textfield', + '#title' => t('Subgroup %number label', array('%number' => $i + 1)), + '#default_value' => isset($group['settings']['multigroup']['labels'][$i]) ? $group['settings']['multigroup']['labels'][$i] : '', + ); + } + + $form['#validate'][] = 'content_multigroup_group_edit_form_validate'; + $form['#submit'][] = 'content_multigroup_group_edit_form_submit'; +} + +/** + * Validate the Fieldgroup edit form. + */ +function content_multigroup_group_edit_form_validate($form, &$form_state) { + $form_values = $form_state['values']; + $group_type = $form_values['group_type']; + if ($group_type != 'multigroup') { + return; + } + $content_type = $form['#content_type']; + $groups = fieldgroup_groups($content_type['type']); + $group = $groups[$form_values['group_name']]; + foreach ($group['fields'] as $field_name => $data) { + // Make sure we don't set the multiple values to a number that + // would result in lost data. + $max_existing = content_max_delta($field_name); + if ($form_values['settings']['multigroup']['multiple'] != 1 + && $max_existing > $form_values['settings']['multigroup']['multiple']) { + form_set_error('settings][multigroup][multiple', t('The field %field in this group already has %multiple values in the database. To prevent the loss of data you cannot set the number of Multigroup values to less than this.', array('%field' => $data['label'], '%multiple' => $max_existing))); + } + } +} + +/** + * Submit the Fieldgroup edit form. + * + * Update multiple values of fields contained in Multigroups. + */ +function content_multigroup_group_edit_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + $group_type = $form_values['group_type']; + if ($group_type != 'multigroup') { + return; + } + module_load_include('inc', 'content', 'includes/content.crud'); + $content_type = $form['#content_type']; + $groups = fieldgroup_groups($content_type['type']); + $group = $groups[$form_values['group_name']]; + $group_fields = array_intersect_key($content_type['fields'], $group['fields']); + if (!empty($group_fields)) { + foreach ($group_fields as $field_name => $field) { + $field['multiple'] = $form_values['settings']['multigroup']['multiple']; + $field = content_field_instance_collapse($field); + content_field_instance_update($field, FALSE); + } + content_clear_type_cache(TRUE); + menu_rebuild(); + } +} diff --git a/sites/all/modules/cck/modules/content_multigroup/content_multigroup.css b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.css new file mode 100644 index 0000000..bd6f48a --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.css @@ -0,0 +1,41 @@ + +label.content-multigroup { + font-weight: bold; +} + +/* Not styled by default, but available to style. */ +hr.content-multigroup { +} + +/* Inline field labels visible within the context of multigroups. */ +.content-multigroup-wrapper .field .field-label-inline { + visibility: visible; +} + +/** + * Hide field labels and description on the node edit form when the multiple + * columns option is enabled. + */ +.content-multigroup-edit-table-multiple-columns label, +.content-multigroup-edit-table-multiple-columns .description { + display: none; +} + +/* Hide field labels when using 'table-multiple' display mode. */ +.content-multigroup-display-table-multiple-columns .field .field-label, +.content-multigroup-display-table-multiple-columns .field .field-label-inline, +.content-multigroup-display-table-multiple-columns .field .field-label-inline-first { + display: none; +} + +/* Display table with a row for each subgroup and all fields in a single column. */ +.content-multigroup-display-table-single-column .content-multigroup-wrapper { + clear: both; +} +.content-multigroup-display-table-single-column .content-multigroup-wrapper label.content-multigroup { + display: block; +} +.content-multigroup-display-table-single-column .content-multigroup-wrapper .field { + float: left; + margin-right: 1em; +} diff --git a/sites/all/modules/cck/modules/content_multigroup/content_multigroup.info b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.info new file mode 100644 index 0000000..39fe544 --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.info @@ -0,0 +1,14 @@ +name = Content Multigroup +description = Combine multiple CCK fields into repeating field collections that work in unison. +dependencies[] = content +dependencies[] = fieldgroup +package = CCK +core = 6.x + + +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/content_multigroup/content_multigroup.install b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.install new file mode 100644 index 0000000..9e12be3 --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.install @@ -0,0 +1,46 @@ + 2, + 'path' => drupal_get_path('module', 'content_multigroup') . '/views', + ); +} + +/** + * Implementation of hook_ctools_plugin_directory(). + */ +function content_multigroup_ctools_plugin_directory($module, $plugin) { + if ($module == 'ctools' && $plugin == 'content_types') { + return 'panels/' . $plugin; + } +} + +/** + * Implementation of hook_menu(). + */ +function content_multigroup_menu() { + $items = array(); + // Callback for AHAH add more buttons. + $items['content_multigroup/js_add_more'] = array( + 'page callback' => 'content_multigroup_add_more_js', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + 'file' => 'content_multigroup.node_form.inc', + ); + return $items; +} + +/** + * Implementation of hook_theme(). + */ +function content_multigroup_theme() { + return array( + 'content_multigroup_node_form' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_form.inc', + ), + 'content_multigroup_node_label' => array( + 'arguments' => array('text' => NULL), + 'file' => 'content_multigroup.node_form.inc', + ), + 'content_multigroup_add_more_label' => array( + 'arguments' => array('group_name' => NULL), + 'file' => 'content_multigroup.node_form.inc', + ), + 'content_multigroup_display_simple' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), + 'content_multigroup_display_fieldset' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), + 'content_multigroup_display_hr' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), + 'content_multigroup_display_table_single' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), + 'content_multigroup_display_table_multiple' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), + 'content_multigroup_display_ul' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), + ); +} + +/** + * Implementation of hook_elements(). + */ +function content_multigroup_elements() { + return array( + 'content_multigroup_display_fieldset' => array('#value' => NULL), + ); +} + +/** + * Implementation of hook_fieldgroup_types(). + */ +function content_multigroup_fieldgroup_types() { + return array('multigroup' => t('Multigroup')); +} + +/** + * Implementation of hook_fieldgroup_default_settings(). + */ +function content_multigroup_fieldgroup_default_settings($group_type) { + if ($group_type == 'multigroup') { + module_load_include('inc', 'content', 'includes/content.admin'); + $settings = array('multigroup' => array('multiple' => 1)); + foreach (array_keys(content_build_modes()) as $key) { + $settings['display'][$key]['format'] = 'fieldset'; + } + return $settings; + } +} + +/** + * Implementation of hook_form_alter(). + */ +function content_multigroup_form_alter(&$form, $form_state, $form_id) { + if ($form_id == 'content_field_edit_form' && isset($form['widget'])) { + // If this is a field edit form and the field is in a Multigroup, + // override the multiple value settings. + $content_type = content_types($form['type_name']['#value']); + $groups = fieldgroup_groups($content_type['type']); + $group_name = _fieldgroup_field_get_group($content_type['type'], $form['field_name']['#value']); + $group = isset($groups[$group_name]) ? $groups[$group_name] : array(); + if (!empty($group) && $group['group_type'] == 'multigroup') { + $form['field']['multiple']['#value'] = $group['settings']['multigroup']['multiple']; + $form['field']['multiple']['#access'] = FALSE; + } + } + elseif ($form_id == 'content_field_edit_form' && isset($form_state['change_basic'])) { + // This is the basic field settings form. It should not be possible to + // choose a widget type that is not compatible with multigroups. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); + content_multigroup_field_basic_form($form, $form_state); + } + elseif ($form_id == 'content_field_overview_form') { + // Validation for creating/moving fields and groups on the + // Manage Fields screen. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); + $form['#validate'][] = 'content_multigroup_field_overview_form_validate'; + } + elseif ($form_id == 'content_display_overview_form' && !empty($form['#groups'])) { + // Add an additional selector for setting multigroup field display + // format to the Display Fields screen. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); + content_multigroup_display_overview_form($form, $form_state); + } + elseif ($form_id == 'fieldgroup_group_edit_form') { + // Alter the Fieldgroup edit form to add Multigroup settings. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); + content_multigroup_group_edit_form($form, $form_state); + } +} + +/** + * After build callback for multigroups in node form. + * + * This proxy function is necessary to prevent from breaking AHAH handlers. + */ +function content_multigroup_node_form_after_build($form, &$form_state) { + module_load_include('inc', 'content_multigroup', 'content_multigroup.node_form'); + return _content_multigroup_node_form_after_build($form, $form_state); +} + +/** + * Implementation of hook_fieldgroup_form(). + */ +function content_multigroup_fieldgroup_form(&$form, &$form_state, $form_id, $group) { + $group_name = $group['group_name']; + if ($group['group_type'] == 'multigroup' && !empty($form[$group_name])) { + if (!isset($form[$group_name]['#access']) || $form[$group_name]['#access']) { + module_load_include('inc', 'content_multigroup', 'content_multigroup.node_form'); + _content_multigroup_fieldgroup_form($form, $form_state, $form_id, $group); + } + } +} + +/** + * Implementation of hook_fieldgroup_view(). + */ +function content_multigroup_fieldgroup_view(&$node, &$element, $group, $context) { + if ($group['group_type'] == 'multigroup') { + module_load_include('inc', 'content_multigroup', 'content_multigroup.node_view'); + _content_multigroup_fieldgroup_view($node, $element, $group, $context); + } +} diff --git a/sites/all/modules/cck/modules/content_multigroup/content_multigroup.node_form.inc b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.node_form.inc new file mode 100644 index 0000000..f1aa5de --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.node_form.inc @@ -0,0 +1,984 @@ + $field) { + if (isset($group['fields'][$field_name]) && isset($form[$group_name][$field_name])) { + if (!isset($form[$group_name][$field_name]['#access']) || $form[$group_name][$field_name]['#access']) { + $group_fields[$field_name] = $field; + } + } + } + + // Quit if there are no field in the form for this group. + if (empty($group_fields)) { + return; + } + + switch ($group_multiple) { + case 0: + $group_deltas = array(0); + $max_delta = 0; + break; + + case 1: + // Compute unique deltas from all deltas used by fields in this multigroup. + $group_deltas = array(); + $max_delta = -1; + foreach (array_keys($group_fields) as $field_name) { + if (!empty($node->$field_name) && is_array($node->$field_name)) { + foreach (array_keys($node->$field_name) as $delta) { + $group_deltas[$delta] = $delta; + } + sort($group_deltas); + $max_delta = max($max_delta, max($group_deltas)); + } + } + $current_item_count = isset($form_state['item_count'][$group_name]) ? $form_state['item_count'][$group_name] : max(1, count($group_deltas)); + while (count($group_deltas) < $current_item_count) { + $max_delta++; + $group_deltas[] = $max_delta; + } + break; + + default: + $max_delta = $group_multiple - 1; + $group_deltas = range(0, $max_delta); + break; + } + + $form[$group_name]['#theme'] = 'content_multigroup_node_form'; + $form[$group_name]['#item_count'] = count($group_deltas); + $form[$group_name]['#type_name'] = $group['type_name']; + $form[$group_name]['#group_name'] = $group_name; + $form[$group_name]['#group_label'] = $group['label']; + $form[$group_name]['#group_fields'] = $group_fields; + $form[$group_name]['#tree'] = TRUE; + // Multigroups cannot be vertical tabs, don't let Vertical Tabs module try to do that. + $form[$group_name]['#group'] = FALSE; + if (!isset($form['#multigroups'])) { + $form['#multigroups'] = array(); + } + $form['#multigroups'][$group_name] = $group_fields; + + // Add a visual indication to the fieldgroup title if the multigroup is required. + if (!empty($group['settings']['multigroup']['required'])) { + $form[$group_name]['#title'] .= ' *'; + } + + // Attach our own after build handler to the form, used to fix posting data + // and the form structure, moving fields back to their original positions. + // That is, move them from group->delta->field back to field->delta. + if (!isset($form['#after_build'])) { + $form['#after_build'] = array(); + } + if (!in_array('content_multigroup_node_form_after_build', $form['#after_build'])) { + array_unshift($form['#after_build'], 'content_multigroup_node_form_after_build'); + } + + // Attach our own validation handler to the form, used to check for empty fields. + if (!isset($form['#validate'])) { + $form['#validate'] = array(); + } + if (!in_array('content_multigroup_node_form_validate', $form['#validate'])) { + array_unshift($form['#validate'], 'content_multigroup_node_form_validate'); + } + + $elements[$group_name] = array(); + foreach ($group_deltas as $delta) { + $element = content_multigroup_group_form($form, $form_state, $group, $delta); + $elements[$group_name] = array_merge($elements[$group_name], $element[$group_name]); + } + $form[$group_name] = $elements[$group_name]; + + // Unset the original group field values now that we've moved them. + foreach (array_keys($group_fields) as $field_name) { + unset($form[$group_name][$field_name]); + } + + // Disable required flag during FormAPI validation, except when building the + // form for an 'Add more values' request, then replace it in pre_render. + // To avoid re-creating this array of values over and over, create it once + // and store it as a form attribute. + $form['#multigroup_required_fields'] = array(); + if (empty($form_state['multigroup_add_more'])) { + foreach ($form['#multigroups'] as $group_name => $group_fields) { + $form['#multigroup_required_fields'][$group_name] = array(); + foreach ($group_fields as $field_name => $field) { + if ($field['required']) { + $form['#multigroup_required_fields'][$group_name][$field_name] = TRUE; + $form['#field_info'][$field_name]['required'] = FALSE; + } + } + } + if (!isset($form['#pre_render'])) { + $form['#pre_render'] = array(); + } + if (!in_array('content_multigroup_node_form_pre_render', $form['#pre_render'])) { + array_unshift($form['#pre_render'], 'content_multigroup_node_form_pre_render'); + } + } + + if (($add_more = content_multigroup_add_more($form, $form_state, $group)) !== FALSE) { + $form[$group_name] += $add_more; + } +} + +/** + * Create a new delta value for the group. + * + * Called in form_alter and by AHAH add more. + */ +function content_multigroup_group_form(&$form, &$form_state, $group, $delta) { + module_load_include('inc', 'content', 'includes/content.node_form'); + $element = array(); + $type_name = $group['type_name']; + $content_type = content_types($type_name); + $group_name = $group['group_name']; + + if (!isset($form[$group_name])) {//nested AHAH, not initial build + $element[$group_name] = array_shift(content_get_nested_elements($form, $group_name)); + } + else {//initial build (via content_multigroup_fieldgroup_form) or non-nested AHAH + $element[$group_name] = $form[$group_name]; + } + if (($group['group_type'] != 'multigroup') + || (!(empty($element[$group['group_name']]['#access'])) && $element[$group['group_name']]['#access'] != TRUE) + || empty($element[$group['group_name']])) { + return; + } + + $group_fields = $form['#multigroups'][$group_name]; + $element[$group_name]['#fields'] = array_keys($group_fields); + $node = $form['#node']; + $group_multiple = $group['settings']['multigroup']['multiple']; + + foreach ($group_fields as $field_name => $field) { + if (empty($element[$group_name][$delta])) { + $element[$group_name] += array($delta => array($field_name => array())); + } + else { + $element[$group_name][$delta][$field_name] = array(); + } + + $item_count = (isset($form_state['item_count'][$group_name]) ? $form_state['item_count'][$group_name] : $element[$group_name]['#item_count']); + $element[$group_name][$delta]['_weight'] = array( + '#type' => 'weight', + '#delta' => $item_count, // this 'delta' is the 'weight' element's property + '#default_value' => $delta, + '#weight' => 100, + ); + + // Add a checkbox to allow users remove a single delta subgroup. + // See content_set_empty() and theme_content_multigroup_node_form(). + if ($group_multiple == 1) { + $element[$group_name][$delta]['_remove'] = array( + '#type' => 'checkbox', + '#attributes' => array('class' => 'content-multiple-remove-checkbox'), + '#default_value' => isset($form_state['multigroup_removed'][$group_name][$delta]) ? $form_state['multigroup_removed'][$group_name][$delta] : 0, + ); + } + + // Make each field into a pseudo single value field + // with the right delta value. + $field['multiple'] = 0; + + $form['#field_info'][$field_name] = $field; + $node_copy = drupal_clone($node); + + // Set the form '#node' to the delta value we want so the Content + // module will feed the right $items to the field module in + // content_field_form(). + + // There may be missing delta values for fields that were + // never created, so check first. + if (!empty($node->$field_name) && isset($node->{$field_name}[$delta])) { + $node_copy->$field_name = array($delta => $node->{$field_name}[$delta]); + } + else { + $value = NULL; + // Try to obtain default values only if the node is being created. + if (!isset($node->nid) && content_callback('widget', 'default value', $field) != CONTENT_CALLBACK_NONE) { + // If a module wants to insert custom default values here, + // it should provide a hook_default_value() function to call, + // otherwise the content module's content_default_value() function + // will be used. + $callback = content_callback('widget', 'default value', $field) == CONTENT_CALLBACK_CUSTOM ? $field['widget']['module'] .'_default_value' : 'content_default_value'; + if (function_exists($callback)) { + $items = $callback($form, $form_state, $field, 0); + $value = !empty($items) ? $items[0] : ''; + } + } + $node_copy->$field_name = array($delta => $value); + } + $form['#node'] = $node_copy; + + // Place the new element into the $delta position in the group form. + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + $field_form = content_field_form($form, $form_state, $field, $delta); + $value = array_key_exists($delta, $field_form[$field_name]) ? $delta : 0; + $element[$group_name][$delta][$field_name] = $field_form[$field_name][$value]; + } + else { + // When the form is submitted, get the element data from the form values. + if (isset($form_state['values'][$field_name])) { + $form_state_copy = $form_state; + if (isset($form_state_copy['values'][$field_name][$delta])) { + $form_state_copy['values'][$field_name] = array($delta => $form_state_copy['values'][$field_name][$delta]); + } + else { + $form_state_copy['values'][$field_name] = array($delta => NULL); + } + $field_form = content_field_form($form, $form_state_copy, $field, $delta); + } + else { + $field_form = content_field_form($form, $form_state, $field, $delta); + } + + // Multiple value fields have an additional level in the array form that + // needs to get fixed in $form_state['values']. + if (!isset($field_form[$field_name]['#element_validate'])) { + $field_form[$field_name]['#element_validate'] = array(); + } + $field_form[$field_name]['#element_validate'][] = 'content_multigroup_fix_multivalue_fields'; + + $element[$group_name][$delta][$field_name] = $field_form[$field_name]; + } + $element[$group_name][$delta][$field_name]['#weight'] = $field['widget']['weight']; + } + + // Reset the form '#node' back to its original value. + $form['#node'] = $node; + + return $element; +} + +/** + * Fix required flag during form rendering stage. + * + * Required fields should display the required star in the rendered form. + */ +function content_multigroup_node_form_pre_render(&$form) { + foreach ($form['#multigroups'] as $group_name => $group_fields) { + if (!empty($form['#multigroup_required_fields'][$group_name])) { + $required_fields = array_keys($form['#multigroup_required_fields'][$group_name]); + content_multigroup_node_form_fix_required($form[$group_name], $required_fields, TRUE); + } + } + return $form; +} + +/** + * Fix form and posting data when the form is submitted. + * + * FormAPI uses form_builder() during form processing to map incoming $_POST + * data to the proper elements in the form. It builds the '#parents' array, + * copies the $_POST array to the '#post' member of all form elements, and it + * also builds the $form_state['values'] array. Then the '#after_build' hook is + * invoked to allow custom processing of the form structure, and that happens + * just before validation and submit handlers are executed. + * + * During hook_form_alter(), the multigroup module altered the form structure + * moving elements from field->delta to multigroup->delta->field position, + * which is what has been processed by FormAPI to build the form structures, + * but field validation (and submit) handlers expect their data to be located + * in their original positions. + * + * We now need to move the fields back to their original positions in the form, + * and we need to do so without altering the form rendering process, which is + * now reflecting the structure the multigroup is interested in. We just need + * to fix the parts of the form that affect validation and submit processing. + */ +function _content_multigroup_node_form_after_build($form, &$form_state) { + // Disable required flag during FormAPI validation, except when building the + // form for an 'Add more values' request. + $required = !empty($form_state['multigroup_add_more']); + foreach ($form['#multigroups'] as $group_name => $group_fields) { + if (!empty($form['#multigroup_required_fields'][$group_name])) { + $required_fields = array_keys($form['#multigroup_required_fields'][$group_name]); + content_multigroup_node_form_fix_required($form[$group_name], $required_fields, $required); + } + } + + + if ($form_state['submitted'] && !$form_state['multigroup_add_more']) { + // Fix value positions in $form_state for the fields in multigroups. + foreach (array_keys($form['#multigroups']) as $group_name) { + content_multigroup_node_form_transpose_elements($form, $form_state, $form['#node']->type, $group_name); + } + + // Fix form element parents for all fields in multigroups. + content_multigroup_node_form_fix_parents($form, $form['#multigroups']); + + // Update posting data to reflect delta changes in the form structure. + if (!empty($_POST)) { + content_multigroup_node_form_fix_post($form); + } + } + + return $form; +} + +/** + * Fix required flag for required fields. + * + * We need to let the user enter an empty set of fields for a delta subgroup, + * even if it contains required fields, which is equivalent to say a subgroup + * should be ignored, not to be stored into the database. + * So, we need to check for required fields, but only for non-empty subgroups. + * + * When the form is processed for rendering, the required flag is enabled for + * all required fields, so the user can see what's required and what's not. + * + * When the form is processed for validation, the required flag is disabled, + * so that FormAPI does not report errors for empty fields. + * + * @see content_multigroup_node_form_validate(). + */ +function content_multigroup_node_form_fix_required(&$elements, $required_fields, $required) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + if (count($elements[$key]['#parents']) >= 3 && in_array($elements[$key]['#parents'][2], $required_fields) && isset($elements[$key]['#required'])) { + $elements[$key]['#required'] = $required; + // Required option lists do not have an empty option available. Add one to avoid 'An illegal choice has been detected' errors. + if (!$required && !empty($elements[$key]['#options']) && substr($elements[$key]['#id'], -7) != '_weight') { + $empty = array('' => ''); + $elements[$key]['#options'] = $empty + $elements[$key]['#options']; + } + } + + // Recurse through all children elements. + content_multigroup_node_form_fix_required($elements[$key], $required_fields, $required); + } + } +} + +/** + * Node form validation handler. + * + * Perform validation for empty fields ignoring subgroups flagged for removal. + * Note that FormAPI validation for required fields is disabled because we need + * to accept empty fields that are flagged for removal. + */ +function content_multigroup_node_form_validate($form, &$form_state) { + $type_name = $form['#node']->type; + $groups = fieldgroup_groups($type_name); + + foreach ($form['#multigroups'] as $group_name => $group_fields) { + $group = $groups[$group_name]; + $group_required = isset($group['settings']['multigroup']['required']) ? $group['settings']['multigroup']['required'] : 0; + + $non_empty_subgroups = $non_removed_subgroups = $required_field_errors = array(); + foreach ($group_fields as $field_name => $field) { + // Tell the content module that it is not needed to enforce requirement + // of fields in this multigroup because we are doing it here. + // See content_multiple_value_nodeapi_validate(). + $form_state['values']['_content_ignore_required_fields'][$field_name] = TRUE; + + foreach ($form_state['values'][$field_name] as $delta => $item) { + // Keep track of the highest delta value for this group. + $max_delta = $delta; + + // Ignore subgroups flagged for removal. + if ($form_state['multigroup_removed'][$group_name][$delta]) { + continue; + } + // Keep track of non-removed subgroups. + $non_removed_subgroups[$delta] = TRUE; + + $is_empty_function = $field['module'] .'_content_is_empty'; + if ($is_empty_function($form_state['values'][$field_name][$delta], $field)) { + // Ignore fields that are not required. + if (!$field['required']) { + continue; + } + + // Build an error message for this field in this subgroup, but do + // not flag it, yet. + if (!empty($item['_error_element'])) { + // Here we don't know the number of elements and subelements a + // widget could have added to the form, so we need to extract + // components from the top, where we have group/delta/field, and + // then push back field/delta on top of the list. + $error_element = explode('][', $item['_error_element']); + array_shift($error_element); + array_shift($error_element); + array_shift($error_element); + array_unshift($error_element, $field_name, $delta); + $error_element = implode('][', $error_element); + } + else { + // Imagefield does not use error_element, sets error on the field. + // Are there others that need different treatment? + $error_element = $field_name; + } + $required_field_errors[$delta][$field_name] = array( + 'element' => $error_element, + 'message' => t('!name field is required in group @group.', array( + '!name' => $form[$group_name][$delta][$field_name]['#title'], + '@group' => t($group['label']), + )), + ); + } + else { + $non_empty_subgroups[$delta] = TRUE; + } + } + } + + // Required multigroups require at least one non-empty subgroup of fields. + if ($group_required && empty($non_empty_subgroups)) { + form_set_error('', t('Group @name requires one collection of fields minimum.', array('@name' => t($group['label'])))); + continue; + } + + // Force remove any empty groups so they will collapse. + // Don't flag errors on empty groups. + for ($delta = 0; $delta <= $max_delta; $delta++) { + if (!isset($non_empty_subgroups[$delta]) && isset($non_removed_subgroups[$delta])) { + unset($non_removed_subgroups[$delta]); + $form_state['multigroup_removed'][$group_name][$delta] = TRUE; + foreach ($group_fields as $field_name => $item) { + $form_state['values'][$field_name][$delta]['_remove'] = TRUE; + } + if (isset($required_field_errors[$delta])) { + unset($required_field_errors[$delta]); + } + } + } + + // Ok, now we can flag errors for all required fields that have not been + // filled in when they should. + foreach ($required_field_errors as $delta => $error_list) { + foreach ($error_list as $field_name => $error_info) { + form_set_error($error_info['element'], $error_info['message']); + } + } + } +} + +/** + * Transpose element positions in $form_state for the fields in a multigroup. + */ +function content_multigroup_node_form_transpose_elements(&$form, &$form_state, $type_name, $group_name) { + $groups = fieldgroup_groups($type_name); + $group = $groups[$group_name]; + $group_fields = $form['#multigroups'][$group_name]; + + // Save the remove state of multigroup items in the $form_state array. + if (!isset($form_state['multigroup_removed'])) { + $form_state['multigroup_removed'] = array(); + } + if (!isset($form_state['multigroup_removed'][$group_name])) { + $form_state['multigroup_removed'][$group_name] = array(); + } + + // Move group data from group->delta->field to field->delta. + $group_data = array(); + foreach ($form_state['values'][$group_name] as $delta => $items) { + // Skip 'add more' button. + if (!is_array($items) || !isset($items['_weight'])) { + continue; + } + foreach ($group_fields as $field_name => $field) { + if (!isset($group_data[$field_name])) { + $group_data[$field_name] = array(); + } + // Get field weight and remove state from the group and keep track of the + // current delta for each field item. + $item_defaults = array( + '_weight' => $items['_weight'], + '_remove' => $items['_remove'], + '_old_delta' => $delta, + ); + $group_data[$field_name][$delta] = (is_array($items[$field_name]) ? array_merge($items[$field_name], $item_defaults) : $item_defaults); + // Store the remove state and the element weight in the form element as + // well, so we can restore them later. + // See content_multigroup_fix_multivalue_fields(). + // See content_multigroup_fix_element_values(). + $form[$group_name][$delta][$field_name]['#_weight'] = $items['_weight']; + $form[$group_name][$delta][$field_name]['#removed'] = $items['_remove']; + + // Insert an element valitation callback of our own at the end of the + // list to ensure the drag'n'drop weight of the element is not lost by + // a form_set_value() operation made by the validation callback of the + // widget element. + if (!isset($form[$group_name][$delta][$field_name]['#element_validate'])) { + $form[$group_name][$delta][$field_name]['#element_validate'] = array(); + } + $form[$group_name][$delta][$field_name]['#element_validate'][] = 'content_multigroup_fix_element_values'; + } + $form_state['multigroup_removed'][$group_name][$delta] = $items['_remove']; + } + + $form_group_sorted = FALSE; + foreach ($group_data as $field_name => $items) { + + // Sort field items according to drag-n-drop reordering. Deltas are also + // rebuilt to start counting from 0 to n. Note that since all fields in the + // group share the same weight, their deltas remain in sync. + usort($items, '_content_sort_items_helper'); + + // Now we need to apply the same ordering to the form elements. Also, + // note that deltas have changed during the sort operation, so we need + // to reflect this delta conversion in the form. + if (!$form_group_sorted) { + $form_group_items = array(); + $form_deltas = array(); + foreach ($items as $new_delta => $item) { + $form_deltas[$item['_old_delta']] = $new_delta; + $form_group_items[$new_delta] = $form[$group_name][$item['_old_delta']]; + unset($form[$group_name][$item['_old_delta']]); + } + foreach ($form_group_items as $new_delta => $form_group_item) { + $form[$group_name][$new_delta] = $form_group_item; + } + content_multigroup_node_form_fix_deltas($form[$group_name], $form_deltas); + $form_group_sorted = TRUE; + } + + // Get rid of the old delta value. + foreach (array_keys($items) as $delta) { + unset($items[$delta]['_old_delta']); + } + + // Fix field and delta positions in the $_POST array. + if (!empty($_POST)) { + $_POST[$field_name] = array(); + foreach ($items as $new_delta => $item) { + $_POST[$field_name][$new_delta] = $item; + } + if (isset($_POST[$group_name])) { + unset($_POST[$group_name]); + } + } + + // Move field items back to their original positions. + $form_state['values'][$field_name] = $items; + } + + // Finally, get rid of the group data in form values. + unset($form_state['values'][$group_name]); +} + +/** + * Fix deltas for all affected form elements. + */ +function content_multigroup_node_form_fix_deltas(&$elements, $form_deltas) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key] && substr($key, -9) != '_add_more') { + + // Fix the second item, the delta value, of the element's '#parents' array. + $elements[$key]['#parents'][1] = $form_deltas[$elements[$key]['#parents'][1]]; + + // If present, fix delta value in '#delta' attribute of the element. + if (isset($elements[$key]['#delta']) && isset($form_deltas[$elements[$key]['#delta']])) { + $elements[$key]['#delta'] = $form_deltas[$elements[$key]['#delta']]; + } + + // Recurse through all children elements. + content_multigroup_node_form_fix_deltas($elements[$key], $form_deltas); + } + } +} + +/** + * Fix form element parents for all fields in multigroups. + * + * The $element['#parents'] array needs to reflect the position of the fields + * in the $form_state['values'] array so that form_set_value() can be safely + * used by field validation handlers. + */ +function content_multigroup_node_form_fix_parents(&$elements, $multigroups) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + + // Check if the current element is child of a multigroup. The #parents + // array for field values has, at least, 3 parent elements, being the + // first one the name of a multigroup. + if (count($elements[$key]['#parents']) >= 3 && isset($multigroups[$elements[$key]['#parents'][0]])) { + + // Extract group name, delta and field name from the #parents array. + array_shift($elements[$key]['#parents']); + $delta = array_shift($elements[$key]['#parents']); + $field_name = array_shift($elements[$key]['#parents']); + + // Now, insert field name and delta to the #parents array. + array_unshift($elements[$key]['#parents'], $field_name, $delta); + } + + // Recurse through all children elements. + content_multigroup_node_form_fix_parents($elements[$key], $multigroups); + } + } +} + +/** + * Update posting data to reflect delta changes in the form structure. + * + * The $_POST array is fixed in content_multigroup_node_form_transpose_elements(). + */ +function content_multigroup_node_form_fix_post(&$elements) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + + // Update the element copy of the $_POST array. + $elements[$key]['#post'] = $_POST; + + // Recurse through all children elements. + content_multigroup_node_form_fix_post($elements[$key]); + } + } + + // Update the form copy of the $_POST array. + $elements['#post'] = $_POST; +} + +/** + * Make sure the '_weight' and '_remove' attributes of the element exist. + * + * @see content_multigroup_node_form_transpose_elements() + */ +function content_multigroup_fix_element_values($element, &$form_state) { + $field_name = $element['#field_name']; + $delta = $element['#delta']; + if (!isset($form_state['values'][$field_name][$delta]['_weight']) || !isset($form_state['values'][$field_name][$delta]['_remove'])) { + $value = array('_weight' => $element['#_weight'], '_remove' => $element['#removed']); + if (isset($form_state['values'][$field_name][$delta]) && is_array($form_state['values'][$field_name][$delta])) { + $value = array_merge($form_state['values'][$field_name][$delta], $value); + } + form_set_value($element, $value, $form_state); + } +} + +/** + * Fix the value for fields that deal with multiple values themselves. + */ +function content_multigroup_fix_multivalue_fields($element, &$form_state) { + $field_name = $element['#field_name']; + $delta = $element['#delta']; + if (isset($form_state['values'][$field_name][$delta][0]) && is_array($form_state['values'][$field_name][$delta][0])) { + $value = array_merge($form_state['values'][$field_name][$delta][0], array('_remove' => $element['#removed'])); + } + else { + $value = array('_remove' => $element['#removed']); + } + form_set_value($element, $value, $form_state); +} + +/** + * Add AHAH add more button, if not working with a programmed form. + */ +function content_multigroup_add_more(&$form, &$form_state, $group) { + $group_multiple = $group['settings']['multigroup']['multiple']; + if ($group_multiple != 1 || !empty($form['#programmed'])) { + return FALSE; + } + + // Make sure the form is cached so ahah can work. + $form['#cache'] = TRUE; + $content_type = content_types($group['type_name']); + $group_name = $group['group_name']; + $group_name_css = str_replace('_', '-', $group_name); + + $form_element = array(); + $form_element[$group_name .'_add_more'] = array( + '#type' => 'submit', + '#name' => $group_name .'_add_more', + '#value' => theme('content_multigroup_add_more_label', $group_name), + '#weight' => $group_multiple + 1, + '#submit' => array('content_multigroup_add_more_submit'), + '#ahah' => array( + 'path' => 'content_multigroup/js_add_more/'. $content_type['url_str'] .'/'. $group_name, + 'wrapper' => $group_name_css .'-items', + 'method' => 'replace', + 'effect' => 'fade', + ), + // When JS is disabled, the content_multigroup_add_more_submit handler will + // find the relevant group information using these entries. + '#group_name' => $group_name, + '#type_name' => $group['type_name'], + '#item_count' => $form[$group_name]['#item_count'], + ); + + // Add wrappers for the group and 'more' button. + $form_element['#prefix'] = '
'; + $form_element['#suffix'] = '
'; + $form_element[$group_name .'_add_more']['#prefix'] = '
'; + $form_element[$group_name .'_add_more']['#suffix'] = '
'; + + return $form_element; +} + +/** + * Submit handler to add more choices to a content form. This handler is used when + * JavaScript is not available. It makes changes to the form state and the + * entire form is rebuilt during the page reload. + */ +function content_multigroup_add_more_submit($form, &$form_state) { + // Set the form to rebuild and run submit handlers. + node_form_submit_build_node($form, $form_state); + $group_name = $form_state['clicked_button']['#group_name']; + $type_name = $form_state['clicked_button']['#type_name']; + + // Make the changes we want to the form state. + if (isset($form_state['clicked_button']['#item_count'])) { + $form_state['item_count'][$group_name] = $form_state['clicked_button']['#item_count'] + 1; + } +} + +/** + * Menu callback for AHAH addition of new empty widgets. + * + * Adapted from content_add_more_js to work with groups instead of fields. + */ +function content_multigroup_add_more_js($type_name_url, $group_name) { + $content_type = content_types($type_name_url); + $groups = fieldgroup_groups($content_type['type']); + $group = $groups[$group_name]; + + if (($group['settings']['multigroup']['multiple'] != 1) || empty($_POST['form_build_id'])) { + // Invalid request. + drupal_json(array('data' => '')); + exit; + } + + // Retrieve the cached form. + $form_state = array('submitted' => FALSE); + $form_build_id = $_POST['form_build_id']; + $form = form_get_cache($form_build_id, $form_state); + if (!$form) { + // Invalid form_build_id. + drupal_json(array('data' => '')); + exit; + } + + // We don't simply return a new empty widget to append to existing ones, because + // - ahah.js won't simply let us add a new row to a table + // - attaching the 'draggable' behavior won't be easy + // So we resort to rebuilding the whole table of widgets including the existing ones, + // which makes us jump through a few hoops. + + // The form that we get from the cache is unbuilt. We need to build it so that + // _value callbacks can be executed and $form_state['values'] populated. + // We only want to affect $form_state['values'], not the $form itself + // (built forms aren't supposed to enter the cache) nor the rest of $form_data, + // so we use copies of $form and $form_data. + $form_copy = $form; + $form_state_copy = $form_state; + $form_copy['#post'] = array(); + form_builder($_POST['form_id'], $form_copy, $form_state_copy); + // Just grab the data we need. + $form_state['values'] = $form_state_copy['values']; + // Reset cached ids, so that they don't affect the actual form we output. + form_clean_id(NULL, TRUE); + + // Sort the $form_state['values'] we just built *and* the incoming $_POST data + // according to d-n-d reordering. + unset($form_state['values'][$group_name][$group['group_name'] .'_add_more']); + foreach ($_POST[$group_name] as $delta => $item) { + $form_state['values'][$group_name][$delta]['_weight'] = $item['_weight']; + $form_state['values'][$group_name][$delta]['_remove'] = isset($item['_remove']) ? $item['_remove'] : 0; + } + $group['multiple'] = $group['settings']['multigroup']['multiple']; + $form_state['values'][$group_name] = _content_sort_items($group, $form_state['values'][$group_name]); + $_POST[$group_name] = _content_sort_items($group, $_POST[$group_name]); + + // Build our new form element for the whole group, asking for one more element. + $delta = max(array_keys($_POST[$group_name])) + 1; + $form_state['item_count'] = array($group_name => count($_POST[$group_name]) + 1); + $form_element = content_multigroup_group_form($form, $form_state, $group, $delta); + // Rebuild weight deltas to make sure they all are equally dimensioned. + foreach ($form_element[$group_name] as $key => $item) { + if (is_numeric($key) && isset($item['_weight']) && is_array($item['_weight'])) { + $form_element[$group_name][$key]['_weight']['#delta'] = $delta; + } + } + // Add the new element at the right place in the (original, unbuilt) form. + $success = content_set_nested_elements($form, $group_name, $form_element[$group_name]); + + // Save the new definition of the form. + $form_state['values'] = array(); + form_set_cache($form_build_id, $form, $form_state); + + // Build the new form against the incoming $_POST values so that we can + // render the new element. + $_POST[$group_name][$delta]['_weight'] = $delta; + $form_state = array('submitted' => FALSE, 'multigroup_add_more' => TRUE); + $form += array( + '#post' => $_POST, + '#programmed' => FALSE, + ); + $form = form_builder($_POST['form_id'], $form, $form_state); + + // Render the new output. + $group_form = array_shift(content_get_nested_elements($form, $group_name)); + + // We add a div around the new content to receive the ahah effect. + $group_form[$delta]['#prefix'] = '
'. (isset($group_form[$delta]['#prefix']) ? $group_form[$delta]['#prefix'] : ''); + $group_form[$delta]['#suffix'] = (isset($group_form[$delta]['#suffix']) ? $group_form[$delta]['#suffix'] : '') .'
'; + // Prevent duplicate wrapper. + unset($group_form['#prefix'], $group_form['#suffix']); + // We're in the AHAH handler, so the fieldset was expanded. + $group_form['#collapsed'] = FALSE; + + // If a newly inserted widget contains AHAH behaviors, they normally won't + // work because AHAH doesn't know about those - it just attaches to the exact + // form elements that were initially specified in the Drupal.settings object. + // The new ones didn't exist then, so we need to update Drupal.settings + // by ourselves in order to let AHAH know about those new form elements. + $javascript = drupal_add_js(NULL, NULL); + $output_js = isset($javascript['setting']) ? '' : ''; + + $output = theme('status_messages') . drupal_render($group_form) . $output_js; + + // Using drupal_json() breaks filefield's file upload, because the jQuery + // Form plugin handles file uploads in a way that is not compatible with + // 'text/javascript' response type. + $GLOBALS['devel_shutdown'] = FALSE; + print drupal_to_js(array('status' => TRUE, 'data' => $output)); + exit; +} + +/** + * Theme an individual form element. + * + * Combine multiple values into a table with drag-n-drop reordering. + */ +function theme_content_multigroup_node_form($element) { + $group_name = $element['#group_name']; + $groups = fieldgroup_groups($element['#type_name']); + $group = $groups[$group_name]; + $group_multiple = $group['settings']['multigroup']['multiple']; + $group_fields = $element['#group_fields']; + + $table_id = $element['#group_name'] .'_values'; + $table_class = 'content-multiple-table'; + $order_class = $element['#group_name'] .'-delta-order'; + $subgroup_settings = isset($group['settings']['multigroup']['subgroup']) ? $group['settings']['multigroup']['subgroup'] : array(); + $show_label = isset($subgroup_settings['label']) ? $subgroup_settings['label'] : 'above'; + $subgroup_labels = isset($group['settings']['multigroup']['labels']) ? $group['settings']['multigroup']['labels'] : array(); + $multiple_columns = isset($group['settings']['multigroup']['multiple-columns']) ? $group['settings']['multigroup']['multiple-columns'] : 0; + + $headers = array(); + if ($group_multiple >= 1) { + $headers[] = array('data' => ''); + } + if ($multiple_columns) { + foreach ($group_fields as $field_name => $field) { + $required = !empty($field['required']) ? ' *' : ''; + $headers[] = array( + 'data' => check_plain(t($field['widget']['label'])) . $required, + 'class' => 'content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + } + $table_class .= ' content-multigroup-edit-table-multiple-columns'; + } + else { + if ($group_multiple >= 1) { + $headers[0]['colspan'] = 2; + } + $table_class .= ' content-multigroup-edit-table-single-column'; + } + if ($group_multiple >= 1) { + $headers[] = array('data' => t('Order'), 'class' => 'content-multiple-weight-header'); + if ($group_multiple == 1) { + $headers[] = array('data' => ''. t('Remove') .'', 'class' => 'content-multiple-remove-header'); + } + } + $rows = array(); + + $i = 0; + foreach (element_children($element) as $delta => $key) { + if (is_numeric($key)) { + $cells = array(); + $label = ($show_label == 'above' && !empty($subgroup_labels[$i]) ? theme('content_multigroup_node_label', check_plain(t($subgroup_labels[$i]))) : ''); + $element[$key]['_weight']['#attributes']['class'] = $order_class; + if ($group_multiple >= 1) { + $cells[] = array('data' => '', 'class' => 'content-multiple-drag'); + $delta_element = drupal_render($element[$key]['_weight']); + if ($group_multiple == 1) { + $remove_element = drupal_render($element[$key]['_remove']); + } + } + else { + $element[$key]['_weight']['#type'] = 'hidden'; + } + if ($multiple_columns) { + foreach ($group_fields as $field_name => $field) { + $cell = array( + 'data' => (isset($element[$key][$field_name]) ? drupal_render($element[$key][$field_name]) : ''), + 'class' => 'content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + if (!empty($cell['data']) && !empty($element[$key][$field_name]['#description'])) { + $cell['title'] = $element[$key][$field_name]['#description']; + } + $cells[] = $cell; + } + } + else { + $cells[] = $label . drupal_render($element[$key]); + } + if ($group_multiple >= 1) { + $row_class = 'draggable'; + $cells[] = array('data' => $delta_element, 'class' => 'delta-order'); + if ($group_multiple == 1) { + if (!empty($element[$key]['_remove']['#value'])) { + $row_class .= ' content-multiple-removed-row'; + } + $cells[] = array('data' => $remove_element, 'class' => 'content-multiple-remove-cell'); + } + $rows[] = array('data' => $cells, 'class' => $row_class); + } + else { + $rows[] = array('data' => $cells); + } + } + $i++; + } + + drupal_add_css(drupal_get_path('module', 'content_multigroup') .'/content_multigroup.css'); + $output = theme('table', $headers, $rows, array('id' => $table_id, 'class' => $table_class)); + $output .= drupal_render($element[$group_name .'_add_more']); + + // Enable drag-n-drop only if the group really allows multiple values. + if ($group_multiple >= 1) { + drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class); + drupal_add_js(drupal_get_path('module', 'content') .'/js/content.node_form.js'); + } + + return $output; +} + +/** + * Theme the sub group label in the node form. + */ +function theme_content_multigroup_node_label($text) { + return !empty($text) ? '

'. $text .'

' : ''; +} + +/** + * Theme the label for the "Add more values" button + */ +function theme_content_multigroup_add_more_label($group_name) { + return t('Add more values'); +} diff --git a/sites/all/modules/cck/modules/content_multigroup/content_multigroup.node_view.inc b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.node_view.inc new file mode 100644 index 0000000..5e3627c --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/content_multigroup.node_view.inc @@ -0,0 +1,262 @@ + $field) { + if (isset($group['fields'][$field_name]) && isset($element[$field_name])) { + if (!isset($element[$field_name]['#access']) || $element[$field_name]['#access']) { + $group_fields[$field_name] = $field; + } + } + } + + switch ($group_multiple) { + case 0: + $group_deltas = array(0); + break; + + case 1: + // Compute unique deltas from all deltas used by fields in this multigroup. + $group_deltas = array(); + foreach (array_keys($group_fields) as $field_name) { + if (is_array($node->content[$field_name]['field']['items'])) { + foreach (array_keys($node->content[$field_name]['field']['items']) as $delta) { + $group_deltas[$delta] = $delta; + } + } + } + sort($group_deltas); + break; + + default: + $group_deltas = range(0, $group_multiple - 1); + break; + } + foreach ($group_deltas as $index => $delta) { + $element[$delta] = array( + '#title' => ($show_label == 'above' && !empty($subgroup_labels[$index]) ? check_plain(t($subgroup_labels[$index])) : ''), + '#attributes' => array('class' => 'content-multigroup-wrapper content-multigroup-'. $index), + '#weight' => $delta, + ); + + // Create a pseudo node that only has the value we want in this group and + // pass it to the formatter. + + // @TODO Watch this change and be sure it doesn't break anything else. + // The delta was previously always set to '1' to keep CCK's inline label + // from assigning a class of 'first'. But changing it back to the right + // delta for the individual item makes the formatters easier to manage + // since the delta is always the correct delta and not occasionally + // a bogus value. It aslo fixes issues like http://drupal.org/node/766734 + // by keeping the layout of multigroups the same as other fieldgroups. + foreach (array_keys($group_fields) as $field_name) { + if (isset($node->content[$field_name])) { + $node_copy->content[$field_name]['field']['items'] = array( + $delta => isset($node->content[$field_name]['field']['items'][$delta]) ? $node->content[$field_name]['field']['items'][$delta] : NULL, + ); + $element[$delta][$field_name] = $node_copy->content[$field_name]; + $element[$delta][$field_name]['#delta'] = $delta; + } + } + + switch ($subgroup_format) { + case 'simple': + $element['#attributes']['class'] = $group_class; + $element[$delta]['#theme'] = 'content_multigroup_display_simple'; + $element[$delta]['#fields'] = $group_fields; + break; + case 'fieldset_collapsed': + $element[$delta]['#collapsed'] = TRUE; + case 'fieldset_collapsible': + $element[$delta]['#collapsible'] = TRUE; + case 'fieldset': + $element['#attributes']['class'] = $group_class; + $element[$delta]['#type'] = 'content_multigroup_display_fieldset'; + $element[$delta]['#fields'] = $group_fields; + break; + case 'hr': + $element['#attributes']['class'] = $group_class; + $element[$delta]['#theme'] = 'content_multigroup_display_hr'; + $element[$delta]['#fields'] = $group_fields; + break; + case 'table-single': + $element['#theme'] = 'content_multigroup_display_table_single'; + $element['#attributes']['class'] = $group_class .' content-multigroup-display-table-single-column'; + $element['#fields'] = $group_fields; + break; + case 'table-multiple': + $element['#theme'] = 'content_multigroup_display_table_multiple'; + $element['#attributes']['class'] = $group_class .' content-multigroup-display-table-multiple-columns'; + $element['#fields'] = $group_fields; + break; + case 'ul': + $element['#theme'] = 'content_multigroup_display_ul'; + $element['#attributes']['class'] = $group_class; + $element['#fields'] = $group_fields; + break; + } + } + + // Unset the original group field values now that we've moved them. + foreach (array_keys($group_fields) as $field_name) { + if (isset($element[$field_name])) { + unset($element[$field_name]); + } + } +} + +/** + * Theme a subgroup of fields in 'simple' format. + * + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_simple($element) { + $children = $output = ''; + foreach (element_children($element) as $key) { + $children .= drupal_render($element[$key]); + } + if (!empty($children)) { + $output .= ''; + if (!empty($element['#title'])) { + $output .= ''; + } + $output .= $children .'
'; + } + return $output; +} + +/** + * Theme a subgroup of fields in 'fieldset' format. + * + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_fieldset($element) { + if (empty($element['#children']) && empty($element['#value'])) { + return ''; + } + return theme('fieldset', $element); +} + +/** + * Theme a subgroup of fields in 'hr' format. + * + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_hr($element) { + $children = $output = ''; + foreach (element_children($element) as $key) { + $children .= drupal_render($element[$key]); + } + if (!empty($children)) { + $output .= '
'; + if (!empty($element['#title'])) { + $output .= ''; + } + $output .= $children .'
'; + } + return $output; +} + +/** + * Theme a multigroup in 'table-single' format. + * + * Each subgroup has its own table row with a single cell for all fields. + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_table_single($element) { + $headers = array(); + $rows = array(); + foreach (element_children($element) as $delta) { + $items = array(); + foreach ($element['#fields'] as $field_name => $field) { + $item = drupal_render($element[$delta][$field_name]); + if (!empty($item)) { + $items[] = $item; + } + } + if (!empty($items)) { + if (!empty($element[$delta]['#title'])) { + array_unshift($items, ''); + } + $rows[] = array('data' => array(implode("\n", $items)), 'class' => $element[$delta]['#attributes']['class']); + } + } + return count($rows) ? theme('table', $headers, $rows, $element['#attributes']) : ''; +} + +/** + * Theme a multigroup in 'table-multiple' format. + * + * Each subgroup has its own table row with a separate cell for each field. + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_table_multiple($element) { + $headers = array(); + foreach ($element['#fields'] as $field_name => $field) { + $label_display = isset($field['display_settings']['label']['format']) ? $field['display_settings']['label']['format'] : 'above'; + $headers[] = array( + 'data' => ($label_display != 'hidden' ? check_plain(t($field['widget']['label'])) : ''), + 'class' => 'content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + } + $rows = array(); + foreach (element_children($element) as $delta) { + $cells = array(); + $empty = TRUE; + foreach ($element['#fields'] as $field_name => $field) { + $item = drupal_render($element[$delta][$field_name]); + $cells[] = array( + 'data' => $item, + 'class' => $element[$delta]['#attributes']['class'] .' content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + if (!empty($item)) { + $empty = FALSE; + } + } + // Get the row only if there is at least one non-empty field. + if (!$empty) { + $rows[] = $cells; + } + } + return count($rows) ? theme('table', $headers, $rows, $element['#attributes']) : ''; +} + +/** + * Theme a subgroup of fields in an unordered list. + * + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_ul($element) { + $items = array(); + foreach (element_children($element) as $delta) { + foreach ($element['#fields'] as $field_name => $field) { + $item = drupal_render($element[$delta][$field_name]); + if (!empty($item)) { + $items[] = $item; + } + } + } + $element['#attributes']['class'] .= ' content-multigroup'; + return count($items) ? theme('item_list', $items, $element['#title'], 'ul', $element['#attributes']) : ''; +} diff --git a/sites/all/modules/cck/modules/content_multigroup/panels/content_types/content_multigroup.inc b/sites/all/modules/cck/modules/content_multigroup/panels/content_types/content_multigroup.inc new file mode 100644 index 0000000..d1e8eef --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/panels/content_types/content_multigroup.inc @@ -0,0 +1,174 @@ + t('Content multigroup'), + 'defaults' => array('label' => 'hidden', 'format' => 'simple', 'subgroup' => 'fieldset', 'empty' => ''), + ); +} + +/** + * Return all multigroup content types available. + */ +function content_multigroup_content_multigroup_content_type_content_types() { + // This will hold all the individual multigroup content types. + $types = array(); + + // The outer loop goes through each node type with groups. + foreach (fieldgroup_groups() as $node_type_groups) { + // The inner loop gives us each fieldgroup on each node type with groups. + foreach ($node_type_groups as $group) { + // Skip field groups that are not of multigroup type. + if ($group['group_type'] != 'multigroup') { + continue; + } + + // Name the content type a combination of fieldgroup and node type names. + $content_type_name = $group['type_name'] . ':' . $group['group_name']; + + // Assemble the information about the content type. + $info = array( + 'category' => t('Node'), + 'icon' => 'icon_cck_field_group.png', + 'title' => t('Multigroup: @group in @type', array( + '@group' => t($group['label']), + '@type' => node_get_types('name', $group['type_name']), + )), + 'description' => t('All fields from this field group on the referenced node.'), + 'required context' => new ctools_context_required(t('Node'), 'node', array('type' => array($group['type_name']))), + ); + + $types[$content_type_name] = $info; + } + } + + return $types; +} + +/** + * Output function for the 'multigroup' content type. + */ +function content_multigroup_content_multigroup_content_type_render($subtype, $conf, $panel_args, $context) { + if (!isset($context->data)) { + return; + } + $node = drupal_clone($context->data); + + // Extract the node type and fieldgroup name from the subtype. + list($node_type, $group_name) = explode(':', $subtype, 2); + + // Get a list of all fieldgroups for this node type. + $groups = fieldgroup_groups($node_type); + + if (!isset($groups[$group_name])) { + return; + } + $group = $groups[$group_name]; + + // Render the field group. + $node->build_mode = NODE_BUILD_NORMAL; + $group['settings']['display']['label'] = $conf['label'] == 'normal' || !empty($conf['override_title']) ? 'hidden' : $conf['label']; + $group['settings']['display']['full']['format'] = $conf['format']; + $group['settings']['display']['full']['exclude'] = 0; + $group['settings']['multigroup']['subgroup']['full']['format'] = $conf['subgroup']; + $group['settings']['multigroup']['subgroup']['full']['exclude'] = 0; + $output = fieldgroup_view_group($group, $node); + + $block = new stdClass(); + if ($conf['label'] == 'normal') { + $block->title = t($group['label']); + } + $block->content = !empty($output) ? $output : $conf['empty']; + return $block; +} + +/** + * Returns a settings form for the custom type. + */ +function content_multigroup_content_multigroup_content_type_edit_form(&$form, &$form_state) { + $conf = $form_state['conf']; + + $label_options = array( + 'normal' => t('Block title'), + 'above' => t('Above'), + ); + $form['label'] = array( + '#type' => 'select', + '#title' => t('Multigroup label'), + '#default_value' => !empty($conf['label']) && isset($label_options[$conf['label']]) ? $conf['label'] : 'hidden', + '#options' => $label_options, + '#description' => t('Configure how the field group label is going to be displayed. This option takes no effect when "Override title" option is enabled, the specified block title is displayed instead.'), + ); + + $format_options = array( + 'simple' => t('Simple'), + 'fieldset' => t('Fieldset'), + 'fieldset_collapsible' => t('Fieldset - Collapsible'), + 'fieldset_collapsed' => t('Fieldset - Collapsed'), + ); + $form['format'] = array( + '#type' => 'select', + '#title' => t('Multigroup format'), + '#default_value' => !empty($conf['format']) && isset($format_options[$conf['format']]) ? $conf['format'] : 'simple', + '#options' => $format_options, + '#description' => t('This option allows you to configure the field group format.'), + ); + + $subgroup_options = array( + 'simple' => t('Simple'), + 'fieldset' => t('Fieldset'), + 'fieldset_collapsible' => t('Fieldset - collapsible'), + 'fieldset_collapsed' => t('Fieldset - collapsed'), + 'hr' => t('Horizontal line'), + 'table-single' => t('Table - Single column'), + 'table-multiple' => t('Table - Multiple columns'), + ); + $form['subgroup'] = array( + '#type' => 'select', + '#title' => t('Subgroup format'), + '#default_value' => !empty($conf['subgroup']) && isset($subgroup_options[$conf['subgroup']]) ? $conf['subgroup'] : 'fieldset', + '#options' => $subgroup_options, + '#description' => t('This option allows you to configure the format of the subgroups in the multigroup.'), + ); + + $form['empty'] = array( + '#type' => 'textarea', + '#title' => t('Empty text'), + '#description' => t('Text to display if group has no data. Note that title will not display unless overridden.'), + '#rows' => 5, + '#default_value' => $conf['empty'], + ); +} + +function content_multigroup_content_multigroup_content_type_edit_form_submit(&$form, &$form_state) { + // Copy everything from our defaults. + foreach (array_keys($form_state['plugin']['defaults']) as $key) { + $form_state['conf'][$key] = $form_state['values'][$key]; + } +} + +/** + * Admin title for multigroup content type. + */ +function content_multigroup_content_multigroup_content_type_admin_title($subtype, $conf, $context) { + // Extract the node type and fieldgroup name from the subtype. + list($node_type, $group_name) = explode(':', $subtype, 2); + + // Get information about this field group for this node type. + $groups = fieldgroup_groups($node_type); + $group = $groups[$group_name]; + + return t('"@s" multigroup: @group in @type', array( + '@s' => $context->identifier, + '@group' => t($group['label']), + '@type' => node_get_types('name', $node_type), + )); +} diff --git a/sites/all/modules/cck/modules/content_multigroup/panels/content_types/icon_cck_field_group.png b/sites/all/modules/cck/modules/content_multigroup/panels/content_types/icon_cck_field_group.png new file mode 100644 index 0000000..8459443 Binary files /dev/null and b/sites/all/modules/cck/modules/content_multigroup/panels/content_types/icon_cck_field_group.png differ diff --git a/sites/all/modules/cck/modules/content_multigroup/views/content_multigroup.views.inc b/sites/all/modules/cck/modules/content_multigroup/views/content_multigroup.views.inc new file mode 100644 index 0000000..22beb92 --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/views/content_multigroup.views.inc @@ -0,0 +1,94 @@ + $groups) { + $type_label = node_get_types('name', $type_name); + + foreach ($groups as $group_name => $group) { + // Let's focus on multigroups that really accept multiple values. + if ($group['group_type'] == 'multigroup' && !empty($group['settings']['multigroup']['multiple'])) { + + // Scan all fields in this multigroup. + $field_labels = array(); + foreach (array_keys($group['fields']) as $field_name) { + // Load information about the field for this particular content type. + $field = content_fields($field_name, $type_name); + + // Get the Views table alias. + $table_alias = content_views_tablename($field); + + // Discard this field if not already exposed to Views. + if (isset($data[$table_alias])) { + $field_labels[$field_name] = t($field['widget']['label']); + } + } + + if (!empty($field_labels)) { + // Build the name for this filter. + // The scope of field groups is the content type itself. You can + // have more than one group with the same name but different fields + // in different content types. Therefore, we need the type name and + // multigroup name. + $db_field = 'multigroup_'. $type_name .'_'. $group_name; + + // Build the labels for the filter. + $label_truncated = truncate_utf8(t($group['label']), 10, TRUE); + $title = t('@group_label multigroup in @type_label', array( + '@group_label' => t($group['label']), + '@type_label' => $type_label, + )); + $title_short = t('@label-truncated in @type_label', array( + '@label-truncated' => $label_truncated, + '@type_label' => $type_label, + )); + $help_text = t('Synchronize multiple values for fields in @group_label multigroup, @type_label type. Fields in this group: @fields.', array( + '@group_label' => t($group['label']), + '@type_label' => $type_label, + '@fields' => implode(', ', $field_labels), + )); + + // Attach the new filter to the node table. + $data['node'][$db_field] = array( + 'group' => t('Content multigroup'), + 'title' => $title, + 'title short' => $title_short, + 'help' => $help_text, + 'filter' => array( + 'field' => $db_field, + 'table' => 'node', + 'handler' => 'content_multigroup_handler_filter', + 'content_type_name' => $type_name, + 'content_group_name' => $group_name, + 'allow empty' => TRUE, + ), + ); + } + } + } + } +} + +/** + * Implementation of hook_views_handlers(). + */ +function content_multigroup_views_handlers() { + return array( + 'info' => array( + 'path' => drupal_get_path('module', 'content_multigroup') . '/views/handlers', + ), + 'handlers' => array( + 'content_multigroup_handler_filter' => array( + 'parent' => 'views_handler_filter', + ), + ), + ); +} diff --git a/sites/all/modules/cck/modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc b/sites/all/modules/cck/modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc new file mode 100644 index 0000000..e86a321 --- /dev/null +++ b/sites/all/modules/cck/modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc @@ -0,0 +1,99 @@ +definition['content_type_name']); + return $groups[$this->definition['content_group_name']]; + } + + /** + * Get information about the fields in this multigroup. + */ + function _get_multigroup_fields() { + if (!isset($this->content_multigroup_fields)) { + $group = $this->_get_multigroup(); + $this->content_multigroup_fields = array(); + foreach (array_keys($group['fields']) as $field_name) { + $field = content_fields($field_name, $this->definition['content_type_name']); + $table_alias = content_views_tablename($field); + $this->content_multigroup_fields[$table_alias] = $field; + } + } + return $this->content_multigroup_fields; + } + + /** + * Define default value for master field. + */ + function options(&$options) { + $multigroup_fields = $this->_get_multigroup_fields(); + // Find the first required field. + foreach ($multigroup_fields as $table_alias => $field) { + if ($field['required']) { + $options['content_multigroup_master_field'] = $table_alias; + break; + } + } + // Default to first field in the multigroup. + if (empty($options['content_multigroup_master_field'])) { + $options['content_multigroup_master_field'] = current(array_keys($multigroup_fields)); + } + } + + /** + * Options from to ask the user for a master field. + */ + function options_form(&$form, &$form_state) { + $group = $this->_get_multigroup(); + $options = array(); + foreach ($this->_get_multigroup_fields() as $table_alias => $field) { + $options[$table_alias] = t($field['widget']['label']); + } + $form['content_multigroup_master_field'] = array( + '#title' => t('Available fields in @group_label multigroup', array('@group_label' => t($group['label']))), + '#type' => 'radios', + '#options' => $options, + '#default_value' => $this->options['content_multigroup_master_field'], + '#description' => t('Select the field in this multigroup that will be used to build the primary join with the content table.'), + ); + } + + /** + * Add joins to the query to synchronize the fields in this multigroup. + */ + function query() { + // Create the join between the master field table and the node table. + $base_alias = $this->query->ensure_table($this->options['content_multigroup_master_field'], $this->relationship); + + // Now we want to join the master field table with all other tables + // related to fields in the same multigroup, but adding the delta + // key to the join condition. This is what allows us to keep delta + // values in sync for all fields in the same multigroup. + foreach ($this->_get_multigroup_fields() as $table_alias => $field) { + if ($table_alias != $this->options['content_multigroup_master_field']) { + $alias = $this->query->ensure_table($table_alias, $this->relationship); + $this->query->table_queue[$alias]['join']->extra = $base_alias .'.delta = '. $alias .'.delta'; + } + } + } +} diff --git a/sites/all/modules/cck/modules/content_permissions/content_permissions.info b/sites/all/modules/cck/modules/content_permissions/content_permissions.info new file mode 100644 index 0000000..4e16068 --- /dev/null +++ b/sites/all/modules/cck/modules/content_permissions/content_permissions.info @@ -0,0 +1,11 @@ +name = Content Permissions +description = Set field-level permissions for CCK fields. +package = CCK +core = 6.x +dependencies[] = content +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/content_permissions/content_permissions.install b/sites/all/modules/cck/modules/content_permissions/content_permissions.install new file mode 100644 index 0000000..0e97815 --- /dev/null +++ b/sites/all/modules/cck/modules/content_permissions/content_permissions.install @@ -0,0 +1,9 @@ +configure your field permissions immediately. All fields are inaccessible by default.', array('!url' => url('admin/user/permissions', array('fragment' => 'content_permissions'))))); +} \ No newline at end of file diff --git a/sites/all/modules/cck/modules/content_permissions/content_permissions.module b/sites/all/modules/cck/modules/content_permissions/content_permissions.module new file mode 100644 index 0000000..d69900c --- /dev/null +++ b/sites/all/modules/cck/modules/content_permissions/content_permissions.module @@ -0,0 +1,27 @@ + + +
+ + +

+ + +
+ + + + +
+ +
+ diff --git a/sites/all/modules/cck/modules/fieldgroup/fieldgroup.css b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.css new file mode 100644 index 0000000..56f9bf0 --- /dev/null +++ b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.css @@ -0,0 +1,7 @@ + +div.fieldgroup { + margin:.5em 0 1em 0; +} +div.fieldgroup .content { + padding-left:1em;/*LTR*/ +} diff --git a/sites/all/modules/cck/modules/fieldgroup/fieldgroup.info b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.info new file mode 100644 index 0000000..e76f7d5 --- /dev/null +++ b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.info @@ -0,0 +1,11 @@ +name = Fieldgroup +description = Create display groups for CCK fields. +dependencies[] = content +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/fieldgroup/fieldgroup.install b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.install new file mode 100644 index 0000000..1d6b3c0 --- /dev/null +++ b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.install @@ -0,0 +1,328 @@ + array( + 'group_type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => 'standard'), + 'type_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'group_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'parent' => array('type' => 'varchar', 'length' => 32, 'not null' => FALSE, 'default' => ''), + 'label' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'settings' => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE), + 'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + ), + 'primary key' => array('type_name', 'group_name'), + ); + + $schema['content_group_fields'] = array( + 'fields' => array( + 'type_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'group_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'field_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + ), + 'primary key' => array('type_name', 'group_name', 'field_name'), + ); + + return $schema; +} + +/** + * rename groups form "group-*" to "group_*" + */ +function fieldgroup_update_1() { + $ret = array(); + if (!db_table_exists('node_group')) { + return $ret; + } + switch ($GLOBALS['db_type']) { + case 'pgsql': + $ret[] = update_sql("UPDATE {node_group} SET group_name = 'group_'||SUBSTRING(group_name FROM 7)"); + $ret[] = update_sql("UPDATE {node_group_fields} SET group_name = 'group_'||SUBSTRING(group_name FROM 7)"); + break; + + case 'mysql': + case 'mysqli': + $ret[] = update_sql("UPDATE {node_group} SET group_name = CONCAT('group_', SUBSTRING(group_name FROM 7))"); + $ret[] = update_sql("UPDATE {node_group_fields} SET group_name = CONCAT('group_', SUBSTRING(group_name FROM 7))"); + break; + } + return $ret; +} + +/** + * add display settings for the group + */ +function fieldgroup_update_2() { + $ret = array(); + if (!db_table_exists('node_group')) { + return $ret; + } + + // set settings column to accept larger values + switch ($GLOBALS['db_type']) { + case 'mysql': + case 'mysqli': + $ret[] = update_sql('ALTER TABLE {node_group} CHANGE settings settings mediumtext NOT NULL'); + break; + + case 'pgsql': + db_change_column($ret, 'node_group', 'settings', 'settings', 'text', array('not null' => TRUE)); + break; + } + + // move description into the settings array, and add new settings + $result = db_query("SELECT * FROM {node_group}"); + while ($group = db_fetch_array($result)) { + $settings = array(); + $settings['form'] = unserialize($group['settings']); + $settings['form']['description'] = $group['description']; + $settings['display'] = array('collapsible' => 0, 'collapsed' => 0, 'description' => ''); + $ret[] = update_sql("UPDATE {node_group} SET settings = '". db_escape_string(serialize($settings)) ."', description = '' WHERE group_name = '". $group['group_name'] ."'"); + } + + // drop description column + switch ($GLOBALS['db_type']) { + case 'mysql': + case 'mysqli': + $ret[] = update_sql('ALTER TABLE {node_group} DROP description'); + break; + + case 'pgsql': + // Postgres only supports dropping of columns since 7.4 + break; + } + + return $ret; +} + + +/** + * converts group settings collapsible/collapsed => style + */ +function fieldgroup_update_3() { + $ret = array(); + if (!db_table_exists('node_group')) { + return $ret; + } + $result = db_query("SELECT * FROM {node_group}"); + while ($group = db_fetch_array($result)) { + $group['settings'] = unserialize($group['settings']); + + if (!isset($group['settings']['form']['style'])) { + foreach (array('form', 'display') as $context) { + if (isset($group['settings'][$context]['collapsible']) && $group['settings'][$context]['collapsible']) { + if (isset($group['settings'][$context]['collapsed']) && $group['settings'][$context]['collapsed']) { + $group['settings'][$context]['style'] = 'fieldset_collapsed'; + } + else { + $group['settings'][$context]['style'] = 'fieldset_collapsible'; + } + } + else { + $group['settings'][$context]['style'] = 'fieldset'; + } + } + + $ret[] = update_sql("UPDATE {node_group} SET settings = '". db_escape_string(serialize($group['settings'])) ."' WHERE group_name = '". $group['group_name'] ."'"); + } + } + + return $ret; +} + +/* + * Increases module weight, so that other modules can form_alter() cck forms before the fields + * are moved in groups + */ +function fieldgroup_update_4() { + $ret = array(); + $ret[] = update_sql("UPDATE {system} SET weight = 9 WHERE name = 'fieldgroup'"); + return $ret; +} + + +/** + * Start D6 upgrades + */ + +/** + * Move fieldgroup tables to the content_* namespace. + */ +function fieldgroup_update_6000() { + if ($abort = content_check_update('fieldgroup')) { + return $abort; + } + + $ret = array(); + + db_rename_table($ret, 'node_group', 'content_group'); + db_rename_table($ret, 'node_group_fields', 'content_group_fields'); + variable_set('fieldgroup_schema_version', 6000); + return $ret; +} + +/* + * Increases module weight, so that other modules can form_alter() cck forms before the fields + * are moved in groups. + * + * Sites upgraded from D5 should have this already set. + * New D6 installs earlier than RC5 need this, as it was missing in fieldgroup_install. + */ +function fieldgroup_update_6001() { + if ($abort = content_check_update('fieldgroup')) { + return $abort; + } + + $ret = array(); + $ret[] = update_sql("UPDATE {system} SET weight = 9 WHERE name = 'fieldgroup'"); + return $ret; +} + +/** + * Same as 6000 : Move fieldgroup tables to the content_* namespace. + * This was missing in D6 releases earlier than RC5. Ensure we don't run this twice. + */ +function fieldgroup_update_6002() { + if ($abort = content_check_update('fieldgroup')) { + return $abort; + } + + $ret = array(); + if (db_table_exists('node_group')) { + db_rename_table($ret, 'node_group', 'content_group'); + db_rename_table($ret, 'node_group_fields', 'content_group_fields'); + variable_set('fieldgroup_schema_version', 6000); + } + return $ret; +} + +/** + * Remove tinyint (127) limitation on group weights. + */ +function fieldgroup_update_6003() { + if ($abort = content_check_update('fieldgroup')) { + return $abort; + } + + $ret = array(); + db_change_field($ret, 'content_group', 'weight', 'weight', array('type' => 'int', 'not null' => TRUE, 'default' => 0)); + return $ret; +} + +/** + * Add 'type' property for fieldgroups. + */ +function fieldgroup_update_6004() { + if ($abort = content_check_update('fieldgroup')) { + return $abort; + } + + $ret = array(); + db_add_field($ret, 'content_group', 'group_type', array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => 'standard')); + $ret[] = update_sql("DELETE FROM {cache_content} WHERE cid='fieldgroup_data'"); + return $ret; +} + +/** + * Add the 'exclude from $content' display setting to all existing groups. + */ +function fieldgroup_update_6005() { + $ret = array(); + $result = db_query("SELECT * FROM {content_group}"); + while ($type = db_fetch_array($result)) { + $new_settings = array(); + $settings = unserialize($type['settings']); + $new_settings = $settings; + $display_settings = !empty($settings['display']) ? $settings['display'] : array(); + if (!empty($display_settings)) { + foreach ($display_settings as $key => $val) { + $new_settings['display'][$key] = $val; + if ($key !== 'label' && is_array($val)) { + $new_settings['display'][$key]['exclude'] = 0; + } + } + } + else { + $new_settings['display'] = array( + 'label' => array('format' => 'above'), + 'full' => array('format' => 'default', 'exclude' => 0), + 'teaser' => array('format' => 'default', 'exclude' => 0), + ); + } + db_query("UPDATE {content_group} SET settings='%s' WHERE group_name='%s' AND type_name='%s'", serialize($new_settings), $type['group_name'], $type['type_name']); + } + return $ret; +} + +/** + * Removed a previous version of "Remove orphaned fields" (6007), broken for db prefixes. + */ +function fieldgroup_update_6006() { + return array(); +} + +/** + * Remove orphaned fields (see http://drupal.org/node/339537). + */ +function fieldgroup_update_6007() { + $ret = array(); + $ret[] = update_sql("DELETE FROM {content_group_fields} WHERE (field_name, type_name) NOT IN (SELECT field_name, type_name FROM {content_node_field_instance})"); + return $ret; +} + +/** + * allow for nesting of fieldgroups + */ +function fieldgroup_update_6008() { + if ($abort = content_check_update('fieldgroup')) { + return $abort; + } + $ret = array(); + db_add_field($ret, 'content_group', 'parent', array('type' => 'varchar', 'length' => 32, 'not null' => FALSE, 'default' => '')); + return $ret; +} \ No newline at end of file diff --git a/sites/all/modules/cck/modules/fieldgroup/fieldgroup.module b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.module new file mode 100644 index 0000000..d7dcc11 --- /dev/null +++ b/sites/all/modules/cck/modules/fieldgroup/fieldgroup.module @@ -0,0 +1,1144 @@ +content. + * - hook_fieldgroup_form: Alter the group portion of the node form. + * - hook_fieldgroup_types: Add additional fieldgroup group_types. + * - hook_fieldgroup_default_settings: Add additional fieldgroup default settings. + * - hook_fieldgroup_save: Do additional processing when a fieldgroup is saved. + */ +/** + * Implementation of hook_init(). + */ +function fieldgroup_init() { + drupal_add_css(drupal_get_path('module', 'fieldgroup') .'/fieldgroup.css'); +} + +/** + * Implementation of hook_ctools_plugin_directory(). + */ +function fieldgroup_ctools_plugin_directory($module, $plugin) { + if ($module == 'ctools' && $plugin == 'content_types') { + return 'panels/' . $plugin; + } +} + +/** + * Implementation of hook_menu(). + */ +function fieldgroup_menu() { + $items = array(); + + // Make sure this doesn't fire until content_types is working, + // needed to avoid errors on initial installation. + if (!defined('MAINTENANCE_MODE')) { + foreach (node_get_types() as $type) { + $type_name = $type->type; + $content_type = content_types($type_name); + $type_url_str = $content_type['url_str']; + $items['admin/content/node-type/'. $type_url_str .'/groups/%'] = array( + 'title' => 'Edit group', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('fieldgroup_group_edit_form', $type_name, 5), + 'access arguments' => array('administer content types'), + 'type' => MENU_CALLBACK, + ); + $items['admin/content/node-type/'. $type_url_str .'/groups/%/remove'] = array( + 'title' => 'Edit group', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('fieldgroup_remove_group', $type_name, 5), + 'access arguments' => array('administer content types'), + 'type' => MENU_CALLBACK, + ); + } + } + return $items; +} + +/** + * Implementation of hook_theme(). + */ +function fieldgroup_theme() { + return array( + 'fieldgroup_simple' => array( + 'template' => 'fieldgroup-simple', + 'arguments' => array('element' => NULL), + ), + 'fieldgroup_fieldset' => array( + 'arguments' => array('element' => NULL), + ), + 'fieldgroup_display_overview_form' => array( + 'arguments' => array('form' => NULL), + ), + ); +} + +/** + * Implementation of hook_elements(). + */ +function fieldgroup_elements() { + return array( + 'fieldgroup_simple' => array(), + 'fieldgroup_fieldset' => array('#collapsible' => FALSE, '#collapsed' => FALSE, '#value' => NULL,), + ); +} + +/** + * Implementation of hook_fieldapi(). + */ +function fieldgroup_content_fieldapi($op, $field) { + switch ($op) { + case 'delete instance': + db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE field_name = '%s' AND type_name = '%s'", $field['field_name'], $field['type_name']); + cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); + break; + } +} + +function fieldgroup_group_edit_form(&$form_state, $type_name, $group_name) { + $content_type = content_types($type_name); + $groups = fieldgroup_groups($content_type['type']); + + if (!$group = $groups[$group_name]) { + drupal_not_found(); + exit; + } + + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#default_value' => $group['label'], + '#required' => TRUE, + ); + + // Set a default value for group type early in the form so it + // can be overridden by subsequent form elements added by other modules. + $group_type = !empty($group['group_type']) ? $group['group_type'] : 'standard'; + $form['group_type'] = array('#type' => 'hidden', '#default_value' => $group_type); + + $form['settings']['#tree'] = TRUE; + $form['settings']['form'] = array( + '#type' => 'fieldset', + '#title' => t('Form settings'), + '#description' => t('These settings apply to the group in the node editing form.'), + ); + $form['settings']['form']['style'] = array( + '#type' => 'radios', + '#title' => t('Style'), + '#default_value' => $group['settings']['form']['style'], + '#options' => array( + 'fieldset' => t('always open'), + 'fieldset_collapsible' => t('collapsible'), + 'fieldset_collapsed' => t('collapsed'), + ) + ); + $form['settings']['form']['description'] = array( + '#type' => 'textarea', + '#title' => t('Help text'), + '#default_value' => $group['settings']['form']['description'], + '#rows' => 5, + '#description' => t('Instructions to present to the user on the editing form.'), + '#required' => FALSE, + ); + $form['settings']['display'] = array( + '#type' => 'fieldset', + '#title' => t('Display settings'), + '#description' => t('These settings apply to the group on node display.'), + ); + $form['settings']['display']['description'] = array( + '#type' => 'textarea', + '#title' => t('Description'), + '#default_value' => $group['settings']['display']['description'], + '#rows' => 5, + '#description' => t('A description of the group.'), + '#required' => FALSE, + ); + + foreach (array_keys(content_build_modes()) as $key) { + $form['settings']['display'][$key]['format'] = array('#type' => 'value', '#value' => isset($group['settings']['display'][$key]['format']) ? $group['settings']['display'][$key]['format'] : 'fieldset'); + $form['settings']['display'][$key]['exclude'] = array('#type' => 'value', '#value' => isset($group['settings']['display'][$key]['exclude']) ? $group['settings']['display'][$key]['exclude'] : 0); + } + $form['settings']['display']['label'] = array('#type' => 'value', '#value' => $group['settings']['display']['label']); + $form['weight'] = array('#type' => 'hidden', '#default_value' => $group['weight']); + $form['group_name'] = array('#type' => 'hidden', '#default_value' => $group_name); + $form['parent'] = array('#type' => 'hidden', '#default_value' => $group['parent']); + + $form['#content_type'] = $content_type; + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#weight' => 10, + ); + + return $form; +} + +function fieldgroup_group_edit_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + $content_type = $form['#content_type']; + fieldgroup_save_group($content_type['type'], $form_values); + $form_state['redirect'] = 'admin/content/node-type/'. $content_type['url_str'] .'/fields'; +} + +function fieldgroup_remove_group(&$form_state, $type_name, $group_name) { + $content_type = content_types($type_name); + $groups = fieldgroup_groups($content_type['type']); + $group = isset($groups[$group_name]) ? $groups[$group_name] : ''; + + if (empty($group)) { + drupal_not_found(); + exit; + } + + $form['#submit'][] = 'fieldgroup_remove_group_submit'; + $form['#content_type'] = $content_type; + $form['#group_name'] = $group_name; + + return confirm_form($form, + t('Are you sure you want to remove the group %label?', + array('%label' => t($group['label']))), + 'admin/content/node-type/'. $content_type['url_str'] .'/fields', t('This action cannot be undone.'), + t('Remove'), t('Cancel')); +} + +function fieldgroup_remove_group_submit($form, &$form_state) { + $form_values = $form_state['values']; + $content_type = $form['#content_type']; + $group_name = $form['#group_name']; + $parent = db_fetch_array(db_query("SELECT parent FROM {". fieldgroup_tablename() ."} WHERE group_name = '%s' AND type_name = '%s'", $group_name, $content_type['type'])); + $result = db_query("UPDATE {". fieldgroup_tablename() ."} SET parent = '%s' WHERE parent = '%s' AND type_name = '%s'", $parent['parent'], $group_name, $content_type['type']); + $result = db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET group_name = '%s' WHERE group_name = '%s' AND type_name = '%s'", $parent['parent'], $group_name, $content_type['type']); + fieldgroup_delete($content_type['type'], $group_name); + drupal_set_message(t('The group %group_name has been removed.', array('%group_name' => $group_name))); + $form_state['redirect'] = 'admin/content/node-type/'. $content_type['url_str'] .'/fields'; +} + +/** + * Returns all groups for a content type + */ +function fieldgroup_groups($content_type = '', $sorted = FALSE, $reset = FALSE) { + global $language; + static $groups, $groups_sorted; + if (!isset($groups) || $reset) { + if ($cached = cache_get('fieldgroup_data:'. $language->language, content_cache_tablename())) { + $data = $cached->data; + $groups = $data['groups']; + $groups_sorted = $data['groups_sorted']; + } + else { + $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} ORDER BY type_name, weight"); + $groups = array(); + $groups_sorted = array(); + while ($group = db_fetch_array($result)) { + $groups[$group['type_name']] = _fieldgroup_get_tree($group['type_name']); + $groups_sorted[$group['type_name']][] = &$groups[$group['type_name']][$group['group_name']]; + } + cache_set('fieldgroup_data:'. $language->language, array('groups' => $groups, 'groups_sorted' => $groups_sorted), content_cache_tablename()); + } + } + if (empty($content_type)) { + return $groups; + } + elseif (empty($groups) || empty($groups[$content_type])) { + return array(); + } + return $sorted ? $groups_sorted[$content_type] : $groups[$content_type]; +} + +/** + * create a tree of fieldgroups for nesting them + */ +function _fieldgroup_get_tree($type_name, $parent = '', $depth = -1, $max_depth = null) { + static $children, $parents, $groups; + + $depth++; + // We cache trees, so it's not CPU-intensive to call get_tree() on a term + // and its children, too. + if (!isset($children[$type_name])) { + $children[$type_name] = array(); + + $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} WHERE type_name='%s' ORDER BY weight", $type_name); + while ($group = db_fetch_array($result)) { + $group['settings'] = unserialize($group['settings']); + $group['fields'] = array(); + + // Allow external modules to translate field group strings. + $group_strings = array( + 'label' => $group['label'], + 'form_description' => $group['settings']['form']['description'], + 'display_description' => $group['settings']['display']['description'], + ); + $group['label'] = $group_strings['label']; + $group['settings']['form']['description'] = $group_strings['form_description']; + $group['settings']['display']['description'] = $group_strings['display_description']; + + $children[$type_name][$group['parent']][] = $group['group_name']; + $parents[$type_name][$group['group_name']][] = $group['parent']; + $groups[$type_name][$group['group_name']] = $group; + } + //load fields + $result = db_query("SELECT nfi.*, ng.group_name FROM {". fieldgroup_tablename() ."} ng ". + "INNER JOIN {". fieldgroup_fields_tablename() ."} ngf ON ngf.type_name = ng.type_name AND ngf.group_name = ng.group_name ". + "INNER JOIN {". content_instance_tablename() ."} nfi ON nfi.field_name = ngf.field_name AND nfi.type_name = ngf.type_name ". + "WHERE nfi.widget_active = 1 ORDER BY nfi.weight"); + while ($field = db_fetch_array($result)) { + $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = $field; + + // Unserialize arrays. + foreach (array('widget_settings', 'display_settings', 'global_settings', 'db_columns') as $key) { + $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = (!empty($groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']][$key])) ? (array) unserialize($groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']][$key]) : array(); + } + + // For fields inside of groups, use the weight given by fieldgroup + $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']]['weight'] = $field['weight']; + } + } + + $max_depth = (is_null($max_depth)) ? count($children[$type_name]) : $max_depth; + if (isset($children[$type_name][$parent])) { + foreach ($children[$type_name][$parent] as $child_group_name) { + if ($max_depth > $depth) { + $group = $groups[$type_name][$child_group_name]; + $group['depth'] = $depth; + $group['parents'] = $parents[$type_name][$child_group_name]; + $tree[$group['group_name']] = $group; + if (!empty($children[$type_name][$child_group_name])) { + $tree = array_merge($tree, _fieldgroup_get_tree($type_name, $child_group_name, $depth, $max_depth)); + } + } + } + } + return $tree ? $tree : array(); +} + +/** + * go through a set of fieldgroups and construct a simple representation of their hierarchy + */ +function _fieldgroup_plain_tree($items) { + $rows = array(); + $rows[''] = '<'. t('none') .'>'; + foreach ($items as $item) { + $group_name = $item['group_name']; + $label = t($item['label']); + if ($group_name) { + $rows[$group_name] = str_repeat('--', $item['depth']) . ' ' . $label; + } + } + return $rows; +} + +function _fieldgroup_groups_label($content_type) { + $groups = fieldgroup_groups($content_type); + + $labels[''] = '<'. t('none') .'>'; + foreach ($groups as $group_name => $group) { + $labels[$group_name] = t($group['label']); + } + return $labels; +} + +function _fieldgroup_field_get_group($content_type, $field_name) { + return db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $field_name)); +} + +/** + * Return an array of the parents for a field or group. + * + * @param $content_type The content type of the field/group. + * @param $name The name of a group or field. + */ +function fieldgroup_get_parents($content_type, $name) { + $is_group = substr($name, 0, 6) == 'group_'; + $counter = 0; + if ($is_group) { + $parents[$counter] = $name; + } + else { + if ($result = db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $name))) { + $parents[$counter] = $result; + } + } + while ($result = db_result(db_query("SELECT parent FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s' AND group_name = '%s'", $content_type, $parents[$counter]))) { + $counter++; + $parents[$counter] = $result; + } + return $parents; +} + +function _fieldgroup_add_group_to_form(&$form, &$form_state, $form_id, $group_name, $group, $groups) { + $form[$group_name] = array( + '#type' => 'fieldset', + '#title' => check_plain(t($group['label'])), + '#collapsed' => $group['settings']['form']['style'] == 'fieldset_collapsed', + '#collapsible' => in_array($group['settings']['form']['style'], array('fieldset_collapsed', 'fieldset_collapsible')), + '#weight' => $group['weight'], + '#depth' => $group['depth'], + '#group_parent' => $group['parent'], + '#description' => content_filter_xss(t($group['settings']['form']['description'])), + '#attributes' => array('class' => strtr($group['group_name'], '_', '-')), + ); + $has_accessible_field = FALSE; + foreach ($group['fields'] as $field_name => $field) { + if (isset($form[$field_name])) { + $form[$field_name]['#weight'] = $field['weight']; + $form[$group_name][$field_name] = $form[$field_name]; + //Track whether this group has any accessible fields within it. + if (!isset($form[$field_name]['#access']) || $form[$field_name]['#access'] !== FALSE) { + $has_accessible_field = TRUE; + } + unset($form[$field_name]); + } + } + if (!empty($group['fields']) && !element_children($form[$group_name])) { + //hide the fieldgroup, because the fields are hidden too + unset($form[$group_name]); + } + + if (!$has_accessible_field) { + // Hide the fieldgroup, because the fields are inaccessible. + $form[$group_name]['#access'] = FALSE; + } + else { + //cascade visibility up + $form[$group_name]['#access'] = TRUE; + } + + // Allow other modules to alter the form. + // Can't use module_invoke_all because we want + // to be able to use a reference to $form and $form_state. + foreach (module_implements('fieldgroup_form') as $module) { + $function = $module .'_fieldgroup_form'; + $function($form, $form_state, $form_id, $group); + } +} + +/** + * This is function fieldgroup_order_fields_and_groups + * + * @param array $group_rows An empty array that we will fill. + * @param array $groups All of the info we need about all of the groups for the content type we're working on. + * @param array $field_check_off This contains the fields. We will unset them as we process them. + * + */ +function fieldgroup_order_fields_groups(&$group_rows, &$groups, &$field_check_off) { + $max_depth = 0; + foreach ($group_rows as $name) { + $depth = $groups[$name]['depth']; + if ($depth > $max_depth) { + $max_depth = $depth; + } + $parent = $groups[$name]['parent']; + + //run through the fields and come up with new weights for display purposes + if (isset($groups[$name]['fields'])) { + foreach ($groups[$name]['fields'] as $name2 => $elements) { + $depth2 = $groups[$name]['depth'] + 1; + $groups[$name]['fields'][$name2]['depth'] = $depth2; + if (in_array($name2, $field_check_off)) { + $index = array_search($name2, $field_check_off); + unset($field_check_off[$index]); + } + } + } + } + return $max_depth; +} + +/** + * Implementation of hook_form_alter() + */ +function fieldgroup_form_alter(&$form, $form_state, $form_id) { + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + $group_rows = array(); + $field_rows = array(); + + //prepare data that will make this easier + $groups = fieldgroup_groups($form['type']['#value']); + if (!empty($groups)) { + foreach ($groups as $name => $more) { + $group_rows[] = $name; + } + } + + $fields = $form['#field_info']; + if (!empty($fields)) { + foreach ($fields as $name => $more) { + $field_rows[] = $name; + } + } + + $max_depth = fieldgroup_order_fields_groups($group_rows, $groups, $field_rows); + + //cover the top level fields that aren't in fieldgroups + if (isset($field_rows)) { + foreach ($field_rows as $name) { + $form[$name]['#depth'] = 0; + } + } + + //now that we have the order of things as we want them, let's create the fieldsets for the fieldgroups + foreach ($groups as $group_name => $group) { + _fieldgroup_add_group_to_form($form, $form_state, $form_id, $group_name, $group, $groups); + } + + //reorder the groups from the inside-out in order to avoid a recursive function + while ($max_depth >= 0) { + foreach ($group_rows as $name) { + if ($form[$name]['#depth'] == $max_depth) { + $parent = $form[$name]['#group_parent']; + if (isset($parent) && $parent != '') { + $form[$parent][$name] = $form[$name]; + if ($form[$name]['#access']) { + $form[$parent]['#access'] = TRUE; + } + unset($form[$name]); + $index = array_search($name, $group_rows); + unset($group_rows[$index]); + } + } + } + $max_depth--; + } + + } + // The group is only added here so it will appear in the export + // when using Content Copy. + elseif ($form_id == 'content_field_edit_form' && isset($form['widget'])) { + $content_type = content_types($form['type_name']['#value']); + $form['widget']['group'] = array( + '#type' => 'value', + '#value' => _fieldgroup_field_get_group($content_type['type'], $form['field_name']['#value']), + ); + } + elseif ($form_id == 'content_field_overview_form') { + $form['#validate'][] = 'fieldgroup_field_overview_form_validate'; + $form['#submit'][] = 'fieldgroup_field_overview_form_submit'; + } + elseif ($form_id == 'content_display_overview_form' && !empty($form['#groups'])) { + $form['#submit'][] = 'fieldgroup_display_overview_form_submit'; + if (!isset($form['submit'])) { + $form['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 10); + } + } + elseif ($form_id == 'content_field_remove_form') { + $form['#submit'][] = 'fieldgroup_field_remove_form_submit'; + } +} + +/** + * API for group name validation. + * + * Pulled into separate function to be re-usable. + */ +function fieldgroup_validate_name($group, $type_name) { + $errors = array(); + + // No label. + if (!$group['label']) { + $errors['label'][] = t('You need to provide a label.'); + } + + // No group name. + if (!$group['group_name']) { + $errors['group_name'][] = t('You need to provide a group name.'); + } + // Group name validation. + else { + $group_name = $group['group_name']; + $group['group_type'] = !empty($group['group_type']) ? $group['group_type'] : 'standard'; + + // Add the 'group_' prefix. + if (substr($group_name, 0, 6) != 'group_') { + $group_name = 'group_'. $group_name; + } + + // Invalid field name. + if (!preg_match('!^group_[a-z0-9_]+$!', $group_name)) { + $errors['group_name'][] = t('The group name %group_name is invalid. The name must include only lowercase unaccentuated letters, numbers, and underscores.', array('%group_name' => $group_name)); + } + if (strlen($group_name) > 32) { + $errors['group_name'][] = t('The group name %group_name is too long. The name is limited to 32 characters, including the \'group_\' prefix.', array('%group_name' => $group_name)); + } + + // Group name already exists. + $groups = fieldgroup_groups($type_name); + if (isset($groups[$group_name])) { + $errors['group_name'][] = t('The group name %group_name already exists.', array('%group_name' => $group_name)); + } + if (empty($errors['group_name'])) { + $group['group_name'] = $group_name; + } + } + return array('group_name' => $group['group_name'], 'errors' => $errors); +} + +function fieldgroup_field_overview_form_validate($form, &$form_state) { + $form_values = $form_state['values']; + $group = $form_values['_add_new_group']; + + if (array_filter(array($group['label'], $group['group_name']))) { + $validation = fieldgroup_validate_name($group, $form['#type_name']); + if (!empty($validation['errors'])) { + foreach ($validation['errors'] as $type => $messages) { + foreach ($messages as $message) { + if ($type == 'label') { + form_set_error('_add_new_group][label', t('Add new group:') .' '. $message); + } + else { + form_set_error('_add_new_group][group_name', t('Add new group:') .' '. $message); + } + } + } + } + $group_name = $validation['group_name']; + form_set_value($form['_add_new_group']['group_name'], $group_name, $form_state); + } + else { + // Fail validation if attempt to nest fields under a new group without the + // proper information. Not raising an error would cause the nested fields + // to get weights the user doesn't expect. + + foreach ($form_values as $key => $values) { + if ($values['parent'] == '_add_new_group') { + form_set_error('_add_new_group][label', t('Add new group: you need to provide a label.')); + form_set_error('_add_new_group][group_name', t('Add new group: you need to provide a group name.')); + break; + } + } + } +} + +function fieldgroup_field_overview_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + $type_name = $form['#type_name']; + // Create new group if needed. + if (!empty($form_values['_add_new_group']['label'])) { + $group = $form_values['_add_new_group']; + $group['settings'] = field_group_default_settings($group['group_type']); + fieldgroup_save_group($type_name, $group); + $new_group_name = $group['group_name']; + } + + // Parse incoming rows. + $add_field_rows = array('_add_new_field', '_add_existing_field'); + $field_rows = array_merge($form['#fields'], $add_field_rows); + $add_group_rows = array($new_group_name); + $group_rows = array_merge($form['#groups'], $add_group_rows); + + + foreach ($form_values as $key => $values) { + // If 'field' row: update field parenting. + if (in_array($key, $field_rows)) { + // If newly added fields were added to a group: + if (in_array($key, $add_field_rows)) { + // We replace the '_add_*_field' key with the actual name of + // the field that got added. + // content_field_overview_form_submit() placed those + // in $form_state['fields_added'] for us. + if (isset($form_state['fields_added'][$key])) { + $key = $form_state['fields_added'][$key]; + } + else { + // No field was actually created : skip to next row. + continue; + } + } + // If the field was added to the newly created group, replace the + // '_add_new_group' value with the actual name of the group. + $parent = ($values['parent'] == '_add_new_group' && isset($new_group_name)) ? $new_group_name : $values['parent']; + // TODO: check the parent group does exist ? + fieldgroup_update_fields(array('field_name' => $key, 'group' => $parent, 'type_name' => $type_name)); + } + } + + foreach ($form_state['values'] as $key => $values) { + // If 'group' row: update groups weights and parent + // (possible newly created group has already been taken care of). + if (in_array($key, $group_rows)) { + $parent = ($values['parent'] == '_add_new_group' && isset($new_group_name)) ? $new_group_name : $values['parent']; + $weight = $values['weight']; + db_query("UPDATE {". fieldgroup_tablename() ."} SET weight = %d, parent = '%s' WHERE type_name = '%s' AND group_name = '%s'", $weight, $parent, $type_name, $key); + } + } + + cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); +} + +function field_group_default_settings($group_type) { + $settings = array( + 'form' => array('style' => 'fieldset', 'description' => ''), + 'display' => array('description' => '', 'label' => 'above'), + ); + module_load_include('inc', 'content', 'includes/content.admin'); + foreach (array_keys(content_build_modes()) as $key) { + $settings['display'][$key]['format'] = 'fieldset'; + $settings['display'][$key]['exclude'] = 0; + } + // Allow other modules to add new default settings. + $settings = array_merge($settings, module_invoke_all('fieldgroup_default_settings', $group_type)); + return $settings; +} + +function fieldgroup_display_overview_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + $groups = fieldgroup_groups($form['#type_name']); + foreach ($form_values as $key => $values) { + if (in_array($key, $form['#groups'])) { + $group = $groups[$key]; + // Multigroup data (namely, subgroup data) ends up in the settings array as well + // Prevent this data from ending up inside of ['settings']['display'] + if (is_array($values['settings']) && array_key_exists('multigroup', $values['settings'])){ + if (is_array($group['settings']['multigroup'])){ + foreach ($values['settings']['multigroup'] as $group_key => $group_values){ + $group['settings']['multigroup'][$group_key] = $values['settings']['multigroup'][$group_key]; + } + } + else { + $group['settings']['multigroup'] = $values['settings']['multigroup']; + } + unset($values['settings']['multigroup']); + } + // We have some numeric keys here, so we can't use array_merge. + $group['settings']['display'] = $values + $group['settings']['display']; + fieldgroup_save_group($form['#type_name'], $group); + } + } +} + +function fieldgroup_field_remove_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + // TODO: + // - when a (non last) field is removed from a group, a 'ghost row' remains in the fields overview + // - when the last field is removed, the group disappears + // seems to be fixed when emptying the cache. + db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']); +} + +/** + * Implementation of hook_nodeapi(). + */ +function fieldgroup_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { + switch ($op) { + case 'view': + // Prevent against invalid 'nodes' built by broken 3rd party code. + if (isset($node->type)) { + //prepare data that will make this easier + $group_rows = array(); + $field_rows = array(); + $groups = fieldgroup_groups($node->type); + if (!empty($groups)) { + foreach ($groups as $name => $more) { + $group_rows[] = $name; + } + } + + $fields = $node->content; + if (!empty($fields)) { + foreach ($fields as $name => $more) { + if (is_string($name) && strstr($name, 'field_')) { + $field_rows[] = $name; + } + } + } + + $max_depth = fieldgroup_order_fields_groups($group_rows, $groups, $field_rows); + + //cover the top level fields that aren't in fieldgroups + if (isset($field_rows)) { + foreach ($field_rows as $name) { + $node->content[$name]['#depth'] = 0; + } + } + + // Build the node content element needed to render each fieldgroup. + foreach ($groups as $group) { + fieldgroup_build_content($group, $node, $teaser, $page); + } + //reorder the groups from the inside-out in order to avoid writing a recursive function + while ($max_depth >= 0) { + foreach ($group_rows as $name) { + if (!empty($node->content[$name]) && $node->content[$name]['#depth'] == $max_depth) { + $parent = $node->content[$name]['#group_parent']; + if (isset($parent) && $parent != '') { + $node->content[$parent]['group'][$name] = $node->content[$name]; + unset($node->content[$name]); + } + } + } + $max_depth--; + } + } + break; + } +} + +/** + * Build the node content element needed to render a fieldgroup. + * + * @param $group + * The field group definition. + * @param $node + * The node containing the field group to display. Can be a 'pseudo-node', + * containing at least 'type', 'nid', 'vid', and the content for fields + * required for the group. + * @param $teaser + * @param $page + * Similar to hook_nodeapi('view'). + * + * @see fieldgroup_nodeapi() + * @see fieldgroup_view_group() + */ +function fieldgroup_build_content($group, &$node, $teaser, $page) { + // NODE_BUILD_NORMAL is 0, and ('whatever' == 0) is TRUE, so we need a ===. + if ($node->build_mode === NODE_BUILD_NORMAL || $node->build_mode == NODE_BUILD_PREVIEW) { + $context = $teaser ? 'teaser' : 'full'; + } + else { + $context = $node->build_mode; + } + + $group_name = $group['group_name']; + + // Do not include group labels when indexing content. + if ($context == NODE_BUILD_SEARCH_INDEX) { + $group['settings']['display']['label'] = 'hidden'; + } + $label = $group['settings']['display']['label'] == 'above'; + $element = array( + '#title' => $label ? check_plain(t($group['label'])) : '', + '#description' => $label && !empty($group['settings']['display']['description']) ? content_filter_xss(t($group['settings']['display']['description'])) : '', + ); + $format = isset($group['settings']['display'][$context]['format']) ? $group['settings']['display'][$context]['format'] : 'fieldset'; + + switch ($format) { + case 'simple': + $element['#type'] = 'fieldgroup_simple'; + $element['#group_name'] = $group_name; + $element['#node'] = $node; + break; + case 'hidden': + $element['#access'] = FALSE; + break; + case 'fieldset_collapsed': + $element['#collapsed'] = TRUE; + case 'fieldset_collapsible': + $element['#collapsible'] = TRUE; + case 'fieldset': + $element['#type'] = 'fieldgroup_fieldset'; + $element['#attributes'] = array('class' => 'fieldgroup '. strtr($group['group_name'], '_', '-')); + break; + } + foreach ($group['fields'] as $field_name => $field) { + if (isset($node->content[$field_name])) { + $element[$field_name] = $node->content[$field_name]; + $element[$field_name]['#weight'] = $field['weight']; + $element[$field_name]['#depth'] = $field['depth']; + } + } + + // Allow other modules to alter the group view. + // Can't use module_invoke_all because we want + // to be able to use a reference to $node and $element. + foreach (module_implements('fieldgroup_view') as $module) { + $function = $module .'_fieldgroup_view'; + $function($node, $element, $group, $context); + } + + // Unset the original field values now that we've moved them. + foreach ($group['fields'] as $field_name => $field) { + if (isset($node->content[$field_name])) { + unset($node->content[$field_name]); + } + } + + // The wrapper lets us get the themed output for the group + // to populate the $GROUP_NAME_rendered variable for node templates, + // and hide it from the $content variable if needed. + // See fieldgroup_preprocess_node(), theme_fieldgroup_wrapper(). + $wrapper = array( + 'group' => $element, + '#weight' => $group['weight'], + '#depth' => $group['depth'], + '#post_render' => array('fieldgroup_wrapper_post_render'), + '#group_name' => $group_name, + '#type_name' => $node->type, + '#context' => $context, + '#group_parent' => $group['parent'], + ); + + $node->content[$group_name] = $wrapper; +} + +/** + * Render a single field group, fully themed with label. + * + * To be used by third-party code (Panels, ...) that needs to output an + * isolated field group. Do *not* use inside node templates, use the + * $GROUP_NAME_rendered variables instead. You can also use the 'simple' + * style format and override the template fieldgroup-simple.tpl.php. + * + * By default, the field group is displayed using the settings defined for the + * 'full node' or 'teaser' contexts (depending on the value of the $teaser param). + * Set $node->build_mode to a different value to use a different context. + * + * Different settings can be specified by adjusting $group['settings']['display']. + * + * @param $group + * The field group definition. + * @param $node + * The node containing the field group to display. Can be a 'pseudo-node', + * containing at least 'type', 'nid', 'vid', and the field data required + * for the group. + * @param $teaser + * @param $page + * Similar to hook_nodeapi('view'). + * @return + * The themed output for the field group. + * + * @see content_view_field() + */ +function fieldgroup_view_group($group, &$node, $teaser = FALSE, $page = FALSE) { + $group_name = $group['group_name']; + $field_types = _content_field_types(); + + // Clone the node to prevent from altering the original. + $node_copy = drupal_clone($node); + + // Use 'full'/'teaser' if not specified otherwise. + $node_copy->build_mode = isset($node_copy->build_mode) ? $node_copy->build_mode : NODE_BUILD_NORMAL; + + // Build the content element for individual fields in the field group. + if (!isset($node_copy->content)) { + $node_copy->content = array(); + } + foreach (array_keys($group['fields']) as $field_name) { + $field = content_fields($field_name, $node_copy->type); + + if (isset($node_copy->{$field_name})) { + $items = $node_copy->{$field_name}; + + // One-field equivalent to _content_field_invoke('sanitize'). + $module = $field_types[$field['type']]['module']; + $function = $module .'_field'; + if (function_exists($function)) { + $function('sanitize', $node_copy, $field, $items, $teaser, $page); + $node_copy->{$field_name} = $items; + } + + $field_view = content_field('view', $node_copy, $field, $items, $teaser, $page); + // content_field('view') adds a wrapper to handle variables and 'excluded' + // fields for node templates. We bypass it and get the actual field. + $node_copy->content[$field_name] = $field_view[$field_name]; + } + } + + // Build the content element of the field group itself. + fieldgroup_build_content($group, $node_copy, $teaser, $page); + + // fieldgroup_build_content() adds a wrapper to handle variables and 'excluded' + // groups for node templates. We bypass it and render the actual field group. + $output = drupal_render($node_copy->content[$group_name]['group']); + + return $output; +} + +/** + * Hide specified fields from the $content variable in node templates. + */ +function fieldgroup_wrapper_post_render($content, $element) { + $groups = fieldgroup_groups($element['#type_name']); + $group = $groups[$element['#group_name']]; + + // The display settings are not in quite the same place in the + // group and the field, so create the value the theme will expect. + $group['display_settings'] = $group['settings']['display']; + if (theme('content_exclude', $content, $group, $element['#context'])) { + return ''; + } + return $content; +} + +/** + * Get the group name for a field. + * If the field isn't in a group, FALSE will be returned. + * @return The name of the group, or FALSE. + */ +function fieldgroup_get_group($content_type, $field_name) { + foreach (fieldgroup_groups($content_type) as $group_name => $group) { + if (in_array($field_name, array_keys($group['fields']))) { + return $group_name; + } + } + return FALSE; +} + +/** + * Implementation of hook_node_type() + * React to change in node types + */ +function fieldgroup_node_type($op, $info) { + if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) { + // update the tables + db_query("UPDATE {". fieldgroup_tablename() ."} SET type_name='%s' WHERE type_name='%s'", array($info->type, $info->old_type)); + db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET type_name='%s' WHERE type_name='%s'", array($info->type, $info->old_type)); + cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); + } + elseif ($op == 'delete') { + db_query("DELETE FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s'", $info->type); + db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s'", $info->type); + } +} + +function fieldgroup_types() { + $types = array('standard' => t('Standard group')); + // Allow other modules to add new group_types. + $types = array_merge($types, module_invoke_all('fieldgroup_types')); + return $types; +} + +function fieldgroup_tablename($version = NULL) { + if (is_null($version)) { + $version = variable_get('fieldgroup_schema_version', 0); + } + return $version < 6000 ? 'node_group' : 'content_group'; +} + +function fieldgroup_fields_tablename($version = NULL) { + if (is_null($version)) { + $version = variable_get('fieldgroup_schema_version', 0); + } + return $version < 6000 ? 'node_group_fields' : 'content_group_fields'; +} + +/** + * CRUD API for fieldgroup module. + * + * @todo + * Make this into more of a real API for groups. + */ +/** + * Saves the given group for this content-type + */ +function fieldgroup_save_group($type_name, $group) { + $groups = fieldgroup_groups($type_name); + + // Allow other modules to intervene when the group is saved. + foreach (module_implements('fieldgroup_save_group') as $module) { + $function = $module .'_fieldgroup_save_group'; + $function($group); + } + + if (!isset($groups[$group['group_name']])) { + // Accept group name from programmed submissions if valid. + db_query("INSERT INTO {". fieldgroup_tablename() ."} (parent, group_type, type_name, group_name, label, settings, weight)". + " VALUES ('%s','%s', '%s', '%s', '%s', '%s', %d)", + isset($group['parent']) ? $group['parent'] : $group['parent'], $group['group_type'], $type_name, $group['group_name'], $group['label'], serialize($group['settings']), $group['weight']); + cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); + return SAVED_NEW; + } + else { + db_query("UPDATE {". fieldgroup_tablename() ."} SET parent = '%s', group_type = '%s', label = '%s', settings = '%s', weight = %d ". + "WHERE type_name = '%s' AND group_name = '%s'", + isset($group['parent']) ? $group['parent'] : $group['parent'], $group['group_type'], $group['label'], serialize($group['settings']), $group['weight'], $type_name, $group['group_name']); + cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); + return SAVED_UPDATED; + } +} + +function fieldgroup_update_fields($form_values) { + $default = _fieldgroup_field_get_group($form_values['type_name'], $form_values['field_name']); + + if ($default != $form_values['group']) { + if ($form_values['group'] && !$default) { + db_query("INSERT INTO {". fieldgroup_fields_tablename() ."} (type_name, group_name, field_name) VALUES ('%s', '%s', '%s')", $form_values['type_name'], $form_values['group'], $form_values['field_name']); + } + elseif ($form_values['group']) { + db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET group_name = '%s' WHERE type_name = '%s' AND field_name = '%s'", $form_values['group'], $form_values['type_name'], $form_values['field_name']); + } + else { + db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']); + } + cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); + } +} + +function fieldgroup_delete($content_type, $group_name) { + db_query("DELETE FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s' AND group_name = '%s'", $content_type, $group_name); + db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND group_name = '%s'", $content_type, $group_name); + cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); +} + +/** + * Format a fieldgroup using a 'fieldset'. + * + * Derived from core's theme_fieldset, with no output if the content is empty. + */ +function theme_fieldgroup_fieldset($element) { + if (empty($element['#children']) && empty($element['#value'])) { + return ''; + } + + if ($element['#collapsible']) { + drupal_add_js('misc/collapse.js'); + + if (!isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = ''; + } + + $element['#attributes']['class'] .= ' collapsible'; + if ($element['#collapsed']) { + $element['#attributes']['class'] .= ' collapsed'; + } + } + return ''. ($element['#title'] ? ''. $element['#title'] .'' : '') . (isset($element['#description']) && $element['#description'] ? '
'. $element['#description'] .'
' : '') . (!empty($element['#children']) ? $element['#children'] : '') . (isset($element['#value']) ? $element['#value'] : '') ."\n"; +} + + +/** + * Process variables for fieldgroup.tpl.php. + * + * The $variables array contains the following arguments: + * - $group_name + * - $group_name_css + * - $label + * - $description + * - $content + * + * @see fieldgroup-simple.tpl.php + */ +function template_preprocess_fieldgroup_simple(&$vars) { + $element = $vars['element']; + + $vars['parent'] = isset($element['#parent']) ? $element['#parent'] : NULL; + $vars['group_name'] = $element['#group_name']; + $vars['group_name_css'] = strtr($element['#group_name'], '_', '-'); + $vars['label'] = isset($element['#title']) ? $element['#title'] : '';; + $vars['description'] = isset($element['#description']) ? $element['#description'] : '';; + $vars['content'] = isset($element['#children']) ? $element['#children'] : ''; + $vars['template_files'] = array( + 'fieldgroup-simple-', + 'fieldgroup-simple-'. $element['#group_name'], + 'fieldgroup-simple-'. $element['#node']->type, + 'fieldgroup-simple-'. $element['#group_name'] .'-'. $element['#node']->type, + ); +} + +/** + * Theme preprocess function for node. + * + * Adds $GROUP_NAME_rendered variables, + * containing the themed output for the whole group. + */ +function fieldgroup_preprocess_node(&$vars) { + $node = $vars['node']; + + foreach (fieldgroup_groups($node->type) as $group_name => $group) { + // '#chilren' might not be set if the group is empty. + $vars[$group_name .'_rendered'] = isset($node->content[$group_name]['#children']) ? $node->content[$group_name]['#children'] : ''; + } +} diff --git a/sites/all/modules/cck/modules/fieldgroup/panels/content_types/content_fieldgroup.inc b/sites/all/modules/cck/modules/fieldgroup/panels/content_types/content_fieldgroup.inc new file mode 100644 index 0000000..7017b12 --- /dev/null +++ b/sites/all/modules/cck/modules/fieldgroup/panels/content_types/content_fieldgroup.inc @@ -0,0 +1,163 @@ + t('Content fieldgroup'), + 'defaults' => array('label' => 'hidden', 'format' => 'simple', 'empty' => ''), + ); +} + +/** + * Return all fieldgroup content types available. + */ +function fieldgroup_content_fieldgroup_content_type_content_types() { + // This will hold all the individual fieldgroup content types. + $types = array(); + + // The outer loop goes through each node type with groups. + foreach (fieldgroup_groups() as $node_type_groups) { + // The inner loop gives us each fieldgroup on each node type with groups. + foreach ($node_type_groups as $group) { + // Skip field groups that are not of standard type. + if ($group['group_type'] != 'standard') { + continue; + } + + // Name the content type a combination of fieldgroup and node type names. + $content_type_name = $group['type_name'] . ':' . $group['group_name']; + + // Assemble the information about the content type. + $info = array( + 'category' => t('Node'), + 'icon' => 'icon_cck_field_group.png', + 'title' => t('Field group: @group in @type', array( + '@group' => t($group['label']), + '@type' => node_get_types('name', $group['type_name']), + )), + 'description' => t('All fields from this field group on the referenced node.'), + 'required context' => new ctools_context_required(t('Node'), 'node', array('type' => array($group['type_name']))), + ); + + $types[$content_type_name] = $info; + } + } + + return $types; +} + +/** + * Output function for the 'fieldgroup' content type. + */ +function fieldgroup_content_fieldgroup_content_type_render($subtype, $conf, $panel_args, $context) { + if (!isset($context->data)) { + return; + } + $node = drupal_clone($context->data); + + // Make sure old data doesn't cause problems: + if (empty($conf['label'])) { + $conf['label'] = 'hidden'; + } + if (empty($conf['format'])) { + $conf['format'] = 'simple'; + } + + // Extract the node type and fieldgroup name from the subtype. + list($node_type, $group_name) = explode(':', $subtype, 2); + + // Get a list of all fieldgroups for this node type. + $groups = fieldgroup_groups($node_type); + + if (!isset($groups[$group_name])) { + return; + } + $group = $groups[$group_name]; + + // Render the field group. + $node->build_mode = NODE_BUILD_NORMAL; + $group['settings']['display']['label'] = $conf['label'] == 'normal' || !empty($conf['override_title']) ? 'hidden' : $conf['label']; + $group['settings']['display']['full']['format'] = $conf['format']; + $group['settings']['display']['full']['exclude'] = 0; + $output = fieldgroup_view_group($group, $node); + + $block = new stdClass(); + if ($conf['label'] == 'normal') { + $block->title = t($group['label']); + } + $block->content = !empty($output) ? $output : $conf['empty']; + return $block; +} + +/** + * Returns a settings form for the custom type. + */ +function fieldgroup_content_fieldgroup_content_type_edit_form(&$form, &$form_state) { + $conf = $form_state['conf']; + + $label_options = array( + 'normal' => t('Block title'), + 'above' => t('Above'), + ); + $form['label'] = array( + '#type' => 'select', + '#title' => t('Field group label'), + '#default_value' => !empty($conf['label']) && isset($label_options[$conf['label']]) ? $conf['label'] : 'hidden', + '#options' => $label_options, + '#description' => t('Configure how the field group label is going to be displayed. This option takes no effect when "Override title" option is enabled, the specified block title is displayed instead.'), + ); + + $format_options = array( + 'simple' => t('Simple'), + 'fieldset' => t('Fieldset'), + 'fieldset_collapsible' => t('Fieldset - Collapsible'), + 'fieldset_collapsed' => t('Fieldset - Collapsed'), + ); + $form['format'] = array( + '#type' => 'select', + '#title' => t('Field group format'), + '#default_value' => !empty($conf['format']) && isset($format_options[$conf['format']]) ? $conf['format'] : 'simple', + '#options' => $format_options, + '#description' => t('This option allows you to configure the field group format.'), + ); + + $form['empty'] = array( + '#type' => 'textarea', + '#title' => t('Empty text'), + '#description' => t('Text to display if group has no data. Note that title will not display unless overridden.'), + '#rows' => 5, + '#default_value' => $conf['empty'], + ); +} + +function fieldgroup_content_fieldgroup_content_type_edit_form_submit(&$form, &$form_state) { + // Copy everything from our defaults. + foreach (array_keys($form_state['plugin']['defaults']) as $key) { + $form_state['conf'][$key] = $form_state['values'][$key]; + } +} + +/** + * Admin title for fieldgroup content type. + */ +function fieldgroup_content_fieldgroup_content_type_admin_title($subtype, $conf, $context) { + // Extract the node type and fieldgroup name from the subtype. + list($node_type, $group_name) = explode(':', $subtype, 2); + + // Get information about this field group for this node type. + $groups = fieldgroup_groups($node_type); + $group = $groups[$group_name]; + + return t('"@s" field group: @group in @type', array( + '@s' => $context->identifier, + '@group' => t($group['label']), + '@type' => node_get_types('name', $node_type), + )); +} diff --git a/sites/all/modules/cck/modules/fieldgroup/panels/content_types/icon_cck_field_group.png b/sites/all/modules/cck/modules/fieldgroup/panels/content_types/icon_cck_field_group.png new file mode 100644 index 0000000..8459443 Binary files /dev/null and b/sites/all/modules/cck/modules/fieldgroup/panels/content_types/icon_cck_field_group.png differ diff --git a/sites/all/modules/cck/modules/nodereference/help/nodereference.help.ini b/sites/all/modules/cck/modules/nodereference/help/nodereference.help.ini new file mode 100644 index 0000000..2542033 --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/help/nodereference.help.ini @@ -0,0 +1,7 @@ + +[advanced help settings] +hide = TRUE + +[nodereference] +title = Nodereference field +parent = content%fields diff --git a/sites/all/modules/cck/modules/nodereference/help/nodereference.html b/sites/all/modules/cck/modules/nodereference/help/nodereference.html new file mode 100644 index 0000000..a62e885 --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/help/nodereference.html @@ -0,0 +1,3 @@ +

The Nodereference field stores the nid of a related node. The title of the related node is usually displayed as the value of this field.

+

The Nodereference field can use an autocomplete widget, or, when used with Optionwidgets, the available values can be presented to the end user in a drop-down select list, checkboxes, or radios.

+

A Nodereference field can be used in Views to create a Relationship to another node, to allow you to use any field, argument, or filter from the related node in your view.

\ No newline at end of file diff --git a/sites/all/modules/cck/modules/nodereference/nodereference.info b/sites/all/modules/cck/modules/nodereference/nodereference.info new file mode 100644 index 0000000..44e126c --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/nodereference.info @@ -0,0 +1,13 @@ +name = Node Reference +description = Defines a field type for referencing one node from another. +dependencies[] = content +dependencies[] = text +dependencies[] = optionwidgets +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/nodereference/nodereference.install b/sites/all/modules/cck/modules/nodereference/nodereference.install new file mode 100644 index 0000000..d819f51 --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/nodereference.install @@ -0,0 +1,164 @@ + $fields) { + foreach ($fields as $field) { + if ($field['type'] == 'nodereference') { + $sandbox['fields'][] = $field; + } + } + } + + if (empty($sandbox['fields'])) { + return $ret; + } + + $sandbox['progress'] = 0; + $sandbox['visited'] = array(); + } + + $field = $sandbox['fields'][$sandbox['progress']]; + + // We only want to process a field once -- if we hit it a second time, + // that means it's its own table and it should have already been updated. + if (!in_array($field['field_name'], $sandbox['visited'])) { + $db_info = content_database_info($field); + $table = $db_info['table']; + $attributes = $db_info['columns']['nid']; + $column = $attributes['column']; + $attributes['not null'] = FALSE; + db_change_field($ret, $table, $column, $column, array('type' => 'int', 'not null' => FALSE)); + db_field_set_no_default($ret, $db_info['table'], $column); + $ret[] = update_sql("UPDATE {". $db_info['table'] ."} SET ". $column ." = NULL WHERE ". $column ." = 0"); + + $sandbox['visited'][] = $field['field_name']; + } + + $sandbox['progress']++; + $ret['#finished'] = $sandbox['progress'] / count($sandbox['fields']); + + return $ret; +} + +/** + * Create an index by node reference column for all fields. + */ +function nodereference_update_6001(&$sandbox) { + include_once('./'. drupal_get_path('module', 'content') .'/content.install'); + drupal_load('module', 'content'); + + $ret = array(); + + if (!isset($sandbox['progress'])) { + if ($abort = content_check_update('nodereference')) { + return $abort; + } + + // Get the latest cache values and schema. + content_clear_type_cache(TRUE, TRUE); + $types = content_types_install(); + + if (empty($types)) { + return $ret; + } + + $sandbox['fields'] = array(); + foreach ($types as $type_name => $fields) { + foreach ($fields as $field) { + if ($field['type'] == 'nodereference') { + $sandbox['fields'][] = $field; + } + } + } + + if (empty($sandbox['fields'])) { + return $ret; + } + + $sandbox['progress'] = 0; + $sandbox['visited'] = array(); + } + + $field = $sandbox['fields'][$sandbox['progress']]; + + // We only want to process a field once -- if we hit it a second time, + // that means it's its own table and it should have already been updated. + if (!in_array($field['field_name'], $sandbox['visited'])) { + $db_info = content_database_info($field); + $table = $db_info['table']; + $attributes = $db_info['columns']['nid']; + $column = $attributes['column']; + if (!content_db_index_exists($table, $column)) { + db_add_index($ret, $table, $column, array($column)); + } + $sandbox['visited'][] = $field['field_name']; + } + + $sandbox['progress']++; + $ret['#finished'] = $sandbox['progress'] / count($sandbox['fields']); + + return $ret; +} diff --git a/sites/all/modules/cck/modules/nodereference/nodereference.module b/sites/all/modules/cck/modules/nodereference/nodereference.module new file mode 100644 index 0000000..5969a4f --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/nodereference.module @@ -0,0 +1,1065 @@ + 'Nodereference autocomplete', + 'page callback' => 'nodereference_autocomplete', + 'access callback' => 'nodereference_autocomplete_access', + 'access arguments' => array(2), + 'type' => MENU_CALLBACK + ); + return $items; +} + +/** + * Implementation of hook_theme(). + */ +function nodereference_theme() { + return array( + 'nodereference_select' => array( + 'arguments' => array('element' => NULL), + ), + 'nodereference_buttons' => array( + 'arguments' => array('element' => NULL), + ), + 'nodereference_autocomplete' => array( + 'arguments' => array('element' => NULL), + ), + 'nodereference_formatter_default' => array( + 'arguments' => array('element'), + ), + 'nodereference_formatter_plain' => array( + 'arguments' => array('element'), + ), + 'nodereference_formatter_full' => array( + 'arguments' => array('element'), + 'function' => 'theme_nodereference_formatter_full_teaser', + ), + 'nodereference_formatter_teaser' => array( + 'arguments' => array('element'), + 'function' => 'theme_nodereference_formatter_full_teaser', + ), + ); +} + +/** + * Implementaion of hook_ctools_plugin_directory(). + */ +function nodereference_ctools_plugin_directory($module, $plugin) { + if ($module == 'ctools' && $plugin == 'relationships') { + return 'panels/' . $plugin; + } +} + +/** + * Implementation of hook_field_info(). + */ +function nodereference_field_info() { + return array( + 'nodereference' => array( + 'label' => t('Node reference'), + 'description' => t('Store the ID of a related node as an integer value.'), +// 'content_icon' => 'icon_content_noderef.png', + ), + ); +} + +/** + * Implementation of hook_field_settings(). + */ +function nodereference_field_settings($op, $field) { + switch ($op) { + case 'form': + $form = array(); + $form['referenceable_types'] = array( + '#type' => 'checkboxes', + '#title' => t('Content types that can be referenced'), + '#multiple' => TRUE, + '#default_value' => is_array($field['referenceable_types']) ? $field['referenceable_types'] : array(), + '#options' => array_map('check_plain', node_get_types('names')), + ); + if (module_exists('views')) { + $views = array('--' => '--'); + $all_views = views_get_all_views(); + foreach ($all_views as $view) { + // Only 'node' views that have fields will work for our purpose. + if ($view->base_table == 'node' && !empty($view->display['default']->display_options['fields'])) { + if ($view->type == 'Default') { + $views[t('Default Views')][$view->name] = $view->name; + } + else { + $views[t('Existing Views')][$view->name] = $view->name; + } + } + } + + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced - Nodes that can be referenced (View)'), + '#collapsible' => TRUE, + '#collapsed' => !isset($field['advanced_view']) || $field['advanced_view'] == '--', + ); + if (count($views) > 1) { + $form['advanced']['advanced_view'] = array( + '#type' => 'select', + '#title' => t('View used to select the nodes'), + '#options' => $views, + '#default_value' => isset($field['advanced_view']) ? $field['advanced_view'] : '--', + '#description' => t('

Choose the "Views module" view that selects the nodes that can be referenced.
Note:

') . + t('
  • Only views that have fields will work for this purpose.
  • This will discard the "Content types" settings above. Use the view\'s "filters" section instead.
  • Use the view\'s "fields" section to display additional informations about candidate nodes on node creation/edition form.
  • Use the view\'s "sort criteria" section to determine the order in which candidate nodes will be displayed.
'), + ); + $form['advanced']['advanced_view_args'] = array( + '#type' => 'textfield', + '#title' => t('View arguments'), + '#default_value' => isset($field['advanced_view_args']) ? $field['advanced_view_args'] : '', + '#required' => FALSE, + '#description' => t('Provide a comma separated list of arguments to pass to the view.'), + ); + } + else { + $form['advanced']['no_view_help'] = array( + '#value' => t('

The list of nodes that can be referenced can be based on a "Views module" view but no appropriate views were found.
Note:

') . + t('
  • Only views that have fields will work for this purpose.
  • This will discard the "Content types" settings above. Use the view\'s "filters" section instead.
  • Use the view\'s "fields" section to display additional informations about candidate nodes on node creation/edition form.
  • Use the view\'s "sort criteria" section to determine the order in which candidate nodes will be displayed.
'), + ); + } + } + return $form; + + case 'save': + $settings = array('referenceable_types'); + if (module_exists('views')) { + $settings[] = 'advanced_view'; + $settings[] = 'advanced_view_args'; + } + return $settings; + + case 'database columns': + $columns = array( + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'index' => TRUE), + ); + return $columns; + + case 'views data': + $data = content_views_field_views_data($field); + $db_info = content_database_info($field); + $table_alias = content_views_tablename($field); + + // Filter: swap the handler to the 'in' operator. + $data[$table_alias][$field['field_name'] .'_nid']['filter']['handler'] = 'content_handler_filter_many_to_one'; + // Argument: use node.title for summaries. + $data["node_$table_alias"]['table']['join']['node'] = array( + 'table' => 'node', + 'field' => 'nid', + 'left_table' => $table_alias, + 'left_field' => $field['field_name'] .'_nid', + ); + $data[$table_alias][$field['field_name'] .'_nid']['argument']['handler'] = 'content_handler_argument_reference'; + $data[$table_alias][$field['field_name'] .'_nid']['argument']['name table'] = "node_$table_alias"; + $data[$table_alias][$field['field_name'] .'_nid']['argument']['name field'] = 'title'; + // Relationship: add a relationship for related node. + $data[$table_alias][$field['field_name'] .'_nid']['relationship'] = array( + 'base' => 'node', + 'field' => $db_info['columns']['nid']['column'], + 'handler' => 'content_handler_relationship', + 'label' => t($field['widget']['label']), + 'content_field_name' => $field['field_name'], + ); + return $data; + } +} + +/** + * Implementation of hook_field(). + */ +function nodereference_field($op, &$node, $field, &$items, $teaser, $page) { + static $sanitized_nodes = array(); + + switch ($op) { + // When preparing a translation, load any translations of existing references. + case 'prepare translation': + $addition = array(); + $addition[$field['field_name']] = array(); + if (isset($node->translation_source->$field['field_name']) && is_array($node->translation_source->$field['field_name'])) { + foreach ($node->translation_source->$field['field_name'] as $key => $reference) { + $reference_node = node_load($reference['nid']); + // Test if the referenced node type is translatable and, if so, + // load translations if the reference is not for the current language. + // We can assume the translation module is present because it invokes 'prepare translation'. + if (translation_supported_type($reference_node->type) && !empty($reference_node->language) && $reference_node->language != $node->language && $translations = translation_node_get_translations($reference_node->tnid)) { + // If there is a translation for the current language, use it. + $addition[$field['field_name']][] = array( + 'nid' => isset($translations[$node->language]) ? $translations[$node->language]->nid : $reference['nid'], + ); + } + } + } + return $addition; + + case 'validate': + // Extract nids to check. + $ids = array(); + foreach ($items as $delta => $item) { + if (is_array($item) && !empty($item['nid'])) { + if (is_numeric($item['nid'])) { + $ids[] = $item['nid']; + } + else { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + form_set_error($error_element, t("%name: invalid input.", array('%name' => t($field['widget']['label'])))); + } + } + } + // Prevent performance hog if there are no ids to check. + if ($ids) { + $refs = _nodereference_potential_references($field, '', NULL, $ids); + foreach ($items as $delta => $item) { + if (is_array($item)) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if (!empty($item['nid']) && !isset($refs[$item['nid']])) { + form_set_error($error_element, t("%name: this post can't be referenced.", array('%name' => t($field['widget']['label'])))); + } + } + } + } + return $items; + + case 'sanitize': + // We can't just check the node is 'referenceable', because Views-mode + // could rely on 'current user' (at edit time). + + // Extract nids to check. + $ids = array(); + foreach ($items as $delta => $item) { + if (is_array($item)) { + // Default to 'non accessible'. + $items[$delta]['safe'] = array(); + if (!empty($item['nid']) && is_numeric($item['nid'])) { + $ids[] = $item['nid']; + } + } + } + if ($ids) { + // Load information about nids that we haven't already loaded during + // this page request. + $missing_ids = array_diff($ids, array_keys($sanitized_nodes)); + if (!empty($missing_ids)) { + $where = array('n.nid in ('. db_placeholders($missing_ids) . ')'); + if (!user_access('administer nodes')) { + $where[] = 'n.status = 1'; + } + $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.status FROM {node} n WHERE '. implode(' AND ', $where)), $missing_ids); + while ($row = db_fetch_array($result)) { + $sanitized_nodes[$row['nid']] = $row; + } + } + foreach ($items as $delta => $item) { + if (is_array($item) && !empty($item['nid']) && isset($sanitized_nodes[$item['nid']])) { + $items[$delta]['safe'] = $sanitized_nodes[$item['nid']]; + } + } + } + return $items; + } +} + +/** + * Implementation of hook_content_is_empty(). + */ +function nodereference_content_is_empty($item, $field) { + if (empty($item['nid'])) { + return TRUE; + } + return FALSE; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function nodereference_field_formatter_info() { + return array( + 'default' => array( + 'label' => t('Title (link)'), + 'field types' => array('nodereference'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + 'plain' => array( + 'label' => t('Title (no link)'), + 'field types' => array('nodereference'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + 'full' => array( + 'label' => t('Full node'), + 'field types' => array('nodereference'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + 'teaser' => array( + 'label' => t('Teaser'), + 'field types' => array('nodereference'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + ); +} + +/** + * Theme function for 'default' nodereference field formatter. + */ +function theme_nodereference_formatter_default($element) { + $output = ''; + if (!empty($element['#item']['safe']['nid'])) { + $output = l($element['#item']['safe']['title'], 'node/'. $element['#item']['safe']['nid']); + if (!$element['#item']['safe']['status']) { + $output = ' '. t('(Unpublished)') ." $output"; + } + } + return $output; +} + +/** + * Theme function for 'plain' nodereference field formatter. + */ +function theme_nodereference_formatter_plain($element) { + $output = ''; + if (!empty($element['#item']['safe']['nid'])) { + $output = check_plain($element['#item']['safe']['title']); + if (!$element['#item']['safe']['status']) { + $output = ' '. t('(Unpublished)') ." $output"; + } + } + return $output; +} + +/** + * Proxy theme function for 'full' and 'teaser' nodereference field formatters. + */ +function theme_nodereference_formatter_full_teaser($element) { + static $recursion_queue = array(); + $output = ''; + if (!empty($element['#item']['safe']['nid'])) { + $nid = $element['#item']['safe']['nid']; + $node = $element['#node']; + $field = content_fields($element['#field_name'], $element['#type_name']); + // If no 'referencing node' is set, we are starting a new 'reference thread' + if (!isset($node->referencing_node)) { + $recursion_queue = array(); + } + $recursion_queue[] = $node->nid; + if (in_array($nid, $recursion_queue)) { + // Prevent infinite recursion caused by reference cycles: + // if the node has already been rendered earlier in this 'thread', + // we fall back to 'default' (node title) formatter. + return theme('nodereference_formatter_default', $element); + } + if ($referenced_node = node_load($nid)) { + $referenced_node->referencing_node = $node; + $referenced_node->referencing_field = $field; + $output = node_view($referenced_node, $element['#formatter'] == 'teaser'); + } + } + return $output; +} + +/** + * Helper function for formatters. + * + * Store node titles collected in the curent request. + */ +function _nodereference_titles($nid, $known_title = NULL) { + static $titles = array(); + if (!isset($titles[$nid])) { + $title = $known_title ? $known_title : db_result(db_query(db_rewrite_sql("SELECT n.title FROM {node} n WHERE n.nid=%d"), $nid)); + $titles[$nid] = $title ? $title : ''; + } + return $titles[$nid]; +} + +/** + * Implementation of hook_widget_info(). + * + * We need custom handling of multiple values for the nodereference_select + * widget because we need to combine them into a options list rather + * than display multiple elements. + * + * We will use the content module's default handling for default value. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function nodereference_widget_info() { + return array( + 'nodereference_select' => array( + 'label' => t('Select list'), + 'field types' => array('nodereference'), + 'multiple values' => CONTENT_HANDLE_MODULE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + 'nodereference_buttons' => array( + 'label' => t('Check boxes/radio buttons'), + 'field types' => array('nodereference'), + 'multiple values' => CONTENT_HANDLE_MODULE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + 'nodereference_autocomplete' => array( + 'label' => t('Autocomplete text field'), + 'field types' => array('nodereference'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + * + * Autocomplete_path is not used by text_widget but other widgets can use it + * (see nodereference and userreference). + */ +function nodereference_elements() { + return array( + 'nodereference_select' => array( + '#input' => TRUE, + '#columns' => array('uid'), '#delta' => 0, + '#process' => array('nodereference_select_process'), + ), + 'nodereference_buttons' => array( + '#input' => TRUE, + '#columns' => array('uid'), '#delta' => 0, + '#process' => array('nodereference_buttons_process'), + ), + 'nodereference_autocomplete' => array( + '#input' => TRUE, + '#columns' => array('name'), '#delta' => 0, + '#process' => array('nodereference_autocomplete_process'), + '#autocomplete_path' => FALSE, + ), + ); +} + +/** + * Implementation of hook_widget_settings(). + */ +function nodereference_widget_settings($op, $widget) { + switch ($op) { + case 'form': + $form = array(); + $match = isset($widget['autocomplete_match']) ? $widget['autocomplete_match'] : 'contains'; + $size = (isset($widget['size']) && is_numeric($widget['size'])) ? $widget['size'] : 60; + if ($widget['type'] == 'nodereference_autocomplete') { + $form['autocomplete_match'] = array( + '#type' => 'select', + '#title' => t('Autocomplete matching'), + '#default_value' => $match, + '#options' => array( + 'starts_with' => t('Starts with'), + 'contains' => t('Contains'), + ), + '#description' => t('Select the method used to collect autocomplete suggestions. Note that Contains can cause performance issues on sites with thousands of nodes.'), + ); + $form['size'] = array( + '#type' => 'textfield', + '#title' => t('Size of textfield'), + '#default_value' => $size, + '#element_validate' => array('_element_validate_integer_positive'), + '#required' => TRUE, + ); + } + else { + $form['autocomplete_match'] = array('#type' => 'hidden', '#value' => $match); + $form['size'] = array('#type' => 'hidden', '#value' => $size); + } + return $form; + + case 'save': + return array('autocomplete_match', 'size'); + } +} + +/** + * Implementation of hook_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * Content module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the content module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * the field array + * @param $items + * array of default values for this field + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function nodereference_widget(&$form, &$form_state, $field, $items, $delta = 0) { + switch ($field['widget']['type']) { + case 'nodereference_select': + $element = array( + '#type' => 'nodereference_select', + '#default_value' => $items, + ); + break; + + case 'nodereference_buttons': + $element = array( + '#type' => 'nodereference_buttons', + '#default_value' => $items, + ); + break; + + case 'nodereference_autocomplete': + $element = array( + '#type' => 'nodereference_autocomplete', + '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL, + '#value_callback' => 'nodereference_autocomplete_value', + ); + break; + } + return $element; +} + +/** + * Value for a nodereference autocomplete element. + * + * Substitute in the node title for the node nid. + */ +function nodereference_autocomplete_value($element, $edit = FALSE) { + $field_key = $element['#columns'][0]; + if (!empty($element['#default_value'][$field_key])) { + $nid = $element['#default_value'][$field_key]; + $value = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $nid)); + $value .= ' [nid:'. $nid .']'; + return array($field_key => $value); + } + return array($field_key => NULL); +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function nodereference_select_process($element, $edit, $form_state, $form) { + // The nodereference_select widget doesn't need to create its own + // element, it can wrap around the optionwidgets_select element. + // This will create a new, nested instance of the field. + // Add a validation step where the value can be unwrapped. + $field_key = $element['#columns'][0]; + $element[$field_key] = array( + '#type' => 'optionwidgets_select', + '#default_value' => isset($element['#value']) ? $element['#value'] : '', + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#required' => $element['#required'], + '#description' => $element['#description'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + if (empty($element[$field_key]['#element_validate'])) { + $element[$field_key]['#element_validate'] = array(); + } + array_unshift($element[$field_key]['#element_validate'], 'nodereference_optionwidgets_validate'); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function nodereference_buttons_process($element, $edit, $form_state, $form) { + // The nodereference_select widget doesn't need to create its own + // element, it can wrap around the optionwidgets_select element. + // This will create a new, nested instance of the field. + // Add a validation step where the value can be unwrapped. + $field_key = $element['#columns'][0]; + $element[$field_key] = array( + '#type' => 'optionwidgets_buttons', + '#default_value' => isset($element['#value']) ? $element['#value'] : '', + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#required' => $element['#required'], + '#description' => $element['#description'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + if (empty($element[$field_key]['#element_validate'])) { + $element[$field_key]['#element_validate'] = array(); + } + array_unshift($element[$field_key]['#element_validate'], 'nodereference_optionwidgets_validate'); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + */ +function nodereference_autocomplete_process($element, $edit, $form_state, $form) { + + // The nodereference autocomplete widget doesn't need to create its own + // element, it can wrap around the text_textfield element and add an autocomplete + // path and some extra processing to it. + // Add a validation step where the value can be unwrapped. + $field_key = $element['#columns'][0]; + + $element[$field_key] = array( + '#type' => 'text_textfield', + '#default_value' => isset($element['#value']) ? $element['#value'] : '', + '#autocomplete_path' => 'nodereference/autocomplete/'. $element['#field_name'], + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#required' => $element['#required'], + '#description' => $element['#description'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + if (empty($element[$field_key]['#element_validate'])) { + $element[$field_key]['#element_validate'] = array(); + } + array_unshift($element[$field_key]['#element_validate'], 'nodereference_autocomplete_validate'); + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + // Wrapping the element around a text_textfield element creates a + // nested element, so the final id will look like 'field-name-0-nid-nid'. + '#value' => implode('][', array_merge($element['#parents'], array($field_key, $field_key))), + ); + return $element; +} + +/** + * Validate a select/buttons element. + * + * Remove the wrapper layer and set the right element's value. + * We don't know exactly where this element is, so we drill down + * through the element until we get to our key. + * + * We use $form_state['values'] instead of $element['#value'] + * to be sure we have the most accurate value when other modules + * like optionwidgets are using #element_validate to alter the value. + */ +function nodereference_optionwidgets_validate($element, &$form_state) { + $field_key = $element['#columns'][0]; + + $value = $form_state['values']; + $new_parents = array(); + foreach ($element['#parents'] as $parent) { + $value = $value[$parent]; + // Use === to be sure we get right results if parent is a zero (delta) value. + if ($parent === $field_key) { + $element['#parents'] = $new_parents; + form_set_value($element, $value, $form_state); + break; + } + $new_parents[] = $parent; + } +} + +/** + * Validate an autocomplete element. + * + * Remove the wrapper layer and set the right element's value. + * This will move the nested value at 'field-name-0-nid-nid' + * back to its original location, 'field-name-0-nid'. + */ +function nodereference_autocomplete_validate($element, &$form_state) { + $field_name = $element['#field_name']; + $type_name = $element['#type_name']; + $field = content_fields($field_name, $type_name); + $field_key = $element['#columns'][0]; + $delta = $element['#delta']; + $value = $element['#value'][$field_key]; + $nid = NULL; + if (!empty($value)) { + preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $value, $matches); + if (!empty($matches)) { + // Explicit [nid:n]. + list(, $title, $nid) = $matches; + if (!empty($title) && ($n = node_load($nid)) && trim($title) != trim($n->title)) { + form_error($element[$field_key], t('%name: title mismatch. Please check your selection.', array('%name' => t($field['widget']['label'])))); + } + } + else { + // No explicit nid. + $reference = _nodereference_potential_references($field, $value, 'equals', NULL, 1); + if (empty($reference)) { + form_error($element[$field_key], t('%name: found no valid post with that title.', array('%name' => t($field['widget']['label'])))); + } + else { + // TODO: + // the best thing would be to present the user with an additional form, + // allowing the user to choose between valid candidates with the same title + // ATM, we pick the first matching candidate... + $nid = key($reference); + } + } + } + form_set_value($element, $nid, $form_state); +} + +/** + * Implementation of hook_allowed_values(). + */ +function nodereference_allowed_values($field) { + $references = _nodereference_potential_references($field); + + $options = array(); + foreach ($references as $key => $value) { + $options[$key] = $value['rendered']; + } + + return $options; +} + +/** + * Fetch an array of all candidate referenced nodes. + * + * This info is used in various places (allowed values, autocomplete results, + * input validation...). Some of them only need the nids, others nid + titles, + * others yet nid + titles + rendered row (for display in widgets). + * The array we return contains all the potentially needed information, and lets + * consumers use the parts they actually need. + * + * @param $field + * The field description. + * @param $string + * Optional string to filter titles on (used by autocomplete). + * @param $match + * Operator to match filtered name against, can be any of: + * 'contains', 'equals', 'starts_with' + * @param $ids + * Optional node ids to lookup (the $string and $match arguments will be + * ignored). + * @param $limit + * If non-zero, limit the size of the result set. + * + * @return + * An array of valid nodes in the form: + * array( + * nid => array( + * 'title' => The node title, + * 'rendered' => The text to display in widgets (can be HTML) + * ), + * ... + * ) + */ +function _nodereference_potential_references($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { + static $results = array(); + + // Create unique id for static cache. + $cid = $field['field_name'] .':'. $match .':'. ($string !== '' ? $string : implode('-', $ids)) .':'. $limit; + if (!isset($results[$cid])) { + $references = FALSE; + if (module_exists('views') && !empty($field['advanced_view']) && $field['advanced_view'] != '--') { + $references = _nodereference_potential_references_views($field, $string, $match, $ids, $limit); + } + // If the view doesn't exist, we got FALSE, and fallback to the regular 'standard mode'. + + if ($references === FALSE) { + $references = _nodereference_potential_references_standard($field, $string, $match, $ids, $limit); + } + + // Store the results. + $results[$cid] = !empty($references) ? $references : array(); + } + + return $results[$cid]; +} + +/** + * Helper function for _nodereference_potential_references(): + * case of Views-defined referenceable nodes. + */ +function _nodereference_potential_references_views($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { + $view_name = $field['advanced_view']; + + if ($view = views_get_view($view_name)) { + // We add a display, and let it derive from the 'default' display. + // TODO: We should let the user pick a display in the fields settings - sort of requires AHAH... + $display = $view->add_display('content_references'); + $view->set_display($display); + + // TODO from merlinofchaos on IRC : arguments using summary view can defeat the style setting. + // We might also need to check if there's an argument, and set *its* style_plugin as well. + $view->display_handler->set_option('style_plugin', 'content_php_array_autocomplete'); + $view->display_handler->set_option('row_plugin', 'fields'); + // Used in content_plugin_style_php_array::render(), to get + // the 'field' to be used as title. + $view->display_handler->set_option('content_title_field', 'title'); + + // Additional options to let content_plugin_display_references::query() + // narrow the results. + $options = array( + 'table' => 'node', + 'field_string' => 'title', + 'string' => $string, + 'match' => $match, + 'field_id' => 'nid', + 'ids' => $ids, + ); + $view->display_handler->set_option('content_options', $options); + + // TODO : for consistency, a fair amount of what's below + // should be moved to content_plugin_display_references + + // Limit result set size. + // - Views 3.x uses set_items_per_page(), + // - Views 2.x uses set_option('items_per_page'). + $limit = isset($limit) ? $limit : 0; + if (method_exists($view, 'set_items_per_page')) { + $view->set_items_per_page($limit); + } + else { + $view->display_handler->set_option('items_per_page', $limit); + } + + // Get arguments for the view. + if (!empty($field['advanced_view_args'])) { + // TODO: Support Tokens using token.module ? + $view_args = array_map('trim', explode(',', $field['advanced_view_args'])); + } + else { + $view_args = array(); + } + + // We do need title field, so add it if not present (unlikely, but...) + $fields = $view->get_items('field', $display); + if (!isset($fields['title'])) { + $view->add_item($display, 'field', 'node', 'title'); + } + + // If not set, make all fields inline and define a separator. + $options = $view->display_handler->get_option('row_options'); + if (empty($options['inline'])) { + $options['inline'] = drupal_map_assoc(array_keys($view->get_items('field', $display))); + } + if (empty($options['separator'])) { + $options['separator'] = '-'; + } + $view->display_handler->set_option('row_options', $options); + + // Make sure the query is not cached + $view->is_cacheable = FALSE; + + // Get the results. + $result = $view->execute_display($display, $view_args); + } + else { + $result = FALSE; + } + + return $result; +} + +/** + * Helper function for _nodereference_potential_references(): + * referenceable nodes defined by content types. + */ +function _nodereference_potential_references_standard($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { + $related_types = array(); + $where = array(); + $args = array(); + + if (is_array($field['referenceable_types'])) { + foreach (array_filter($field['referenceable_types']) as $related_type) { + $related_types[] = "n.type = '%s'"; + $args[] = $related_type; + } + } + + $where[] = implode(' OR ', $related_types); + + if (!count($related_types)) { + return array(); + } + + if ($string !== '') { + $like = $GLOBALS["db_type"] == 'pgsql' ? "ILIKE" : "LIKE"; + $match_clauses = array( + 'contains' => "$like '%%%s%%'", + 'equals' => "= '%s'", + 'starts_with' => "$like '%s%%'", + ); + $where[] = 'n.title '. (isset($match_clauses[$match]) ? $match_clauses[$match] : $match_clauses['contains']); + $args[] = $string; + } + elseif ($ids) { + $where[] = 'n.nid IN (' . db_placeholders($ids) . ')'; + $args = array_merge($args, $ids); + } + + $where_clause = $where ? 'WHERE ('. implode(') AND (', $where) .')' : ''; + $sql = db_rewrite_sql("SELECT n.nid, n.title AS node_title, n.type AS node_type FROM {node} n $where_clause ORDER BY n.title, n.type"); + $result = $limit ? db_query_range($sql, $args, 0, $limit) : db_query($sql, $args); + $references = array(); + while ($node = db_fetch_object($result)) { + $references[$node->nid] = array( + 'title' => $node->node_title, + 'rendered' => check_plain($node->node_title), + ); + } + + return $references; +} + +/** + * Check access to the menu callback of the autocomplete widget. + * + * Check for both 'edit' and 'view' access in the unlikely event + * a user has edit but not view access. + */ +function nodereference_autocomplete_access($field_name) { + return user_access('access content') && ($field = content_fields($field_name)) && isset($field['field_name']) && content_access('view', $field) && content_access('edit', $field); +} + +/** + * Menu callback; Retrieve a pipe delimited string of autocomplete suggestions for existing users + */ +function nodereference_autocomplete($field_name, $string = '') { + $fields = content_fields(); + $field = $fields[$field_name]; + $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains'; + $matches = array(); + + $references = _nodereference_potential_references($field, $string, $match, array(), 10); + foreach ($references as $id => $row) { + // Add a class wrapper for a few required CSS overrides. + $matches[$row['title'] ." [nid:$id]"] = '
'. $row['rendered'] . '
'; + } + drupal_json($matches); +} + +/** + * Implementation of hook_node_types. + */ +function nodereference_node_type($op, $info) { + switch ($op) { + case 'update': + // Reflect type name changes to the 'referenceable types' settings. + if (!empty($info->old_type) && $info->old_type != $info->type) { + // content.module's implementaion of hook_node_type() has already + // refreshed _content_type_info(). + $fields = content_fields(); + $rebuild = FALSE; + foreach ($fields as $field_name => $field) { + if ($field['type'] == 'nodereference' && isset($field['referenceable_types'][$info->old_type])) { + $field['referenceable_types'][$info->type] = empty($field['referenceable_types'][$info->old_type]) ? 0 : $info->type; + unset($field['referenceable_types'][$info->old_type]); + content_field_instance_update($field, FALSE); + $rebuild = TRUE; + } + } + + // Clear caches and rebuild menu only if any field has been updated. + if ($rebuild) { + content_clear_type_cache(TRUE); + menu_rebuild(); + } + } + break; + } +} + +/** + * Theme preprocess function. + * + * Allows specific node templates for nodes displayed as values of a + * nodereference field with the 'full node' / 'teaser' formatters. + */ +function nodereference_preprocess_node(&$vars) { + // The 'referencing_field' attribute of the node is added by the 'teaser' + // and 'full node' formatters. + if (!empty($vars['node']->referencing_field)) { + $node = $vars['node']; + $field = $node->referencing_field; + $vars['template_files'][] = 'node-nodereference'; + $vars['template_files'][] = 'node-nodereference-'. $field['field_name']; + $vars['template_files'][] = 'node-nodereference-'. $node->type; + $vars['template_files'][] = 'node-nodereference-'. $field['field_name'] .'-'. $node->type; + } +} + +/** + * FAPI theme for an individual elements. + * + * The textfield or select is already rendered by the + * textfield or select themes and the html output + * lives in $element['#children']. Override this theme to + * make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_nodereference_select($element) { + return $element['#children']; +} + +function theme_nodereference_buttons($element) { + return $element['#children']; +} + +function theme_nodereference_autocomplete($element) { + return $element['#children']; +} diff --git a/sites/all/modules/cck/modules/nodereference/nodereference.rules.inc b/sites/all/modules/cck/modules/nodereference/nodereference.rules.inc new file mode 100644 index 0000000..c4511c1 --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/nodereference.rules.inc @@ -0,0 +1,59 @@ + t('Load a referenced node'), + 'arguments' => array( + 'node' => array( + 'type' => 'node', + 'label' => t('Content containing the node reference field'), + ), + ), + 'new variables' => array( + 'referenced_node' => array( + 'type' => 'node', + 'label' => t('Referenced content'), + ), + ), + 'module' => 'CCK', + 'help' => t('Note that if the field has multiple values, only the first content node will be loaded.'), + ); + return $info; +} + +function nodereference_rules_action_load($node, $settings) { + if ($nid = $node->{$settings['field']}[0]['nid']) { + return array('referenced_node' => node_load(array('nid' => $nid))); + } +} + +function nodereference_rules_action_load_form($settings, &$form) { + $settings += array('field' => ''); + $options = content_rules_get_field_names_by_type('nodereference'); + $form['settings']['field'] = array( + '#type' => 'select', + '#title' => t('Field'), + '#default_value' => $settings['field'], + '#options' => $options, + '#required' => TRUE, + '#disabled' => empty($options), + '#description' => empty($options) ? t('There are no nodereference fields defined.') : '', + ); +} + +/** + * Helps upgrading from the workflow-ng action + * "workflow_ng_action_load_referenced_node" to the equivalent rules action. + */ +function workflow_ng_action_load_referenced_node_upgrade(&$element) { + $element['#name'] = 'nodereference_rules_action_load'; +} diff --git a/sites/all/modules/cck/modules/nodereference/nodereference.token.inc b/sites/all/modules/cck/modules/nodereference/nodereference.token.inc new file mode 100644 index 0000000..e80c66b --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/nodereference.token.inc @@ -0,0 +1,43 @@ + TRUE)) : ''; + + return $tokens; + } +} diff --git a/sites/all/modules/cck/modules/nodereference/panels/relationships/node_from_noderef.inc b/sites/all/modules/cck/modules/nodereference/panels/relationships/node_from_noderef.inc new file mode 100644 index 0000000..f9b555d --- /dev/null +++ b/sites/all/modules/cck/modules/nodereference/panels/relationships/node_from_noderef.inc @@ -0,0 +1,75 @@ + t('Node from reference'), + 'keyword' => 'nodereference', + 'description' => t('Adds a node from a node reference in a node context; if multiple nodes are referenced, this will get the first referenced node only.'), + 'required context' => new ctools_context_required(t('Node'), 'node'), + 'context' => 'nodereference_node_from_noderef_context', + 'settings form' => 'nodereference_node_from_noderef_settings_form', + 'settings form validate' => 'nodereference_node_from_noderef_settings_form_validate', + ); +} + +/** + * Return a new ctools context based on an existing context. + */ +function nodereference_node_from_noderef_context($context, $conf) { + $field = content_fields($conf['field_name']); + + // If unset it wants a generic, unfilled context, which is just NULL. + if (empty($context->data)) { + $new_context = ctools_context_create_empty('node', NULL); + } + else if (isset($context->data->{$conf['field_name']}[0]['nid']) && ($nid = $context->data->{$conf['field_name']}[0]['nid'])) { + if ($node = node_load($nid)) { + $new_context = ctools_context_create('node', $node); + } + } + + if (!empty($new_context)) { + // Have nodereference relationships limit CCK field availability as well. + $restrictions = array_keys(array_filter($field['referenceable_types'])); + if ($restrictions) { + if (isset($new_context->restrictions['type'])) { + $new_context->restrictions['type'] = array_unique(array_merge($new_context->restrictions['type'], $restrictions)); + } + else { + $new_context->restrictions['type'] = $restrictions; + } + } + + return $new_context; + } +} + +/** + * Settings form for the ctools relationship. + */ +function nodereference_node_from_noderef_settings_form($conf) { + $options = array(); + foreach (content_fields() as $field) { + if ($field['type'] == 'nodereference') { + $options[$field['field_name']] = t($field['widget']['label']); + } + } + $form['field_name'] = array( + '#title' => t('Node reference field'), + '#type' => 'select', + '#options' => $options, + '#default_value' => isset($conf['field_name']) ? $conf['field_name'] : '', + '#prefix' => '
', + '#suffix' => '
', + ); + + return $form; +} diff --git a/sites/all/modules/cck/modules/number/help/number.help.ini b/sites/all/modules/cck/modules/number/help/number.help.ini new file mode 100644 index 0000000..9003c5c --- /dev/null +++ b/sites/all/modules/cck/modules/number/help/number.help.ini @@ -0,0 +1,7 @@ + +[advanced help settings] +hide = TRUE + +[number] +title = Number field +parent = content%fields diff --git a/sites/all/modules/cck/modules/number/help/number.html b/sites/all/modules/cck/modules/number/help/number.html new file mode 100644 index 0000000..8725d75 --- /dev/null +++ b/sites/all/modules/cck/modules/number/help/number.html @@ -0,0 +1,2 @@ +

The Number field stores numeric data in the database. It can either be an integer value, a decimal value, or a float value.

+

The Number field provides a place for the administrator to create a list of 'Allowed values' for the field. When used with Optionwidgets, the allowed values are presented to the end user in a drop-down select list, checkboxes, or radios.

\ No newline at end of file diff --git a/sites/all/modules/cck/modules/number/number.info b/sites/all/modules/cck/modules/number/number.info new file mode 100644 index 0000000..67b1d1d --- /dev/null +++ b/sites/all/modules/cck/modules/number/number.info @@ -0,0 +1,11 @@ +name = Number +description = Defines numeric field types. +dependencies[] = content +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/number/number.install b/sites/all/modules/cck/modules/number/number.install new file mode 100644 index 0000000..0fc566d --- /dev/null +++ b/sites/all/modules/cck/modules/number/number.install @@ -0,0 +1,59 @@ + array('arguments' => array('element' => NULL)), + 'number_formatter_default' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_us_0' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_us_1' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_us_2' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_be_0' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_be_1' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_be_2' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_fr_0' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_fr_1' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_fr_2' => array('arguments' => array('element' => NULL), 'function' => 'theme_number_formatter_generic'), + 'number_formatter_unformatted' => array('arguments' => array('element' => NULL)), + ); +} + +/** + * Implementation of hook_field_info(). + */ +function number_field_info() { + return array( + 'number_integer' => array( + 'label' => t('Integer'), + 'description' => t('Store a number in the database as an integer.'), +// 'content_icon' => 'icon_content_number.png', + ), + 'number_decimal' => array( + 'label' => t('Decimal'), + 'description' => t('Store a number in the database in a fixed decimal format.'), +// 'content_icon' => 'icon_content_number.png', + ), + 'number_float' => array( + 'label' => t('Float'), + 'description' => t('Store a number in the database in a floating point format.'), +// 'content_icon' => 'icon_content_number.png', + ), + ); +} + +/** + * Implementation of hook_field_settings(). + */ +function number_field_settings($op, $field) { + switch ($op) { + case 'form': + $form = array(); + $form['min'] = array( + '#type' => 'textfield', + '#title' => t('Minimum'), + '#element_validate' => array('_element_validate_number'), + '#default_value' => is_numeric($field['min']) ? $field['min'] : '', + ); + $form['max'] = array( + '#type' => 'textfield', + '#title' => t('Maximum'), + '#element_validate' => array('_element_validate_number'), + '#default_value' => is_numeric($field['max']) ? $field['max'] : '', + ); + if ($field['type'] == 'number_decimal') { + $form['precision'] = array( + '#type' => 'select', + '#options' => drupal_map_assoc(range(10, 32)), + '#title' => t('Precision'), + '#description' => t('The total number of digits to store in the database, including those to the right of the decimal.'), + '#default_value' => is_numeric($field['precision']) ? $field['precision'] : 10, + ); + $form['scale'] = array( + '#type' => 'select', + '#options' => drupal_map_assoc(range(0, 10)), + '#title' => t('Scale'), + '#description' => t('The number of digits to the right of the decimal.'), + '#default_value' => is_numeric($field['scale']) ? $field['scale'] : 2, + ); + $form['decimal'] = array( + '#type' => 'select', + '#options' => array('.' => 'decimal point', ',' => 'comma', ' ' => 'space'), + '#title' => t('Decimal marker'), + '#description' => t('The character users will input to mark the decimal point in forms.'), + '#default_value' => !empty($field['decimal']) ? $field['decimal'] : '.', + ); + } + $form['append']['prefix'] = array( + '#type' => 'textfield', + '#title' => t('Prefix'), + '#size' => 60, + '#default_value' => !empty($field['prefix']) ? $field['prefix'] : '', + '#description' => t('Define a string that should be prefixed to the value, like $ or €. Leave blank for none. Separate singular and plural values with a pipe (pound|pounds).'), + ); + $form['append']['suffix'] = array( + '#type' => 'textfield', + '#title' => t('Suffix'), + '#size' => 60, + '#default_value' => !empty($field['suffix']) ? $field['suffix'] : '', + '#description' => t('Define a string that should suffixed to the value, like m², m/s², kb/s. Leave blank for none. Separate singular and plural values with a pipe (pound|pounds).'), + ); + $form['allowed_values_fieldset'] = array( + '#type' => 'fieldset', + '#title' => t('Allowed values'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['allowed_values_fieldset']['allowed_values'] = array( + '#type' => 'textarea', + '#title' => t('Allowed values list'), + '#default_value' => !empty($field['allowed_values']) ? $field['allowed_values'] : '', + '#required' => FALSE, + '#rows' => 10, + '#description' => t('The possible values this field can contain. Enter one value per line, in the format key|label. The key is the value that will be stored in the database, and it must match the field storage type (%type). The label is optional, and the key will be used as the label if no label is specified.
Allowed HTML tags: @tags', array('%type' => $field['type'], '@tags' => _content_filter_xss_display_allowed_tags())), + ); + $form['allowed_values_fieldset']['advanced_options'] = array( + '#type' => 'fieldset', + '#title' => t('PHP code'), + '#collapsible' => TRUE, + '#collapsed' => empty($field['allowed_values_php']), + ); + if (user_access('Use PHP input for field settings (dangerous - grant with care)')) { + $form['allowed_values_fieldset']['advanced_options']['allowed_values_php'] = array( + '#type' => 'textarea', + '#title' => t('Code'), + '#default_value' => !empty($field['allowed_values_php']) ? $field['allowed_values_php'] : '', + '#rows' => 6, + '#description' => t('Advanced usage only: PHP code that returns a keyed array of allowed values. Should not include <?php ?> delimiters. If this field is filled out, the array returned by this code will override the allowed values list above.'), + ); + } + else { + $form['allowed_values_fieldset']['advanced_options']['markup_allowed_values_php'] = array( + '#type' => 'item', + '#title' => t('Code'), + '#value' => !empty($field['allowed_values_php']) ? ''. check_plain($field['allowed_values_php']) .'' : t('<none>'), + '#description' => empty($field['allowed_values_php']) ? t("You're not allowed to input PHP code.") : t('This PHP code was set by an administrator and will override the allowed values list above.'), + ); + } + return $form; + + case 'save': + $values = array('prefix', 'suffix', 'min', 'max', 'allowed_values', 'allowed_values_php'); + if ($field['type'] == 'number_decimal') { + $values = array_merge($values, array('precision', 'scale', 'decimal')); + } + return $values; + + case 'database columns': + if ($field['type'] == 'number_integer') { + return array( + 'value' => array('type' => 'int', 'not null' => FALSE, 'sortable' => TRUE), + ); + } + if ($field['type'] == 'number_float') { + return array( + 'value' => array('type' => 'float', 'not null' => FALSE, 'sortable' => TRUE), + ); + } + if ($field['type'] == 'number_decimal') { + $precision = isset($field['precision']) ? $field['precision'] : 10; + $scale = isset($field['scale']) ? $field['scale'] : 2; + return array( + 'value' => array('type' => 'numeric', 'precision' => $precision, 'scale' => $scale, 'not null' => FALSE, 'sortable' => TRUE), + ); + } + + case 'views data': + $allowed_values = content_allowed_values($field); + if (count($allowed_values)) { + $data = content_views_field_views_data($field); + $db_info = content_database_info($field); + $table_alias = content_views_tablename($field); + + // Filter: Add a 'many to one' filter. + $copy = $data[$table_alias][$field['field_name'] .'_value']; + $copy['title'] = t('@label (!name) - Allowed values', array('@label' => t($field['widget']['label']), '!name' => $field['field_name'])); + $copy['filter']['handler'] = 'content_handler_filter_many_to_one'; + $copy['filter']['numeric'] = TRUE; + unset($copy['field'], $copy['argument'], $copy['sort']); + $data[$table_alias][$field['field_name'] .'_value_many_to_one'] = $copy; + // Argument: swap the handler to the 'many to one' operator + $data[$table_alias][$field['field_name'] .'_value']['argument']['handler'] = 'content_handler_argument_many_to_one'; + $data[$table_alias][$field['field_name'] .'_value']['argument']['numeric'] = TRUE; + return $data; + } + break; + } +} + +function _number_widget_settings_min_validate($element, &$form_state) { + $value = $form_state['values']['min']; + if ($value && !is_numeric($value)) { + form_set_error('min', t('"Minimum" must be a number.')); + } +} + +function _number_widget_settings_max_validate($element, &$form_state) { + $value = $form_state['values']['max']; + if ($value && !is_numeric($value)) { + form_set_error('max', t('"Maximum" must be a number.')); + } +} + +/** + * Implementation of hook_field(). + */ +function number_field($op, &$node, $field, &$items, $teaser, $page) { + switch ($op) { + case 'validate': + $allowed_values = content_allowed_values($field); + if (is_array($items)) { + foreach ($items as $delta => $item) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if ($item['value'] != '') { + if (is_numeric($field['min']) && $item['value'] < $field['min']) { + form_set_error($error_element, t('%name: the value may be no smaller than %min.', array('%name' => t($field['widget']['label']), '%min' => $field['min']))); + } + if (is_numeric($field['max']) && $item['value'] > $field['max']) { + form_set_error($error_element, t('%name: the value may be no larger than %max.', array('%name' => t($field['widget']['label']), '%max' => $field['max']))); + } + if (count($allowed_values)) { + // We cannot use array_key_exists() because allowed values are + // stored as strings, and we need to compare numeric equality. + $valid = FALSE; + foreach ($allowed_values as $kay => $value) { + if ((float) $item['value'] == (float) $kay) { + $valid = TRUE; + break; + } + } + if (!$valid) { + form_set_error($error_element, t('%name: illegal value.', array('%name' => t($field['widget']['label'])))); + } + } + } + } + } + return $items; + } +} + +/** + * Implementation of hook_content_is_empty(). + */ +function number_content_is_empty($item, $field) { + if (empty($item['value']) && (string)$item['value'] !== '0') { + return TRUE; + } + return FALSE; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function number_field_formatter_info() { + return array( + 'default' => array('label' => '9999', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_integer', 'number_decimal', 'number_float')), + 'us_0' => array('label' => '9,999', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_integer', 'number_decimal', 'number_float')), + 'us_1' => array('label' => '9,999.9', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_decimal', 'number_float')), + 'us_2' => array('label' => '9,999.99', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_decimal', 'number_float')), + 'be_0' => array('label' => '9.999', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_integer', 'number_decimal', 'number_float')), + 'be_1' => array('label' => '9.999,9', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_decimal', 'number_float')), + 'be_2' => array('label' => '9.999,99', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_decimal', 'number_float')), + 'fr_0' => array('label' => '9 999', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_integer', 'number_decimal', 'number_float')), + 'fr_1' => array('label' => '9 999, 9', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_decimal', 'number_float')), + 'fr_2' => array('label' => '9 999, 99', 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_decimal', 'number_float')), + 'unformatted' => array('label' => t('unformatted'), 'multiple values' => CONTENT_HANDLE_CORE, 'field types' => array('number_integer', 'number_decimal', 'number_float')), + ); +} + +/** + * Proxy theme function for 'unformatted' number field formatter. + */ +function theme_number_formatter_unformatted($element) { + return $element['#item']['value']; +} + +/** + * Proxy theme function for number field formatters. + */ +function theme_number_formatter_generic($element) { + $field = content_fields($element['#field_name'], $element['#type_name']); + $value = $element['#item']['value']; + + if (($allowed_values = content_allowed_values($field))) { + if (isset($allowed_values[$value]) && $allowed_values[$value] != $value) { + return $allowed_values[$value]; + } + } + + if (empty($value) && $value !== '0') { + return ''; + } + + switch ($element['#formatter']) { + case 'us_0': + $output = number_format($value, 0, '.', ','); + break; + case 'us_1': + $output = number_format($value, 1, '.', ','); + break; + case 'us_2': + $output = number_format($value, 2, '.', ','); + break; + case 'be_0': + $output = number_format($value, 0, ',', '.'); + break; + case 'be_1': + $output = number_format($value, 1, ',', '.'); + break; + case 'be_2': + $output = number_format($value, 2, ',', '.'); + break; + case 'fr_0': + $output = number_format($value, 0, ', ', ' '); + break; + case 'fr_1': + $output = number_format($value, 1, ', ', ' '); + break; + case 'fr_2': + $output = number_format($value, 2, ', ', ' '); + break; + default: + $output = $value; + break; + } + + $prefixes = isset($field['prefix']) ? array_map('content_filter_xss', explode('|', $field['prefix'])) : array(''); + $suffixes = isset($field['suffix']) ? array_map('content_filter_xss', explode('|', $field['suffix'])) : array(''); + $prefix = (count($prefixes) > 1) ? format_plural($value, $prefixes[0], $prefixes[1]) : $prefixes[0]; + $suffix = (count($suffixes) > 1) ? format_plural($value, $suffixes[0], $suffixes[1]) : $suffixes[0]; + + return $prefix . $output . $suffix; +} + +/** + * Implementation of hook_widget_info(). + * + * Here we indicate that the content module will handle + * the default value and multiple values for these widgets. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function number_widget_info() { + return array( + 'number' => array( + 'label' => t('Text field'), + 'field types' => array('number_integer', 'number_decimal', 'number_float'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + * + * Includes a regex to check for valid values as an additional parameter + * the validator can use. The regex can be overridden if necessary. + */ +function number_elements() { + return array( + 'number' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('number_process'), + ), + ); +} + +/** + * Implementation of hook_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * Content module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the content module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * the field array + * @param $items + * array of default values for this field + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function number_widget(&$form, &$form_state, $field, $items, $delta = 0) { + $element = array( + '#type' => $field['widget']['type'], + '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL, + ); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function number_process($element, $edit, $form_state, $form) { + $field_name = $element['#field_name']; + $field = $form['#field_info'][$field_name]; + $field_key = $element['#columns'][0]; + + $value = isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : ''; + $value = isset($field['decimal']) ? str_replace('.', $field['decimal'], $value) : $value; + $element[$field_key] = array( + '#type' => 'textfield', + '#default_value' => $value, + // Need to allow a slightly larger size that the field length to allow + // for some configurations where all characters won't fit in input field. + '#size' => isset($field['precision']) ? $field['precision'] + 2 : 12, + '#maxlength' => isset($field['precision']) ? $field['precision'] : 10, + '#attributes' => array('class' => 'number'), + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + + $prefixes = array(); + $suffixes = array(); + + // Make sure we don't wipe out element validation added elsewhere. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + if (!empty($field['prefix'])) { + $prefixes = explode('|', $field['prefix']); + $element[$field_key]['#field_prefix'] = content_filter_xss(array_pop($prefixes)); + } + if (!empty($field['suffix'])) { + $suffixes = explode('|', $field['suffix']); + $element[$field_key]['#field_suffix'] = content_filter_xss(array_pop($suffixes)); + } + switch ($field['type']) { + case 'number_float': + $element['#element_validate'][] = 'number_float_validate'; + break; + case 'number_integer': + $element['#element_validate'][] = 'number_integer_validate'; + break; + case 'number_decimal': + $element['#element_validate'][] = 'number_decimal_validate'; + $element['#decimal'] = isset($field['decimal']) ? $field['decimal'] : '.'; + $element['#precision'] = isset($field['precision']) ? $field['precision'] : 10; + $element['#scale'] = isset($field['scale']) ? $field['scale'] : 2; + break; + } + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + '#value' => implode('][', array_merge($element['#parents'], array($field_key))), + ); + + return $element; +} + +/** + * FAPI validation of an individual float element. + */ +function number_float_validate($element, &$form_state) { + $field_name = $element['#field_name']; + $type_name = $element['#type_name']; + $field = content_fields($field_name, $type_name); + $field_key = $element['#columns'][0]; + $value = $element['#value'][$field_key]; + + if (($element[$field_key]['#required'] || !empty($value))) { + $start = $value; + $value = preg_replace('@[^-0-9\.]@', '', $value); + if ($start != $value) { + $error_field = implode('][', $element['#parents']) .']['. $field_key; + form_set_error($error_field, t('Only numbers and decimals are allowed in %field.', array('%field' => t($field['widget']['label'])))); + } + else { + form_set_value($element[$field_key], $value, $form_state); + } + } +} + +/** + * FAPI validation of an individual integer element. + */ +function number_integer_validate($element, &$form_state) { + $field_name = $element['#field_name']; + $type_name = $element['#type_name']; + $field = content_fields($field_name, $type_name); + $field_key = $element['#columns'][0]; + $value = $element['#value'][$field_key]; + + if (($element[$field_key]['#required'] || !empty($value))) { + $start = $value; + $value = preg_replace('@[^-0-9]@', '', $value); + if ($start != $value) { + $error_field = implode('][', $element['#parents']) .']['. $field_key; + form_set_error($error_field, t('Only numbers are allowed in %field.', array('%field' => t($field['widget']['label'])))); + } + else { + form_set_value($element[$field_key], $value, $form_state); + } + } +} + +/** + * FAPI validation of an individual decimal element. + */ +function number_decimal_validate($element, &$form_state) { + $field_name = $element['#field_name']; + $type_name = $element['#type_name']; + $field = content_fields($field_name, $type_name); + $field_key = $element['#columns'][0]; + $value = $element['#value'][$field_key]; + + if (($element[$field_key]['#required'] || !empty($value))) { + $decimal = $element['#decimal'] ? $element['#decimal'] : '.'; + $start = $value; + $value = preg_replace('@[^-0-9\\'. $decimal .']@', '', $value); + if ($start != $value) { + $error_field = implode('][', $element['#parents']) .']['. $field_key; + form_set_error($error_field, t('Only numbers and the decimal character (%decimal) are allowed in %field.', array('%decimal' => $element['#decimal'], '%field' => t($field['widget']['label'])))); + } + else { + $value = str_replace($element['#decimal'], '.', $value); + $value = round($value, $element['#scale']); + form_set_value($element[$field_key], $value, $form_state); + } + } +} + +/** + * FAPI theme for an individual number element. + * + * The textfield is already rendered by the textfield + * theme and the HTML output lives in $element['#children']. + * Override this theme to make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_number($element) { + return $element['#children']; +} \ No newline at end of file diff --git a/sites/all/modules/cck/modules/number/number.token.inc b/sites/all/modules/cck/modules/number/number.token.inc new file mode 100644 index 0000000..77c04a0 --- /dev/null +++ b/sites/all/modules/cck/modules/number/number.token.inc @@ -0,0 +1,34 @@ +Optionwidgets creates drop-down select lists, checkboxes, and radio widgets that can be used with fields that have specific allowed values.

+

The Number and Text fields have settings for creating lists of allowed values that can be used in Optionwidgets. The Nodereference and Userreference fields use Optionwidgets to present the possible values in select lists or checkboxes.

\ No newline at end of file diff --git a/sites/all/modules/cck/modules/optionwidgets/optionwidgets.info b/sites/all/modules/cck/modules/optionwidgets/optionwidgets.info new file mode 100644 index 0000000..084e44c --- /dev/null +++ b/sites/all/modules/cck/modules/optionwidgets/optionwidgets.info @@ -0,0 +1,11 @@ +name = Option Widgets +description = Defines selection, check box and radio button widgets for text and numeric fields. +dependencies[] = content +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/optionwidgets/optionwidgets.install b/sites/all/modules/cck/modules/optionwidgets/optionwidgets.install new file mode 100644 index 0000000..bdc967c --- /dev/null +++ b/sites/all/modules/cck/modules/optionwidgets/optionwidgets.install @@ -0,0 +1,75 @@ +'. t('Create a list of options as a list in Allowed values list or as an array in PHP code. These values will be the same for %field in all content types.', array('%field' => $label)) .'

'; + + if ($widget_type == 'optionwidgets_onoff') { + $output .= '

'. t("For a 'single on/off checkbox' widget, define the 'off' value first, then the 'on' value in the Allowed values section. Note that the checkbox will be labeled with the label of the 'on' value.") .'

'; + } + elseif ($widget_type == 'optionwidgets_buttons') { + $output .= '

'. t("The 'checkboxes/radio buttons' widget will display checkboxes if the multiple values option is selected for this field, otherwise radios will be displayed.") .'

'; + } + + if (in_array($field_type, array('text', 'number_integer', 'number_float', 'number_decimal')) + && in_array($widget_type, array('optionwidgets_onoff', 'optionwidgets_buttons', 'optionwidgets_select'))) { + $form['field']['allowed_values_fieldset']['#collapsed'] = FALSE; + $form['field']['allowed_values_fieldset']['#description'] = $output; + + // If no 'allowed values' were set yet, add a remainder in the messages area. + if (empty($form_state['post']) + && empty($form['field']['allowed_values_fieldset']['allowed_values']['#default_value']) + && empty($form['field']['allowed_values_fieldset']['advanced_options']['allowed_values_php']['#default_value'])) { + drupal_set_message(t("You need to specify the 'allowed values' for this field."), 'warning'); + } + } + } +} + +/** + * Implementation of hook_theme(). + */ +function optionwidgets_theme() { + return array( + 'optionwidgets_select' => array( + 'arguments' => array('element' => NULL), + ), + 'optionwidgets_buttons' => array( + 'arguments' => array('element' => NULL), + ), + 'optionwidgets_onoff' => array( + 'arguments' => array('element' => NULL), + ), + 'optionwidgets_none' => array( + 'arguments' => array('widget_type' => NULL, 'field_name' => NULL, 'node_type' => NULL), + ), + ); +} + +/** + * Implementation of hook_widget_info(). + * + * We need custom handling of multiple values because we need + * to combine them into a options list rather than display + * multiple elements. We will use the content module's default + * handling for default values. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function optionwidgets_widget_info() { + + return array( + 'optionwidgets_select' => array( + 'label' => t('Select list'), + 'field types' => array('text', 'number_integer', 'number_decimal', 'number_float'), + 'multiple values' => CONTENT_HANDLE_MODULE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + 'optionwidgets_buttons' => array( + 'label' => t('Check boxes/radio buttons'), + 'field types' => array('text', 'number_integer', 'number_decimal', 'number_float'), + 'multiple values' => CONTENT_HANDLE_MODULE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + 'optionwidgets_onoff' => array( + 'label' => t('Single on/off checkbox'), + 'field types' => array('text', 'number_integer', 'number_decimal', 'number_float'), + 'multiple values' => CONTENT_HANDLE_MODULE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + */ +function optionwidgets_elements() { + return array( + 'optionwidgets_select' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('optionwidgets_select_process'), + ), + 'optionwidgets_buttons' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('optionwidgets_buttons_process'), + ), + 'optionwidgets_onoff' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('optionwidgets_onoff_process'), + ), + ); +} + +/** + * Implementation of hook_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * Content module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the content module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * the field array + * @param $items + * an array of default values for this element + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function optionwidgets_widget(&$form, &$form_state, $field, $items, $delta = NULL) { + $element = array( + '#type' => $field['widget']['type'], + '#default_value' => !empty($items) ? $items : array(), + ); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function optionwidgets_buttons_process($element, $edit, &$form_state, $form) { + $field_name = $element['#field_name']; + $field = $form['#field_info'][$field_name]; + $field_key = $element['#columns'][0]; + + // See if this element is in the database format or the transformed format, + // and transform it if necessary. + if (is_array($element['#value']) && !array_key_exists($field_key, $element['#value'])) { + $element['#value'] = optionwidgets_data2form($element, $element['#default_value'], $field); + } + $options = optionwidgets_options($field); + $element[$field_key] = array( + '#type' => $field['multiple'] ? 'checkboxes' : 'radios', + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => isset($element['#required']) ? $element['#required'] : $field['required'], + '#multiple' => isset($element['#multiple']) ? $element['#multiple'] : $field['multiple'], + '#options' => $options, + '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, + ); + + // Set #element_validate in a way that it will not wipe out other + // validation functions already set by other modules. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + array_unshift($element['#element_validate'], 'optionwidgets_validate'); + + // Make sure field info will be available to the validator which + // does not get the values in $form. + $form_state['#field_info'][$field['field_name']] = $field; + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function optionwidgets_select_process($element, $edit, &$form_state, $form) { + $field_name = $element['#field_name']; + $field = $form['#field_info'][$field_name]; + $field_key = $element['#columns'][0]; + + // See if this element is in the database format or the transformed format, + // and transform it if necessary. + if (is_array($element['#value']) && !array_key_exists($field_key, $element['#value'])) { + $element['#value'] = optionwidgets_data2form($element, $element['#default_value'], $field); + } + + $options = optionwidgets_options($field, FALSE); + + // For this specific widget, HTML should be filtered out and entities left unencoded. + // See content_allowed_values / content_filter_xss / filter_xss. + content_allowed_values_filter_html($options); + + $element[$field_key] = array( + '#type' => 'select', + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => isset($element['#required']) ? $element['#required'] : $field['required'], + '#multiple' => isset($element['#multiple']) ? $element['#multiple'] : $field['multiple'], + '#options' => $options, + '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, + ); + + // Set #element_validate in a way that it will not wipe out other + // validation functions already set by other modules. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + array_unshift($element['#element_validate'], 'optionwidgets_validate'); + + // Make sure field info will be available to the validator which + // does not get the values in $form. + + // TODO for some reason putting the $field array into $form_state['storage'] + // causes the node's hook_form_alter to be invoked twice, garbling the + // results. Need to investigate why that is happening (a core bug?), but + // in the meantime avoid using $form_state['storage'] to store anything. + $form_state['#field_info'][$field['field_name']] = $field; + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function optionwidgets_onoff_process($element, $edit, &$form_state, $form) { + $field_name = $element['#field_name']; + $field = $form['#field_info'][$field_name]; + $field_key = $element['#columns'][0]; + + // See if this element is in the database format or the transformed format, + // and transform it if necessary. + if (is_array($element['#value']) && !array_key_exists($field_key, $element['#value'])) { + $element['#value'] = optionwidgets_data2form($element, $element['#default_value'], $field); + } + $options = optionwidgets_options($field); + $keys = array_keys($options); + $on_value = (!empty($keys) && isset($keys[1])) ? $keys[1] : NULL; + $element[$field_key] = array( + '#type' => 'checkbox', + '#title' => isset($options[$on_value]) ? $options[$on_value] : '', + '#description' => $element['#description'], + '#required' => isset($element['#required']) ? $element['#required'] : $field['required'], + '#default_value' => isset($element['#value'][$field_key][0]) ? $element['#value'][$field_key][0] == $on_value : FALSE, + '#return_value' => $on_value, + ); + + // Set #element_validate in a way that it will not wipe out other + // validation functions already set by other modules. + if (empty($element['#element_validate'])) { + $element['#element_validate'] = array(); + } + array_unshift($element['#element_validate'], 'optionwidgets_validate'); + + // Make sure field info will be available to the validator which + // does not get the values in $form. + $form_state['#field_info'][$field['field_name']] = $field; + return $element; +} + +/** + * FAPI function to validate optionwidgets element. + */ +function optionwidgets_validate($element, &$form_state) { + // Transpose selections from field => delta to delta => field, + // turning multiple selected options into multiple parent elements. + // Immediate parent is the delta, need to get back to parent's parent + // to create multiple elements. + $field = $form_state['#field_info'][$element['#field_name']]; + $items = optionwidgets_form2data($element, $field); + form_set_value($element, $items, $form_state); + + // Check we don't exceed the allowed number of values. + if ($field['multiple'] >= 2) { + // Filter out 'none' value (if present, will always be in key 0) + $field_key = $element['#columns'][0]; + if (isset($items[0][$field_key]) && $items[0][$field_key] === '') { + unset($items[0]); + } + if (count($items) > $field['multiple']) { + $field_key = $element['#columns'][0]; + form_error($element[$field_key], t('%name: this field cannot hold more than @count values.', array('%name' => t($field['widget']['label']), '@count' => $field['multiple']))); + } + } +} + +/** + * Helper function to transpose the values as stored in the database + * to the format the widget needs. Can be called anywhere this + * transformation is needed. + */ +function optionwidgets_data2form($element, $items, $field) { + $field_key = $element['#columns'][0]; + $options = optionwidgets_options($field); + + $items_transposed = content_transpose_array_rows_cols($items); + $values = (isset($items_transposed[$field_key]) && is_array($items_transposed[$field_key])) ? $items_transposed[$field_key] : array(); + $keys = array(); + foreach ($values as $value) { + $key = array_search($value, array_keys($options)); + if (isset($key)) { + $keys[] = $value; + } + } + if ($field['multiple'] || $element['#type'] == 'optionwidgets_onoff') { + return array($field_key => $keys); + } + else { + return !empty($keys) ? array($field_key => $value) : array(); + } +} + +/** + * Helper function to transpose the values returned by submitting the widget + * to the format to be stored in the field. Can be called anywhere this + * transformation is needed. + */ +function optionwidgets_form2data($element, $field) { + $field_key = $element['#columns'][0]; + $items = (array) $element[$field_key]['#value']; + $options = optionwidgets_options($field); + + $values = array_values($items); + + if ($element['#type'] == 'optionwidgets_onoff' && ($values[0] === 0)) { + $keys = array_keys($options); + $values = array(array_key_exists(0, $keys) ? $keys[0] : NULL); + } + + if (empty($values)) { + $values[] = NULL; + } + $result = content_transpose_array_rows_cols(array($field_key => $values)); + return $result; +} + +/** + * Helper function for finding the allowed values list for a field. + * + * See if there is a module hook for the option values. + * Otherwise, try content_allowed_values() for an options list. + * + * @param $field + * The field whose allowed values are requested. + * @param $flatten + * Optional. Use TRUE to return a flattened array (default). + * FALSE can be used to support optgroups for select widgets + * when allowed values list is generated using PHP code. + */ +function optionwidgets_options($field, $flatten = TRUE) { + $function = $field['module'] .'_allowed_values'; + $options = function_exists($function) ? $function($field) : (array) content_allowed_values($field, $flatten); + // Add an empty choice for : + // - non required radios + // - non required selects + if (!$field['required']) { + if ((in_array($field['widget']['type'], array('optionwidgets_buttons', 'nodereference_buttons', 'userreference_buttons')) && !$field['multiple']) + || (in_array($field['widget']['type'], array('optionwidgets_select', 'nodereference_select', 'userreference_select')))) { + $options = array('' => theme('optionwidgets_none', $field)) + $options; + } + } + return $options; +} + +/** + * Theme the label for the empty value for options that are not required. + * The default theme will display N/A for a radio list and blank for a select. + */ +function theme_optionwidgets_none($field) { + switch ($field['widget']['type']) { + case 'optionwidgets_buttons': + case 'nodereference_buttons': + case 'userreference_buttons': + return t('N/A'); + case 'optionwidgets_select': + case 'nodereference_select': + case 'userreference_select': + return t('- None -'); + default : + return ''; + } +} + +/** + * FAPI themes for optionwidgets. + * + * The select, checkboxes or radios are already rendered by the + * select, checkboxes, or radios themes and the HTML output + * lives in $element['#children']. Override this theme to + * make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_optionwidgets_select($element) { + return $element['#children']; +} + +function theme_optionwidgets_onoff($element) { + return $element['#children']; +} + +function theme_optionwidgets_buttons($element) { + return $element['#children']; +} \ No newline at end of file diff --git a/sites/all/modules/cck/modules/text/help/text.help.ini b/sites/all/modules/cck/modules/text/help/text.help.ini new file mode 100644 index 0000000..86e633a --- /dev/null +++ b/sites/all/modules/cck/modules/text/help/text.help.ini @@ -0,0 +1,7 @@ + +[advanced help settings] +hide = TRUE + +[text] +title = Text field +parent = content%fields diff --git a/sites/all/modules/cck/modules/text/help/text.html b/sites/all/modules/cck/modules/text/help/text.html new file mode 100644 index 0000000..e5a1f6a --- /dev/null +++ b/sites/all/modules/cck/modules/text/help/text.html @@ -0,0 +1,2 @@ +

The Text field stores textual data in the database. It can either be a simple textfield to hold up to 255 characters, or a textarea of an unlimited size that can be combined with input filters to store lengthy descriptions and HTML code.

+

The Text field provides a place for the administrator to create a list of 'Allowed values' for the field. When used with Optionwidgets, the allowed values are presented to the end user in a drop-down select list, checkboxes, or radios.

\ No newline at end of file diff --git a/sites/all/modules/cck/modules/text/text.info b/sites/all/modules/cck/modules/text/text.info new file mode 100644 index 0000000..36c26a5 --- /dev/null +++ b/sites/all/modules/cck/modules/text/text.info @@ -0,0 +1,11 @@ +name = Text +description = Defines simple text field types. +dependencies[] = content +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/text/text.install b/sites/all/modules/cck/modules/text/text.install new file mode 100644 index 0000000..3aee9be --- /dev/null +++ b/sites/all/modules/cck/modules/text/text.install @@ -0,0 +1,155 @@ + 1) ? 'text_textarea' : 'text_textfield'; + $ret[] = update_sql("UPDATE {". content_instance_tablename() ."} SET widget_module = 'text', widget_type = '". $new_widget_type ."' WHERE field_name = '{$field_instance['field_name']}' AND type_name = '{$field_instance['type_name']}'"); + } + content_associate_fields('text'); + return $ret; +} + +/** + * Set all columns to accept NULL values and set empty string values in the + * database to NULL. + * + * Leaving it up to module developers to handle conversion of numbers to + * NULL values, since there are times when zeros are valid data and times + * when they should be NULL. + * + */ +function text_update_6001(&$sandbox) { + include_once('./'. drupal_get_path('module', 'content') .'/content.install'); + drupal_load('module', 'content'); + + $ret = array(); + + if (!isset($sandbox['progress'])) { + if ($abort = content_check_update('text')) { + return $abort; + } + + // Get the latest cache values and schema. + content_clear_type_cache(TRUE, TRUE); + $types = content_types_install(); + + if (empty($types)) { + return $ret; + } + + $sandbox['fields'] = array(); + foreach ($types as $type_name => $fields) { + foreach ($fields as $field) { + if ($field['type'] == 'text') { + $sandbox['fields'][] = $field; + } + } + } + + if (empty($sandbox['fields'])) { + return $ret; + } + + $sandbox['progress'] = 0; + $sandbox['visited'] = array(); + } + + $field = $sandbox['fields'][$sandbox['progress']]; + + // We only want to process a field once -- if we hit it a second time, + // that means it's its own table and it should have already been updated. + if (!in_array($field['field_name'], $sandbox['visited'])) { + $db_info = content_database_info($field); + $table = $db_info['table']; + foreach ($db_info['columns'] as $column => $attributes) { + $attributes['not null'] = FALSE; + $column = $attributes['column']; + db_change_field($ret, $table, $column, $column, $attributes); + // TODO: errors on text/blob columns: no default value allowed (!) + db_field_set_no_default($ret, $table, $column); + if ($attributes['type'] == 'varchar' || $attributes['type'] == 'text') { + $ret[] = update_sql("UPDATE {". $table ."} SET ". $column ." = NULL WHERE ". $column ." = ''"); + } + else { + // TODO: replace format = 0 with format = NULL ?? Is this right ? + $ret[] = update_sql("UPDATE {". $table ."} SET ". $column ." = NULL WHERE ". $column ." = 0"); + } + } + $sandbox['visited'][] = $field['field_name']; + } + + $sandbox['progress']++; + $ret['#finished'] = $sandbox['progress'] / count($sandbox['fields']); + + return $ret; +} + +/** + * Update 6000 and 6001 were possibly broken if they were executed while + * the modules were still disabled, so we re-run them. + * Having them run a second time on sites that got updated correctly has no + * side-effect (see http://drupal.org/node/310873). + */ +function text_update_6002() { + return text_update_6000(); +} +function text_update_6003(&$sandbox) { + return text_update_6001($sandbox); +} diff --git a/sites/all/modules/cck/modules/text/text.module b/sites/all/modules/cck/modules/text/text.module new file mode 100644 index 0000000..4d4304f --- /dev/null +++ b/sites/all/modules/cck/modules/text/text.module @@ -0,0 +1,489 @@ + array( + 'arguments' => array('element' => NULL), + ), + 'text_textfield' => array( + 'arguments' => array('element' => NULL), + ), + 'text_formatter_default' => array( + 'arguments' => array('element' => NULL), + ), + 'text_formatter_plain' => array( + 'arguments' => array('element' => NULL), + ), + 'text_formatter_trimmed' => array( + 'arguments' => array('element' => NULL), + ), + 'text_formatter_foo' => array( + 'arguments' => array('element' => NULL), + ), + ); +} + +/** + * Implementation of hook_field_info(). + */ +function text_field_info() { + return array( + 'text' => array( + 'label' => t('Text'), + 'description' => t('Store text in the database.'), +// 'content_icon' => 'icon_content_text.png', + ), + ); +} + +/** + * Implementation of hook_field_settings(). + */ +function text_field_settings($op, $field) { + switch ($op) { + case 'form': + $form = array(); + $options = array(0 => t('Plain text'), 1 => t('Filtered text (user selects input format)')); + $form['text_processing'] = array( + '#type' => 'radios', + '#title' => t('Text processing'), + '#default_value' => is_numeric($field['text_processing']) ? $field['text_processing'] : 0, + '#options' => $options, + ); + $form['max_length'] = array( + '#type' => 'textfield', + '#title' => t('Maximum length'), + '#default_value' => is_numeric($field['max_length']) ? $field['max_length'] : '', + '#required' => FALSE, + '#element_validate' => array('_element_validate_integer_positive'), + '#description' => t('The maximum length of the field in characters. Leave blank for an unlimited size.'), + ); + $form['allowed_values_fieldset'] = array( + '#type' => 'fieldset', + '#title' => t('Allowed values'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['allowed_values_fieldset']['allowed_values'] = array( + '#type' => 'textarea', + '#title' => t('Allowed values list'), + '#default_value' => !empty($field['allowed_values']) ? $field['allowed_values'] : '', + '#required' => FALSE, + '#rows' => 10, + '#description' => t('The possible values this field can contain. Enter one value per line, in the format key|label. The key is the value that will be stored in the database, and it must match the field storage type (%type). The label is optional, and the key will be used as the label if no label is specified.
Allowed HTML tags: @tags', array('%type' => $field['type'], '@tags' => _content_filter_xss_display_allowed_tags())), + ); + $form['allowed_values_fieldset']['advanced_options'] = array( + '#type' => 'fieldset', + '#title' => t('PHP code'), + '#collapsible' => TRUE, + '#collapsed' => empty($field['allowed_values_php']), + ); + if (user_access('Use PHP input for field settings (dangerous - grant with care)')) { + $form['allowed_values_fieldset']['advanced_options']['allowed_values_php'] = array( + '#type' => 'textarea', + '#title' => t('Code'), + '#default_value' => !empty($field['allowed_values_php']) ? $field['allowed_values_php'] : '', + '#rows' => 6, + '#description' => t('Advanced usage only: PHP code that returns a keyed array of allowed values. Should not include <?php ?> delimiters. If this field is filled out, the array returned by this code will override the allowed values list above.'), + ); + } + else { + $form['allowed_values_fieldset']['advanced_options']['markup_allowed_values_php'] = array( + '#type' => 'item', + '#title' => t('Code'), + '#value' => !empty($field['allowed_values_php']) ? ''. check_plain($field['allowed_values_php']) .'' : t('<none>'), + '#description' => empty($field['allowed_values_php']) ? t("You're not allowed to input PHP code.") : t('This PHP code was set by an administrator and will override the allowed values list above.'), + ); + } + return $form; + + case 'save': + return array('text_processing', 'max_length', 'allowed_values', 'allowed_values_php'); + + case 'database columns': + if (empty($field['max_length']) || $field['max_length'] > 255) { + $columns['value'] = array('type' => 'text', 'size' => 'big', 'not null' => FALSE, 'sortable' => TRUE, 'views' => TRUE); + } + else { + $columns['value'] = array('type' => 'varchar', 'length' => $field['max_length'], 'not null' => FALSE, 'sortable' => TRUE, 'views' => TRUE); + } + if (!empty($field['text_processing'])) { + $columns['format'] = array('type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'views' => FALSE); + } + return $columns; + + case 'views data': + $allowed_values = content_allowed_values($field); + if (count($allowed_values)) { + $data = content_views_field_views_data($field); + $db_info = content_database_info($field); + $table_alias = content_views_tablename($field); + + // Filter: Add a 'many to one' filter. + $copy = $data[$table_alias][$field['field_name'] .'_value']; + $copy['title'] = t('@label (!name) - Allowed values', array('@label' => t($field['widget']['label']), '!name' => $field['field_name'])); + $copy['filter']['handler'] = 'content_handler_filter_many_to_one'; + unset($copy['field'], $copy['argument'], $copy['sort']); + $data[$table_alias][$field['field_name'] .'_value_many_to_one'] = $copy; + // Argument : swap the handler to the 'many to one' operator. + $data[$table_alias][$field['field_name'] .'_value']['argument']['handler'] = 'content_handler_argument_many_to_one'; + return $data; + } + } +} + +/** + * Implementation of hook_field(). + */ +function text_field($op, &$node, $field, &$items, $teaser, $page) { + switch ($op) { + case 'validate': + $allowed_values = content_allowed_values($field); + if (is_array($items)) { + foreach ($items as $delta => $item) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if (!empty($item['value'])) { + if (count($allowed_values) && !array_key_exists($item['value'], $allowed_values)) { + form_set_error($error_element, t('%name: illegal value.', array('%name' => t($field['widget']['label'])))); + } + if (!empty($field['max_length']) && drupal_strlen($item['value']) > $field['max_length']) { + form_set_error($error_element, t('%name: the value may not be longer than %max characters.', array('%name' => $field['widget']['label'], '%max' => $field['max_length']))); + } + } + } + } + return $items; + + case 'sanitize': + foreach ($items as $delta => $item) { + if (!empty($field['text_processing'])) { + $text = isset($item['value']) ? check_markup($item['value'], $item['format'], FALSE) : ''; + } + else { + $text = isset($item['value']) ? check_plain($item['value']) : ''; + } + $items[$delta]['safe'] = $text; + } + } +} + +/** + * Implementation of hook_content_is_empty(). + */ +function text_content_is_empty($item, $field) { + if (empty($item['value']) && (string)$item['value'] !== '0') { + return TRUE; + } + return FALSE; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function text_field_formatter_info() { + return array( + 'default' => array( + 'label' => t('Default'), + 'field types' => array('text'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + 'plain' => array( + 'label' => t('Plain text'), + 'field types' => array('text'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + 'trimmed' => array( + 'label' => t('Trimmed'), + 'field types' => array('text'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + ); +} + +/** + * Theme function for 'default' text field formatter. + */ +function theme_text_formatter_default($element) { + return ($allowed =_text_allowed_values($element)) ? $allowed : $element['#item']['safe']; +} + +/** + * Theme function for 'plain' text field formatter. + */ +function theme_text_formatter_plain($element) { + return ($allowed =_text_allowed_values($element)) ? $allowed : strip_tags($element['#item']['safe']); +} + +/** + * Theme function for 'trimmed' text field formatter. + */ +function theme_text_formatter_trimmed($element) { + $field = content_fields($element['#field_name'], $element['#type_name']); + return ($allowed =_text_allowed_values($element)) ? $allowed : node_teaser($element['#item']['safe'], $field['text_processing'] ? $element['#item']['format'] : NULL); +} + +function _text_allowed_values($element) { + $field = content_fields($element['#field_name'], $element['#type_name']); + if (($allowed_values = content_allowed_values($field)) && isset($allowed_values[$element['#item']['value']])) { + return $allowed_values[$element['#item']['value']]; + } +} + +/** + * Implementation of hook_widget_info(). + * + * Here we indicate that the content module will handle + * the default value and multiple values for these widgets. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function text_widget_info() { + return array( + 'text_textfield' => array( + 'label' => t('Text field'), + 'field types' => array('text'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + 'text_textarea' => array( + 'label' => t('Text area (multiple rows)'), + 'field types' => array('text'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + * + * Autocomplete_path is not used by text_widget but other widgets can use it + * (see nodereference and userreference). + */ +function text_elements() { + return array( + 'text_textfield' => array( + '#input' => TRUE, + '#columns' => array('value'), '#delta' => 0, + '#process' => array('text_textfield_process'), + '#autocomplete_path' => FALSE, + ), + 'text_textarea' => array( + '#input' => TRUE, + '#columns' => array('value', 'format'), '#delta' => 0, + '#process' => array('text_textarea_process'), + '#filter_value' => FILTER_FORMAT_DEFAULT, + ), + ); +} + +/** + * Implementation of hook_widget_settings(). + */ +function text_widget_settings($op, $widget) { + switch ($op) { + case 'form': + $form = array(); + $rows = (isset($widget['rows']) && is_numeric($widget['rows'])) ? $widget['rows'] : 5; + $size = (isset($widget['size']) && is_numeric($widget['size'])) ? $widget['size'] : 60; + if ($widget['type'] == 'text_textfield') { + $form['rows'] = array('#type' => 'hidden', '#value' => $rows); + $form['size'] = array( + '#type' => 'textfield', + '#title' => t('Size of textfield'), + '#default_value' => $size, + '#element_validate' => array('_element_validate_integer_positive'), + '#required' => TRUE, + ); + } + else { + $form['rows'] = array( + '#type' => 'textfield', + '#title' => t('Rows'), + '#default_value' => $rows, + '#element_validate' => array('_element_validate_integer_positive'), + '#required' => TRUE, + ); + $form['size'] = array('#type' => 'hidden', '#value' => $size); + } + return $form; + + case 'save': + return array('rows', 'size'); + } +} + +/** + * Implementation of hook_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * Content module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the content module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * the field array + * @param $items + * array of default values for this field + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function text_widget(&$form, &$form_state, $field, $items, $delta = 0) { + $element = array( + '#type' => $field['widget']['type'], + '#default_value' => isset($items[$delta]) ? $items[$delta] : '', + ); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function text_textfield_process($element, $edit, $form_state, $form) { + $field = $form['#field_info'][$element['#field_name']]; + $field_key = $element['#columns'][0]; + $delta = $element['#delta']; + $element[$field_key] = array( + '#type' => 'textfield', + '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, + '#autocomplete_path' => $element['#autocomplete_path'], + '#size' => !empty($field['widget']['size']) ? $field['widget']['size'] : 60, + '#attributes' => array('class' => 'text'), + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + + $element[$field_key]['#maxlength'] = !empty($field['max_length']) ? $field['max_length'] : NULL; + + if (!empty($field['text_processing'])) { + $filter_key = $element['#columns'][1]; + $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; + $parents = array_merge($element['#parents'] , array($filter_key)); + $element[$filter_key] = filter_form($format, 1, $parents); + } + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + '#value' => implode('][', array_merge($element['#parents'], array($field_key))), + ); + + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function text_textarea_process($element, $edit, $form_state, $form) { + $field = $form['#field_info'][$element['#field_name']]; + $field_key = $element['#columns'][0]; + $element[$field_key] = array( + '#type' => 'textarea', + '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, + '#rows' => !empty($field['widget']['rows']) ? $field['widget']['rows'] : 10, + '#weight' => 0, + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#description' => $element['#description'], + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + + if (!empty($field['text_processing'])) { + $filter_key = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format'; + $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; + $parents = array_merge($element['#parents'] , array($filter_key)); + $element[$filter_key] = filter_form($format, 1, $parents); + } + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + '#value' => implode('][', array_merge($element['#parents'], array($field_key))), + ); + + return $element; +} + +/** + * FAPI theme for an individual text elements. + * + * The textfield or textarea is already rendered by the + * textfield or textarea themes and the html output + * lives in $element['#children']. Override this theme to + * make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_text_textfield($element) { + return $element['#children']; +} + +function theme_text_textarea($element) { + return $element['#children']; +} diff --git a/sites/all/modules/cck/modules/text/text.token.inc b/sites/all/modules/cck/modules/text/text.token.inc new file mode 100644 index 0000000..150eac0 --- /dev/null +++ b/sites/all/modules/cck/modules/text/text.token.inc @@ -0,0 +1,33 @@ +The Userreference field stores the uid of a related user. The name of the related user is usually displayed as the value of this field.

+

The Userreference field can use an autocomplete widget, or, when used with Optionwidgets, the available values can be presented to the end user in a drop-down select list, checkboxes, or radios.

+

A Userreference field can be used in Views to create a Relationship to a user, to allow you to use any field, argument, or filter from the related user in your view.

\ No newline at end of file diff --git a/sites/all/modules/cck/modules/userreference/panels/relationships/user_from_userref.inc b/sites/all/modules/cck/modules/userreference/panels/relationships/user_from_userref.inc new file mode 100644 index 0000000..d3d7e9d --- /dev/null +++ b/sites/all/modules/cck/modules/userreference/panels/relationships/user_from_userref.inc @@ -0,0 +1,64 @@ + t('User from reference'), + 'keyword' => 'userreference', + 'description' => t('Adds a user from a user reference in a node context; if multiple users are referenced, this will get the first referenced user only.'), + 'required context' => new ctools_context_required(t('Node'), 'node'), + 'context' => 'userreference_user_from_userref_context', + 'settings form' => 'userreference_user_from_userref_settings_form', + 'settings form validate' => 'userreference_user_from_userref_settings_form_validate', + ); +} + +/** + * Return a new ctools context based on an existing context. + */ +function userreference_user_from_userref_context($context, $conf) { + // If unset it wants a generic, unfilled context, which is just NULL. + if (empty($context->data)) { + return ctools_context_create_empty('user', NULL); + } + + // Prevent whitescreens on stale data. + if (empty($conf)) { + return ctools_context_create_empty('user', NULL); + } + + if (isset($context->data->{$conf['field_name']}[0]['uid']) && ($uid = $context->data->{$conf['field_name']}[0]['uid'])) { + if ($account = user_load(array('uid' => $uid))) { + return ctools_context_create('user', $account); + } + } +} + +/** + * Settings form for the ctools relationship. + */ +function userreference_user_from_userref_settings_form($conf) { + $options = array(); + foreach (content_fields() as $field) { + if ($field['type'] == 'userreference') { + $options[$field['field_name']] = t($field['widget']['label']); + } + } + $form['field_name'] = array( + '#title' => t('User reference field'), + '#type' => 'select', + '#options' => $options, + '#default_value' => isset($conf['field_name']) ? $conf['field_name'] : '', + '#prefix' => '
', + '#suffix' => '
', + ); + + return $form; +} diff --git a/sites/all/modules/cck/modules/userreference/userreference.info b/sites/all/modules/cck/modules/userreference/userreference.info new file mode 100644 index 0000000..c982484 --- /dev/null +++ b/sites/all/modules/cck/modules/userreference/userreference.info @@ -0,0 +1,13 @@ +name = User Reference +description = Defines a field type for referencing a user from a node. +dependencies[] = content +dependencies[] = text +dependencies[] = optionwidgets +package = CCK +core = 6.x +; Information added by Drupal.org packaging script on 2015-06-25 +version = "6.x-3.0-alpha4" +core = "6.x" +project = "cck" +datestamp = "1435195385" + diff --git a/sites/all/modules/cck/modules/userreference/userreference.install b/sites/all/modules/cck/modules/userreference/userreference.install new file mode 100644 index 0000000..04b0aea --- /dev/null +++ b/sites/all/modules/cck/modules/userreference/userreference.install @@ -0,0 +1,152 @@ + $fields) { + foreach ($fields as $field) { + if ($field['type'] == 'userreference') { + $sandbox['fields'][] = $field; + } + } + } + + if (empty($sandbox['fields'])) { + return $ret; + } + + $sandbox['progress'] = 0; + $sandbox['visited'] = array(); + } + + $field = $sandbox['fields'][$sandbox['progress']]; + + // We only want to process a field once -- if we hit it a second time, + // that means it's its own table and it should have already been updated. + if (!in_array($field['field_name'], $sandbox['visited'])) { + $db_info = content_database_info($field); + $table = $db_info['table']; + $attributes = $db_info['columns']['uid']; + $column = $attributes['column']; + if (!content_db_index_exists($table, $column)) { + db_add_index($ret, $table, $column, array($column)); + } + $sandbox['visited'][] = $field['field_name']; + } + + $sandbox['progress']++; + $ret['#finished'] = $sandbox['progress'] / count($sandbox['fields']); + + return $ret; +} + +/** + * Convert 'referenceable_status' option from array to integer to match the + * change in the field settings form where the element has been changed from + * a checkboxes element (array) to a radios element (integer). + * + * Reference: @link http://drupal.org/node/416134 @endlink + */ +function userreference_update_6002() { + $ret = array(); + + drupal_load('module', 'content'); + + $result = db_query("SELECT field_name, global_settings FROM {". content_field_tablename() ."} WHERE type = 'userreference'"); + while ($userreference = db_fetch_object($result)) { + $global_settings = unserialize($userreference->global_settings); + + if (isset($global_settings['referenceable_status']) && is_array($global_settings['referenceable_status'])) { + $referenceable_status = array_filter($global_settings['referenceable_status']); + $global_settings['referenceable_status'] = (!empty($referenceable_status) ? 1 : ''); + + // We can't use update_sql() here because of curly braces in serialized + // array. + db_query("UPDATE {". content_field_tablename() ."} SET global_settings = '%s' WHERE field_name = '%s'", serialize($global_settings), $userreference->field_name); + $ret[] = array( + 'success' => TRUE, + 'query' => t("The 'referenceable_status' option for %field has been fixed.", array('%field' => $userreference->field_name)), + ); + } + } + + // Rebuild content caches only if necessary. + if (!empty($ret)) { + content_clear_type_cache(); + } + + return $ret; +} diff --git a/sites/all/modules/cck/modules/userreference/userreference.module b/sites/all/modules/cck/modules/userreference/userreference.module new file mode 100644 index 0000000..e063dbb --- /dev/null +++ b/sites/all/modules/cck/modules/userreference/userreference.module @@ -0,0 +1,944 @@ + 'Userreference autocomplete', + 'page callback' => 'userreference_autocomplete', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK + ); + return $items; +} + +/** + * Implementation of hook_theme(). + */ +function userreference_theme() { + return array( + 'userreference_select' => array( + 'arguments' => array('element' => NULL), + ), + 'userreference_buttons' => array( + 'arguments' => array('element' => NULL), + ), + 'userreference_autocomplete' => array( + 'arguments' => array('element' => NULL), + ), + 'userreference_formatter_default' => array( + 'arguments' => array('element'), + ), + 'userreference_formatter_plain' => array( + 'arguments' => array('element'), + ), + ); +} + +/** + * Implementaion of hook_ctools_plugin_directory(). + */ +function userreference_ctools_plugin_directory($module, $plugin) { + if ($module == 'ctools' && $plugin == 'relationships') { + return 'panels/' . $plugin; + } +} + +/** + * Implementation of hook_field_info(). + */ +function userreference_field_info() { + return array( + 'userreference' => array( + 'label' => t('User reference'), + 'description' => t('Store the ID of a related user as an integer value.'), +// 'content_icon' => 'icon_content_noderef.png', + ), + ); +} + +/** + * Implementation of hook_field_settings(). + */ +function userreference_field_settings($op, $field) { + switch ($op) { + case 'form': + $form = array(); + $form['referenceable_roles'] = array( + '#type' => 'checkboxes', + '#title' => t('User roles that can be referenced'), + '#default_value' => isset($field['referenceable_roles']) && is_array($field['referenceable_roles']) ? array_filter($field['referenceable_roles']) : array(), + '#options' => user_roles(1), + ); + $form['referenceable_status'] = array( + '#type' => 'radios', + '#title' => t('User status that can be referenced'), + '#default_value' => isset($field['referenceable_status']) ? $field['referenceable_status'] : '', + '#options' => array('' => t('All users'), 1 => t('Active users'), 0 => t('Blocked users')), + ); + if (module_exists('views')) { + $views = array('--' => '--'); + $all_views = views_get_all_views(); + foreach ($all_views as $view) { + // Only 'users' views that have fields will work for our purpose. + if ($view->base_table == 'users' && !empty($view->display['default']->display_options['fields'])) { + if ($view->type == 'Default') { + $views[t('Default Views')][$view->name] = $view->name; + } + else { + $views[t('Existing Views')][$view->name] = $view->name; + } + } + } + + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced - Users that can be referenced (View)'), + '#collapsible' => TRUE, + '#collapsed' => !isset($field['advanced_view']) || $field['advanced_view'] == '--', + ); + if (count($views) > 1) { + $form['advanced']['advanced_view'] = array( + '#type' => 'select', + '#title' => t('View used to select the users'), + '#options' => $views, + '#default_value' => isset($field['advanced_view']) ? $field['advanced_view'] : '--', + '#description' => t('

Choose the "Views module" view that selects the users that can be referenced.
Note:

') . + t('
  • Only views that have fields will work for this purpose.
  • This will discard the "Referenceable Roles" and "Referenceable Status" settings above. Use the view\'s "filters" section instead.
  • Use the view\'s "fields" section to display additional informations about candidate users on user creation/edition form.
  • Use the view\'s "sort criteria" section to determine the order in which candidate users will be displayed.
'), + ); + $form['advanced']['advanced_view_args'] = array( + '#type' => 'textfield', + '#title' => t('View arguments'), + '#default_value' => isset($field['advanced_view_args']) ? $field['advanced_view_args'] : '', + '#required' => FALSE, + '#description' => t('Provide a comma separated list of arguments to pass to the view.'), + ); + } + else { + $form['advanced']['no_view_help'] = array( + '#value' => t('

The list of user that can be referenced can be based on a "Views module" view but no appropriate views were found.
Note:

') . + t('
  • Only views that have fields will work for this purpose.
  • This will discard the "Referenceable Roles" and "Referenceable Status" settings above. Use the view\'s "filters" section instead.
  • Use the view\'s "fields" section to display additional informations about candidate users on user creation/edition form.
  • Use the view\'s "sort criteria" section to determine the order in which candidate users will be displayed.
'), + ); + } + } + return $form; + + case 'save': + $settings = array('referenceable_roles', 'referenceable_status'); + if (module_exists('views')) { + $settings[] = 'advanced_view'; + $settings[] = 'advanced_view_args'; + } + return $settings; + + case 'database columns': + $columns = array( + 'uid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'index' => TRUE), + ); + return $columns; + + case 'views data': + $data = content_views_field_views_data($field); + $db_info = content_database_info($field); + $table_alias = content_views_tablename($field); + + // Filter : swap the handler to the 'in' operator. + $data[$table_alias][$field['field_name'] .'_uid']['filter']['handler'] = 'content_handler_filter_many_to_one'; + // Argument: get the user name for summaries. + // We need to join a new instance of the users table. + $data["users_$table_alias"]['table']['join']['node'] = array( + 'table' => 'users', + 'field' => 'uid', + 'left_table' => $table_alias, + 'left_field' => $field['field_name'] .'_uid', + ); + $data[$table_alias][$field['field_name'] .'_uid']['argument']['handler'] = 'content_handler_argument_reference'; + $data[$table_alias][$field['field_name'] .'_uid']['argument']['name table'] = "users_$table_alias"; + $data[$table_alias][$field['field_name'] .'_uid']['argument']['name field'] = 'name'; + // Relationship: Add a relationship for related user. + $data[$table_alias][$field['field_name'] .'_uid']['relationship'] = array( + 'base' => 'users', + 'field' => $db_info['columns']['uid']['column'], + 'handler' => 'content_handler_relationship', + 'label' => t($field['widget']['label']), + 'content_field_name' => $field['field_name'], + ); + return $data; + + } +} + +/** + * Implementation of hook_field(). + */ +function userreference_field($op, &$node, $field, &$items, $teaser, $page) { + switch ($op) { + case 'validate': + // Extract uids to check. + $ids = array(); + foreach ($items as $delta => $item) { + if (is_array($item) && !empty($item['uid'])) { + if (is_numeric($item['uid'])) { + $ids[] = $item['uid']; + } + else { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + form_set_error($error_element, t('%name: invalid input.', array('%name' => t($field['widget']['label'])))); + } + } + } + // Prevent performance hog if there are no ids to check. + if ($ids) { + $refs = _userreference_potential_references($field, '', NULL, $ids); + foreach ($items as $delta => $item) { + if (is_array($item)) { + $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; + if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); + if (!empty($item['uid']) && !isset($refs[$item['uid']])) { + form_set_error($error_element, t('%name: invalid user.', array('%name' => t($field['widget']['label'])))); + } + } + } + } + return $items; + } +} + +/** + * Implementation of hook_content_is_empty(). + */ +function userreference_content_is_empty($item, $field) { + if (empty($item['uid'])) { + return TRUE; + } + return FALSE; +} + +/** + * Implementation of hook_field_formatter_info(). + */ +function userreference_field_formatter_info() { + return array( + 'default' => array( + 'label' => t('Default'), + 'field types' => array('userreference'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + 'plain' => array( + 'label' => t('Plain text'), + 'field types' => array('userreference'), + 'multiple values' => CONTENT_HANDLE_CORE, + ), + ); +} + +/** + * Theme function for 'default' userreference field formatter. + */ +function theme_userreference_formatter_default($element) { + $output = ''; + + if (isset($element['#item']['uid']) && $account = user_load(array('uid' => $element['#item']['uid']))) { + $output = theme('username', $account); + } + return $output; +} + +/** + * Theme function for 'plain' userreference field formatter. + */ +function theme_userreference_formatter_plain($element) { + $output = ''; + if (isset($element['#item']['uid']) && $account = user_load(array('uid' => $element['#item']['uid']))) { + $output = $account->name; + } + return $output; +} + +/** + * Implementation of hook_widget_info(). + * + * We need custom handling of multiple values for the userreference_select + * widget because we need to combine them into a options list rather + * than display multiple elements. + * + * We will use the content module's default handling for default value. + * + * Callbacks can be omitted if default handing is used. + * They're included here just so this module can be used + * as an example for custom modules that might do things + * differently. + */ +function userreference_widget_info() { + return array( + 'userreference_select' => array( + 'label' => t('Select list'), + 'field types' => array('userreference'), + 'multiple values' => CONTENT_HANDLE_MODULE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + 'userreference_buttons' => array( + 'label' => t('Check boxes/radio buttons'), + 'field types' => array('userreference'), + 'multiple values' => CONTENT_HANDLE_MODULE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + 'userreference_autocomplete' => array( + 'label' => t('Autocomplete text field'), + 'field types' => array('userreference'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'callbacks' => array( + 'default value' => CONTENT_CALLBACK_DEFAULT, + ), + ), + ); +} + +/** + * Implementation of FAPI hook_elements(). + * + * Any FAPI callbacks needed for individual widgets can be declared here, + * and the element will be passed to those callbacks for processing. + * + * Drupal will automatically theme the element using a theme with + * the same name as the hook_elements key. + * + * Autocomplete_path is not used by text_widget but other widgets can use it + * (see nodereference and userreference). + */ +function userreference_elements() { + return array( + 'userreference_select' => array( + '#input' => TRUE, + '#columns' => array('uid'), '#delta' => 0, + '#process' => array('userreference_select_process'), + ), + 'userreference_buttons' => array( + '#input' => TRUE, + '#columns' => array('uid'), '#delta' => 0, + '#process' => array('userreference_buttons_process'), + ), + 'userreference_autocomplete' => array( + '#input' => TRUE, + '#columns' => array('name'), '#delta' => 0, + '#process' => array('userreference_autocomplete_process'), + '#autocomplete_path' => FALSE, + ), + ); +} + +/** + * Implementation of hook_widget_settings(). + */ +function userreference_widget_settings($op, $widget) { + switch ($op) { + case 'form': + $form = array(); + $match = isset($widget['autocomplete_match']) ? $widget['autocomplete_match'] : 'contains'; + $size = (isset($widget['size']) && is_numeric($widget['size'])) ? $widget['size'] : 60; + if ($widget['type'] == 'userreference_autocomplete') { + $form['autocomplete_match'] = array( + '#type' => 'select', + '#title' => t('Autocomplete matching'), + '#default_value' => $match, + '#options' => array( + 'starts_with' => t('Starts with'), + 'contains' => t('Contains'), + ), + '#description' => t('Select the method used to collect autocomplete suggestions. Note that Contains can cause performance issues on sites with thousands of users.'), + ); + $form['size'] = array( + '#type' => 'textfield', + '#title' => t('Size of textfield'), + '#default_value' => $size, + '#element_validate' => array('_element_validate_integer_positive'), + '#required' => TRUE, + ); + } + else { + $form['autocomplete_match'] = array('#type' => 'hidden', '#value' => $match); + $form['size'] = array('#type' => 'hidden', '#value' => $size); + } + $form['reverse_link'] = array( + '#type' => 'checkbox', + '#title' => t('Reverse link'), + '#default_value' => isset($widget['reverse_link']) ? $widget['reverse_link'] : 0, + '#description' => t('If selected, a reverse link back to the referencing node will displayed on the referenced user record.'), + ); + return $form; + + case 'save': + return array('autocomplete_match', 'size', 'reverse_link'); + } +} + +/** + * Implementation of hook_widget(). + * + * Attach a single form element to the form. It will be built out and + * validated in the callback(s) listed in hook_elements. We build it + * out in the callbacks rather than here in hook_widget so it can be + * plugged into any module that can provide it with valid + * $field information. + * + * Content module will set the weight, field name and delta values + * for each form element. This is a change from earlier CCK versions + * where the widget managed its own multiple values. + * + * If there are multiple values for this field, the content module will + * call this function as many times as needed. + * + * @param $form + * the entire form array, $form['#node'] holds node information + * @param $form_state + * the form_state, $form_state['values'][$field['field_name']] + * holds the field's form values. + * @param $field + * the field array + * @param $items + * array of default values for this field + * @param $delta + * the order of this item in the array of subelements (0, 1, 2, etc) + * + * @return + * the form item for a single element for this field + */ +function userreference_widget(&$form, &$form_state, $field, $items, $delta = 0) { + switch ($field['widget']['type']) { + case 'userreference_select': + $element = array( + '#type' => 'userreference_select', + '#default_value' => $items, + ); + break; + + case 'userreference_buttons': + $element = array( + '#type' => 'userreference_buttons', + '#default_value' => $items, + ); + break; + + case 'userreference_autocomplete': + $element = array( + '#type' => 'userreference_autocomplete', + '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL, + '#value_callback' => 'userreference_autocomplete_value', + ); + break; + } + return $element; +} + +/** + * Value for a userreference autocomplete element. + * + * Substitute in the user name for the uid. + */ +function userreference_autocomplete_value($element, $edit = FALSE) { + $field_key = $element['#columns'][0]; + if (!empty($element['#default_value'][$field_key])) { + $value = db_result(db_query("SELECT name FROM {users} WHERE uid = '%d'", $element['#default_value'][$field_key])); + return array($field_key => $value); + } + return array($field_key => NULL); +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function userreference_select_process($element, $edit, $form_state, $form) { + // The userreference_select widget doesn't need to create its own + // element, it can wrap around the optionwidgets_select element. + // Add a validation step where the value can be unwrapped. + $field_key = $element['#columns'][0]; + $element[$field_key] = array( + '#type' => 'optionwidgets_select', + '#default_value' => isset($element['#value']) ? $element['#value'] : '', + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#required' => $element['#required'], + '#description' => $element['#description'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + if (empty($element[$field_key]['#element_validate'])) { + $element[$field_key]['#element_validate'] = array(); + } + array_unshift($element[$field_key]['#element_validate'], 'userreference_optionwidgets_validate'); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + * The $fields array is in $form['#field_info'][$element['#field_name']]. + */ +function userreference_buttons_process($element, $edit, $form_state, $form) { + // The userreference_select widget doesn't need to create its own + // element, it can wrap around the optionwidgets_select element. + // Add a validation step where the value can be unwrapped. + $field_key = $element['#columns'][0]; + $element[$field_key] = array( + '#type' => 'optionwidgets_buttons', + '#default_value' => isset($element['#value']) ? $element['#value'] : '', + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#required' => $element['#required'], + '#description' => $element['#description'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + if (empty($element[$field_key]['#element_validate'])) { + $element[$field_key]['#element_validate'] = array(); + } + array_unshift($element[$field_key]['#element_validate'], 'userreference_optionwidgets_validate'); + return $element; +} + +/** + * Process an individual element. + * + * Build the form element. When creating a form using FAPI #process, + * note that $element['#value'] is already set. + * + */ +function userreference_autocomplete_process($element, $edit, $form_state, $form) { + // The userreference autocomplete widget doesn't need to create its own + // element, it can wrap around the text_textfield element and add an autocomplete + // path and some extra processing to it. + // Add a validation step where the value can be unwrapped. + $field_key = $element['#columns'][0]; + + $element[$field_key] = array( + '#type' => 'text_textfield', + '#default_value' => isset($element['#value']) ? $element['#value'] : '', + '#autocomplete_path' => 'userreference/autocomplete/'. $element['#field_name'], + // The following values were set by the content module and need + // to be passed down to the nested element. + '#title' => $element['#title'], + '#required' => $element['#required'], + '#description' => $element['#description'], + '#field_name' => $element['#field_name'], + '#type_name' => $element['#type_name'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + ); + if (empty($element[$field_key]['#element_validate'])) { + $element[$field_key]['#element_validate'] = array(); + } + array_unshift($element[$field_key]['#element_validate'], 'userreference_autocomplete_validate'); + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + // Wrapping the element around a text_textfield element creates a + // nested element, so the final id will look like 'field-name-0-uid-uid'. + '#value' => implode('][', array_merge($element['#parents'], array($field_key, $field_key))), + ); + return $element; +} + +/** + * Validate a select/buttons element. + * + * Remove the wrapper layer and set the right element's value. + * We don't know exactly where this element is, so we drill down + * through the element until we get to our key. + * + * We use $form_state['values'] instead of $element['#value'] + * to be sure we have the most accurate value when other modules + * like optionwidgets are using #element_validate to alter the value. + */ +function userreference_optionwidgets_validate($element, &$form_state) { + $field_key = $element['#columns'][0]; + + $value = $form_state['values']; + $new_parents = array(); + foreach ($element['#parents'] as $parent) { + $value = $value[$parent]; + // Use === to be sure we get right results if parent is a zero (delta) value. + if ($parent === $field_key) { + $element['#parents'] = $new_parents; + form_set_value($element, $value, $form_state); + break; + } + $new_parents[] = $parent; + } +} + +/** + * Validate an autocomplete element. + * + * Remove the wrapper layer and set the right element's value. + * This will move the nested value at 'field-name-0-uid-uid' + * back to its original location, 'field-name-0-uid'. + */ +function userreference_autocomplete_validate($element, &$form_state) { + $field_name = $element['#field_name']; + $type_name = $element['#type_name']; + $field = content_fields($field_name, $type_name); + $field_key = $element['#columns'][0]; + $value = $element['#value'][$field_key]; + $uid = NULL; + if (!empty($value)) { + $reference = _userreference_potential_references($field, $value, 'equals', NULL, 1); + if (empty($reference)) { + form_error($element[$field_key], t('%name: found no valid user with that name.', array('%name' => t($field['widget']['label'])))); + } + else { + $uid = key($reference); + } + } + form_set_value($element, $uid, $form_state); +} + +/** + * Implementation of hook_allowed_values(). + */ +function userreference_allowed_values($field) { + $references = _userreference_potential_references($field); + + $options = array(); + foreach ($references as $key => $value) { + $options[$key] = $value['rendered']; + } + + return $options; +} + +/** + * Fetch an array of all candidate referenced users. + * + * This info is used in various places (aloowed values, autocomplete results, + * input validation...). Some of them only need the uids, others nid + names, + * others yet uid + names + rendered row (for display in widgets). + * The array we return contains all the potentially needed information, and lets + * consumers use the parts they actually need. + * + * @param $field + * The field description. + * @param $string + * Optional string to filter usernames on (used by autocomplete) + * @param $match + * Operator to match filtered name against, can be any of: + * 'contains', 'equals', 'starts_with' + * @param $ids + * Optional user ids to lookup (the $string and $match arguments will be + * ignored). + * @param $limit + * If non-zero, limit the size of the result set. + * + * @return + * An array of valid users in the form: + * array( + * uid => array( + * 'title' => The user name, + * 'rendered' => The text to display in widgets (can be HTML) + * ), + * ... + * ) + */ +function _userreference_potential_references($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { + static $results = array(); + + // Create unique id for static cache. + $cid = $field['field_name'] .':'. $match .':'. ($string !== '' ? $string : implode('-', $ids)) .':'. $limit; + if (!isset($results[$cid])) { + $references = FALSE; + if (module_exists('views') && !empty($field['advanced_view']) && $field['advanced_view'] != '--') { + $references = _userreference_potential_references_views($field, $string, $match, $ids, $limit); + } + // If the view doesn't exist, we got FALSE, and fallback to the regular 'standard mode'. + + if ($references === FALSE) { + $references = _userreference_potential_references_standard($field, $string, $match, $ids, $limit); + } + + // Store the results. + $results[$cid] = !empty($references) ? $references : array(); + } + + return $results[$cid]; +} + +/** + * Helper function for _userreference_potential_references(): + * case of Views-defined referenceable users. + */ +function _userreference_potential_references_views($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { + $view_name = $field['advanced_view']; + + if ($view = views_get_view($view_name)) { + // We add a display, and let it derive from the 'default' display. + // TODO: We should let the user pick a display in the fields settings - sort of requires AHAH... + $display = $view->add_display('content_references'); + $view->set_display($display); + + // TODO from merlinofchaos on IRC : arguments using summary view can defeat the style setting. + // We might also need to check if there's an argument, and set *its* style_plugin as well. + $view->display_handler->set_option('style_plugin', 'content_php_array_autocomplete'); + $view->display_handler->set_option('row_plugin', 'fields'); + // Used in content_plugin_style_php_array::render(), to get + // the 'field' to be used as title. + $view->display_handler->set_option('content_title_field', 'name'); + + // Additional options to let content_plugin_display_references::query() + // narrow the results. + $options = array( + 'table' => 'users', + 'field_string' => 'name', + 'string' => $string, + 'match' => $match, + 'field_id' => 'uid', + 'ids' => $ids, + ); + $view->display_handler->set_option('content_options', $options); + + // TODO : for consistency, a fair amount of what's below + // should be moved to content_plugin_display_references + + // Limit result set size. + // - Views 3.x uses set_items_per_page(), + // - Views 2.x uses set_option('items_per_page'). + $limit = isset($limit) ? $limit : 0; + if (method_exists($view, 'set_items_per_page')) { + $view->set_items_per_page($limit); + } + else { + $view->display_handler->set_option('items_per_page', $limit); + } + + // Get arguments for the view. + if (!empty($field['advanced_view_args'])) { + // TODO: Support Tokens using token.module ? + $view_args = array_map('trim', explode(',', $field['advanced_view_args'])); + } + else { + $view_args = array(); + } + + // We do need name field, so add it if not present (unlikely, but...) + $fields = $view->get_items('field', $display); + if (!isset($fields['name'])) { + $view->add_item($display, 'field', 'users', 'name'); + } + + // If not set, make all fields inline and define a separator. + $options = $view->display_handler->get_option('row_options'); + if (empty($options['inline'])) { + $options['inline'] = drupal_map_assoc(array_keys($view->get_items('field', $display))); + } + if (empty($options['separator'])) { + $options['separator'] = '-'; + } + $view->display_handler->set_option('row_options', $options); + + // Make sure the query is not cached + $view->is_cacheable = FALSE; + + // Get the results. + $result = $view->execute_display($display, $view_args); + } + else { + $result = FALSE; + } + + return $result; +} + +/** + * Helper function for _userreference_potential_references(): + * referenceable users defined by user role and status + */ +function _userreference_potential_references_standard($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { + $where = array(); + $args = array(); + $join = array(); + + if ($string !== '') { + $like = $GLOBALS["db_type"] == 'pgsql' ? "ILIKE" : "LIKE"; + $match_clauses = array( + 'contains' => "$like '%%%s%%'", + 'equals' => "= '%s'", + 'starts_with' => "$like '%s%%'", + ); + $where[] = 'u.name '. (isset($match_clauses[$match]) ? $match_clauses[$match] : $match_clauses['contains']); + $args[] = $string; + } + elseif ($ids) { + $where[] = 'u.uid IN (' . db_placeholders($ids) . ')'; + $args = array_merge($args, $ids); + } + else { + $where[] = "u.uid > 0"; + } + + $roles = array(); + if (isset($field['referenceable_roles']) && is_array($field['referenceable_roles'])) { + // keep only selected checkboxes + $roles = array_filter($field['referenceable_roles']); + // filter invalid values that seems to get through sometimes ?? + $roles = array_intersect(array_keys(user_roles(1)), $roles); + } + if (!empty($roles) && !in_array(DRUPAL_AUTHENTICATED_RID, $roles)) { + $where[] = "r.rid IN (". implode($roles, ',') .")"; + $join[] = 'LEFT JOIN {users_roles} r ON u.uid = r.uid'; + } + + if (isset($field['referenceable_status']) && is_numeric($field['referenceable_status'])) { + $where[] = 'u.status = %d'; + $args[] = $field['referenceable_status']; + } + + $users = array(); + $where_clause = $where ? 'WHERE ('. implode(') AND (', $where) .')' : ''; + $result = db_query('SELECT u.name, u.uid FROM {users} u '. implode(' ', $join) ." $where_clause ORDER BY u.name ASC", $args); + while ($user = db_fetch_object($result)) { + $users[$user->uid] = array( + 'title' => $user->name, + 'rendered' => check_plain($user->name), + ); + } + return $users; +} + +/** + * Menu callback; Retrieve a pipe delimited string of autocomplete suggestions for existing users + */ +function userreference_autocomplete($field_name, $string = '') { + $fields = content_fields(); + $field = $fields[$field_name]; + $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains'; + $matches = array(); + + $references = _userreference_potential_references($field, $string, $match, array(), 10); + foreach ($references as $id => $row) { + // Add a class wrapper for a few required CSS overrides. + $matches[$row['title']] = '
'. $row['rendered'] . '
'; + } + drupal_json($matches); +} + +/** + * Implementation of hook_user(). + */ +function userreference_user($type, &$edit, &$account) { + switch ($type) { + case 'load': + // Only add links if we are on the user 'view' page. + if (arg(0) != 'user' || arg(2)) { + return; + } + // find CCK userreference field tables + // search through them for matching user ids and load those nodes + $additions = array(); + $types = content_types(); + + // Find the table and columns to search through, if the same + // table comes up in more than one content type, we only need + // to search it once. + $search_tables = array(); + foreach ($types as $type_name => $type) { + foreach ($type['fields'] as $field) { + // Only add tables when reverse link has been selected. + if ($field['type'] == 'userreference' && !empty($field['widget']['reverse_link'])) { + $db_info = content_database_info($field); + $search_tables[$db_info['table']][] = $db_info['columns']['uid']['column']; + } + } + } + foreach ($search_tables as $table => $columns) { + foreach ($columns as $column) { + $ids = db_query(db_rewrite_sql("SELECT DISTINCT(n.nid), n.title, n.type FROM {node} n LEFT JOIN {". $table ."} f ON n.vid = f.vid WHERE f.". $column ."=". $account->uid. " AND n.status = 1")); + while ($data = db_fetch_object($ids)) { + $additions[$data->type][$data->nid] = $data->title; + } + } + } + $account->userreference = $additions; + break; + + case 'view': + if (!empty($account->userreference)) { + $node_types = content_types(); + $additions = array(); + $values = array(); + foreach ($account->userreference as $node_type => $nodes) { + foreach ($nodes as $nid => $title) { + $values[$node_type][] = l($title, 'node/'. $nid); + } + if (isset($values[$node_type])) { + $additions[] = array( + '#type' => 'user_profile_item', + '#title' => check_plain($node_types[$node_type]['name']), + '#value' => theme('item_list', $values[$node_type]), + ); + } + } + if ($additions) { + $account->content['userreference'] = $additions + array( + '#type' => 'user_profile_category', + '#attributes' => array('class' => 'user-member'), + '#title' => t('Related content'), + '#weight' => 10, + ); + } + } + break; + } +} + +/** + * FAPI theme for an individual elements. + * + * The textfield or select is already rendered by the + * textfield or select themes and the html output + * lives in $element['#children']. Override this theme to + * make custom changes to the output. + * + * $element['#field_name'] contains the field name + * $element['#delta] is the position of this element in the group + */ +function theme_userreference_select($element) { + return $element['#children']; +} + +function theme_userreference_buttons($element) { + return $element['#children']; +} + +function theme_userreference_autocomplete($element) { + return $element['#children']; +} \ No newline at end of file diff --git a/sites/all/modules/cck/modules/userreference/userreference.rules.inc b/sites/all/modules/cck/modules/userreference/userreference.rules.inc new file mode 100644 index 0000000..e0ed151 --- /dev/null +++ b/sites/all/modules/cck/modules/userreference/userreference.rules.inc @@ -0,0 +1,61 @@ + t('Load a referenced user'), + 'arguments' => array( + 'node' => array( + 'type' => 'node', + 'label' => t('Content containing the user reference field'), + ), + ), + 'new variables' => array( + 'referenced_user' => array( + 'type' => 'user', + 'label' => t('Referenced user'), + ), + ), + 'module' => 'CCK', + 'help' => t('Note that if the field has multiple values, only the first user will be loaded.'), + ); + return $info; +} + +function userreference_rules_action_load($node, $settings) { + $uid = $node->{$settings['field']}[0]['uid']; + if (isset($uid)) { + $user = user_load(array('uid' => $uid)); + return array('referenced_user' => $user); + } +} + +function userreference_rules_action_load_form($settings, &$form) { + $settings += array('field' => ''); + $options = content_rules_get_field_names_by_type('userreference'); + $form['settings']['field'] = array( + '#type' => 'select', + '#title' => t('Field'), + '#default_value' => $settings['field'], + '#options' => $options, + '#required' => TRUE, + '#disabled' => empty($options), + '#description' => empty($options) ? t('There are no userreference fields defined.') : '', + ); +} + +/** + * Helps upgrading from the workflow-ng action. + * "workflow_ng_action_load_referenced_user" to the equivalent rules action. + */ +function workflow_ng_action_load_referenced_user_upgrade(&$element) { + $element['#name'] = 'userreference_rules_action_load'; +} diff --git a/sites/all/modules/cck/modules/userreference/userreference.token.inc b/sites/all/modules/cck/modules/userreference/userreference.token.inc new file mode 100644 index 0000000..2704e7d --- /dev/null +++ b/sites/all/modules/cck/modules/userreference/userreference.token.inc @@ -0,0 +1,40 @@ + TRUE)) : ''; + + return $tokens; + } +} diff --git a/sites/all/modules/cck/theme/content-admin-display-overview-form.tpl.php b/sites/all/modules/cck/theme/content-admin-display-overview-form.tpl.php new file mode 100644 index 0000000..5026305 --- /dev/null +++ b/sites/all/modules/cck/theme/content-admin-display-overview-form.tpl.php @@ -0,0 +1,39 @@ + +
+ +
+ + + + + + + + + $value): ?> + + + + + + + + + + + + + $title): ?> + + + + + + +
indentation; ?>human_name; ?>label; ?>{$context}->format; ?>{$context}->exclude; ?>
+ + diff --git a/sites/all/modules/cck/theme/content-admin-field-overview-form.tpl.php b/sites/all/modules/cck/theme/content-admin-field-overview-form.tpl.php new file mode 100644 index 0000000..237c3d7 --- /dev/null +++ b/sites/all/modules/cck/theme/content-admin-field-overview-form.tpl.php @@ -0,0 +1,101 @@ + +
+ +
+ + + + + + + + + + + + + + row_type): + case 'field': ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ indentation; ?> + label; ?> + weight . $row->parent . $row->hidden_name; ?>field_name; ?>type; ?>configure; ?>  remove; ?> + indentation; ?> + label; ?> + weight . $row->parent . $row->hidden_name; ?>group_name; ?>group_type; ?>configure; ?>  remove; ?> + indentation; ?> + label; ?> + weight . $row->parent . $row->hidden_name; ?>description; ?>configure; ?>  remove; ?> + indentation; ?> +
+
+ label; ?> +
+
 
weight . $row->parent . $row->hidden_name; ?>
 
field_name; ?>
 
type; ?>
 
widget_type; ?>
+ indentation; ?> +
+
+ label; ?> +
+
 
weight . $row->parent . $row->hidden_name; ?>
 
field_name; ?>
 
widget_type; ?>
+ indentation; ?> +
+
+ label; ?> +
+
 
weight . $row->parent . $row->hidden_name; ?>
 
group_name; ?>
 
group_type; ?>
 
group_option; ?>
+ + + diff --git a/sites/all/modules/cck/theme/content-field.tpl.php b/sites/all/modules/cck/theme/content-field.tpl.php new file mode 100644 index 0000000..5f899be --- /dev/null +++ b/sites/all/modules/cck/theme/content-field.tpl.php @@ -0,0 +1,48 @@ + + +
+ +
+ +
+ $item) : + if (!$item['empty']) : ?> +
+ +
+
+ + +
+ +
+
+ diff --git a/sites/all/modules/cck/theme/content-module-rtl.css b/sites/all/modules/cck/theme/content-module-rtl.css new file mode 100644 index 0000000..9a6416e --- /dev/null +++ b/sites/all/modules/cck/theme/content-module-rtl.css @@ -0,0 +1,21 @@ + +/* Node form display */ +.node-form .content-multiple-table td.content-multiple-drag { + padding-left:0; + padding-right:0.5em; +} +.node-form .content-multiple-table td.content-multiple-drag a.tabledrag-handle{ + padding-left:.5em; +} + +/* 'Manage fields' overview */ +#content-field-overview-form .advanced-help-link, +#content-display-overview-form .advanced-help-link { + margin: 4px 0 0 4px; +} + +table#content-field-overview .label-add-new-field, +table#content-field-overview .label-add-existing-field, +table#content-field-overview .label-add-new-group { + float: right; +} diff --git a/sites/all/modules/cck/theme/content-module.css b/sites/all/modules/cck/theme/content-module.css new file mode 100644 index 0000000..2a29acb --- /dev/null +++ b/sites/all/modules/cck/theme/content-module.css @@ -0,0 +1,132 @@ + +/* Node display */ +.field .field-label, +.field .field-label-inline, +.field .field-label-inline-first { + font-weight:bold; +} +.field .field-label-inline, +.field .field-label-inline-first { + display:inline; +} +.field .field-label-inline { + visibility:hidden; +} + +/* Node form display */ +.node-form .content-multiple-table td.content-multiple-drag { + width:30px; + padding-right:0;/*LTR*/ +} +.node-form .content-multiple-table td.content-multiple-drag a.tabledrag-handle{ + padding-right:.5em;/*LTR*/ +} + +.node-form .content-add-more .form-submit{ + margin:0; +} + +.content-multiple-remove-button { + display: block; + float: right; + height: 14px; + width: 16px; + margin: 2px 0 1px 0; + padding: 0; + background:transparent url(../images/remove.png) no-repeat 0 0; + border-bottom: #C2C9CE 1px solid; + border-right: #C2C9CE 1px solid; +} +.content-multiple-remove-button:hover { + background-position: 0 -14px; +} +.content-multiple-removed-row .content-multiple-remove-button { + background-position: 0 -28px; +} +.content-multiple-removed-row .content-multiple-remove-button:hover { + background-position: 0 -42px; +} +html.js .content-multiple-removed-row { + background-color: #ffffcc; +} +.content-multiple-weight-header, +.content-multiple-remove-header, +.content-multiple-remove-cell, +.content-multiple-table td.delta-order { + text-align: center; +} +html.js .content-multiple-weight-header, +html.js .content-multiple-remove-header span, +html.js .content-multiple-table td.delta-order, +html.js .content-multiple-remove-checkbox { + display: none; +} + +.node-form .number { + display:inline; + width:auto; +} +.node-form .text { + width:auto; +} + +/* CSS overrides for Views-based autocomplete results. + - #autocomplete uses "white-space:pre", which is no good with + Views' template-based rendering + - Field titles are rendered with