From b20b38f514a09bec61fa8a69999597e432fcf6d7 Mon Sep 17 00:00:00 2001 From: bachy Date: Wed, 9 Jan 2013 10:53:26 +0100 Subject: [PATCH 1/5] first import Signed-off-by: bachy --- D7UPGRADE.txt | 2 + LICENSE.txt | 339 + README.txt | 19 + css/ie/views-admin.ie7.css | 91 + css/views-admin-rtl.css | 98 + css/views-admin.advanced_help.css | 24 + css/views-admin.bartik-rtl.css | 12 + css/views-admin.bartik.css | 233 + css/views-admin.contextual.css | 63 + css/views-admin.css | 361 ++ css/views-admin.ctools-rtl.css | 82 + css/views-admin.ctools.css | 232 + css/views-admin.garland-rtl.css | 13 + css/views-admin.garland.css | 263 + css/views-admin.seven-rtl.css | 43 + css/views-admin.seven.css | 552 ++ css/views-admin.theme-rtl.css | 208 + css/views-admin.theme.css | 1083 ++++ css/views-rtl.css | 5 + css/views.css | 42 + documentation-standards.txt | 5 + drush/views.drush.inc | 496 ++ handlers/views_handler_area.inc | 133 + handlers/views_handler_area_result.inc | 96 + handlers/views_handler_area_text.inc | 110 + handlers/views_handler_area_text_custom.inc | 56 + handlers/views_handler_area_view.inc | 83 + handlers/views_handler_argument.inc | 1240 ++++ handlers/views_handler_argument_date.inc | 101 + handlers/views_handler_argument_formula.inc | 63 + ...iews_handler_argument_group_by_numeric.inc | 29 + .../views_handler_argument_many_to_one.inc | 185 + handlers/views_handler_argument_null.inc | 67 + handlers/views_handler_argument_numeric.inc | 116 + handlers/views_handler_argument_string.inc | 274 + handlers/views_handler_field.inc | 1630 +++++ handlers/views_handler_field_boolean.inc | 81 + .../views_handler_field_contextual_links.inc | 102 + handlers/views_handler_field_counter.inc | 50 + handlers/views_handler_field_custom.inc | 55 + handlers/views_handler_field_date.inc | 101 + handlers/views_handler_field_entity.inc | 104 + handlers/views_handler_field_machine_name.inc | 73 + handlers/views_handler_field_markup.inc | 59 + handlers/views_handler_field_math.inc | 84 + handlers/views_handler_field_numeric.inc | 137 + .../views_handler_field_prerender_list.inc | 158 + handlers/views_handler_field_serialized.inc | 65 + .../views_handler_field_time_interval.inc | 37 + handlers/views_handler_field_url.inc | 46 + handlers/views_handler_filter.inc | 1410 +++++ .../views_handler_filter_boolean_operator.inc | 179 + ...handler_filter_boolean_operator_string.inc | 35 + handlers/views_handler_filter_combine.inc | 170 + handlers/views_handler_filter_date.inc | 183 + .../views_handler_filter_entity_bundle.inc | 122 + handlers/views_handler_filter_equality.inc | 45 + .../views_handler_filter_group_by_numeric.inc | 56 + handlers/views_handler_filter_in_operator.inc | 426 ++ handlers/views_handler_filter_many_to_one.inc | 125 + handlers/views_handler_filter_numeric.inc | 325 + handlers/views_handler_filter_string.inc | 338 + handlers/views_handler_relationship.inc | 186 + ...ews_handler_relationship_groupwise_max.inc | 382 ++ handlers/views_handler_sort.inc | 240 + handlers/views_handler_sort_date.inc | 74 + .../views_handler_sort_group_by_numeric.inc | 38 + .../views_handler_sort_menu_hierarchy.inc | 54 + handlers/views_handler_sort_random.inc | 22 + help/about.html | 62 + help/advanced-settings.html | 43 + help/advanced-style-settings.html | 30 + help/aggregation.html | 1 + help/alter-exposed-filter.html | 31 + help/analyze-theme.html | 24 + help/api-default-views.html | 103 + help/api-example.html | 179 + help/api-forms.html | 88 + help/api-handler-area.html | 45 + help/api-tables.html | 262 + help/api-upgrading.html | 224 + help/api.html | 24 + help/argument.html | 106 + help/basic-settings.html | 20 + help/demo-video.html | 5 + help/display-attachment.html | 1 + help/display-block.html | 11 + help/display-default.html | 3 + help/display-feed.html | 1 + help/display-page.html | 7 + help/display.html | 13 + help/drush.html | 13 + help/embed.html | 24 + help/empty-text.html | 3 + help/example-author-block.html | 77 + help/example-filter-by-current-user.html | 46 + help/example-recent-stories.html | 57 + help/example-slideshow-thumb-pager.html | 54 + help/example-user-feed.html | 73 + help/example-users-by-role.html | 47 + help/exposed-form.html | 24 + help/field.html | 27 + help/filter.html | 35 + help/get-total-rows.html | 16 + help/getting-started.html | 23 + help/group-by.html | 17 + help/header.html | 3 + .../images/node-term_node-term_data-large.png | Bin 0 -> 4141 bytes help/images/node-term_node-term_data.png | Bin 0 -> 3457 bytes help/images/overview-ui-large.png | Bin 0 -> 83826 bytes help/images/overview-ui-small.png | Bin 0 -> 44890 bytes help/images/style-breakdown-large.png | Bin 0 -> 47381 bytes help/images/style-breakdown.png | Bin 0 -> 15182 bytes help/images/views1-admin-large.png | Bin 0 -> 67878 bytes help/images/views1-admin.png | Bin 0 -> 24372 bytes help/images/views1-changeviewtype-large.png | Bin 0 -> 37394 bytes help/images/views1-changeviewtype.png | Bin 0 -> 17456 bytes help/images/views2-addaview-large.png | Bin 0 -> 46121 bytes help/images/views2-addaview.png | Bin 0 -> 19262 bytes help/images/views2-adddisplay-large.png | Bin 0 -> 43413 bytes help/images/views2-adddisplay.png | Bin 0 -> 19976 bytes help/images/views2-addfields-large.png | Bin 0 -> 29487 bytes help/images/views2-addfields.png | Bin 0 -> 13043 bytes help/images/views2-addfieldsajax-large.png | Bin 0 -> 26423 bytes help/images/views2-addfieldsajax.png | Bin 0 -> 16005 bytes help/images/views2-admin-large.png | Bin 0 -> 53418 bytes help/images/views2-admin.png | Bin 0 -> 19994 bytes .../views2-changedisplaystyle-large.png | Bin 0 -> 43090 bytes help/images/views2-changedisplaystyle.png | Bin 0 -> 16163 bytes help/images/views2-fieldspreview-large.png | Bin 0 -> 40484 bytes help/images/views2-fieldspreview.png | Bin 0 -> 12480 bytes help/images/views2-newview-large.png | Bin 0 -> 36263 bytes help/images/views2-newview.png | Bin 0 -> 17308 bytes help/images/views2-rearrangefields-large.png | Bin 0 -> 34183 bytes help/images/views2-rearrangefields.png | Bin 0 -> 19129 bytes help/images/views2-tablestyle-large.png | Bin 0 -> 38890 bytes help/images/views2-tablestyle.png | Bin 0 -> 20917 bytes .../images/views3-group-aggregation-types.png | Bin 0 -> 16742 bytes help/images/views3-group-aggregation.png | Bin 0 -> 55825 bytes help/images/views3-jump-style-menu.png | Bin 0 -> 40328 bytes help/images/views3-semanticviews.png | Bin 0 -> 5611 bytes help/images/views3-views-all.png | Bin 0 -> 38583 bytes help/menu.html | 21 + help/misc-notes.html | 11 + help/new.html | 131 + help/other-help.html | 9 + help/overrides.html | 6 + help/path.html | 7 + help/performance-views-vs-displays.html | 5 + help/performance.html | 1 + help/relationship-representative.html | 14 + help/relationship.html | 17 + help/reports.html | 3 + ...elect-multple-nids-contextual-filters.html | 28 + help/semantic-views.html | 18 + help/sort.html | 28 + help/style-comment-rss.html | 1 + help/style-fields.html | 16 + help/style-grid.html | 22 + help/style-grouping.html | 7 + help/style-jump.html | 48 + help/style-list.html | 20 + help/style-node-rss.html | 1 + help/style-node.html | 11 + help/style-row.html | 11 + help/style-rss.html | 5 + help/style-settings.html | 3 + help/style-summary-unformatted.html | 3 + help/style-summary.html | 3 + help/style-table.html | 13 + help/style-unformatted.html | 1 + help/style.html | 15 + help/taxonomy-page-override.html | 41 + help/theme-css.html | 76 + help/top-pager.html | 18 + help/ui-crashes.html | 25 + help/updating-view3.html | 1 + help/updating.html | 7 + help/upgrading.html | 8 + help/using-theme.html | 50 + help/view-add.html | 25 + help/view-settings.html | 5 + help/view-type.html | 21 + help/views.help.ini | 359 ++ images/arrow-active.png | Bin 0 -> 313 bytes images/close.png | Bin 0 -> 227 bytes images/expanded-options.png | Bin 0 -> 228 bytes images/loading-small.gif | Bin 0 -> 2112 bytes images/loading.gif | Bin 0 -> 6733 bytes images/overridden.gif | Bin 0 -> 175 bytes images/sprites.png | Bin 0 -> 1777 bytes images/status-active.gif | Bin 0 -> 2196 bytes includes/admin.inc | 5461 +++++++++++++++++ includes/ajax.inc | 376 ++ includes/analyze.inc | 122 + includes/base.inc | 359 ++ includes/cache.inc | 193 + includes/handlers.inc | 1713 ++++++ includes/plugins.inc | 584 ++ includes/view.inc | 2657 ++++++++ js/ajax.js | 235 + js/ajax_view.js | 136 + js/base.js | 110 + js/jquery.ui.dialog.patch.js | 27 + js/views-admin.js | 1027 ++++ js/views-contextual.js | 16 + js/views-list.js | 21 + modules/aggregator.views.inc | 406 ++ ...ndler_argument_aggregator_category_cid.inc | 26 + .../views_handler_argument_aggregator_fid.inc | 26 + .../views_handler_argument_aggregator_iid.inc | 30 + ...iews_handler_field_aggregator_category.inc | 60 + ...ws_handler_field_aggregator_title_link.inc | 55 + .../views_handler_field_aggregator_xss.inc | 18 + ...handler_filter_aggregator_category_cid.inc | 26 + .../views_plugin_row_aggregator_rss.inc | 74 + modules/book.views.inc | 131 + ...iews_plugin_argument_default_book_root.inc | 21 + modules/comment.views.inc | 662 ++ modules/comment.views_default.inc | 283 + ...iews_handler_argument_comment_user_uid.inc | 61 + .../comment/views_handler_field_comment.inc | 73 + .../views_handler_field_comment_depth.inc | 21 + .../views_handler_field_comment_link.inc | 69 + ...ews_handler_field_comment_link_approve.inc | 36 + ...iews_handler_field_comment_link_delete.inc | 29 + .../views_handler_field_comment_link_edit.inc | 52 + ...views_handler_field_comment_link_reply.inc | 29 + .../views_handler_field_comment_node_link.inc | 64 + .../views_handler_field_comment_username.inc | 58 + ...s_handler_field_last_comment_timestamp.inc | 28 + ...ws_handler_field_ncs_last_comment_name.inc | 54 + .../views_handler_field_ncs_last_updated.inc | 18 + .../views_handler_field_node_comment.inc | 26 + .../views_handler_field_node_new_comments.inc | 115 + .../views_handler_filter_comment_user_uid.inc | 29 + .../views_handler_filter_ncs_last_updated.inc | 25 + .../views_handler_filter_node_comment.inc | 21 + .../views_handler_sort_comment_thread.inc | 28 + ...ews_handler_sort_ncs_last_comment_name.inc | 30 + .../views_handler_sort_ncs_last_updated.inc | 19 + .../comment/views_plugin_row_comment_rss.inc | 152 + .../comment/views_plugin_row_comment_view.inc | 97 + modules/contact.views.inc | 21 + .../views_handler_field_contact_link.inc | 57 + modules/field.views.inc | 504 ++ .../views_handler_argument_field_list.inc | 58 + ...ews_handler_argument_field_list_string.inc | 59 + modules/field/views_handler_field_field.inc | 931 +++ .../field/views_handler_filter_field_list.inc | 32 + ...ws_handler_relationship_entity_reverse.inc | 84 + modules/file.views.inc | 73 + modules/filter.views.inc | 33 + ...views_handler_field_filter_format_name.inc | 36 + modules/image.views.inc | 72 + modules/locale.views.inc | 221 + .../views_handler_argument_locale_group.inc | 40 + ...views_handler_argument_locale_language.inc | 38 + .../views_handler_field_locale_group.inc | 21 + .../views_handler_field_locale_language.inc | 36 + .../views_handler_field_locale_link_edit.inc | 60 + .../views_handler_field_node_language.inc | 37 + .../views_handler_filter_locale_group.inc | 23 + .../views_handler_filter_locale_language.inc | 26 + .../views_handler_filter_locale_version.inc | 28 + .../views_handler_filter_node_language.inc | 26 + modules/node.views.inc | 784 +++ modules/node.views_default.inc | 315 + modules/node.views_template.inc | 134 + .../views_handler_argument_dates_various.inc | 177 + .../views_handler_argument_node_language.inc | 36 + .../node/views_handler_argument_node_nid.inc | 24 + .../node/views_handler_argument_node_type.inc | 39 + ...ews_handler_argument_node_uid_revision.inc | 18 + .../node/views_handler_argument_node_vid.inc | 26 + ...s_handler_field_history_user_timestamp.inc | 82 + modules/node/views_handler_field_node.inc | 80 + .../node/views_handler_field_node_link.inc | 48 + .../views_handler_field_node_link_delete.inc | 31 + .../views_handler_field_node_link_edit.inc | 31 + .../node/views_handler_field_node_path.inc | 47 + .../views_handler_field_node_revision.inc | 74 + ...views_handler_field_node_revision_link.inc | 66 + ...andler_field_node_revision_link_delete.inc | 36 + ...andler_field_node_revision_link_revert.inc | 36 + .../node/views_handler_field_node_type.inc | 49 + ..._handler_filter_history_user_timestamp.inc | 87 + .../node/views_handler_filter_node_access.inc | 40 + .../node/views_handler_filter_node_status.inc | 22 + .../node/views_handler_filter_node_type.inc | 26 + ...views_handler_filter_node_uid_revision.inc | 25 + .../views_plugin_argument_default_node.inc | 26 + .../views_plugin_argument_validate_node.inc | 135 + modules/node/views_plugin_row_node_rss.inc | 174 + modules/node/views_plugin_row_node_view.inc | 110 + modules/poll.views.inc | 47 + modules/profile.views.inc | 217 + .../views_handler_field_profile_date.inc | 89 + .../views_handler_field_profile_list.inc | 41 + ...views_handler_filter_profile_selection.inc | 30 + modules/search.views.inc | 202 + modules/search.views_default.inc | 118 + .../search/views_handler_argument_search.inc | 100 + .../views_handler_field_search_score.inc | 81 + .../search/views_handler_filter_search.inc | 234 + .../views_handler_sort_search_score.inc | 32 + .../search/views_plugin_row_search_view.inc | 39 + modules/statistics.views.inc | 263 + modules/statistics.views_default.inc | 252 + .../views_handler_field_accesslog_path.inc | 58 + modules/system.views.inc | 578 ++ .../views_handler_argument_file_fid.inc | 28 + modules/system/views_handler_field_file.inc | 61 + .../views_handler_field_file_extension.inc | 19 + .../views_handler_field_file_filemime.inc | 38 + .../views_handler_field_file_status.inc | 18 + .../system/views_handler_field_file_uri.inc | 35 + .../views_handler_filter_file_status.inc | 19 + .../views_handler_filter_system_type.inc | 21 + modules/taxonomy.views.inc | 540 ++ modules/taxonomy.views_default.inc | 108 + .../views_handler_argument_taxonomy.inc | 29 + .../views_handler_argument_term_node_tid.inc | 49 + ...s_handler_argument_term_node_tid_depth.inc | 145 + ..._argument_term_node_tid_depth_modifier.inc | 64 + ...ndler_argument_vocabulary_machine_name.inc | 26 + .../views_handler_argument_vocabulary_vid.inc | 26 + .../taxonomy/views_handler_field_taxonomy.inc | 85 + .../views_handler_field_term_link_edit.inc | 62 + .../views_handler_field_term_node_tid.inc | 145 + .../views_handler_filter_term_node_tid.inc | 360 ++ ...ews_handler_filter_term_node_tid_depth.inc | 100 + ...handler_filter_vocabulary_machine_name.inc | 25 + .../views_handler_filter_vocabulary_vid.inc | 25 + ...ws_handler_relationship_node_term_data.inc | 95 + ...s_plugin_argument_default_taxonomy_tid.inc | 154 + ...plugin_argument_validate_taxonomy_term.inc | 222 + modules/tracker.views.inc | 183 + ...dler_argument_tracker_comment_user_uid.inc | 26 + ...andler_filter_tracker_boolean_operator.inc | 31 + ...andler_filter_tracker_comment_user_uid.inc | 23 + modules/translation.views.inc | 121 + .../views_handler_argument_node_tnid.inc | 26 + ...iews_handler_field_node_link_translate.inc | 29 + ...ws_handler_field_node_translation_link.inc | 49 + .../views_handler_filter_node_tnid.inc | 45 + .../views_handler_filter_node_tnid_child.inc | 22 + ...views_handler_relationship_translation.inc | 103 + modules/user.views.inc | 575 ++ .../user/views_handler_argument_user_uid.inc | 33 + ...views_handler_argument_users_roles_rid.inc | 23 + modules/user/views_handler_field_user.inc | 55 + .../views_handler_field_user_language.inc | 39 + .../user/views_handler_field_user_link.inc | 58 + .../views_handler_field_user_link_cancel.inc | 33 + .../views_handler_field_user_link_edit.inc | 30 + .../user/views_handler_field_user_mail.inc | 44 + .../user/views_handler_field_user_name.inc | 83 + .../views_handler_field_user_permissions.inc | 68 + .../user/views_handler_field_user_picture.inc | 114 + .../user/views_handler_field_user_roles.inc | 57 + .../views_handler_filter_user_current.inc | 36 + .../user/views_handler_filter_user_name.inc | 162 + .../views_handler_filter_user_permissions.inc | 35 + .../user/views_handler_filter_user_roles.inc | 28 + ...s_plugin_argument_default_current_user.inc | 18 + .../views_plugin_argument_default_user.inc | 77 + .../views_plugin_argument_validate_user.inc | 140 + modules/user/views_plugin_row_user_view.inc | 81 + modules/views.views.inc | 114 + plugins/export_ui/views_ui.class.php | 447 ++ plugins/export_ui/views_ui.inc | 37 + plugins/views_plugin_access.inc | 96 + plugins/views_plugin_access_none.inc | 17 + plugins/views_plugin_access_perm.inc | 62 + plugins/views_plugin_access_role.inc | 66 + plugins/views_plugin_argument_default.inc | 94 + .../views_plugin_argument_default_fixed.inc | 46 + plugins/views_plugin_argument_default_php.inc | 57 + plugins/views_plugin_argument_default_raw.inc | 50 + plugins/views_plugin_argument_validate.inc | 99 + ...views_plugin_argument_validate_numeric.inc | 17 + .../views_plugin_argument_validate_php.inc | 57 + plugins/views_plugin_cache.inc | 313 + plugins/views_plugin_cache_none.inc | 25 + plugins/views_plugin_cache_time.inc | 110 + plugins/views_plugin_display.inc | 3075 ++++++++++ plugins/views_plugin_display_attachment.inc | 282 + plugins/views_plugin_display_block.inc | 243 + plugins/views_plugin_display_default.inc | 57 + plugins/views_plugin_display_embed.inc | 14 + plugins/views_plugin_display_extender.inc | 62 + plugins/views_plugin_display_feed.inc | 222 + plugins/views_plugin_display_page.inc | 557 ++ plugins/views_plugin_exposed_form.inc | 334 + plugins/views_plugin_exposed_form_basic.inc | 13 + ...ews_plugin_exposed_form_input_required.inc | 96 + plugins/views_plugin_localization.inc | 171 + plugins/views_plugin_localization_core.inc | 109 + plugins/views_plugin_localization_none.inc | 36 + plugins/views_plugin_pager.inc | 236 + plugins/views_plugin_pager_full.inc | 422 ++ plugins/views_plugin_pager_mini.inc | 70 + plugins/views_plugin_pager_none.inc | 75 + plugins/views_plugin_pager_some.inc | 62 + plugins/views_plugin_query.inc | 185 + plugins/views_plugin_query_default.inc | 1657 +++++ plugins/views_plugin_row.inc | 152 + plugins/views_plugin_row_fields.inc | 86 + plugins/views_plugin_row_rss_fields.inc | 180 + plugins/views_plugin_style.inc | 598 ++ plugins/views_plugin_style_default.inc | 25 + plugins/views_plugin_style_grid.inc | 70 + plugins/views_plugin_style_jump_menu.inc | 159 + plugins/views_plugin_style_list.inc | 53 + plugins/views_plugin_style_mapping.inc | 125 + plugins/views_plugin_style_rss.inc | 123 + plugins/views_plugin_style_summary.inc | 76 + .../views_plugin_style_summary_jump_menu.inc | 138 + ...views_plugin_style_summary_unformatted.inc | 34 + plugins/views_plugin_style_table.inc | 307 + plugins/views_wizard/comment.inc | 44 + plugins/views_wizard/file_managed.inc | 26 + plugins/views_wizard/node.inc | 42 + plugins/views_wizard/node_revision.inc | 43 + plugins/views_wizard/taxonomy_term.inc | 30 + plugins/views_wizard/users.inc | 35 + .../views_ui_base_views_wizard.class.php | 929 +++ .../views_ui_comment_views_wizard.class.php | 107 + ...ews_ui_file_managed_views_wizard.class.php | 40 + ...ws_ui_node_revision_views_wizard.class.php | 68 + .../views_ui_node_views_wizard.class.php | 136 + ...ws_ui_taxonomy_term_views_wizard.class.php | 41 + .../views_ui_users_views_wizard.class.php | 42 + ...ews_handler_argument_comment_user_uid.test | 106 + ...views_handler_filter_comment_user_uid.test | 41 + tests/field/views_fieldapi.test | 494 ++ tests/handlers/views_handler_area_text.test | 52 + .../handlers/views_handler_argument_null.test | 72 + .../views_handler_argument_string.test | 96 + tests/handlers/views_handler_field.test | 314 + .../handlers/views_handler_field_boolean.test | 76 + .../handlers/views_handler_field_counter.test | 70 + .../handlers/views_handler_field_custom.test | 47 + tests/handlers/views_handler_field_date.test | 87 + .../views_handler_field_file_size.test | 64 + tests/handlers/views_handler_field_math.test | 45 + tests/handlers/views_handler_field_url.test | 60 + tests/handlers/views_handler_field_xss.test | 60 + .../views_handler_filter_combine.test | 105 + tests/handlers/views_handler_filter_date.test | 190 + .../views_handler_filter_equality.test | 173 + .../views_handler_filter_in_operator.test | 196 + .../views_handler_filter_numeric.test | 409 ++ .../handlers/views_handler_filter_string.test | 810 +++ tests/handlers/views_handler_sort.test | 121 + tests/handlers/views_handler_sort_date.test | 198 + tests/handlers/views_handler_sort_random.test | 89 + tests/node/views_node_revision_relations.test | 177 + tests/plugins/views_plugin_display.test | 194 + tests/styles/views_plugin_style.test | 264 + tests/styles/views_plugin_style_base.test | 33 + .../styles/views_plugin_style_jump_menu.test | 151 + tests/styles/views_plugin_style_mapping.test | 144 + .../views_plugin_style_unformatted.test | 53 + ...s_handler_relationship_node_term_data.test | 122 + tests/templates/views-view--frontpage.tpl.php | 85 + .../views_test_plugin_access_test_dynamic.inc | 26 + .../views_test_plugin_access_test_static.inc | 26 + .../views_test_plugin_style_test_mapping.inc | 52 + tests/user/views_handler_field_user_name.test | 96 + tests/user/views_user.test | 143 + tests/user/views_user_argument_default.test | 90 + tests/user/views_user_argument_validate.test | 115 + tests/views_access.test | 285 + tests/views_analyze.test | 51 + tests/views_argument_default.test | 137 + tests/views_argument_validator.test | 106 + tests/views_basic.test | 178 + tests/views_cache.test | 244 + tests/views_cache.test.css | 5 + tests/views_cache.test.js | 5 + tests/views_exposed_form.test | 170 + tests/views_glossary.test | 60 + tests/views_groupby.test | 326 + tests/views_handlers.test | 150 + tests/views_module.test | 163 + tests/views_pager.test | 496 ++ tests/views_plugin_localization_test.inc | 40 + tests/views_query.test | 433 ++ tests/views_test.info | 13 + tests/views_test.install | 13 + tests/views_test.module | 117 + tests/views_test.views_default.inc | 222 + tests/views_translatable.test | 200 + tests/views_ui.test | 973 +++ tests/views_upgrade.test | 277 + tests/views_view.test | 290 + theme/theme.inc | 1155 ++++ theme/views-exposed-form.tpl.php | 80 + theme/views-more.tpl.php | 19 + theme/views-ui-display-tab-bucket.tpl.php | 17 + theme/views-ui-display-tab-setting.tpl.php | 15 + theme/views-ui-edit-item.tpl.php | 45 + theme/views-ui-edit-view.tpl.php | 46 + theme/views-view-field.tpl.php | 25 + theme/views-view-fields.tpl.php | 36 + theme/views-view-grid.tpl.php | 32 + theme/views-view-grouping.tpl.php | 25 + theme/views-view-list.tpl.php | 21 + theme/views-view-row-comment.tpl.php | 18 + theme/views-view-row-rss.tpl.php | 15 + theme/views-view-rss.tpl.php | 20 + theme/views-view-summary-unformatted.tpl.php | 20 + theme/views-view-summary.tpl.php | 20 + theme/views-view-table.tpl.php | 48 + theme/views-view-unformatted.tpl.php | 17 + theme/views-view.tpl.php | 90 + views.api.php | 1107 ++++ views.info | 320 + views.install | 633 ++ views.module | 2532 ++++++++ views.tokens.inc | 94 + views_export/views_export.module | 10 + views_ui.info | 15 + views_ui.module | 866 +++ 526 files changed, 76993 insertions(+) create mode 100644 D7UPGRADE.txt create mode 100644 LICENSE.txt create mode 100644 README.txt create mode 100644 css/ie/views-admin.ie7.css create mode 100644 css/views-admin-rtl.css create mode 100644 css/views-admin.advanced_help.css create mode 100644 css/views-admin.bartik-rtl.css create mode 100644 css/views-admin.bartik.css create mode 100644 css/views-admin.contextual.css create mode 100644 css/views-admin.css create mode 100644 css/views-admin.ctools-rtl.css create mode 100644 css/views-admin.ctools.css create mode 100644 css/views-admin.garland-rtl.css create mode 100644 css/views-admin.garland.css create mode 100644 css/views-admin.seven-rtl.css create mode 100644 css/views-admin.seven.css create mode 100644 css/views-admin.theme-rtl.css create mode 100644 css/views-admin.theme.css create mode 100644 css/views-rtl.css create mode 100644 css/views.css create mode 100644 documentation-standards.txt create mode 100644 drush/views.drush.inc create mode 100644 handlers/views_handler_area.inc create mode 100644 handlers/views_handler_area_result.inc create mode 100644 handlers/views_handler_area_text.inc create mode 100644 handlers/views_handler_area_text_custom.inc create mode 100644 handlers/views_handler_area_view.inc create mode 100644 handlers/views_handler_argument.inc create mode 100644 handlers/views_handler_argument_date.inc create mode 100644 handlers/views_handler_argument_formula.inc create mode 100644 handlers/views_handler_argument_group_by_numeric.inc create mode 100644 handlers/views_handler_argument_many_to_one.inc create mode 100644 handlers/views_handler_argument_null.inc create mode 100644 handlers/views_handler_argument_numeric.inc create mode 100644 handlers/views_handler_argument_string.inc create mode 100644 handlers/views_handler_field.inc create mode 100644 handlers/views_handler_field_boolean.inc create mode 100644 handlers/views_handler_field_contextual_links.inc create mode 100644 handlers/views_handler_field_counter.inc create mode 100644 handlers/views_handler_field_custom.inc create mode 100644 handlers/views_handler_field_date.inc create mode 100644 handlers/views_handler_field_entity.inc create mode 100644 handlers/views_handler_field_machine_name.inc create mode 100644 handlers/views_handler_field_markup.inc create mode 100644 handlers/views_handler_field_math.inc create mode 100644 handlers/views_handler_field_numeric.inc create mode 100644 handlers/views_handler_field_prerender_list.inc create mode 100644 handlers/views_handler_field_serialized.inc create mode 100644 handlers/views_handler_field_time_interval.inc create mode 100644 handlers/views_handler_field_url.inc create mode 100644 handlers/views_handler_filter.inc create mode 100644 handlers/views_handler_filter_boolean_operator.inc create mode 100644 handlers/views_handler_filter_boolean_operator_string.inc create mode 100644 handlers/views_handler_filter_combine.inc create mode 100644 handlers/views_handler_filter_date.inc create mode 100644 handlers/views_handler_filter_entity_bundle.inc create mode 100644 handlers/views_handler_filter_equality.inc create mode 100644 handlers/views_handler_filter_group_by_numeric.inc create mode 100644 handlers/views_handler_filter_in_operator.inc create mode 100644 handlers/views_handler_filter_many_to_one.inc create mode 100644 handlers/views_handler_filter_numeric.inc create mode 100644 handlers/views_handler_filter_string.inc create mode 100644 handlers/views_handler_relationship.inc create mode 100644 handlers/views_handler_relationship_groupwise_max.inc create mode 100644 handlers/views_handler_sort.inc create mode 100644 handlers/views_handler_sort_date.inc create mode 100644 handlers/views_handler_sort_group_by_numeric.inc create mode 100644 handlers/views_handler_sort_menu_hierarchy.inc create mode 100644 handlers/views_handler_sort_random.inc create mode 100644 help/about.html create mode 100644 help/advanced-settings.html create mode 100644 help/advanced-style-settings.html create mode 100644 help/aggregation.html create mode 100644 help/alter-exposed-filter.html create mode 100644 help/analyze-theme.html create mode 100644 help/api-default-views.html create mode 100644 help/api-example.html create mode 100644 help/api-forms.html create mode 100644 help/api-handler-area.html create mode 100644 help/api-tables.html create mode 100644 help/api-upgrading.html create mode 100644 help/api.html create mode 100644 help/argument.html create mode 100644 help/basic-settings.html create mode 100644 help/demo-video.html create mode 100644 help/display-attachment.html create mode 100644 help/display-block.html create mode 100644 help/display-default.html create mode 100644 help/display-feed.html create mode 100644 help/display-page.html create mode 100644 help/display.html create mode 100644 help/drush.html create mode 100644 help/embed.html create mode 100644 help/empty-text.html create mode 100644 help/example-author-block.html create mode 100644 help/example-filter-by-current-user.html create mode 100644 help/example-recent-stories.html create mode 100644 help/example-slideshow-thumb-pager.html create mode 100644 help/example-user-feed.html create mode 100644 help/example-users-by-role.html create mode 100644 help/exposed-form.html create mode 100644 help/field.html create mode 100644 help/filter.html create mode 100644 help/get-total-rows.html create mode 100644 help/getting-started.html create mode 100644 help/group-by.html create mode 100644 help/header.html create mode 100644 help/images/node-term_node-term_data-large.png create mode 100644 help/images/node-term_node-term_data.png create mode 100644 help/images/overview-ui-large.png create mode 100644 help/images/overview-ui-small.png create mode 100644 help/images/style-breakdown-large.png create mode 100644 help/images/style-breakdown.png create mode 100644 help/images/views1-admin-large.png create mode 100644 help/images/views1-admin.png create mode 100644 help/images/views1-changeviewtype-large.png create mode 100644 help/images/views1-changeviewtype.png create mode 100644 help/images/views2-addaview-large.png create mode 100644 help/images/views2-addaview.png create mode 100644 help/images/views2-adddisplay-large.png create mode 100644 help/images/views2-adddisplay.png create mode 100644 help/images/views2-addfields-large.png create mode 100644 help/images/views2-addfields.png create mode 100644 help/images/views2-addfieldsajax-large.png create mode 100644 help/images/views2-addfieldsajax.png create mode 100644 help/images/views2-admin-large.png create mode 100644 help/images/views2-admin.png create mode 100644 help/images/views2-changedisplaystyle-large.png create mode 100644 help/images/views2-changedisplaystyle.png create mode 100644 help/images/views2-fieldspreview-large.png create mode 100644 help/images/views2-fieldspreview.png create mode 100644 help/images/views2-newview-large.png create mode 100644 help/images/views2-newview.png create mode 100644 help/images/views2-rearrangefields-large.png create mode 100644 help/images/views2-rearrangefields.png create mode 100644 help/images/views2-tablestyle-large.png create mode 100644 help/images/views2-tablestyle.png create mode 100644 help/images/views3-group-aggregation-types.png create mode 100644 help/images/views3-group-aggregation.png create mode 100644 help/images/views3-jump-style-menu.png create mode 100644 help/images/views3-semanticviews.png create mode 100644 help/images/views3-views-all.png create mode 100644 help/menu.html create mode 100644 help/misc-notes.html create mode 100644 help/new.html create mode 100644 help/other-help.html create mode 100644 help/overrides.html create mode 100644 help/path.html create mode 100644 help/performance-views-vs-displays.html create mode 100644 help/performance.html create mode 100644 help/relationship-representative.html create mode 100644 help/relationship.html create mode 100644 help/reports.html create mode 100644 help/select-multple-nids-contextual-filters.html create mode 100644 help/semantic-views.html create mode 100644 help/sort.html create mode 100644 help/style-comment-rss.html create mode 100644 help/style-fields.html create mode 100644 help/style-grid.html create mode 100644 help/style-grouping.html create mode 100644 help/style-jump.html create mode 100644 help/style-list.html create mode 100644 help/style-node-rss.html create mode 100644 help/style-node.html create mode 100644 help/style-row.html create mode 100644 help/style-rss.html create mode 100644 help/style-settings.html create mode 100644 help/style-summary-unformatted.html create mode 100644 help/style-summary.html create mode 100644 help/style-table.html create mode 100644 help/style-unformatted.html create mode 100644 help/style.html create mode 100644 help/taxonomy-page-override.html create mode 100644 help/theme-css.html create mode 100644 help/top-pager.html create mode 100644 help/ui-crashes.html create mode 100644 help/updating-view3.html create mode 100644 help/updating.html create mode 100644 help/upgrading.html create mode 100644 help/using-theme.html create mode 100644 help/view-add.html create mode 100644 help/view-settings.html create mode 100644 help/view-type.html create mode 100644 help/views.help.ini create mode 100644 images/arrow-active.png create mode 100644 images/close.png create mode 100644 images/expanded-options.png create mode 100644 images/loading-small.gif create mode 100644 images/loading.gif create mode 100644 images/overridden.gif create mode 100644 images/sprites.png create mode 100644 images/status-active.gif create mode 100644 includes/admin.inc create mode 100644 includes/ajax.inc create mode 100644 includes/analyze.inc create mode 100644 includes/base.inc create mode 100644 includes/cache.inc create mode 100644 includes/handlers.inc create mode 100644 includes/plugins.inc create mode 100644 includes/view.inc create mode 100644 js/ajax.js create mode 100644 js/ajax_view.js create mode 100644 js/base.js create mode 100644 js/jquery.ui.dialog.patch.js create mode 100644 js/views-admin.js create mode 100644 js/views-contextual.js create mode 100644 js/views-list.js create mode 100644 modules/aggregator.views.inc create mode 100644 modules/aggregator/views_handler_argument_aggregator_category_cid.inc create mode 100644 modules/aggregator/views_handler_argument_aggregator_fid.inc create mode 100644 modules/aggregator/views_handler_argument_aggregator_iid.inc create mode 100644 modules/aggregator/views_handler_field_aggregator_category.inc create mode 100644 modules/aggregator/views_handler_field_aggregator_title_link.inc create mode 100644 modules/aggregator/views_handler_field_aggregator_xss.inc create mode 100644 modules/aggregator/views_handler_filter_aggregator_category_cid.inc create mode 100644 modules/aggregator/views_plugin_row_aggregator_rss.inc create mode 100644 modules/book.views.inc create mode 100644 modules/book/views_plugin_argument_default_book_root.inc create mode 100644 modules/comment.views.inc create mode 100644 modules/comment.views_default.inc create mode 100644 modules/comment/views_handler_argument_comment_user_uid.inc create mode 100644 modules/comment/views_handler_field_comment.inc create mode 100644 modules/comment/views_handler_field_comment_depth.inc create mode 100644 modules/comment/views_handler_field_comment_link.inc create mode 100644 modules/comment/views_handler_field_comment_link_approve.inc create mode 100644 modules/comment/views_handler_field_comment_link_delete.inc create mode 100644 modules/comment/views_handler_field_comment_link_edit.inc create mode 100644 modules/comment/views_handler_field_comment_link_reply.inc create mode 100644 modules/comment/views_handler_field_comment_node_link.inc create mode 100644 modules/comment/views_handler_field_comment_username.inc create mode 100644 modules/comment/views_handler_field_last_comment_timestamp.inc create mode 100644 modules/comment/views_handler_field_ncs_last_comment_name.inc create mode 100644 modules/comment/views_handler_field_ncs_last_updated.inc create mode 100644 modules/comment/views_handler_field_node_comment.inc create mode 100644 modules/comment/views_handler_field_node_new_comments.inc create mode 100644 modules/comment/views_handler_filter_comment_user_uid.inc create mode 100644 modules/comment/views_handler_filter_ncs_last_updated.inc create mode 100644 modules/comment/views_handler_filter_node_comment.inc create mode 100644 modules/comment/views_handler_sort_comment_thread.inc create mode 100644 modules/comment/views_handler_sort_ncs_last_comment_name.inc create mode 100644 modules/comment/views_handler_sort_ncs_last_updated.inc create mode 100644 modules/comment/views_plugin_row_comment_rss.inc create mode 100644 modules/comment/views_plugin_row_comment_view.inc create mode 100644 modules/contact.views.inc create mode 100644 modules/contact/views_handler_field_contact_link.inc create mode 100644 modules/field.views.inc create mode 100644 modules/field/views_handler_argument_field_list.inc create mode 100644 modules/field/views_handler_argument_field_list_string.inc create mode 100644 modules/field/views_handler_field_field.inc create mode 100644 modules/field/views_handler_filter_field_list.inc create mode 100644 modules/field/views_handler_relationship_entity_reverse.inc create mode 100644 modules/file.views.inc create mode 100644 modules/filter.views.inc create mode 100644 modules/filter/views_handler_field_filter_format_name.inc create mode 100644 modules/image.views.inc create mode 100644 modules/locale.views.inc create mode 100644 modules/locale/views_handler_argument_locale_group.inc create mode 100644 modules/locale/views_handler_argument_locale_language.inc create mode 100644 modules/locale/views_handler_field_locale_group.inc create mode 100644 modules/locale/views_handler_field_locale_language.inc create mode 100644 modules/locale/views_handler_field_locale_link_edit.inc create mode 100644 modules/locale/views_handler_field_node_language.inc create mode 100644 modules/locale/views_handler_filter_locale_group.inc create mode 100644 modules/locale/views_handler_filter_locale_language.inc create mode 100644 modules/locale/views_handler_filter_locale_version.inc create mode 100644 modules/locale/views_handler_filter_node_language.inc create mode 100644 modules/node.views.inc create mode 100644 modules/node.views_default.inc create mode 100644 modules/node.views_template.inc create mode 100644 modules/node/views_handler_argument_dates_various.inc create mode 100644 modules/node/views_handler_argument_node_language.inc create mode 100644 modules/node/views_handler_argument_node_nid.inc create mode 100644 modules/node/views_handler_argument_node_type.inc create mode 100644 modules/node/views_handler_argument_node_uid_revision.inc create mode 100644 modules/node/views_handler_argument_node_vid.inc create mode 100644 modules/node/views_handler_field_history_user_timestamp.inc create mode 100644 modules/node/views_handler_field_node.inc create mode 100644 modules/node/views_handler_field_node_link.inc create mode 100644 modules/node/views_handler_field_node_link_delete.inc create mode 100644 modules/node/views_handler_field_node_link_edit.inc create mode 100644 modules/node/views_handler_field_node_path.inc create mode 100644 modules/node/views_handler_field_node_revision.inc create mode 100644 modules/node/views_handler_field_node_revision_link.inc create mode 100644 modules/node/views_handler_field_node_revision_link_delete.inc create mode 100644 modules/node/views_handler_field_node_revision_link_revert.inc create mode 100644 modules/node/views_handler_field_node_type.inc create mode 100644 modules/node/views_handler_filter_history_user_timestamp.inc create mode 100644 modules/node/views_handler_filter_node_access.inc create mode 100644 modules/node/views_handler_filter_node_status.inc create mode 100644 modules/node/views_handler_filter_node_type.inc create mode 100644 modules/node/views_handler_filter_node_uid_revision.inc create mode 100644 modules/node/views_plugin_argument_default_node.inc create mode 100644 modules/node/views_plugin_argument_validate_node.inc create mode 100644 modules/node/views_plugin_row_node_rss.inc create mode 100644 modules/node/views_plugin_row_node_view.inc create mode 100644 modules/poll.views.inc create mode 100644 modules/profile.views.inc create mode 100644 modules/profile/views_handler_field_profile_date.inc create mode 100644 modules/profile/views_handler_field_profile_list.inc create mode 100644 modules/profile/views_handler_filter_profile_selection.inc create mode 100644 modules/search.views.inc create mode 100644 modules/search.views_default.inc create mode 100644 modules/search/views_handler_argument_search.inc create mode 100644 modules/search/views_handler_field_search_score.inc create mode 100644 modules/search/views_handler_filter_search.inc create mode 100644 modules/search/views_handler_sort_search_score.inc create mode 100644 modules/search/views_plugin_row_search_view.inc create mode 100644 modules/statistics.views.inc create mode 100644 modules/statistics.views_default.inc create mode 100644 modules/statistics/views_handler_field_accesslog_path.inc create mode 100644 modules/system.views.inc create mode 100644 modules/system/views_handler_argument_file_fid.inc create mode 100644 modules/system/views_handler_field_file.inc create mode 100644 modules/system/views_handler_field_file_extension.inc create mode 100644 modules/system/views_handler_field_file_filemime.inc create mode 100644 modules/system/views_handler_field_file_status.inc create mode 100644 modules/system/views_handler_field_file_uri.inc create mode 100644 modules/system/views_handler_filter_file_status.inc create mode 100644 modules/system/views_handler_filter_system_type.inc create mode 100644 modules/taxonomy.views.inc create mode 100644 modules/taxonomy.views_default.inc create mode 100644 modules/taxonomy/views_handler_argument_taxonomy.inc create mode 100644 modules/taxonomy/views_handler_argument_term_node_tid.inc create mode 100644 modules/taxonomy/views_handler_argument_term_node_tid_depth.inc create mode 100644 modules/taxonomy/views_handler_argument_term_node_tid_depth_modifier.inc create mode 100644 modules/taxonomy/views_handler_argument_vocabulary_machine_name.inc create mode 100644 modules/taxonomy/views_handler_argument_vocabulary_vid.inc create mode 100644 modules/taxonomy/views_handler_field_taxonomy.inc create mode 100644 modules/taxonomy/views_handler_field_term_link_edit.inc create mode 100644 modules/taxonomy/views_handler_field_term_node_tid.inc create mode 100644 modules/taxonomy/views_handler_filter_term_node_tid.inc create mode 100644 modules/taxonomy/views_handler_filter_term_node_tid_depth.inc create mode 100644 modules/taxonomy/views_handler_filter_vocabulary_machine_name.inc create mode 100644 modules/taxonomy/views_handler_filter_vocabulary_vid.inc create mode 100644 modules/taxonomy/views_handler_relationship_node_term_data.inc create mode 100644 modules/taxonomy/views_plugin_argument_default_taxonomy_tid.inc create mode 100644 modules/taxonomy/views_plugin_argument_validate_taxonomy_term.inc create mode 100644 modules/tracker.views.inc create mode 100644 modules/tracker/views_handler_argument_tracker_comment_user_uid.inc create mode 100644 modules/tracker/views_handler_filter_tracker_boolean_operator.inc create mode 100644 modules/tracker/views_handler_filter_tracker_comment_user_uid.inc create mode 100644 modules/translation.views.inc create mode 100644 modules/translation/views_handler_argument_node_tnid.inc create mode 100644 modules/translation/views_handler_field_node_link_translate.inc create mode 100644 modules/translation/views_handler_field_node_translation_link.inc create mode 100644 modules/translation/views_handler_filter_node_tnid.inc create mode 100644 modules/translation/views_handler_filter_node_tnid_child.inc create mode 100644 modules/translation/views_handler_relationship_translation.inc create mode 100644 modules/user.views.inc create mode 100644 modules/user/views_handler_argument_user_uid.inc create mode 100644 modules/user/views_handler_argument_users_roles_rid.inc create mode 100644 modules/user/views_handler_field_user.inc create mode 100644 modules/user/views_handler_field_user_language.inc create mode 100644 modules/user/views_handler_field_user_link.inc create mode 100644 modules/user/views_handler_field_user_link_cancel.inc create mode 100644 modules/user/views_handler_field_user_link_edit.inc create mode 100644 modules/user/views_handler_field_user_mail.inc create mode 100644 modules/user/views_handler_field_user_name.inc create mode 100644 modules/user/views_handler_field_user_permissions.inc create mode 100644 modules/user/views_handler_field_user_picture.inc create mode 100644 modules/user/views_handler_field_user_roles.inc create mode 100644 modules/user/views_handler_filter_user_current.inc create mode 100644 modules/user/views_handler_filter_user_name.inc create mode 100644 modules/user/views_handler_filter_user_permissions.inc create mode 100644 modules/user/views_handler_filter_user_roles.inc create mode 100644 modules/user/views_plugin_argument_default_current_user.inc create mode 100644 modules/user/views_plugin_argument_default_user.inc create mode 100644 modules/user/views_plugin_argument_validate_user.inc create mode 100644 modules/user/views_plugin_row_user_view.inc create mode 100644 modules/views.views.inc create mode 100644 plugins/export_ui/views_ui.class.php create mode 100644 plugins/export_ui/views_ui.inc create mode 100644 plugins/views_plugin_access.inc create mode 100644 plugins/views_plugin_access_none.inc create mode 100644 plugins/views_plugin_access_perm.inc create mode 100644 plugins/views_plugin_access_role.inc create mode 100644 plugins/views_plugin_argument_default.inc create mode 100644 plugins/views_plugin_argument_default_fixed.inc create mode 100644 plugins/views_plugin_argument_default_php.inc create mode 100644 plugins/views_plugin_argument_default_raw.inc create mode 100644 plugins/views_plugin_argument_validate.inc create mode 100644 plugins/views_plugin_argument_validate_numeric.inc create mode 100644 plugins/views_plugin_argument_validate_php.inc create mode 100644 plugins/views_plugin_cache.inc create mode 100644 plugins/views_plugin_cache_none.inc create mode 100644 plugins/views_plugin_cache_time.inc create mode 100644 plugins/views_plugin_display.inc create mode 100644 plugins/views_plugin_display_attachment.inc create mode 100644 plugins/views_plugin_display_block.inc create mode 100644 plugins/views_plugin_display_default.inc create mode 100644 plugins/views_plugin_display_embed.inc create mode 100644 plugins/views_plugin_display_extender.inc create mode 100644 plugins/views_plugin_display_feed.inc create mode 100644 plugins/views_plugin_display_page.inc create mode 100644 plugins/views_plugin_exposed_form.inc create mode 100644 plugins/views_plugin_exposed_form_basic.inc create mode 100644 plugins/views_plugin_exposed_form_input_required.inc create mode 100644 plugins/views_plugin_localization.inc create mode 100644 plugins/views_plugin_localization_core.inc create mode 100644 plugins/views_plugin_localization_none.inc create mode 100644 plugins/views_plugin_pager.inc create mode 100644 plugins/views_plugin_pager_full.inc create mode 100644 plugins/views_plugin_pager_mini.inc create mode 100644 plugins/views_plugin_pager_none.inc create mode 100644 plugins/views_plugin_pager_some.inc create mode 100644 plugins/views_plugin_query.inc create mode 100644 plugins/views_plugin_query_default.inc create mode 100644 plugins/views_plugin_row.inc create mode 100644 plugins/views_plugin_row_fields.inc create mode 100644 plugins/views_plugin_row_rss_fields.inc create mode 100644 plugins/views_plugin_style.inc create mode 100644 plugins/views_plugin_style_default.inc create mode 100644 plugins/views_plugin_style_grid.inc create mode 100644 plugins/views_plugin_style_jump_menu.inc create mode 100644 plugins/views_plugin_style_list.inc create mode 100644 plugins/views_plugin_style_mapping.inc create mode 100644 plugins/views_plugin_style_rss.inc create mode 100644 plugins/views_plugin_style_summary.inc create mode 100644 plugins/views_plugin_style_summary_jump_menu.inc create mode 100644 plugins/views_plugin_style_summary_unformatted.inc create mode 100644 plugins/views_plugin_style_table.inc create mode 100644 plugins/views_wizard/comment.inc create mode 100644 plugins/views_wizard/file_managed.inc create mode 100644 plugins/views_wizard/node.inc create mode 100644 plugins/views_wizard/node_revision.inc create mode 100644 plugins/views_wizard/taxonomy_term.inc create mode 100644 plugins/views_wizard/users.inc create mode 100644 plugins/views_wizard/views_ui_base_views_wizard.class.php create mode 100644 plugins/views_wizard/views_ui_comment_views_wizard.class.php create mode 100644 plugins/views_wizard/views_ui_file_managed_views_wizard.class.php create mode 100644 plugins/views_wizard/views_ui_node_revision_views_wizard.class.php create mode 100644 plugins/views_wizard/views_ui_node_views_wizard.class.php create mode 100644 plugins/views_wizard/views_ui_taxonomy_term_views_wizard.class.php create mode 100644 plugins/views_wizard/views_ui_users_views_wizard.class.php create mode 100644 tests/comment/views_handler_argument_comment_user_uid.test create mode 100644 tests/comment/views_handler_filter_comment_user_uid.test create mode 100644 tests/field/views_fieldapi.test create mode 100644 tests/handlers/views_handler_area_text.test create mode 100644 tests/handlers/views_handler_argument_null.test create mode 100644 tests/handlers/views_handler_argument_string.test create mode 100644 tests/handlers/views_handler_field.test create mode 100644 tests/handlers/views_handler_field_boolean.test create mode 100644 tests/handlers/views_handler_field_counter.test create mode 100644 tests/handlers/views_handler_field_custom.test create mode 100644 tests/handlers/views_handler_field_date.test create mode 100644 tests/handlers/views_handler_field_file_size.test create mode 100644 tests/handlers/views_handler_field_math.test create mode 100644 tests/handlers/views_handler_field_url.test create mode 100644 tests/handlers/views_handler_field_xss.test create mode 100644 tests/handlers/views_handler_filter_combine.test create mode 100644 tests/handlers/views_handler_filter_date.test create mode 100644 tests/handlers/views_handler_filter_equality.test create mode 100644 tests/handlers/views_handler_filter_in_operator.test create mode 100644 tests/handlers/views_handler_filter_numeric.test create mode 100644 tests/handlers/views_handler_filter_string.test create mode 100644 tests/handlers/views_handler_sort.test create mode 100644 tests/handlers/views_handler_sort_date.test create mode 100644 tests/handlers/views_handler_sort_random.test create mode 100644 tests/node/views_node_revision_relations.test create mode 100644 tests/plugins/views_plugin_display.test create mode 100644 tests/styles/views_plugin_style.test create mode 100644 tests/styles/views_plugin_style_base.test create mode 100644 tests/styles/views_plugin_style_jump_menu.test create mode 100644 tests/styles/views_plugin_style_mapping.test create mode 100644 tests/styles/views_plugin_style_unformatted.test create mode 100644 tests/taxonomy/views_handler_relationship_node_term_data.test create mode 100644 tests/templates/views-view--frontpage.tpl.php create mode 100644 tests/test_plugins/views_test_plugin_access_test_dynamic.inc create mode 100644 tests/test_plugins/views_test_plugin_access_test_static.inc create mode 100644 tests/test_plugins/views_test_plugin_style_test_mapping.inc create mode 100644 tests/user/views_handler_field_user_name.test create mode 100644 tests/user/views_user.test create mode 100644 tests/user/views_user_argument_default.test create mode 100644 tests/user/views_user_argument_validate.test create mode 100644 tests/views_access.test create mode 100644 tests/views_analyze.test create mode 100644 tests/views_argument_default.test create mode 100644 tests/views_argument_validator.test create mode 100644 tests/views_basic.test create mode 100644 tests/views_cache.test create mode 100644 tests/views_cache.test.css create mode 100644 tests/views_cache.test.js create mode 100644 tests/views_exposed_form.test create mode 100644 tests/views_glossary.test create mode 100644 tests/views_groupby.test create mode 100644 tests/views_handlers.test create mode 100644 tests/views_module.test create mode 100644 tests/views_pager.test create mode 100644 tests/views_plugin_localization_test.inc create mode 100644 tests/views_query.test create mode 100644 tests/views_test.info create mode 100644 tests/views_test.install create mode 100644 tests/views_test.module create mode 100644 tests/views_test.views_default.inc create mode 100644 tests/views_translatable.test create mode 100644 tests/views_ui.test create mode 100644 tests/views_upgrade.test create mode 100644 tests/views_view.test create mode 100644 theme/theme.inc create mode 100644 theme/views-exposed-form.tpl.php create mode 100644 theme/views-more.tpl.php create mode 100644 theme/views-ui-display-tab-bucket.tpl.php create mode 100644 theme/views-ui-display-tab-setting.tpl.php create mode 100644 theme/views-ui-edit-item.tpl.php create mode 100644 theme/views-ui-edit-view.tpl.php create mode 100644 theme/views-view-field.tpl.php create mode 100644 theme/views-view-fields.tpl.php create mode 100644 theme/views-view-grid.tpl.php create mode 100644 theme/views-view-grouping.tpl.php create mode 100644 theme/views-view-list.tpl.php create mode 100644 theme/views-view-row-comment.tpl.php create mode 100644 theme/views-view-row-rss.tpl.php create mode 100644 theme/views-view-rss.tpl.php create mode 100644 theme/views-view-summary-unformatted.tpl.php create mode 100644 theme/views-view-summary.tpl.php create mode 100644 theme/views-view-table.tpl.php create mode 100644 theme/views-view-unformatted.tpl.php create mode 100644 theme/views-view.tpl.php create mode 100644 views.api.php create mode 100644 views.info create mode 100644 views.install create mode 100644 views.module create mode 100644 views.tokens.inc create mode 100644 views_export/views_export.module create mode 100644 views_ui.info create mode 100644 views_ui.module diff --git a/D7UPGRADE.txt b/D7UPGRADE.txt new file mode 100644 index 00000000..08d3a3b4 --- /dev/null +++ b/D7UPGRADE.txt @@ -0,0 +1,2 @@ +Information about upgrading existing views from Drupal 6 to Drupal 7 is located +in the module's advanced help under api upgrading. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/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/README.txt b/README.txt new file mode 100644 index 00000000..8097eb45 --- /dev/null +++ b/README.txt @@ -0,0 +1,19 @@ + +Welcome to Views 3. Please see the advanced help for more information. + +If you're having trouble installing this module, please ensure that your +tar program is not flattening the directory tree, truncating filenames +or losing files. + +Installing Views: + +Place the entirety of this directory in sites/all/modules/views +You must also install the CTools module (http://www.drupal.org/project/ctools) to use Views. + +Navigate to administer >> build >> modules. Enable Views and Views UI. + +If you're new to Views, try the Simple Views module which can create some +often used Views for you, this might save you some time. + +Here you can find many modules extending the functionality of Views: + http://drupal.org/taxonomy/term/89 diff --git a/css/ie/views-admin.ie7.css b/css/ie/views-admin.ie7.css new file mode 100644 index 00000000..9a8852a3 --- /dev/null +++ b/css/ie/views-admin.ie7.css @@ -0,0 +1,91 @@ +/** + * The query details collapsible divs are not visible in IE7 because has-layout + * is not being triggered. Trigger has-layout with height: 1%; + */ +.views-edit-view .collapsible > .fieldset-wrapper { + height: 1%; +} + +/** + * The column width for the bucket containers in the query details section + * is not being calculated to 32% correctly. Give IE7 a slightly smaller + * width so that the three columns line up next to each other + */ +.views-edit-view .views-display-column { + width: 31.95%; +} + +/** + * IE7 has no idea how large this container should be and it doesn't + * apply has-layout. Expand it's width to 100% and trigger has-layout. + */ +.views-edit-view .views-displays { + height: 1%; + width: 100%; +} + +/** + * IE7 isn't positioning the span correctly as a display-inline element + */ +.views-edit-view .views-displays .icon { + display: block; + float: left; +} + +.views-edit-view .views-displays .icon-add { + top: 2px; +} + +/** + * The add display query dropdown needs a lot of help + */ + +.views-edit-view .views-displays .tabs.secondary { + position: relative; + z-index: 100; +} + +.views-edit-view .views-displays .secondary .open > a { + border-bottom: 1px solid #f1f1f1; +} + +.views-edit-view .views-displays .secondary .action-list { + border-bottom: 1px solid #cbcbcb; + top: 30px; +} + +.views-edit-view .views-displays .secondary input { + text-align: left; +} + +/** + * IE7 does not interpret div > * correctly + */ +.page-admin-structure-views #content .views-ui-display-tab-bucket { + padding-left: 0; + padding-right: 0; + zoom: 1; +} + +.page-admin-structure-views #content .views-display-column + .views-display-column { + margin-top: 0; +} + +/** + * IE7 is interpreting a top margin of 18px from somewhere. remove it + */ + +.page-admin-structure-views #content .views-display-setting { + margin-top: 0; +} + +/** + * IE7 can't handle the + selector that indents form wrappers after a checkbox on the add page + * zoom is necessary to trigger has layout. !imporant is necessary because IE7 is precedent + * to the theme.css stylesheet, even though it is included before this file. + */ + +.page-admin-structure-views #content .form-type-checkbox + .form-wrapper { + margin-left: 27px !important; + zoom: 1; +} diff --git a/css/views-admin-rtl.css b/css/views-admin-rtl.css new file mode 100644 index 00000000..6d1e03a0 --- /dev/null +++ b/css/views-admin-rtl.css @@ -0,0 +1,98 @@ +/** + * The .css file is intended to only contain positioning and size declarations + * For example: display, position, float, clear, and overflow. + */ + +/* @group Inline lists */ + +.horizontal > * { + float: right; +} + +.horizontal.right { + float: left; +} + +/* @end */ + +/* @group Attachment details + * + * The attachment details section, its tabs for each section and the buttons + * to add a new section + */ + +.form-actions { + float: left; +} + +/* @end */ + +/* @group Attachment details tabs + * + * The tabs that switch between sections + */ + +.views-displays .secondary > li { + float: right; +} + +/* @end */ + +/* @group Attachment details new section button */ + +.views-displays .secondary .action-list { + left: auto; + right: 0; +} + +/* @end */ + +/* @group Attachment details collapsible fieldset */ + +.views-display-tab .fieldset-legend { + left: auto; + right: -5px; +} + +/* @end */ + +/* @group Attachment details actions + * + * Display the "Delete" and "Duplicate" buttons to the right. + */ +.views-display-tab .fieldset-wrapper > .views-ui-display-tab-bucket .actions { + left: 0; + right: auto; +} + +/* @end */ + +/* @group Attachment configuration columns */ + +.views-display-columns > * { + float: right; + margin-left: 0; + margin-right: 1%; + padding-left: 0; + padding-right: 1%; +} + +.views-display-columns > *:first-child { + margin-right: 0; + padding-right: 0; +} + +/* @end */ + +/* @group Settings forms */ + +.views-dependent { + margin-right: 1.5em; +} + +.views-display-setting .label, +.views-display-setting .views-ajax-link { + float: right; +} + +/* @end */ diff --git a/css/views-admin.advanced_help.css b/css/views-admin.advanced_help.css new file mode 100644 index 00000000..71d9cb16 --- /dev/null +++ b/css/views-admin.advanced_help.css @@ -0,0 +1,24 @@ +/** + * The .advanced_help.css file is intended to contain styles that override declarations + * in the Advanced Help module. + */ + +/** + * Adjust the advanced help icons + */ +.views-ui-display-tab-bucket .advanced-help-link { + padding: 0; + margin: 5px 3px 0px 6px; /* LTR */ +} + +.views-ui-display-tab-bucket .icon-text { + padding-left: 25px; /* LTR */ +} + +.views-ui-display-tab-bucket .icon-linked { + background-position: 6px -151px; /* LTR */ +} + +.views-ui-display-tab-bucket .icon-unlinked { + background-position: 6px -193px; /* LTR */ +} diff --git a/css/views-admin.bartik-rtl.css b/css/views-admin.bartik-rtl.css new file mode 100644 index 00000000..abf2a308 --- /dev/null +++ b/css/views-admin.bartik-rtl.css @@ -0,0 +1,12 @@ +/** + * The .bartik.css file is intended to contain styles that override declarations + * in the Bartik theme. + */ + + /* @group Lists */ + +.views-display-top .secondary .action-list { + padding-right: 0; +} + +/* @end */ diff --git a/css/views-admin.bartik.css b/css/views-admin.bartik.css new file mode 100644 index 00000000..c6f06925 --- /dev/null +++ b/css/views-admin.bartik.css @@ -0,0 +1,233 @@ +/** + * The .bartik.css file is intended to contain styles that override declarations + * in the Bartik theme. + */ + +/* @group Lists */ + +.views-display-top .secondary .action-list { + padding-left: 0; /* LTR */ +} + +/* @end */ + +/* @group Attachment details tabs + * + * The tabs that switch between sections + */ + +.views-displays .region-content .secondary, +.views-displays .region-content .secondary { + padding-bottom: 0; + padding-left: 0; +} + +.views-displays .secondary a { + font-size: smaller; +} + +.views-displays .secondary > li a { + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.views-displays .secondary > li.open a { + -moz-border-radius: 5px 5px 0 0; + -webkit-border-bottom-left-radius: 0; + -webkit-border-bottom-right-radius: 0; + -webkit-border-top-left-radius: 5px; + -webkit-border-top-right-radius: 5px; + border-radius: 5px 5px 0 0; +} + +.views-displays .secondary .open > a:hover { + color: #0071B3; +} + +.views-displays .secondary input.form-submit { + font-size: smaller; +} + +/* @end */ + +/* @group Modal dialog box + * + * The contents of the popup dialog on the views edit form. + */ + +.views-filterable-options .even .form-type-checkbox { + background-color: #F9F9F9; +} + +.views-ui-dialog .ui-dialog-titlebar-close, +.views-ui-dialog #views-ajax-title, +.views-ui-dialog .views-override, +.views-ui-dialog .form-buttons { + background-color: #f6f6f6; +} + +.views-ui-dialog a { + color: #0071b3; +} + +.views-ui-dialog a:hover, +.views-ui-dialog a:focus { + color: #018fe2; +} + +/* @end */ + +/* @group CTools */ + +/* @group Buttons */ + +.ctools-button-processed { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(249, 249, 249, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + padding-bottom: 1px; + padding-top: 1px; +} + +.ctools-button-processed:hover { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(241, 241, 241, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); +} + +.ctools-button-processed li a, +.views-ui-display-tab-actions .ctools-button-processed input { + padding-left: 9px; + padding-right: 9px; +} + +.ctools-content ul.actions { + padding-bottom: 0; +} + +.ctools-dropbutton-processed.open:hover { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(249, 249, 249, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); +} + +.ctools-dropbutton-processed.open { + -moz-box-shadow: 1px 1px 2px rgba(0,0,0,0.25); + -webkit-box-shadow: 1px 1px 2px rgba(0,0,0,0.25); + box-shadow: 1px 1px 2px rgba(0,0,0,0.25); +} + +.ctools-twisty { + top: 0.6667em; +} + +.ctools-dropbutton-processed.open .ctools-twisty { + top: 0.3333em; +} + +.ctools-dropbutton-processed li a, +.views-ui-display-tab-actions .ctools-dropbutton-processed input { + padding-right: 7px; +} + +.views-ui-display-tab-actions .ctools-button-processed input.form-submit { + margin-right: 0; + margin-top: 0; +} + +/* @end */ + +/* @group Collapsible */ + +.ctools-toggle { + margin-top: 0.9em; +} + +.ctools-toggle.ctools-toggle-collapsed { + margin-top: 0.72em; +} + +.views-display-column > .ctools-toggle { + margin-top: 14px; +} + +.views-display-column > .ctools-toggle.ctools-toggle-collapsed { + margin-top: 12px; +} + +.views-ui-display-tab-actions .ctools-button input { + color: #0071B3; +} + +.views-ui-display-tab-actions .ctools-button input:hover, +.views-ui-display-tab-actions .ctools-button input:focus { + color: #018FE2; +} + +/* @end */ + +/* @end */ diff --git a/css/views-admin.contextual.css b/css/views-admin.contextual.css new file mode 100644 index 00000000..502e9494 --- /dev/null +++ b/css/views-admin.contextual.css @@ -0,0 +1,63 @@ +/** + * The .contextual.css file is intended to contain styles that override declarations + * in the Contextual module. + */ + +/* @group Wrapper */ + +#views-live-preview .contextual-links-region-active { + outline: medium none; +} + +#views-live-preview div.contextual-links-wrapper { + right: auto; + top: auto; +} + +html.js #views-live-preview div.contextual-links-wrapper { + display: inline; +} + +/* @end */ + +/* @group Trigger */ + +#views-live-preview a.contextual-links-trigger { + display: block; +} + +/* @end */ + +/* @group List */ + +div.contextual-links-wrapper ul.contextual-links { + -moz-border-radius: 0 4px 4px 4px; + -webkit-border-radius: 0 4px 4px 4px; + border-radius: 0 4px 4px 4px; + min-width: 10em; + padding: 6px 6px 9px 6px; + right: auto; +} + +ul.contextual-links li a, +ul.contextual-links li span { + padding-bottom: 0.25em; + padding-right: 0.1667em; + padding-top: 0.25em; +} + +ul.contextual-links li span { + font-weight: bold; +} + +ul.contextual-links li a { + color: #666666 !important; + margin: 0.25em 0; + padding-left: 1em; +} + +ul.contextual-links li a:hover { + background-color: #badbec; +} + +/* @end */ diff --git a/css/views-admin.css b/css/views-admin.css new file mode 100644 index 00000000..7ce70882 --- /dev/null +++ b/css/views-admin.css @@ -0,0 +1,361 @@ +/** + * The .css file is intended to only contain positioning and size declarations + * For example: display, position, float, clear, and overflow. + */ + +/* @group Resets */ + +.views-admin ul, +.views-admin menu, +.views-admin dir { + padding-left: 0; /* LTR for IE */ + /* padding-start is used so that RTL works out of the box */ + -moz-padding-start: 0; + -webkit-padding-start: 0; + padding-start: 0; +} + +.views-admin pre { + margin-bottom: 0; + margin-top: 0; + white-space: pre-wrap; +} + +/* @end */ + +/* @group Inline lists */ + +.horizontal > * { + clear: none; + float: left; /* LTR */ +} + +.horizontal.right { + float: right; +} + +.horizontal label { + position: absolute; +} + +.horizontal .form-item > [class] { + margin-top: 25px; +} + +.horizontal .form-item > [class] + [class] { + margin-top: 0; +} + +/* @end */ + +/* @group Columns */ + +.views-left-25 { + float: left; + width: 25%; +} + +.views-left-30 { + float: left; + width: 30%; +} + +.views-left-40 { + float: left; + width: 40%; +} + +.views-left-50 { + float: left; + width: 50%; +} + +.views-left-75 { + float: left; + width: 75%; +} + +.views-right-50 { + float: right; + width: 50%; +} + +.views-right-60 { + float: right; + width: 60%; +} + +.views-right-70 { + float: right; + width: 70%; +} + +.views-group-box .form-item { + margin-left: 3px; + margin-right: 3px; +} + +/* @end */ + +/* @group Attachment details + * + * The attachment details section, its tabs for each section and the buttons + * to add a new section + */ + +.form-edit .form-actions { + float: right; /* LTR */ +} + +.views-displays { + clear: both; +} + +/* @end */ + +/* @group Attachment details tabs + * + * The tabs that switch between sections + */ + .views-displays .secondary { + border-bottom: 0 none; + margin: 0; + overflow: visible; + padding: 0; + } + +.views-displays .secondary > li { + border-right: 0 none; + display: inline-block; + float: left; /* LTR */ + padding: 0; +} + +.views-displays .secondary .open > a { + position: relative; + z-index: 51; +} + +.views-displays .views-display-deleted-link { + text-decoration: line-through; +} + +.views-display-deleted > fieldset > legend, +.views-display-deleted .fieldset-wrapper > .views-ui-display-tab-bucket > *, +.views-display-deleted .views-display-columns { + opacity: 0.25; +} + +.views-display-disabled > fieldset > legend, +.views-display-disabled .fieldset-wrapper > .views-ui-display-tab-bucket > *, +.views-display-disabled .views-display-columns { + opacity: 0.5; +} + +.views-display-tab .fieldset-wrapper > .views-ui-display-tab-bucket .actions { + opacity: 1.0; +} +/* @end */ + +/* @group Attachment details new section button */ + +.views-displays .secondary li.add { + position: relative; +} + +.views-displays .secondary .action-list { + left: 0; /* LTR */ + margin: 0; + position: absolute; + top: 23px; + z-index: 50; +} + +.views-displays .secondary .action-list li { + display: block; +} + +/* @end */ + +/* @group Attachment details collapsible fieldset */ + +.views-display-tab .fieldset-legend { + left: -5px; /* LTR */ + position: relative; +} + +.views-display-tab .fieldset-wrapper { + position: relative; +} + +/* @end */ + +/* @group Attachment details actions + * + * Display the "Delete" and "Duplicate" buttons to the right. + */ +.views-display-tab .fieldset-wrapper > .views-ui-display-tab-bucket .actions { + position: absolute; + right: 0; /* LTR */ + top: -5px; +} + +/* @end */ + +/* @group Attachment configuration columns */ + +.views-display-columns > * { + float: left; /* LTR */ + margin-left: 1%; /* LTR */ + padding-left: 1%; /* LTR */ + width: 32%; +} + +.views-display-columns > *:first-child { + margin-left: 0; /* LTR */ + padding-left: 0; /* LTR */ +} + +/* @end */ + +/* @group Modal dialog box */ + +.views-ui-dialog { + /* We need this so the button is visible. */ + overflow: visible; + position: fixed; +} + +.views-ui-dialog .ui-dialog-titlebar-close { + border: 1px solid transparent; + display: block; + margin: 0; + padding: 0; + position: absolute; + right: 0; + top: 2px; + /* Make sure this is in front of the modal backdrop. */ + z-index: 1002; +} + +.views-ui-dialog .ui-dialog-titlebar { + padding: 0; + margin: 0; +} + +.views-ui-dialog .ui-dialog-title { + display: none; +} + +.views-ui-dialog #views-ajax-popup { + padding: 0; + overflow: hidden; +} + +.views-ui-dialog #views-ajax-title, +.views-ui-dialog #views-ajax-body { + margin: 0; + padding: 0; +} + +.views-ui-dialog #views-ajax-popup { + overflow: hidden; +} + +.views-ui-dialog .scroll { + max-height: 400px; + overflow: auto; +} + +#views-filterable-options-controls { + display: none; +} + +.views-ui-dialog #views-filterable-options-controls { + display: block; +} + +/* Don't let the messages overwhelm the modal */ +.views-ui-dialog .views-messages { + max-height: 200px; + overflow: auto; +} + +/* @end */ + +/* @group Settings forms */ + +.views-display-setting .label, +.views-display-setting .views-ajax-link { + display: inline-block; + float: left; /* LTR */ +} + +/* @end */ + +/* @group Filter Settings form */ + +div.form-item-options-value-all { + display: none; +} +/* @end */ + +/* @group Drupal overrides */ + +/* The .progress-disabled class added to the form on submit floats the element + * left and causes the form width to shrink-wrap to the content. Setting the + * width to 100% prevents this. + */ +#views-ajax-body form { + width: 100%; +} + +/* @end */ + + + +/* @group Javascript dependent styling */ + +.js-only { + display: none; +} + +html.js .js-only { + display: inherit; +} + +html.js span.js-only { + display: inline; +} + +/* @end */ + +/* @group AJAX throbber */ + +/* Base Page */ +#views-ui-list-page .ajax-progress-throbber, +.views-admin .ajax-progress-throbber { + /* Can't do center:50% middle: 50%, so approximate it for a typical window size. */ + left: 49%; + position: fixed; + top: 48.5%; + z-index: 1000; +} +#views-ui-list-page .ajax-progress-throbber .message, +.views-admin .ajax-progress-throbber .message { + display: none; +} + +/* Modal */ +#views-ajax-popup .ajax-progress-throbber { + /* Can't do center:50% middle: 50%, so approximate it for a typical window size. */ + left: 49%; + position: fixed; + top: 48.5%; + z-index: 1000; +} +#views-ajax-popup .ajax-progress-throbber .message { + display: none; +} + +/* @end */ diff --git a/css/views-admin.ctools-rtl.css b/css/views-admin.ctools-rtl.css new file mode 100644 index 00000000..338b2fb7 --- /dev/null +++ b/css/views-admin.ctools-rtl.css @@ -0,0 +1,82 @@ +/* @group Buttons */ + +.ctools-dropbutton .ctools-content { + border-left: 1px solid #e8e8e8; +} + +.ctools-content ul.actions { + padding-left: auto; + padding-right: 0; +} + +.ctools-dropbutton .ctools-link { + border-right: 1px solid #ffffff; +} + +.ctools-dropbutton li { + padding-left: 9px; + padding-left: auto; +} + +.views-display-top .ctools-button { + left: 12px; + right: auto; +} + +.views-ui-display-tab-bucket .ctools-button { + left: 5px; + right: auto; +} + +/* @end */ + +/* @group Collapsible */ + +.ctools-toggle { + float: right; + margin-left: 2px; + margin-right: 0; +} + +.ctools-toggle.ctools-toggle-collapsed { + border-left: 0; + border-right: 4px solid; + border-left-color: transparent; + border-width: 5px 5px 5px 0; + margin-left: 5px; + margin-right: 2px; +} + +.ctools-export-ui-row label { + float: right; +} + +.views-display-column > .ctools-toggle { + margin-left: 3px; + margin-right: 6px; +} +.views-display-column > .ctools-toggle.ctools-toggle-collapsed { + margin-left: 5px; + margin-right: 9px; +} + +/* @end */ + +/* @group Dependent */ + +.dependent-options { + margin-left: 0; + margin-right: 18px; +} + +/* @end */ + +/* @group Export */ + +/* Override for filter button on the views list screen */ +#ctools-export-ui-list-form .form-submit { + margin-left: 0em; + margin-right: auto; +} + +/* @end */ diff --git a/css/views-admin.ctools.css b/css/views-admin.ctools.css new file mode 100644 index 00000000..b1f0e299 --- /dev/null +++ b/css/views-admin.ctools.css @@ -0,0 +1,232 @@ +/* @group Buttons */ + +.ctools-button-processed { + background-color: #ffffff; + border-color: #cccccc; + font-size: 11px; + padding-bottom: 2px; + padding-top: 2px; +} + +.ctools-button-processed:hover { + border-color: #b8b8b8; +} + +.ctools-button-processed:active { + border-color: #a0a0a0; +} + +.ctools-button-processed .ctools-content { + padding-bottom: 0; + padding-top: 0; +} + +.ctools-dropbutton-processed { + position: absolute; +} + +.ctools-dropbutton-processed .ctools-content { + border-right: 1px solid #e8e8e8; +} + +.ctools-dropbutton-processed .ctools-content ul { + margin: 0; + padding: 0; +} + +.ctools-content ul.actions { + margin-top: 0; + margin-bottom: 0; + padding-left: 0; +} + +.ctools-button-processed .ctools-content a { + background-image: none; + border: medium none; +} + +.ctools-dropbutton-processed.open:hover { + border-color: #D0D0D0; +} + +.ctools-dropbutton-processed.open { + z-index: 100; +} + +.ctools-dropbutton-processed .ctools-link { + border-left: 1px solid #ffffff; +} + +.ctools-dropbutton-processed.open .ctools-content { + padding-bottom: 4px; +} + +.ctools-dropbutton-processed li a, +.ctools-dropbutton-processed li input { + padding-right: 9px; +} + +.ctools-dropbutton-processed.open li + li { + border-top: 1px solid #efefef; + margin-top: 4px; + padding-bottom: 0; + padding-top: 4px; +} + +.ctools-twisty:focus { + outline: medium none; +} + +.ctools-no-js .ctools-content ul { + margin-bottom: 0; + margin-top: 0; + padding-left: 0; +} + +.views-display-top .ctools-button-processed { + font-size: 12px; + position: absolute; + right: 12px; + top: 7px; +} + +.views-ui-display-tab-bucket .ctools-button-processed { + position: absolute; + right: 5px; + top: 4px; +} + +.views-ui-display-tab-actions .ctools-button-processed li a, +.views-ui-display-tab-actions .ctools-button-processed input { + background: none; + border: medium; + font-family: inherit; + font-size: 12px; + padding-bottom: 0; + padding-left: 12px; + padding-top: 0; + margin-bottom: 0; +} + +.views-ui-display-tab-actions .ctools-button-processed input:hover { + background: none; +} + +/* @end */ + +/* @group Collapsible */ + +.ctools-toggle { + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + border-style: solid; + border-width: 5px 5px 0; + display: inline-block; + float: left; + height: 0; + margin-right: 2px; + margin-top: 0.4545em; + width: 0; +} + +.ctools-toggle.ctools-toggle-collapsed { + border-bottom-color: transparent; + border-left: 4px solid; + border-right-color: transparent; + border-top-color: transparent; + border-width: 5px 0 5px 5px; + margin-left: 2px; + margin-right: 5px; + margin-top: 0.3333em; +} + +.ctools-toggle:hover, +.ctools-collapsible-handle:hover { + cursor: pointer; +} + +.ctools-export-ui-row { + margin-bottom: 0; + padding-top: 0; +} + +.ctools-export-ui-row label { + display: block; + float: left; + width: 55px; +} + +.views-display-settings .ctools-toggle { + color: #000000; +} + +.views-display-column > .ctools-toggle { + margin-left: 6px; + margin-right: 3px; + margin-top: 10px; +} +.views-display-column > .ctools-toggle.ctools-toggle-collapsed { + margin-left: 9px; + margin-right: 5px; + margin-top: 8px; +} + +.views-display-column > .ctools-collapsible-handle { + border-color: #F3F3F3; + border-style: solid; + border-width: 1px 1px 0; + font-size: 13px; + font-weight: normal; + margin: 0; + padding: 6px 3px; +} + +.views-display-column > .ctools-toggle.ctools-toggle-collapsed + .ctools-collapsible-handle { + border-width: 1px; +} + +.views-display-column > .ctools-collapsible-content > .views-ui-display-tab-bucket:first-child { + border-top: medium none; +} + +h2.ctools-collapsible-handle { + display: inline; + clear: both; +} + +/* @end */ + +/* @group Dependent */ + +.dependent-options { + margin-left: 18px; /* LTR */ +} + +/* @end */ + +/* @group Export */ + +/* Override for filter button on the views list screen */ +#ctools-export-ui-list-form .form-submit { + margin-top: 0em !important; + margin-right: 0em; +} + +.ctools-export-ui-row + .ctools-export-ui-row { + margin-top: 1em; +} + +.ctools-export-ui-fourth-row input { + margin-top: 5px !important; +} + +/* @end */ + +/* @group Jump list */ + +#views-live-preview .ctools-jump-menu-select{ + max-width: 450px; +} + +/* @end */ diff --git a/css/views-admin.garland-rtl.css b/css/views-admin.garland-rtl.css new file mode 100644 index 00000000..9a8de6dc --- /dev/null +++ b/css/views-admin.garland-rtl.css @@ -0,0 +1,13 @@ +/** + * The .garland.css file is intended to contain styles that override declarations + * in the Garland theme. + */ + + /* @group Lists */ + +.views-displays .secondary .action-list { + left: auto; + right: 1px; +} + + /* @end */ diff --git a/css/views-admin.garland.css b/css/views-admin.garland.css new file mode 100644 index 00000000..cd9010a5 --- /dev/null +++ b/css/views-admin.garland.css @@ -0,0 +1,263 @@ +/** + * The .garland.css file is intended to contain styles that override declarations + * in the Garland theme. + */ + +/* @group Attachment details tabs + * + * The tabs that switch between sections + */ + +.views-displays .region-content .secondary, +.views-displays .region-content .secondary { + padding-bottom: 0; + padding-left: 0; +} + +.views-displays .secondary .action-list { + left: 1px; + top: 20px; +} + +.views-displays .secondary .action-list li, +.views-displays .secondary a { + border-color: #e9e9e9; +} + +.views-displays .secondary a { + font-size: 12px; + padding: 2px 7px; +} + +.views-displays ul.secondary li a:hover, +.views-displays ul.secondary li.active a { + border: 1px solid transparent; + padding: 2px 7px; +} + +.views-displays .secondary > li a { + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.views-displays .secondary > li.open a { + background-image: none; + -moz-border-radius: 5px 5px 0 0; + -webkit-border-bottom-left-radius: 0; + -webkit-border-bottom-right-radius: 0; + -webkit-border-top-left-radius: 5px; + -webkit-border-top-right-radius: 5px; + border-radius: 5px 5px 0 0; +} + +.views-displays .secondary .open > a:hover { + border-color: #e9e9e9 #e9e9e9 #f1f1f1 #e9e9e9; + border-width: 1px 1px 1px 1px; + color: #0071B3; +} + +.views-displays .secondary input.form-submit { + font-size: 11px; +} + +/* @end */ + +/* @group Attachment buckets + * + * These are the individual "buckets," or boxes, inside the display settings area + */ + +.views-ui-display-tab-bucket h3 { + font-weight: bold; +} + +/* @end */ + +/* @group Modal dialog box + * + * The contents of the popup dialog on the views edit form. + */ + +.views-filterable-options .form-type-checkbox { + margin: 0; +} + +.views-filterable-options .even .form-type-checkbox { + background-color: #F9F9F9; +} + +.views-ui-dialog .ui-dialog-titlebar-close, +.views-ui-dialog #views-ajax-title, +.views-ui-dialog .views-override, +.views-ui-dialog .form-buttons { + background-color: #f6f6f6; +} + +/* @end */ + +/* @group CTools */ + +/* @group Buttons */ + +.ctools-button-processed { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(249, 249, 249, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + padding-bottom: 1px; + padding-top: 1px; +} + +.ctools-button-processed:hover { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(241, 241, 241, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); +} + +.ctools-button-processed ol li, +.ctools-button-processed ul li { + margin: 0; + padding-bottom: 0; +} + +.views-ui-display-tab-actions .ctools-button-processed input { + margin-right: 0; +} + +.ctools-content ul.actions { + padding-bottom: 0; +} + +.ctools-dropbutton-processed.open:hover { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(249, 249, 249, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); +} + +.ctools-dropbutton-processed.open { + -moz-box-shadow: 1px 1px 2px rgba(0,0,0,0.25); + -webkit-box-shadow: 1px 1px 2px rgba(0,0,0,0.25); + box-shadow: 1px 1px 2px rgba(0,0,0,0.25); +} + +.ctools-twisty { + top: 0.6667em; +} + +.ctools-dropbutton-processed.open .ctools-twisty { + top: 0.3333em; +} + +/* @end */ + +/* @group Dependent */ + +.dependent-options, +.form-checkboxes.dependent-options, +.form-radios.dependent-options, +.form-checkboxes .form-item.dependent-options, +.form-radios .form-item.dependent-options { + margin-left: 25px; +} + +/* @end */ + +/* @group Collapsible */ + +.ctools-toggle { + margin-top: 0.5em; +} + +.ctools-toggle, +.views-display-settings .ctools-toggle { + color: #494949; +} + +.ctools-toggle.ctools-toggle-collapsed { + margin-top: 0.25em; +} + +.views-display-column > .ctools-toggle { + margin-top: 14px; +} + +.views-display-column > .ctools-toggle.ctools-toggle-collapsed { + margin-top: 12px; +} + +.views-ui-display-tab-actions .ctools-button input { + color: #027AC6; + font-size: 12px; +} + +.views-ui-display-tab-actions .ctools-button input:hover, +.views-ui-display-tab-actions .ctools-button input:focus { + color: #0062A0; +} + +/* @end */ + +/* @end */ diff --git a/css/views-admin.seven-rtl.css b/css/views-admin.seven-rtl.css new file mode 100644 index 00000000..abdd5aa2 --- /dev/null +++ b/css/views-admin.seven-rtl.css @@ -0,0 +1,43 @@ +/** + * The .seven.css file is intended to contain styles that override declarations + * in the Seven admin theme. + */ + +/* @group Forms */ + +.views-admin .form-submit, +.views-admin a.button { + margin-left: 0; +} + +/* @end */ + +/* @group Lists */ + +.views-admin .links li { + padding-left: 0; +} + +/* @end */ + +/* @group Attachments */ + +.views-displays .secondary { + text-align: right; +} + +/* @end */ + +/* @group Attachment details tabs */ + +.views-display-top ul.secondary { + float: right; +} + +.views-displays .secondary .action-list li:first-child { + -moz-border-radius: 7px 0 0 0; + -webkit-border-radius: 7px 0 0 0; + border-radius: 7px 0 0 0; +} + +/* @end */ diff --git a/css/views-admin.seven.css b/css/views-admin.seven.css new file mode 100644 index 00000000..d58bb1be --- /dev/null +++ b/css/views-admin.seven.css @@ -0,0 +1,552 @@ +/** + * The .seven.css file is intended to contain styles that override declarations + * in the Seven admin theme. + */ + +/* @group Content */ + +.views-ui-display-tab-bucket h1, +.views-ui-display-tab-bucket h2, +.views-ui-display-tab-bucket h3, +.views-ui-display-tab-bucket h4, +.views-ui-display-tab-bucket h5 { + margin-bottom: 0; + margin-top: 0; +} + +/* @end */ + +/* @group Forms */ + +.views-ui-dialog fieldset { + padding-top: 2.5em; +} + +fieldset fieldset { + border: medium none; +} + +/** + * Seven positions the legend absolutely, but does not have a way to ignore + * fieldsets without a legend so we make one up. + */ +fieldset.fieldset-no-legend { + padding-top: 0; +} + +/** + * Being extra safe here and scoping this to the add view wizard form (where + * a layout problem occurs for the Display format fieldset if we don't fix its + * padding), but it's probably safe to just let it apply everywhere. + */ +#views-ui-add-form fieldset fieldset .fieldset-wrapper { + padding-left: 0; + padding-right: 0; +} + +.views-display-tab fieldset { + padding: 0 12px; +} + +.views-display-tab .fieldset-wrapper { + padding: 10px 12px 12px; +} + +.views-display-tab fieldset.box-padding .fieldset-wrapper { + padding: 0; +} + +.views-display-tab legend + .fieldset-wrapper { + padding-top: 2.5em; +} + +.views-admin .form-item label.option, +#views-ui-preview-form .form-item label.option { + font-size: 1em; +} + +#views-ui-preview-form .form-submit { + margin-top: 3px; +} + +.views-admin input.form-submit, +.views-ui-dialog input.form-submit, +.views-admin a.button, +.views-ui-dialog a.button { + margin-bottom: 0; + margin-right: 0; /* LTR */ + margin-top: 0; +} + +/* Override for a button on the edit display screen */ +#edit-displays-preview-controls .form-submit { + display: inline-block; + margin-right: 1em; +} + +/* Override for filter button on the views list screen */ +#ctools-export-ui-list-form .form-submit { + margin-bottom: 0; +} + +#ctools-export-ui-list-form .ctools-export-ui-first-row .form-item { + margin-top: 3px; + margin-right: 5px; /* LTR */ +} + +.form-item, +.form-item .form-item { + margin-bottom: 0; + margin-top: 9px; + padding-bottom: 0; + padding-top: 0; +} + +.form-actions { + margin-bottom: 0; + margin-top: 0; +} + +.form-item .form-item { + padding-bottom: 0; + padding-top: 0; +} + +.form-radios > .form-item { + margin-top: 3px; +} + +/* @group Dependent options + * + * Dependent options are identified in CTools dependent.js + */ + +/* The .dependent-options.form-item is necessary to supercede the Seven .form-item + * reset declaration that sets the margin to zero. + */ +.dependent-options, +.dependent-options.form-item, +.form-item-options-expose-required, +.form-item-options-expose-label, +.form-item-options-expose-description { + margin-left: 1.5em; +} + +.views-admin-dependent .form-item .form-item, +.views-admin-dependent .form-type-checkboxes, +.views-admin-dependent .form-type-radios, +.views-admin-dependent .dependent-options, +.views-admin-dependent .form-item .form-item, +.views-admin-dependent .dependent-options .form-type-select, +.views-admin-dependent .dependent-options .form-type-textfield, +.form-item-options-expose-required, +.form-item-options-expose-label, +.form-item-options-expose-description { + margin-bottom: 6px; + margin-top: 6px; +} + +.views-admin-dependent .form-type-radio, +.views-admin-dependent .form-radios .form-item { + margin-bottom: 2px; + margin-top: 2px; +} + +/* @end */ + +/* @end */ + +/* @group Lists */ + +.views-admin ul.secondary, +.views-admin .item-list ul { + margin: 0; + padding: 0; +} + +.views-admin ul.secondary { + clear: none; +} + +.views-displays ul.secondary li a, +.views-displays ul.secondary li.active a, +.views-displays ul.secondary li.active a.active { + padding: 2px 7px 3px; +} + +.views-displays ul.secondary li.active a, +.views-displays ul.secondary li.active a.active { + border: 1px solid transparent; +} + +.views-admin .links li { + padding-right: 0; /* LTR */ +} + +.views-admin .button .links li { + padding-right: 12px; /* LTR */ +} + +.page-admin-structure-views #content ul.action-links { + padding-left: 0; + padding-right: 0; +} + +.views-display-top ul.secondary { + background-color: transparent; + float: left +} + +.views-display-top .secondary .action-list li { + float: none; + margin: 0; +} + +/* @end */ + +/* @group Buttons */ + +.ctools-button-processed ul { + margin: 0; +} + +/* Override for input elements that are themed like ctools-buttons */ +.ctools-button-processed input.form-submit:hover { + background-image: none; + color: #0074BD; + text-shadow: none; +} + +.ctools-button-processed input.form-submit:active { + background: none; + border: medium none; + color: #0074BD; + text-shadow: none; +} + +/* @end */ + +/* @group Tables */ + +table td, +table th { + vertical-align: top; +} + +/* @end */ + +/* @group Attachment details */ + +#edit-display-settings-title { + color: #008BCB; +} + +/* @end */ + +/* @group Attachment details tabs + * + * The tabs that switch between sections + */ + +.views-displays .secondary { + text-align: left; /* LTR */ +} + +.views-displays .secondary > li:first-child { + padding-left: 0; +} + +.views-admin .icon.add { + background-position: center 3px; +} + +.views-displays .secondary a { + background-color: #f1f1f1; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + color: #008BCB; +} + +.views-displays .secondary a:hover > .icon.add { + background-position: center -25px; +} + +.views-displays .secondary .open > a { + -moz-border-radius: 7px 7px 0 0; + -webkit-border-radius: 7px 7px 0 0; + border-radius: 7px 7px 0 0; +} + +.views-displays .secondary .open > a:hover { + background-color: #f1f1f1; + color: #008BCB; +} + +.views-displays .secondary .action-list li:first-child { + -moz-border-radius: 0 7px 0 0; + -webkit-border-radius: 0 7px 0 0; + border-radius: 0 7px 0 0; +} + +.views-displays .secondary .action-list li:last-child { + -moz-border-radius: 0 0 7px 7px; + -webkit-border-radius: 0 0 7px 7px; + border-radius: 0 0 7px 7px; +} + +.views-displays .secondary .action-list input.form-submit { + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + color: #008BCB; +} + +/* @end */ + +/* @group Attachment buckets + * + * These are the individual "buckets," or boxes, inside the display settings area + */ + +.views-ui-display-tab-bucket h3 { + font-size: 12px; + text-transform: uppercase; +} + +.views-ui-display-tab-bucket .links { + padding: 2px 6px 4px; +} + +.views-ui-display-tab-bucket .links li + li { + margin-left: 3px; +} + +/* @end */ + +/* @group Rearrange filter criteria */ + +#views-ui-rearrange-filter-form .action-links { + margin: 0; + padding: 0; +} + +#views-ui-rearrange-filter-form table { + border: medium none; +} + +#views-ui-rearrange-filter-form [id^="views-row"] { + border: medium none; +} + +#views-ui-rearrange-filter-form tr td:last-child { + border-right: medium none; +} + +#views-ui-rearrange-filter-form .filter-group-operator-row { + border-left: 1px solid transparent !important; + border-right: 1px solid transparent !important; +} + +#views-ui-rearrange-filter-form tr.drag td { + background-color: #FFEE77 !important; +} + +#views-ui-rearrange-filter-form tr.drag-previous td { + background-color: #FFFFBB !important; +} + +/* @end */ + +/* @group Live preview elements */ + +.views-query-info pre { + margin-bottom: 0; + margin-top: 0; +} + +/* @group Query info table */ + +.views-query-info table { + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + -webkit-border-horizontal-spacing: 1px; + -webkit-border-vertical-spacing: 1px; +} + +.views-query-info table tr td:last-child { + /* Fixes a Seven style that bleeds down into this table unnecessarily */ + border-right: 0 none; +} + +/* @end */ + +/* @end */ + +/* @group Add view */ + +.form-item-page-create, +.form-item-block-create { + margin-top: 13px; +} + +/* @end */ + +/* @group Modal dialog box + * + * The contents of the popup dialog on the views edit form. + */ + +.views-ui-dialog .ui-dialog-titlebar-close { + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; + border-color: #cccccc; + right: -27px; + top: -1px; +} + +.views-ui-dialog fieldset.collapsible { + padding-top: 1.5em; +} + +.views-ui-dialog fieldset.collapsed { + padding-top: 2.5em; +} + +.filterable-option .form-item.form-type-checkbox { + /* This selector is aggressive because Seven's reset for .form-items is aggressive. */ + padding-bottom: 4px; + padding-left: 4px; + padding-top: 4px; +} + +/* @end */ + +/* @group CTools */ + +/* @group Buttons */ + +.ctools-button-processed { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(249, 249, 249, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + -moz-border-radius: 11px 11px 11px 11px; + -webkit-border-radius: 11px 11px 11px 11px; + border-radius: 11px 11px 11px 11px; +} + +.ctools-button-processed:hover { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(241, 241, 241, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f1f1f1 100%); +} + +.ctools-dropbutton-processed.open:hover { + background-image: + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(249, 249, 249, 1.0)) + ); + background-image: + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); + background-image: + linear-gradient( + -90deg, + #ffffff 0px, + #f9f9f9 100%); +} + +.ctools-dropbutton-processed.open { + -moz-box-shadow: 1px 1px 2px rgba(0,0,0,0.25); + -webkit-box-shadow: 1px 1px 2px rgba(0,0,0,0.25); + box-shadow: 1px 1px 2px rgba(0,0,0,0.25); +} + +/* @end */ + +/* @group Collapsible */ + +.ctools-toggle { + margin-top: 0.6667em; +} + +.ctools-toggle.ctools-toggle-collapsed { + margin-top: 0.5em; +} + +.views-display-settings .ctools-toggle { + color: #008BCB; +} + +.views-display-column > .ctools-toggle { + margin-top: 14px; +} + +.views-display-column > .ctools-toggle.ctools-toggle-collapsed { + margin-top: 12px; +} + +.views-display-column > .ctools-collapsible-handle { + color: #008BCB; +} + +.views-ui-display-tab-actions .ctools-button-processed input { + color: #0074BD; +} + +/* @end */ + +/* @end */ diff --git a/css/views-admin.theme-rtl.css b/css/views-admin.theme-rtl.css new file mode 100644 index 00000000..f966432f --- /dev/null +++ b/css/views-admin.theme-rtl.css @@ -0,0 +1,208 @@ +/** + * The .theme.css file is intended to contain presentation declarations including + * images, borders, colors, and fonts. + */ + +/* @end */ + +/* @group Icons */ + +.actions a, +.views-admin .icon, +.views-admin .icon-text { + background-position: right top; +} + +/* Targets any element with an icon -> text combo */ +.views-admin .icon-text { + padding-right: 19px; +} + +.views-admin .icon-linked { + background-position: right -153px; +} + +.views-admin .icon-unlinked { + background-position: right -195px; +} + +.actions .views-button-add { + background-position: right -39px; +} + +.actions .views-button-rearrange { + background-position: right -96px; +} + +.actions .views-button-add:hover { + background-position: right -58px; +} + +.actions .views-button-rearrange:hover { + background-position: right -115px; +} + +.actions .views-button-add:active { + background-position: right -77px; +} + +.actions .views-button-rearrange:active { + background-position: right -134px; +} + +.views-displays .icon-add { + background-position: right -3px; +} + +.views-displays .secondary a:hover > .icon-add { + background-position: right -21px; +} + +.views-displays .secondary .open a:hover > .icon-add { + background-position: right -3px; +} + +/* @end */ + +/* @group Forms */ + +.form-submit + .form-submit, +.views-admin a.button + a.button { + margin-right: 1em; +} + +.container-inline > * + *, +.container-inline .fieldset-wrapper > * + * { + padding-left: 0; + padding-right: 4pt; +} + +.views-admin .form-type-checkbox + .form-wrapper { + margin-right: 16px; +} + +/* @end */ + +/* @group Lists */ + +.horizontal > * + * { + margin-right: 9px; + padding-right: 9px; +} + +/* @end */ + +/* @group Attachments */ + +.views-displays .secondary { + padding: 6px 8px 8px; +} + +.views-displays .views-display-top > ul > li + li { + margin-right: 3px; +} + +.views-displays .views-extra-actions { + left: 10px; +} + +/* @end */ + +/* @group Attachment details tabs + * + * The tabs that switch between sections + */ + +.views-displays .secondary .action-list li:first-child { + -moz-border-radius: 7px 0 0 0; + -webkit-border-top-left-radius: 7px; + -webkit-border-top-right-radius: 0; + border-radius: 7px 0 0 0; +} + +/* @end */ + +/* @group Attachment details collapsible fieldset + * + * The attachment details section is a collapsible fieldset, but should not + * have a border around it. + */ + +.views-display-tab .fieldset-legend { + left: auto; + right: -5px; +} + +/* @end */ + +/* @group Auto preview + * + * The auto-preview checkbox line. This may have more stuff added to it. + */ + +div.form-item-displays-live-preview { + text-align: left; +} + +/* @end */ + +/* @group Attachment buckets + * + * These are the individual "buckets," or boxes, inside the three columns in the + * attachment details section. + */ + +.views-ui-display-tab-bucket .icon-text { + padding-right: 25px; +} + +/* @end */ + +/* @group Attachment bucket rows + * + * This is each row within one of the "boxes." + */ + +.views-display-setting .label { + margin-left: 3pt; +} + +/* @end */ + +/* @group Modal dialog box + * + * The contents of the popup dialog on the views edit form. + */ + +#views-filterable-options-controls .form-item { + margin-left: 2%; +} + +.views-ui-dialog #views-progress-indicator { + left: 10px; + right: auto; +} + +/* @end */ + +/* @group Rearrange filters + * + * Styling for the form that allows views filters to be rearranged. + */ +.views-operator-label { + padding-right: 0.5em; +} + +/* @end */ + +/* @group Live preview elements */ + +/* @group HTML list */ + +#views-live-preview .view-content > .item-list > ul { + padding-right: 21px; +} + +/* @end */ + +/* @end */ diff --git a/css/views-admin.theme.css b/css/views-admin.theme.css new file mode 100644 index 00000000..8611783f --- /dev/null +++ b/css/views-admin.theme.css @@ -0,0 +1,1083 @@ +/** + * The .theme.css file is intended to contain presentation declarations including + * images, borders, colors, and fonts. + */ + +/* @group Reset */ + +.views-admin .links { + list-style: none outside none; + margin: 0; +} + +.views-admin a:hover { + text-decoration: none; +} + +/* @end */ + +/* @group Layout */ + +.box-padding { + padding-left: 12px; + padding-right: 12px; +} + +.box-margin { + margin: 12px 12px 0 12px; +} + +/* @end */ + +/* @group Icons */ + +.views-admin .icon { + height: 16px; + width: 16px; +} + +.views-admin .icon, +.views-admin .icon-text { + background-attachment: scroll; + background-image: url("../images/sprites.png"); + background-position: left top; /* LTR */ + background-repeat: no-repeat; +} + +.views-admin a.icon { + background-image: + url("../images/sprites.png"), + -moz-linear-gradient( + -90deg, + #ffffff 0px, + #e8e8e8 100%); + background-image: + url("../images/sprites.png"), + -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0.0, rgba(255, 255, 255, 1.0)), + color-stop(1.0, rgba(232, 232, 232, 1.0)) + ); + background-image: + url("../images/sprites.png"), + -webkit-linear-gradient( + -90deg, + #ffffff 0px, + #e8e8e8 100%); + background-repeat: no-repeat, repeat-y; + border: 1px solid #dddddd; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + -moz-box-shadow: 0 0 0 rgba(0,0,0,0.3333) inset; + -webkit-box-shadow: 0 0 0 rgba(0,0,0,0.3333) inset; + box-shadow: 0 0 0 rgba(0,0,0,0.3333) inset; +} + +.views-admin a.icon:hover { + border-color: #d0d0d0; + -moz-box-shadow: 0 0 1px rgba(0,0,0,0.3333) inset; + -webkit-box-shadow: 0 0 1px rgba(0,0,0,0.3333) inset; + box-shadow: 0 0 1px rgba(0,0,0,0.3333) inset; +} + +.views-admin a.icon:active { + border-color: #c0c0c0; +} + +/** + * Targets a element inside an element. + * This assumes no visible text from the span. + */ +.views-admin span.icon { + display: inline-block; + float: left; + position: relative; +} + +.views-admin .icon.compact { + display: block; + overflow: hidden; + text-indent: -9999px; +} + +/* Targets any element with an icon -> text combo */ +.views-admin .icon-text { + padding-left: 19px; /* LTR */ +} + +.views-admin .icon.linked { + background-position: center -153px; +} + +.views-admin .icon.unlinked { + background-position: center -195px; +} + +.views-admin .icon.add { + background-position: center 3px; +} + +.views-admin a.icon.add { + background-position: center 3px, left top; +} + +.views-admin .icon.delete { + background-position: center -52px; +} + +.views-admin a.icon.delete { + background-position: center -52px, left top; +} + +.views-admin .icon.rearrange { + background-position: center -111px; +} + +.views-admin a.icon.rearrange { + background-position: center -111px, left top; +} + +.views-displays .secondary a:hover > .icon.add { + background-position: center -25px; +} + +.views-displays .secondary .open a:hover > .icon.add { + background-position: center 3px; +} + +/* @end */ + +/* @group Forms */ + +fieldset.box-padding { + border: none; +} + +.views-admin fieldset fieldset { + margin-bottom: 0; +} + +.form-item { + margin-top: 9px; + padding-bottom: 0; + padding-top: 0; +} + +.form-type-checkbox { + margin-top: 6px; +} + +input.form-checkbox, +input.form-radio { + vertical-align: baseline; +} + +.form-submit:not(.js-hide) + .form-submit, +.views-admin a.button:not(.js-hide) + a.button { + margin-left: 1em; /* LTR */ +} + +.container-inline { + padding-top: 15px; +} + +.container-inline > * + *, +.container-inline .fieldset-wrapper > * + * { + padding-left: 4pt; /* LTR */ +} + +.views-admin fieldset fieldset.container-inline { + margin-bottom: 1em; + margin-top: 1em; + padding-top: 0; +} + +.views-admin fieldset fieldset.container-inline > .fieldset-wrapper { + padding-bottom: 0; +} + +/* Indent form elements so they're directly underneath the label of the checkbox that reveals them */ +.views-admin .form-type-checkbox + .form-wrapper { + margin-left: 16px; /* LTR */ +} + +/* Hide 'remove' checkboxes. */ +.views-remove-checkbox { + display: none; +} + +/* sizes the labels of checkboxes and radio button to the height of the text */ +.views-admin .form-type-checkbox label, +.views-admin .form-type-radio label { + line-height: 2; +} + +/* @group Dependent options */ + +.views-admin-dependent .form-item { + margin-bottom: 6px; + margin-top: 6px; +} + +/* @end */ + +/* @end */ + +/* @group Lists */ + +.horizontal > * + * { + margin-left: 9px; /* LTR */ + padding-left: 9px; /* LTR */ +} + +.views-ui-view-title { + font-weight: bold; +} + +/* @end */ + +/* @group Messages */ + +.view-changed { + margin-bottom: 21px; +} + +/* @end */ + +/* @group Headings */ + +/* Intentionally targeting h1 */ +.views-admin h1.unit-title { + font-size: 15px; + line-height: 1.6154; + margin-bottom: 0; + margin-top: 18px; +} + +/* @end */ + +/* @group Tables */ + +table td, +table th { + vertical-align: top; +} + +/* @end */ + +/* @group List views */ + +/* These header classes are ambiguous and should be scoped to th elements */ + +th.views-ui-name { + width: 18%; +} + +th.views-ui-description { + width: 26%; +} + +th.views-ui-tag { + width: 8%; +} + +th.views-ui-path { + width: auto; +} + +th.views-ui-operations { + width: 24%; +} + +/* @end */ + +/* @group Add view */ + +/** + * Drupal core forces AJAX triggering elements to float left when they are + * disabled due to AJAX processing. On the add view page, we have inline + * containers where we don't want that behavior; it causes the select dropdown + * which is triggered to jump to the left while the AJAX throbber is active. + * + * See also http://drupal.org/node/769936 (Drupal core issue); when that is + * fixed it may no longer be necessary to do this. + */ +.views-admin .container-inline .progress-disabled { + float: none; +} + +/** + * I wish this didn't have to be so specific + */ +.form-item-description-enable + .form-item-description { + margin-top: 0; +} + +.form-item-description-enable label { + font-weight: bold; +} + +.form-item-page-create, +.form-item-block-create { + margin-top: 13px; +} + +.form-item-page-create label, +.form-item-block-create label { + font-weight: bold; +} + +/* This makes the form elements after the "Display Format" label flow underneath the label */ +.form-item-page-style-style-plugin > label, +.form-item-block-style-style-plugin > label { + display: block; +} + +.views-attachment .options-set label { + font-weight: normal; +} + +/* @end */ + +/* @group Rearrange filters + * + * Styling for the form that allows views filters to be rearranged. + */ + +.group-populated { + display: none; +} + +td.group-title { + font-weight: bold; +} + +.views-ui-dialog td.group-title { + margin: 0; + padding: 0; +} + +.views-ui-dialog td.group-title span { + display: block; + height: 1px; + overflow: hidden; +} + +.group-message .form-submit, +.views-remove-group-link, +#views-add-group { + float: right; + clear: both; +} + +.views-operator-label { + font-style: italic; + font-weight: bold; + padding-left: 0.5em; /* LTR */ + text-transform: uppercase; +} + +.grouped-description, +.exposed-description { + float: left; + padding-top: 3px; + padding-right: 10px; +} + +/* This keeps the collapsible fieldsets of options from crashing into the bottom + * of the edit option columns. Because the edit option columns are floated, the collapsible + * fieldsets need to be floated as well so that the margin above the fieldset interacts with + * the float edit option columns. + */ +#edit-options .collapsible { + float: left; + width: 100%; +} + +#edit-options-more { + clear: both; +} + +/* @end */ + +/* @group Attachments */ + +.views-displays { + border: 1px solid #CCC; + padding-bottom: 36px; +} + +.views-display-top { + background-color: #F9F9F9; + border-bottom: 1px solid #CCCCCC; + padding: 8px 8px 8px; /* LTR */ + position: relative; +} + +.views-display-top .secondary { + margin-right: 18em; +} + +.views-display-top .secondary > li { + margin-right: 6px; + padding-left: 0; +} + +.views-display-top .secondary > li:last-child { + margin-right: 0; +} + +#views-display-extra-actions li { + padding: 3px 9px; +} + +.views-display-top #views-display-top { + max-width: 180px; +} + +/* @end */ + +/* @group Attachment details tabs + * + * The tabs that switch between sections + */ + +ul#views-display-menu-tabs { + margin-right: 200px; +} + +ul#views-display-menu-tabs li { + margin-bottom: 5px; +} + +ul#views-display-menu-tabs li.add ul.action-list li{ + margin: 0; +} + +.views-displays .secondary a { + border: 1px solid #cbcbcb; + display: inline-block; + font-size: small; + line-height: 1.3333; + padding: 3px 7px; +} + +/** + * Display a red border if the display doesn't validate. + */ +.views-displays ul.secondary li.active a.active.error, +.views-displays .secondary a.error { + border: 2px solid #ED541D; + padding: 1px 6px; +} + +.views-displays .secondary a:focus { + outline: none; +} + +.views-displays .secondary a:hover, +.views-displays .secondary .active a { + background-color: #666666; + color: #ffffff; + border-bottom-width: 1px; +} + +.views-displays .secondary .open > a { + background-color: #f1f1f1; + border-bottom: 1px solid transparent; + position: relative; +} + +.views-displays .secondary .open > a:hover { + background-color: #f1f1f1; +} + +.views-displays .secondary .action-list li { + background-color: #f1f1f1; + border-color: #cbcbcb; + border-style: solid; + border-width: 0 1px; + padding: 2px 9px; +} + +.views-displays .secondary .action-list li:first-child { + border-width: 1px 1px 0; +} + +.views-displays .secondary .action-list li.last { + border-width: 0 1px 1px; +} + +.views-displays .secondary .action-list li:last-child { + border-width: 0 1px 1px; +} + +.views-displays .secondary .action-list input.form-submit { + background: none repeat scroll 0 0 transparent; + border: medium none; + margin: 0; + padding: 0; +} + +.views-displays .secondary .action-list li:hover { + background-color: #dddddd; +} + +/* @end */ + +/* @group Attachment details */ + +#edit-display-settings-title { + font-size: 14px; + line-height: 1.5; + margin: 0; +} + +#edit-display-settings-top { + padding-bottom: 4px; +} + +#edit-display-settings-content { + margin-top: 12px; +} + +#edit-display-settings-main { + margin-top: 15px; +} + +/* @end */ + +/* @group Attachment columns + * + * The columns that contain the option buckets e.g. Format and Basic Settings + */ + +.views-display-column + .views-display-column { + margin-top: 0; + } + + /* @end */ + +/* @group Auto preview + * + * The auto-preview checkbox line. + */ + +#views-ui-preview-form > div > div, +#views-ui-preview-form > div > input { + float: left; +} + +#views-ui-preview-form .form-type-checkbox { + margin-top: 2px; + margin-left: 2px; +} + +#views-ui-preview-form .form-type-textfield { + margin-top: 5px; +} + +#views-ui-preview-form .arguments-preview { + font-size: 1em; +} + +#views-ui-preview-form .arguments-preview, +#views-ui-preview-form .form-type-textfield { + margin-left: 14px; +} + +#views-ui-preview-form .form-type-textfield label { + display: inline-block; + float: left; + font-weight: normal; + height: 6ex; + margin-right: 0.75em; +} + +#views-ui-preview-form .form-type-textfield .description { + white-space: nowrap; +} + +/* @end */ + +/* @group Attachment buckets + * + * These are the individual "buckets," or boxes, inside the display settings area + */ + +.views-ui-display-tab-bucket { + border: 1px solid #f3f3f3; + line-height: 20px; + margin: 0; + padding-top: 4px; +} + +.views-ui-display-tab-bucket + .views-ui-display-tab-bucket { + border-top: medium none; +} + +.views-ui-display-tab-bucket > h3, +.views-ui-display-tab-bucket > .views-display-setting { + padding: 2px 6px 4px; +} + +.views-ui-display-tab-bucket h3 { + font-size: small; + margin: 0; +} + +.views-ui-display-tab-bucket .horizontal.actions { + margin-right: 6px; +} + +.views-ui-display-tab-bucket .actions.horizontal li + li { + margin-left: 3px; + padding-left: 3px; +} + +.views-ui-display-tab-bucket.access { + padding-top: 0; +} + +.views-ui-display-tab-bucket.page-settings { + border-bottom: medium none; +} + +.views-display-setting .views-ajax-link { + margin-left: 0.2083em; + margin-right: 0.2083em; +} + +/* @end */ + +/* @group Attachment bucket overridden + * + * Applies a overriden(italics) font style to overridden buckets. + * The better way to implement this would be to add the overridden class + * to the bucket header when the bucket is overridden and style it as a + * generic icon classed element. For the moment, we'll style the bucket + * header specifically with the overriden font style. + */ + +.views-ui-display-tab-setting.overridden, +.views-ui-display-tab-bucket.overridden > h3 { + font-style: italic; +} + +/* @end */ + +/* @group Attachment bucket drop button */ + +.views-ui-display-tab-bucket { + position: relative; +} + +/* @end */ + +/* @group Attachment bucket rows + * + * This is each row within one of the "boxes." + */ + +.views-ui-display-tab-bucket .views-display-setting { + color: #666666; + font-size: 12px; + padding-bottom: 2px; +} + +.views-ui-display-tab-bucket .even { + background-color: #f9f9f9; +} + +.views-ui-display-tab-bucket .views-group-text { + margin-top: 6px; + margin-bottom: 6px; +} + +.views-display-setting .label { + margin-right: 3pt; /* LTR */ +} + +/* @end */ + +/* @group Preview + * + * The preview controls and the preview pane + */ + +#edit-displays-preview-controls .fieldset-wrapper > * { + float: left; +} + +#edit-displays-preview-controls .fieldset-wrapper > .form-item { + margin-top: 0.3333em; +} + +#edit-displays-preview-controls .form-submit { + display: inline-block; + margin-right: 1em; +} + +#edit-displays-preview-controls .form-type-textfield { + margin-left: 1em; + position: relative; +} + +#edit-displays-preview-controls .form-type-textfield label { + border-left: 1px solid #999; + padding-left: 1em; + position: absolute; +} + +#edit-displays-preview-controls .form-type-textfield label:after { + content: ":"; +} + +#edit-displays-preview-controls .form-type-textfield label ~ * { + margin-left: 105px; +} + +/* @end */ + +/* @group Modal dialog box + * + * The contents of the popup dialog on the views edit form. + */ + +.views-ui-dialog { + font-size: small; + padding: 0; +} + +.views-ui-dialog .ui-dialog-titlebar-close { + background: url("../images/close.png") no-repeat scroll 6px 3px #F3F4EE; + border-color: #aaaaaa; + -moz-border-radius: 0 10px 12px 0; + -webkit-border-radius: 0 10px 12px 0; + border-radius: 0 10px 12px 0; + border-style: solid; + border-width: 1px 1px 1px 0; + -moz-box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 -2px 0 rgba(0, 0, 0, 0.1); + height: 22px; + right: -28px; + top: 0; + width: 26px; +} + +.views-ui-dialog .ui-dialog-titlebar-close span { + display: none; +} + +.views-filterable-options .form-type-checkbox { + border: 1px solid #CCC; + padding: 5px 8px; + border-top: none; +} + +.views-filterable-options { + border-top: 1px solid #CCC; +} + +.views-filterable-options .even .form-type-checkbox { + background-color: #F3F4EE; +} + +.filterable-option .form-item { + margin-bottom: 0; + margin-top: 0; +} + +.views-filterable-options .form-type-checkbox .description { + margin-top: 0; + margin-bottom: 0; +} + +#views-filterable-options-controls { + margin: 1em 0; +} + +#views-filterable-options-controls .form-item { + width: 45%; + margin-right: 2%; /* LTR */ +} + +#views-filterable-options-controls input, +#views-filterable-options-controls select { + width: 200px; +} + +.views-ui-dialog .views-filterable-options { + margin-bottom: 10px; +} + +.views-ui-dialog .views-add-form-selected.container-inline { + padding-top: 0; +} + +.views-ui-dialog .views-add-form-selected.container-inline > div { + display: block; +} + +.views-ui-dialog #edit-selected { + margin: 0; + padding: 6px 16px; +} + +.views-ui-dialog #views-ajax-title, +.views-ui-dialog .views-override { + background-color: #F3F4EE; +} + +.views-ui-dialog .views-override { + padding: 0 13px 8px; +} + +.views-ui-dialog .views-override > * { + margin: 0; +} + +.views-ui-dialog #views-ajax-title { + font-size: 15px; + padding: 8px 13px; +} + +.views-ui-dialog #views-progress-indicator { + font-size: 11px; + position: absolute; + right: 10px; /* LTR */ + top: 8px; +} + +.views-ui-dialog #views-progress-indicator:before { + content: "\003C\00A0"; +} + +.views-ui-dialog #views-progress-indicator:after { + content: "\00A0\003E"; +} + +.views-ui-dialog .scroll { + border: 1px solid #CCC; + border-width: 1px 0; + padding: 8px 13px; +} + +.views-ui-dialog fieldset .item-list { + padding-left: 2em; +} + +.views-ui-dialog .form-buttons { + background-color: #F3F4EE; + padding: 8px 13px; +} +.views-ui-dialog .form-buttons input { + margin-bottom: 0; + margin-right: 0; +} + +/* @end */ + +/* @group Configure filter criteria */ + +/* @todo the width and border info could be moved into a more generic class */ +/* @todo Make this a class to be used anywhere there's node types? */ +.form-type-checkboxes #edit-options-value, +.form-type-checkboxes #edit-options-validate-options-node-types { + border-color: #CCCCCC; + border-style: solid; + border-width: 1px; + max-height: 210px; + overflow: auto; + margin-top: 5px; + padding: 0 5px; + width: 190px; +} + +/* @end */ + +/* @group Rearrange filter criteria */ + +#views-ui-rearrange-filter-form table { + border-collapse: collapse; +} + +#views-ui-rearrange-filter-form tr td[rowspan] { + border-color: #CDCDCD; + border-style: solid; + border-width: 0 1px 1px 1px; +} + +#views-ui-rearrange-filter-form tr[id^="views-row"] { + border-right: 1px solid #CDCDCD; +} + +#views-ui-rearrange-filter-form tr[id^="views-row"].even td { + background-color: #F3F4ED; +} + +#views-ui-rearrange-filter-form .views-group-title { + border-top: 1px solid #CDCDCD; +} + +#views-ui-rearrange-filter-form .group-empty { + border-bottom: 1px solid #CDCDCD; +} + +/* @end */ + +/* @group Expose filter form items */ + +.form-item-options-expose-required, +.form-item-options-expose-label, +.form-item-options-expose-description { + margin-bottom: 6px; + margin-left: 18px; + margin-top: 6px; +} + +/* @end */ + +/* @group Live preview elements */ + +#views-preview-wrapper { + border: 1px solid #CCC; + border-top: 2px solid #CCC; + padding-bottom: 12px; + padding-top: 12px; +} + +#views-ui-preview-form { + margin: 12px; +} + +#views-live-preview { + margin: 0 12px; +} + +#views-live-preview .views-query-info { + overflow: auto; +} + +/* Intentionally targeting h1 */ +#views-live-preview h1.section-title { + color: #818181; + display: inline-block; + font-size: 13px; + font-weight: normal; + line-height: 1.6154; + margin-bottom: 0; + margin-top: 0; +} + +#views-live-preview .view > * { + margin-top: 18px; +} + +#views-live-preview .preview-section { + border: 1px dashed #DEDEDE; + margin: 0 -5px; + padding: 3px 5px; +} + +#views-live-preview li.views-row + li.views-row { + margin-top: 18px; +} + +/* The div.views-row is intentional and excludes li.views-row, for example */ +#views-live-preview div.views-row + div.views-row { + margin-top: 36px; +} + +/* @group Query info table */ + +.views-query-info table { + border-collapse: separate; + border-color: #dddddd; + border-spacing: 0; + margin: 10px 0; +} + +.views-query-info table tr { + background-color: #f9f9f9; +} + +.views-query-info table th, +.views-query-info table td { + color: #666666; + padding: 4px 10px; +} + +/* @end */ + +/* @group Grid */ + +#views-live-preview .views-view-grid th, +#views-live-preview .views-view-grid td { + vertical-align: top; +} + +/* @end */ + +/* @group HTML list */ + +#views-live-preview .view-content > .item-list > ul { + list-style-position: outside; + padding-left: 21px; /* LTR */ +} + +/* @end */ + +/* @end */ + +/* @group Add/edit argument form */ + +#edit-options-default-action { + width: 300px; + float: left; +} + +#edit-options-exception.collapsible { + float: right; + width: 250px; + margin-top: -2px; +} + +/* @end */ + +/* @group AJAX */ + +/* Hide the drupal system throbber image */ +.ajax-progress .throbber { + display: none; +} + +.ajax-progress-throbber { + background-color: #232323; + background-image: url("../images/loading-small.gif"); + background-position: center center; + background-repeat: no-repeat; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + height: 24px; + opacity: .9; + padding: 4px; + width: 24px; +} + +/* @end */ + +/* @group Drupal + * + * Overrides to Drupal system CSS + */ +div.messages { + margin-bottom: 18px; +} + +/* @end */ diff --git a/css/views-rtl.css b/css/views-rtl.css new file mode 100644 index 00000000..f804639f --- /dev/null +++ b/css/views-rtl.css @@ -0,0 +1,5 @@ + +.views-exposed-form .views-exposed-widget { + float: right; /* RTL */ + padding: .5em 1em 0 0; /* RTL */ +} diff --git a/css/views.css b/css/views.css new file mode 100644 index 00000000..bf96f70b --- /dev/null +++ b/css/views.css @@ -0,0 +1,42 @@ +.views-exposed-form .views-exposed-widget { + float: left; /* LTR */ + padding: .5em 1em 0 0; /* LTR */ +} + +.views-exposed-form .views-exposed-widget .form-submit { + margin-top: 1.6em; +} + +.views-exposed-form .form-item, +.views-exposed-form .form-submit { + margin-top: 0; + margin-bottom: 0; +} + +.views-exposed-form label { + font-weight: bold; +} + +.views-exposed-widgets { + margin-bottom: .5em; +} + +/* table style column align */ +.views-align-left { + text-align: left; +} +.views-align-right { + text-align: right; +} +.views-align-center { + text-align: center; +} + +/* Remove the border on tbody that system puts in */ +.views-view-grid tbody { + border-top: none; +} + +.view .progress-disabled { + float: none; +} diff --git a/documentation-standards.txt b/documentation-standards.txt new file mode 100644 index 00000000..6a9c5921 --- /dev/null +++ b/documentation-standards.txt @@ -0,0 +1,5 @@ +- If the interface text is *bolded*, it got strong tags. +- If it's a button they need to click, that's *bold* too. +- If the text is not bolded (ex: links to click, options to check), it +got /italicized/. +- If it's user-entered text it got 'single quotes'. diff --git a/drush/views.drush.inc b/drush/views.drush.inc new file mode 100644 index 00000000..dd8af1b6 --- /dev/null +++ b/drush/views.drush.inc @@ -0,0 +1,496 @@ + 'views_revert_views', + 'drupal dependencies' => array('views'), + 'description' => 'Revert overridden views to their default state. Make sure to backup first.', + 'arguments' => array( + 'views' => 'A space delimited list of view names.', + ), + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, + 'aliases' => array('vr'), + 'examples' => array( + 'drush vr archive' => 'Reverts the "archive" view.', + 'drush rln archive frontpage' => 'Reverts the "archive" and "frontpage" view.', + 'drush vr' => 'Will present you with a list of overridden views to choose from, and an option to revert all overridden views.', + ), + ); + $items['views-dev'] = array( + 'callback' => 'views_development_settings', + 'drupal dependencies' => array('views'), + 'description' => 'Set the Views settings to more developer-oriented values.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, + 'aliases' => array('vd'), + ); + + $items['views-list'] = array( + 'drupal dependencies' => array('views'), + 'description' => 'Get a list of all views in the system.', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, + 'aliases' => array('vl'), + 'options' => array( + 'name' => 'String contained in view\'s name by which filter the results.', + 'tags' => 'A comma-separated list of views tags by which to filter the results.', + 'status' => 'Status of the views by which to filter the results. Choices: enabled, disabled.', + 'type' => 'Type of the views by which to filter the results. Choices: normal, default or overridden.', + ), + 'examples' => array( + 'drush vl' => 'Show a list of all available views.', + 'drush vl --name=blog' => 'Show a list of views which names contain "blog".', + 'drush vl --tags=tag1,tag2' => 'Show a list of views tagged with "tag1" or "tag2".', + 'drush vl --status=enabled' => 'Show a list of enabled views.', + 'drush vl --type=overridden' => 'Show a list of overridden views.', + ), + ); + $items['views-analyze'] = array( + 'drupal dependencies' => array('views', 'views_ui'), + 'description' => 'Get a list of all Views analyze warnings', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, + 'aliases' => array('va'), + ); + $items['views-enable'] = array( + 'drupal dependencies' => array('views'), + 'description' => 'Enable the specified views.', + 'arguments' => array( + 'views' => 'A space delimited list of view names.', + ), + 'aliases' => array('ven'), + 'examples' => array( + 'drush ven frontpage taxonomy_term' => 'Enable the frontpage and taxonomy_term views.', + ), + ); + $items['views-disable'] = array( + 'drupal dependencies' => array('views'), + 'description' => 'Disable the specified views.', + 'arguments' => array( + 'views' => 'A space delimited list of view names.', + ), + 'aliases' => array('vdis'), + 'examples' => array( + 'drush vdis frontpage taxonomy_term' => 'Disable the frontpage and taxonomy_term views.', + ), + ); + + return $items; +} + +/** + * Callback function for views-revert command. + */ +function views_revert_views() { + $views = views_get_all_views(); + $i = 0; + // The provided views names specified in the command. + $viewnames = _convert_csv_to_array(func_get_args()); + + // Find all overridden views. + foreach ($views as $view) { + if ($view->disabled) { + continue; + } + if ($view->type == dt('Overridden')) { + $overridden[$view->name] = $view->name; + } + } + + // Return early if there are no overridden views in the system. + if (empty($overridden)) { + return drush_set_error(dt('There are no overridden views in the system.')); + } + + // If the user specified in the command the views to be overridden. + if (!empty($viewnames)) { + foreach ($viewnames as $key => $viewname) { + $is_overridden = key_exists($viewname, $overridden); + // Check if the provided view name is in the system + if ($viewname && !key_exists($viewname, $views)) { + drush_set_error(dt("'@viewname' view is not present in the system.", array('@viewname' => $viewname))); + } + // Check if the provided view is overridden. + elseif (!$is_overridden) { + drush_set_error(dt("The view specified '@viewname' is not overridden.", array('@viewname' => $viewname))); + } + // If the view is overriden, revert it. + elseif ($is_overridden){ + views_revert_view($views[$viewname]); + $i++; + } + // We should never get here but well... + else { + drush_set_error(dt("The view specified '@viewname' is not provided in code, and thus cannot be reverted.", array('@viewname' => $viewname))); + } + } + } + + // The user did not specify any views in the command, prompt the user + else { + // list of choices for the user + $overridden['all'] = dt('Revert all overridden views'); // add a choice at the end + $choice = drush_choice($overridden, 'Enter a number to choose which view to revert.', '!key'); // prompt the user + + if ($choice !== FALSE) { + // revert all views option + if ($choice == 'all') { + $i = views_revert_allviews($views); + } + // else the user specified a single view + else { + views_revert_view($views[$choice]); + $i++; + } + } + + } + + // final results output + if ($i == 0) { + drush_log(dt('No views were reverted.'), 'ok'); + } + else { + drush_log(dt('Reverted a total of @count views.', array('@count' => $i)), 'ok'); + } +} + +/** + * Reverts all views + * @param $views + * All views in the system as provided by views_get_all_views(). + */ +function views_revert_allviews($views) { + $i = 0; + foreach ($views as $view) { + if ($view->disabled) { + continue; + } + + if ($view->type == t('Overridden')) { + views_revert_view($view); + $i++; + } + } + return $i; +} + +/** + * Revert a specified view + * @param $view + * The view object to be reverted + * + * Checks on wether or not the view is overridden is handled in views_revert_views_revert() + * We perform a check here anyway in case someone somehow calls this function on their own... + */ +function views_revert_view($view) { + // check anyway just in case + if ($view->type == t('Overridden')) { + // Revert the view. + $view->delete(); + // Clear its cache. + ctools_include('object-cache'); + ctools_object_cache_clear('view', $view->name); + // Give feedback. + $message = dt("Reverted the view '@viewname'", array('@viewname' => $view->name)); + drush_log($message, 'success'); + // Reverted one more view. + } + else { + drush_set_error(dt("The view '@viewname' is not overridden.", array('@viewname' => $view->name))); + } +} + +/** + * Change the settings to a more developer oriented value. + */ +function views_development_settings() { + variable_set('views_ui_show_listing_filters', TRUE); + variable_set('views_ui_show_master_display', TRUE); + variable_set('views_ui_show_advanced_column', TRUE); + variable_set('views_ui_always_live_preview', FALSE); + variable_set('views_ui_always_live_preview_button', TRUE); + variable_set('views_ui_show_preview_information', TRUE); + variable_set('views_ui_show_sql_query', TRUE); + variable_set('views_ui_show_performance_statistics', TRUE); + variable_set('views_show_additional_queries', TRUE); + variable_set('views_devel_output', TRUE); + variable_set('views_devel_region', 'message'); + variable_set('views_ui_display_embed', TRUE); + $message = dt("Setup the new views settings."); + drush_log($message, 'success'); +} + + +/** + * Callback function for views-list command. + */ +function drush_views_list() { + // Initialize stuf + $rows = array(); + $disabled_views = array(); + $enabled_views = array(); + $overridden = 0; + $indb = 0; + $incode = 0; + $disabled = 0; + $total = 0; + + $views = views_get_all_views(); + + // get the --name option + // TODO : take into account the case off a comma-separated list of names + $name = drush_get_option_list('name'); + $with_name = !empty($name) ? TRUE : FALSE; + + // get the --tags option + $tags = drush_get_option_list('tags'); + $with_tags = !empty($tags) ? TRUE : FALSE; + + // get the --status option + // store user input appart to reuse it after + $status_opt = drush_get_option_list('status'); + // use the same logic than $view->disabled + if (in_array('disabled', $status_opt)) { + $status = TRUE; + $with_status = TRUE; + } + elseif (in_array('enabled', $status_opt)) { + $status = FALSE; + $with_status = TRUE; + } + else { + $status = NULL; + // wrong or empty --status option + $with_status = FALSE; + } + + // get the --type option + $type = drush_get_option_list('type'); + // use the same logic than $view->type + $with_type = FALSE; + if (in_array('normal', $type) || in_array('default', $type)|| in_array('overridden', $type)) { + $with_type = TRUE; + } + + // set the table headers + $header = array( + dt('Machine name'), + dt('Description'), + dt('Type'), + dt('Status'), + dt('Tag'), + ); + + // setup a row for each view + foreach($views as $id => $view){ + // if options were specified, check that first + // mismatch push the loop to the next view + if ($with_tags && !in_array($view->tag, $tags)) { + continue; + } + if ($with_status && !$view->disabled == $status) { + continue; + } + if ($with_type && strtolower($view->type) !== $type[0]) { + continue; + } + if ($with_name && !stristr($view->name, $name[0])) { + continue; + } + + $row = array(); + // each row entry should be in the same order as the header + $row[] = $view->name; + $row[] = $view->description; + $row[] = $view->type; + $row[] = $view->disabled ? dt('Disabled') : dt('Enabled'); + $row[] = $view->tag; + + // place the row in the appropiate array, + // so we can have disabled views at the bottom + if($view->disabled) { + $disabled_views[] = $row; + } + else{ + $enabled_views[] = $row; + } + unset($row); + + // gather some statistics + switch($view->type) { + case dt('Normal'): + $indb++; + break; + + case dt('Overridden'): + $overridden++; + break; + + case dt('Default'): + $incode++; + break; + } + $total++; + } + + $disabled = count($disabled_views); + + // sort alphabeticaly + asort($disabled_views); + asort($enabled_views); + + // if options were used + $summary = ""; + if ($with_name || $with_tags || $with_status || $with_type) { + $summary = "Views"; + + if ($with_name) { + $summary .= " named $name[0]"; + } + + if ($with_tags) { + $tags = implode(" or ", $tags); + $summary .= " tagged $tags"; + } + + if ($with_status) { + $status_opt = implode("", $status_opt); + $summary .= " which status is '$status_opt'"; + } + + if ($with_type) { + $type = ucfirst($type[0]); + $summary .= " of type '$type'"; + } + } + + if (!empty($summary)) { + drush_print($summary . "\n"); + } + + // print all rows as a table + if ($total > 0) { + $rows = array_merge($enabled_views, $disabled_views); + // put the headers as first row + array_unshift($rows, $header); + + drush_print_table($rows, TRUE); + } + + // print the statistics messages + drush_print(dt("A total of @total views were found in this Drupal installation:", array('@total' => $total))); + drush_print(dt(" @indb views reside only in the database", array('@indb' => $indb ))); + drush_print(dt(" @over views are overridden", array('@over' => $overridden))); + drush_print(dt(" @incode views are in their default state", array('@incode' => $incode))); + drush_print(dt(" @dis views are disabled\n", array('@dis' => $disabled))); +} + +function drush_views_analyze() { + views_include('analyze'); + $messages_count = 0; + $total = 0; + + foreach (views_get_all_views() as $view_name => $view) { + $total++; + if ($messages = views_analyze_view($view)) { + drush_print($view_name); + foreach ($messages as $message) { + $messages_count++; + drush_print($message['type'] .': '. $message['message'], 2); + } + } + } + drush_log(dt('A total of @total views were analyzed and @messages problems were found.', array('@total' => $total, '@messages' => $messages_count)), 'ok'); +} + +/** + * Enables views + */ +function drush_views_enable() { + $viewnames = _convert_csv_to_array(func_get_args()); + // Return early if no view names were specified. + if (empty($viewnames)) { + return drush_set_error(dt('Please specify a space delimited list of view names to enable')); + } + _views_drush_changestatus($viewnames, FALSE); +} + +/** + * Disables views + */ +function drush_views_disable() { + $viewnames = _convert_csv_to_array(func_get_args()); + // Return early if no view names were specified. + if (empty($viewnames)) { + return drush_set_error(dt('Please specify a space delimited list of view names to disable')); + } + _views_drush_changestatus($viewnames, TRUE); +} + +/** + * Helper function to enable / disable views + * @param $viewnames: array of viewnames to process + * @param $status: TRUE to disable or FALSE to enable the view + */ +function _views_drush_changestatus($viewnames = array(), $status = NULL) { + if ($status !== NULL && !empty($viewnames)) { + $changed = FALSE; + $processed = $status ? dt('disabled') : dt('enabled'); + $views_status = variable_get('views_defaults', array()); + + foreach ($viewnames as $key => $viewname) { + if ($views_status[$viewname] !== $status) { + $views_status[$viewname] = $status; + $changed = TRUE; + drush_log(dt("The view '!name' has been !processed", array('!name' => $viewname, '!processed' => $processed)), 'success'); + } + else { + drush_set_error(dt("The view '!name' is already !processed", array('!name' => $viewname, '!processed' => $processed))); + } + } + // If we made changes to views status, save them and clear caches + if ($changed) { + variable_set('views_defaults', $views_status); + views_invalidate_cache(); + drush_log(dt("Views cache was cleared"), 'ok'); + drush_log(dt("Menu cache is set to be rebuilt on the next request."), 'ok'); + } + } +} + +/** + * Adds a cache clear option for views. + */ +function views_drush_cache_clear(&$types) { + $types['views'] = 'views_invalidate_cache'; +} diff --git a/handlers/views_handler_area.inc b/handlers/views_handler_area.inc new file mode 100644 index 00000000..9fed11c3 --- /dev/null +++ b/handlers/views_handler_area.inc @@ -0,0 +1,133 @@ +handler_type == 'empty') { + $this->options['empty'] = TRUE; + } + } + + /** + * Get this field's label. + */ + function label() { + if (!isset($this->options['label'])) { + return $this->ui_name(); + } + return $this->options['label']; + } + + function option_definition() { + $options = parent::option_definition(); + + $this->definition['field'] = !empty($this->definition['field']) ? $this->definition['field'] : ''; + $label = !empty($this->definition['label']) ? $this->definition['label'] : $this->definition['field']; + $options['label'] = array('default' => $label, 'translatable' => TRUE); + $options['empty'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide extra data to the administration form + */ + function admin_summary() { + return $this->label(); + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#default_value' => isset($this->options['label']) ? $this->options['label'] : '', + '#description' => t('The label for this area that will be displayed only administratively.'), + ); + + if ($form_state['type'] != 'empty') { + $form['empty'] = array( + '#type' => 'checkbox', + '#title' => t('Display even if view has no result'), + '#default_value' => isset($this->options['empty']) ? $this->options['empty'] : 0, + ); + } + } + + /** + * Don't run a query + */ + function query() { } + + /** + * Render the area + */ + function render($empty = FALSE) { + return ''; + } + + /** + * Area handlers shouldn't have groupby. + */ + function use_group_by() { + return FALSE; + } +} + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_area_handlers + */ +class views_handler_area_broken extends views_handler_area { + function ui_name($short = FALSE) { + return t('Broken/missing handler'); + } + + function ensure_my_table() { /* No table to ensure! */ } + function query($group_by = FALSE) { /* No query to run */ } + function render($empty = FALSE) { return ''; } + function options_form(&$form, &$form_state) { + $form['markup'] = array( + '#prefix' => '
', + '#value' => t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.'), + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + function broken() { return TRUE; } +} + +/** + * @} + */ diff --git a/handlers/views_handler_area_result.inc b/handlers/views_handler_area_result.inc new file mode 100644 index 00000000..86b1849b --- /dev/null +++ b/handlers/views_handler_area_result.inc @@ -0,0 +1,96 @@ + 'Displaying @start - @end of @total', + 'translatable' => TRUE, + ); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $variables = array( + 'items' => array( + '@start -- the initial record number in the set', + '@end -- the last record number in the set', + '@total -- the total records in the set', + '@name -- the human-readable name of the view', + '@per_page -- the number of items per page', + '@current_page -- the current page number', + '@current_record_count -- the current page record count', + '@page_count -- the total page count', + ), + ); + $list = theme('item_list', $variables); + $form['content'] = array( + '#title' => t('Display'), + '#type' => 'textarea', + '#rows' => 3, + '#default_value' => $this->options['content'], + '#description' => t('You may use HTML code in this field. The following tokens are supported:') . $list, + ); + } + + + /** + * Find out the information to render. + */ + function render($empty = FALSE) { + // Must have options and does not work on summaries. + if (!isset($this->options['content']) || $this->view->plugin_name == 'default_summary') { + return; + } + $output = ''; + $format = $this->options['content']; + // Calculate the page totals. + $current_page = (int) $this->view->get_current_page() + 1; + $per_page = (int) $this->view->get_items_per_page(); + $count = count($this->view->result); + // @TODO: Maybe use a possible is views empty functionality. + // Not every view has total_rows set, use view->result instead. + $total = isset($this->view->total_rows) ? $this->view->total_rows : count($this->view->result); + $name = check_plain($this->view->human_name); + if ($per_page === 0) { + $page_count = 1; + $start = 1; + $end = $total; + } + else { + $page_count = (int) ceil($total / $per_page); + $total_count = $current_page * $per_page; + if ($total_count > $total) { + $total_count = $total; + } + $start = ($current_page - 1) * $per_page + 1; + $end = $total_count; + } + $current_record_count = ($end - $start) + 1; + // Get the search information. + $items = array('start', 'end', 'total', 'name', 'per_page', 'current_page', 'current_record_count', 'page_count'); + $replacements = array(); + foreach ($items as $item) { + $replacements["@$item"] = ${$item}; + } + // Send the output. + if (!empty($total)) { + $output .= filter_xss_admin(str_replace(array_keys($replacements), array_values($replacements), $format)); + } + return $output; + } +} diff --git a/handlers/views_handler_area_text.inc b/handlers/views_handler_area_text.inc new file mode 100644 index 00000000..d7727866 --- /dev/null +++ b/handlers/views_handler_area_text.inc @@ -0,0 +1,110 @@ + '', 'translatable' => TRUE, 'format_key' => 'format'); + $options['format'] = array('default' => NULL); + $options['tokenize'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['content'] = array( + '#type' => 'text_format', + '#default_value' => $this->options['content'], + '#rows' => 6, + '#format' => isset($this->options['format']) ? $this->options['format'] : filter_default_format(), + '#wysiwyg' => FALSE, + ); + + // @TODO: Refactor token handling into a base class. + $form['tokenize'] = array( + '#type' => 'checkbox', + '#title' => t('Use replacement tokens from the first row'), + '#default_value' => $this->options['tokenize'], + ); + + // Get a list of the available fields and arguments for token replacement. + $options = array(); + foreach ($this->view->display_handler->get_handlers('field') as $field => $handler) { + $options[t('Fields')]["[$field]"] = $handler->ui_name(); + } + + $count = 0; // This lets us prepare the key as we want it printed. + foreach ($this->view->display_handler->get_handlers('argument') as $arg => $handler) { + $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->ui_name())); + $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->ui_name())); + } + + if (!empty($options)) { + $output = '

' . t('The following tokens are available. If you would like to have the characters \'[\' and \']\' please use the html entity codes \'%5B\' or \'%5D\' or they will get replaced with empty space.' . '

'); + foreach (array_keys($options) as $type) { + if (!empty($options[$type])) { + $items = array(); + foreach ($options[$type] as $key => $value) { + $items[] = $key . ' == ' . $value; + } + $output .= theme('item_list', + array( + 'items' => $items, + 'type' => $type + )); + } + } + + $form['token_help'] = array( + '#type' => 'fieldset', + '#title' => t('Replacement patterns'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#value' => $output, + '#id' => 'edit-options-token-help', + '#dependency' => array( + 'edit-options-tokenize' => array(1), + ), + '#prefix' => '
', + '#suffix' => '
', + ); + } + } + + function options_submit(&$form, &$form_state) { + $form_state['values']['options']['format'] = $form_state['values']['options']['content']['format']; + $form_state['values']['options']['content'] = $form_state['values']['options']['content']['value']; + parent::options_submit($form, $form_state); + } + + function render($empty = FALSE) { + $format = isset($this->options['format']) ? $this->options['format'] : filter_default_format(); + if (!$empty || !empty($this->options['empty'])) { + return $this->render_textarea($this->options['content'], $format); + } + return ''; + } + + /** + * Render a text area, using the proper format. + */ + function render_textarea($value, $format) { + if ($value) { + if ($this->options['tokenize']) { + $value = $this->view->style_plugin->tokenize_value($value, 0); + } + return check_markup($value, $format, '', FALSE); + } + } +} diff --git a/handlers/views_handler_area_text_custom.inc b/handlers/views_handler_area_text_custom.inc new file mode 100644 index 00000000..3627f0c7 --- /dev/null +++ b/handlers/views_handler_area_text_custom.inc @@ -0,0 +1,56 @@ +options['empty'])) { + return $this->render_textarea_custom($this->options['content']); + } + + return ''; + } + + /** + * Render a text area with filter_xss_admin. + */ + function render_textarea_custom($value) { + if ($value) { + if ($this->options['tokenize']) { + $value = $this->view->style_plugin->tokenize_value($value, 0); + } + return $this->sanitize_value($value, 'xss_admin'); + } + } + +} diff --git a/handlers/views_handler_area_view.inc b/handlers/views_handler_area_view.inc new file mode 100644 index 00000000..45ea4999 --- /dev/null +++ b/handlers/views_handler_area_view.inc @@ -0,0 +1,83 @@ + ''); + $options['inherit_arguments'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $view_display = $this->view->name . ':' . $this->view->current_display; + + $options = array('' => t('-Select-')); + $options += views_get_views_as_options(FALSE, 'all', $view_display, FALSE, TRUE); + $form['view_to_insert'] = array( + '#type' => 'select', + '#title' => t('View to insert'), + '#default_value' => $this->options['view_to_insert'], + '#description' => t('The view to insert into this area.'), + '#options' => $options, + ); + + $form['inherit_arguments'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit contextual filters'), + '#default_value' => $this->options['inherit_arguments'], + '#description' => t('If checked, this view will receive the same contextual filters as its parent.'), + ); + } + + /** + * Render the area + */ + function render($empty = FALSE) { + if (!empty($this->options['view_to_insert'])) { + list($view_name, $display_id) = explode(':', $this->options['view_to_insert']); + + $view = views_get_view($view_name); + if (empty($view) || !$view->access($display_id)) { + return; + } + $view->set_display($display_id); + + // Avoid recursion + $view->parent_views += $this->view->parent_views; + $view->parent_views[] = "$view_name:$display_id"; + + // Check if the view is part of the parent views of this view + $search = "$view_name:$display_id"; + if (in_array($search, $this->view->parent_views)) { + drupal_set_message(t("Recursion detected in view @view display @display.", array('@view' => $view_name, '@display' => $display_id)), 'error'); + } + else { + if (!empty($this->options['inherit_arguments']) && !empty($this->view->args)) { + return $view->preview($display_id, $this->view->args); + } + else { + return $view->preview($display_id); + } + } + } + return ''; + } +} diff --git a/handlers/views_handler_argument.inc b/handlers/views_handler_argument.inc new file mode 100644 index 00000000..42617e7a --- /dev/null +++ b/handlers/views_handler_argument.inc @@ -0,0 +1,1240 @@ +definition['name field'])) { + $this->name_field = $this->definition['name field']; + } + if (!empty($this->definition['name table'])) { + $this->name_table = $this->definition['name table']; + } + } + + function init(&$view, &$options) { + parent::init($view, $options); + + // Compatibility: The new UI changed several settings. + if (!empty($options['wildcard']) && !isset($options['exception']['value'])) { + $this->options['exception']['value'] = $options['wildcard']; + } + if (!empty($options['wildcard_substitution']) && !isset($options['exception']['title'])) { + // Enable the checkbox if the title is filled in. + $this->options['exception']['title_enable'] = 1; + $this->options['exception']['title'] = $options['wildcard_substitution']; + } + + if (!isset($options['summary']['format']) && !empty($options['style_plugin'])) { + $this->options['summary']['format'] = $options['style_plugin']; + } + + // Setup default value. + $options['style_options'] = isset($options['style_options']) ? $options['style_options'] : array(); + + if (!isset($options['summary']['sort_order']) && !empty($options['default_action']) && $options['default_action'] == 'summary asc') { + $this->options['default_action'] = 'summary'; + $this->options['summary']['sort_order'] = 'asc'; + $this->options['summary']['number_of_records'] = 0; + $this->options['summary_options'] = $options['style_options']; + } + elseif (!isset($options['summary']['sort_order']) && !empty($options['default_action']) && $options['default_action'] == 'summary desc') { + $this->options['default_action'] = 'summary'; + $this->options['summary']['sort_order'] = 'desc'; + $this->options['summary']['number_of_records'] = 0; + $this->options['summary_options'] = $options['style_options']; + } + elseif (!isset($options['summary']['sort_order']) && !empty($options['default_action']) && $options['default_action'] == 'summary asc by count') { + $this->options['default_action'] = 'summary'; + $this->options['summary']['sort_order'] = 'asc'; + $this->options['summary']['number_of_records'] = 1; + $this->options['summary_options'] = $options['style_options']; + } + elseif (!isset($options['summary']['sort_order']) && !empty($options['default_action']) && $options['default_action'] == 'summary desc by count') { + $this->options['default_action'] = 'summary'; + $this->options['summary']['sort_order'] = 'desc'; + $this->options['summary']['number_of_records'] = 1; + $this->options['summary_options'] = $options['style_options']; + } + + if (!empty($options['title']) && !isset($options['title_enable'])) { + $this->options['title_enable'] = 1; + } + if (!empty($options['breadcrumb']) && !isset($options['breadcrumb_enable'])) { + $this->options['breadcrumb_enable'] = 1; + } + + if (!empty($options['validate_type']) && !isset($options['validate']['type'])) { + $this->options['validate']['type'] = $options['validate_type']; + $this->options['specify_validation'] = 1; + } + if (!empty($options['validate_fail']) && !isset($options['validate']['fail'])) { + $this->options['validate']['fail'] = $options['validate_fail']; + $this->options['specify_validation'] = 1; + } + } + + /** + * Give an argument the opportunity to modify the breadcrumb, if it wants. + * This only gets called on displays where a breadcrumb is actually used. + * + * The breadcrumb will be in the form of an array, with the keys being + * the path and the value being the already sanitized title of the path. + */ + function set_breadcrumb(&$breadcrumb) { } + + /** + * Determine if the argument can generate a breadcrumb + * + * @return TRUE/FALSE + */ + function uses_breadcrumb() { + $info = $this->default_actions($this->options['default_action']); + return !empty($info['breadcrumb']); + } + + function is_exception($arg = NULL) { + if (!isset($arg)) { + $arg = isset($this->argument) ? $this->argument : NULL; + } + return !empty($this->options['exception']['value']) && $this->options['exception']['value'] === $arg; + } + + function exception_title() { + // If title overriding is off for the exception, return the normal title. + if (empty($this->options['exception']['title_enable'])) { + return $this->get_title(); + } + return $this->options['exception']['title']; + } + + /** + * Determine if the argument needs a style plugin. + * + * @return TRUE/FALSE + */ + function needs_style_plugin() { + $info = $this->default_actions($this->options['default_action']); + $validate_info = $this->default_actions($this->options['validate']['fail']); + return !empty($info['style plugin']) || !empty($validate_info['style plugin']); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['default_action'] = array('default' => 'ignore'); + $options['exception'] = array( + 'contains' => array( + 'value' => array('default' => 'all'), + 'title_enable' => array('default' => FALSE, 'bool' => TRUE), + 'title' => array('default' => 'All', 'translatable' => TRUE), + ), + ); + $options['title_enable'] = array('default' => FALSE, 'bool' => TRUE); + $options['title'] = array('default' => '', 'translatable' => TRUE); + $options['breadcrumb_enable'] = array('default' => FALSE, 'bool' => TRUE); + $options['breadcrumb'] = array('default' => '', 'translatable' => TRUE); + $options['default_argument_type'] = array('default' => 'fixed', 'export' => 'export_plugin'); + $options['default_argument_options'] = array('default' => array(), 'export' => FALSE); + $options['default_argument_skip_url'] = array('default' => FALSE, 'bool' => TRUE); + $options['summary_options'] = array('default' => array(), 'export' => FALSE); + $options['summary'] = array( + 'contains' => array( + 'sort_order' => array('default' => 'asc'), + 'number_of_records' => array('default' => 0), + 'format' => array('default' => 'default_summary', 'export' => 'export_summary'), + ), + ); + $options['specify_validation'] = array('default' => FALSE, 'bool' => TRUE); + $options['validate'] = array( + 'contains' => array( + 'type' => array('default' => 'none', 'export' => 'export_validation'), + 'fail' => array('default' => 'not found'), + ), + ); + $options['validate_options'] = array('default' => array(), 'export' => FALSE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $argument_text = $this->view->display_handler->get_argument_text(); + + $form['#pre_render'][] = 'views_ui_pre_render_move_argument_options'; + + $form['description'] = array( + '#markup' => $argument_text['description'], + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('description')), + ); + + $form['no_argument'] = array( + '#type' => 'fieldset', + '#title' => $argument_text['filter value not present'], + ); + // Everything in the fieldset is floated, so the last element needs to + // clear those floats. + $form['no_argument']['clearfix'] = array( + '#weight' => 1000, + '#markup' => '
', + ); + $form['default_action'] = array( + '#type' => 'radios', + '#process' => array('views_ui_process_container_radios'), + '#default_value' => $this->options['default_action'], + '#fieldset' => 'no_argument', + ); + + $form['exception'] = array( + '#type' => 'fieldset', + '#title' => t('Exceptions'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#fieldset' => 'no_argument', + ); + $form['exception']['value'] = array( + '#type' => 'textfield', + '#title' => t('Exception value'), + '#size' => 20, + '#default_value' => $this->options['exception']['value'], + '#description' => t('If this value is received, the filter will be ignored; i.e, "all values"'), + ); + $form['exception']['title_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Override title'), + '#default_value' => $this->options['exception']['title_enable'], + ); + $form['exception']['title'] = array( + '#type' => 'textfield', + '#title' => t('Override title'), + '#title_display' => 'invisible', + '#size' => 20, + '#default_value' => $this->options['exception']['title'], + '#description' => t('Override the view and other argument titles. Use "%1" for the first argument, "%2" for the second, etc.'), + '#dependency' => array( + 'edit-options-exception-title-enable' => array('1'), + ), + ); + + $options = array(); + $defaults = $this->default_actions(); + $validate_options = array(); + foreach ($defaults as $id => $info) { + $options[$id] = $info['title']; + if (empty($info['default only'])) { + $validate_options[$id] = $info['title']; + } + if (!empty($info['form method'])) { + $this->{$info['form method']}($form, $form_state); + } + } + $form['default_action']['#options'] = $options; + + $form['argument_present'] = array( + '#type' => 'fieldset', + '#title' => $argument_text['filter value present'], + ); + $form['title_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Override title'), + '#default_value' => $this->options['title_enable'], + '#fieldset' => 'argument_present', + ); + $form['title'] = array( + '#type' => 'textfield', + '#title' => t('Provide title'), + '#title_display' => 'invisible', + '#default_value' => $this->options['title'], + '#description' => t('Override the view and other argument titles. Use "%1" for the first argument, "%2" for the second, etc.'), + '#dependency' => array( + 'edit-options-title-enable' => array('1'), + ), + '#fieldset' => 'argument_present', + ); + + $form['breadcrumb_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Override breadcrumb'), + '#default_value' => $this->options['breadcrumb_enable'], + '#fieldset' => 'argument_present', + ); + $form['breadcrumb'] = array( + '#type' => 'textfield', + '#title' => t('Provide breadcrumb'), + '#title_display' => 'invisible', + '#default_value' => $this->options['breadcrumb'], + '#description' => t('Enter a breadcrumb name you would like to use. See "Title" for percent substitutions.'), + '#dependency' => array( + 'edit-options-breadcrumb-enable' => array('1'), + ), + '#fieldset' => 'argument_present', + ); + + $form['specify_validation'] = array( + '#type' => 'checkbox', + '#title' => t('Specify validation criteria'), + '#default_value' => $this->options['specify_validation'], + '#fieldset' => 'argument_present', + ); + + $form['validate'] = array( + '#type' => 'container', + '#fieldset' => 'argument_present', + ); + // @todo The mockup wanted to use "Validate using" here, but it doesn't + // work well with many options (they'd need to be changed as well) + $form['validate']['type'] = array( + '#type' => 'select', + '#title' => t('Validator'), + '#default_value' => $this->options['validate']['type'], + '#dependency' => array( + 'edit-options-specify-validation' => array('1'), + ), + ); + + $validate_types = array('none' => t('- Basic validation -')); + $plugins = views_fetch_plugin_data('argument validator'); + foreach ($plugins as $id => $info) { + if (!empty($info['no ui'])) { + continue; + } + + $valid = TRUE; + if (!empty($info['type'])) { + $valid = FALSE; + if (empty($this->definition['validate type'])) { + continue; + } + foreach ((array) $info['type'] as $type) { + if ($type == $this->definition['validate type']) { + $valid = TRUE; + break; + } + } + } + + // If we decide this validator is ok, add it to the list. + if ($valid) { + $plugin = $this->get_plugin('argument validator', $id); + if ($plugin) { + if ($plugin->access() || $this->options['validate']['type'] == $id) { + $form['validate']['options'][$id] = array( + '#prefix' => '
', + '#suffix' => '
', + '#type' => 'item', + // Even if the plugin has no options add the key to the form_state. + '#input' => TRUE, // trick it into checking input to make #process run + '#dependency' => array( + 'edit-options-specify-validation' => array('1'), + 'edit-options-validate-type' => array($id), + ), + '#dependency_count' => 2, + '#id' => 'edit-options-validate-options-' . $id, + ); + $plugin->options_form($form['validate']['options'][$id], $form_state); + $validate_types[$id] = $info['title']; + } + } + } + } + + asort($validate_types); + $form['validate']['type']['#options'] = $validate_types; + + $form['validate']['fail'] = array( + '#type' => 'select', + '#title' => t('Action to take if filter value does not validate'), + '#default_value' => $this->options['validate']['fail'], + '#options' => $validate_options, + '#dependency' => array( + 'edit-options-specify-validation' => array('1'), + ), + '#fieldset' => 'argument_present', + ); + } + + function options_validate(&$form, &$form_state) { + if (empty($form_state['values']['options'])) { + return; + } + + // Let the plugins do validation. + $default_id = $form_state['values']['options']['default_argument_type']; + $plugin = $this->get_plugin('argument default', $default_id); + if ($plugin) { + $plugin->options_validate($form['argument_default'][$default_id], $form_state, $form_state['values']['options']['argument_default'][$default_id]); + } + + // summary plugin + $summary_id = $form_state['values']['options']['summary']['format']; + $plugin = $this->get_plugin('style', $summary_id); + if ($plugin) { + $plugin->options_validate($form['summary']['options'][$summary_id], $form_state, $form_state['values']['options']['summary']['options'][$summary_id]); + } + + $validate_id = $form_state['values']['options']['validate']['type']; + $plugin = $this->get_plugin('argument validator', $validate_id); + if ($plugin) { + $plugin->options_validate($form['validate']['options'][$default_id], $form_state, $form_state['values']['options']['validate']['options'][$validate_id]); + } + + } + + function options_submit(&$form, &$form_state) { + if (empty($form_state['values']['options'])) { + return; + } + + // Let the plugins make submit modifications if necessary. + $default_id = $form_state['values']['options']['default_argument_type']; + $plugin = $this->get_plugin('argument default', $default_id); + if ($plugin) { + $options = &$form_state['values']['options']['argument_default'][$default_id]; + $plugin->options_submit($form['argument_default'][$default_id], $form_state, $options); + // Copy the now submitted options to their final resting place so they get saved. + $form_state['values']['options']['default_argument_options'] = $options; + } + + // summary plugin + $summary_id = $form_state['values']['options']['summary']['format']; + $plugin = $this->get_plugin('style', $summary_id); + if ($plugin) { + $options = &$form_state['values']['options']['summary']['options'][$summary_id]; + $plugin->options_submit($form['summary']['options'][$summary_id], $form_state, $options); + // Copy the now submitted options to their final resting place so they get saved. + $form_state['values']['options']['summary_options'] = $options; + } + + $validate_id = $form_state['values']['options']['validate']['type']; + $plugin = $this->get_plugin('argument validator', $validate_id); + if ($plugin) { + $options = &$form_state['values']['options']['validate']['options'][$validate_id]; + $plugin->options_submit($form['validate']['options'][$validate_id], $form_state, $options); + // Copy the now submitted options to their final resting place so they get saved. + $form_state['values']['options']['validate_options'] = $options; + } + + // Clear out the content of title if it's not enabled. + $options =& $form_state['values']['options']; + if (empty($options['title_enable'])) { + $options['title'] = ''; + } + } + + /** + * Provide a list of default behaviors for this argument if the argument + * is not present. + * + * Override this method to provide additional (or fewer) default behaviors. + */ + function default_actions($which = NULL) { + $defaults = array( + 'ignore' => array( + 'title' => t('Display all results for the specified field'), + 'method' => 'default_ignore', + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'default' => array( + 'title' => t('Provide default value'), + 'method' => 'default_default', + 'form method' => 'default_argument_form', + 'has default argument' => TRUE, + 'default only' => TRUE, // this can only be used for missing argument, not validation failure + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'not found' => array( + 'title' => t('Hide view'), + 'method' => 'default_not_found', + 'hard fail' => TRUE, // This is a hard fail condition + ), + 'summary' => array( + 'title' => t('Display a summary'), + 'method' => 'default_summary', + 'form method' => 'default_summary_form', + 'style plugin' => TRUE, + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'empty' => array( + 'title' => t('Display contents of "No results found"'), + 'method' => 'default_empty', + 'breadcrumb' => TRUE, // generate a breadcrumb to here + ), + 'access denied' => array( + 'title' => t('Display "Access Denied"'), + 'method' => 'default_access_denied', + 'breadcrumb' => FALSE, // generate a breadcrumb to here + ), + ); + + if ($this->view->display_handler->has_path()) { + $defaults['not found']['title'] = t('Show "Page not found"'); + } + + if ($which) { + if (!empty($defaults[$which])) { + return $defaults[$which]; + } + } + else { + return $defaults; + } + } + + /** + * Provide a form for selecting the default argument when the + * default action is set to provide default argument. + */ + function default_argument_form(&$form, &$form_state) { + $plugins = views_fetch_plugin_data('argument default'); + $options = array(); + + $form['default_argument_skip_url'] = array( + '#type' => 'checkbox', + '#title' => t('Skip default argument for view URL'), + '#default_value' => $this->options['default_argument_skip_url'], + '#description' => t('Select whether to include this default argument when constructing the URL for this view. Skipping default arguments is useful e.g. in the case of feeds.') + ); + + $form['default_argument_type'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#type' => 'select', + '#id' => 'edit-options-default-argument-type', + '#title' => t('Type'), + '#default_value' => $this->options['default_argument_type'], + + '#dependency' => array('radio:options[default_action]' => array('default')), + // Views custom key, moves this element to the appropriate container + // under the radio button. + '#argument_option' => 'default', + ); + + foreach ($plugins as $id => $info) { + if (!empty($info['no ui'])) { + continue; + } + $plugin = $this->get_plugin('argument default', $id); + if ($plugin) { + if ($plugin->access() || $this->options['default_argument_type'] == $id) { + $form['argument_default']['#argument_option'] = 'default'; + $form['argument_default'][$id] = array( + '#prefix' => '
', + '#suffix' => '
', + '#id' => 'edit-options-argument-default-options-' . $id, + '#type' => 'item', + // Even if the plugin has no options add the key to the form_state. + '#input' => TRUE, + '#dependency' => array( + 'radio:options[default_action]' => array('default'), + 'edit-options-default-argument-type' => array($id) + ), + '#dependency_count' => 2, + ); + $options[$id] = $info['title']; + $plugin->options_form($form['argument_default'][$id], $form_state); + } + } + } + + asort($options); + $form['default_argument_type']['#options'] = $options; + } + + /** + * Provide a form for selecting further summary options when the + * default action is set to display one. + */ + function default_summary_form(&$form, &$form_state) { + $style_plugins = views_fetch_plugin_data('style'); + $summary_plugins = array(); + $format_options = array(); + foreach ($style_plugins as $key => $plugin) { + if (isset($plugin['type']) && $plugin['type'] == 'summary') { + $summary_plugins[$key] = $plugin; + $format_options[$key] = $plugin['title']; + } + } + + $form['summary'] = array( + // Views custom key, moves this element to the appropriate container + // under the radio button. + '#argument_option' => 'summary', + ); + $form['summary']['sort_order'] = array( + '#type' => 'radios', + '#title' => t('Sort order'), + '#options' => array('asc' => t('Ascending'), 'desc' => t('Descending')), + '#default_value' => $this->options['summary']['sort_order'], + '#dependency' => array('radio:options[default_action]' => array('summary')), + ); + $form['summary']['number_of_records'] = array( + '#type' => 'radios', + '#title' => t('Sort by'), + '#default_value' => $this->options['summary']['number_of_records'], + '#options' => array( + 0 => $this->get_sort_name(), + 1 => t('Number of records') + ), + '#dependency' => array('radio:options[default_action]' => array('summary')), + ); + + $form['summary']['format'] = array( + '#type' => 'radios', + '#title' => t('Format'), + '#options' => $format_options, + '#default_value' => $this->options['summary']['format'], + '#dependency' => array('radio:options[default_action]' => array('summary')), + ); + + foreach ($summary_plugins as $id => $info) { + if (empty($info['uses options'])) { + continue; + } + $plugin = $this->get_plugin('style', $id); + if ($plugin) { + $form['summary']['options'][$id] = array( + '#prefix' => '
', + '#suffix' => '
', + '#id' => 'edit-options-summary-options-' . $id, + '#type' => 'item', + '#input' => TRUE, // trick it into checking input to make #process run + '#dependency' => array( + 'radio:options[default_action]' => array('summary'), + 'radio:options[summary][format]' => array($id), + ), + '#dependency_count' => 2, + ); + $options[$id] = $info['title']; + $plugin->options_form($form['summary']['options'][$id], $form_state); + } + } + } + + /** + * Handle the default action, which means our argument wasn't present. + * + * Override this method only with extreme care. + * + * @return + * A boolean value; if TRUE, continue building this view. If FALSE, + * building the view will be aborted here. + */ + function default_action($info = NULL) { + if (!isset($info)) { + $info = $this->default_actions($this->options['default_action']); + } + + if (!$info) { + return FALSE; + } + + if (!empty($info['method args'])) { + return call_user_func_array(array(&$this, $info['method']), $info['method args']); + } + else { + return $this->{$info['method']}(); + } + } + + /** + * How to act if validation failes + */ + function validate_fail() { + $info = $this->default_actions($this->options['validate']['fail']); + return $this->default_action($info); + } + /** + * Default action: ignore. + * + * If an argument was expected and was not given, in this case, simply + * ignore the argument entirely. + */ + function default_ignore() { + return TRUE; + } + + /** + * Default action: not found. + * + * If an argument was expected and was not given, in this case, report + * the view as 'not found' or hide it. + */ + function default_not_found() { + // Set a failure condition and let the display manager handle it. + $this->view->build_info['fail'] = TRUE; + return FALSE; + } + + /** + * Default action: access denied. + * + * If an argument was expected and was not given, in this case, report + * the view as 'access denied'. + */ + function default_access_denied() { + $this->view->build_info['denied'] = TRUE; + return FALSE; + } + + /** + * Default action: empty + * + * If an argument was expected and was not given, in this case, display + * the view's empty text + */ + function default_empty() { + // We return with no query; this will force the empty text. + $this->view->built = TRUE; + $this->view->executed = TRUE; + $this->view->result = array(); + return FALSE; + } + + /** + * This just returns true. The view argument builder will know where + * to find the argument from. + */ + function default_default() { + return TRUE; + } + + /** + * Determine if the argument is set to provide a default argument. + */ + function has_default_argument() { + $info = $this->default_actions($this->options['default_action']); + return !empty($info['has default argument']); + } + + /** + * Get a default argument, if available. + */ + function get_default_argument() { + $plugin = $this->get_plugin('argument default'); + if ($plugin) { + return $plugin->get_argument(); + } + } + + /** + * Process the summary arguments for display. + * + * For example, the validation plugin may want to alter an argument for use in + * the URL. + */ + function process_summary_arguments(&$args) { + if ($this->options['validate']['type'] != 'none') { + if (isset($this->validator) || $this->validator = $this->get_plugin('argument validator')) { + $this->validator->process_summary_arguments($args); + } + } + } + + /** + * Default action: summary. + * + * If an argument was expected and was not given, in this case, display + * a summary query. + */ + function default_summary() { + $this->view->build_info['summary'] = TRUE; + $this->view->build_info['summary_level'] = $this->options['id']; + + // Change the display style to the summary style for this + // argument. + $this->view->plugin_name = $this->options['summary']['format']; + $this->view->style_options = $this->options['summary_options']; + + // Clear out the normal primary field and whatever else may have + // been added and let the summary do the work. + $this->query->clear_fields(); + $this->summary_query(); + + $by = $this->options['summary']['number_of_records'] ? 'num_records' : NULL; + $this->summary_sort($this->options['summary']['sort_order'], $by); + + // Summaries have their own sorting and fields, so tell the View not + // to build these. + $this->view->build_sort = $this->view->build_fields = FALSE; + return TRUE; + } + + /** + * Build the info for the summary query. + * + * This must: + * - add_groupby: group on this field in order to create summaries. + * - add_field: add a 'num_nodes' field for the count. Usually it will + * be a count on $view->base_field + * - set_count_field: Reset the count field so we get the right paging. + * + * @return + * The alias used to get the number of records (count) for this entry. + */ + function summary_query() { + $this->ensure_my_table(); + // Add the field. + $this->base_alias = $this->query->add_field($this->table_alias, $this->real_field); + + $this->summary_name_field(); + return $this->summary_basics(); + } + + /** + * Add the name field, which is the field displayed in summary queries. + * This is often used when the argument is numeric. + */ + function summary_name_field() { + // Add the 'name' field. For example, if this is a uid argument, the + // name field would be 'name' (i.e, the username). + + if (isset($this->name_table)) { + // if the alias is different then we're probably added, not ensured, + // so look up the join and add it instead. + if ($this->table_alias != $this->name_table) { + $j = views_get_table_join($this->name_table, $this->table); + if ($j) { + $join = clone $j; + $join->left_table = $this->table_alias; + $this->name_table_alias = $this->query->add_table($this->name_table, $this->relationship, $join); + } + } + else { + $this->name_table_alias = $this->query->ensure_table($this->name_table, $this->relationship); + } + } + else { + $this->name_table_alias = $this->table_alias; + } + + if (isset($this->name_field)) { + $this->name_alias = $this->query->add_field($this->name_table_alias, $this->name_field); + } + else { + $this->name_alias = $this->base_alias; + } + } + + /** + * Some basic summary behavior that doesn't need to be repeated as much as + * code that goes into summary_query() + */ + function summary_basics($count_field = TRUE) { + // Add the number of nodes counter + $distinct = ($this->view->display_handler->get_option('distinct') && empty($this->query->no_distinct)); + + $count_alias = $this->query->add_field($this->query->base_table, $this->query->base_field, 'num_records', + array('count' => TRUE, 'distinct' => $distinct)); + $this->query->add_groupby($this->name_alias); + + if ($count_field) { + $this->query->set_count_field($this->table_alias, $this->real_field); + } + + $this->count_alias = $count_alias; + } + + /** + * Sorts the summary based upon the user's selection. The base variant of + * this is usually adequte. + * + * @param $order + * The order selected in the UI. + */ + function summary_sort($order, $by = NULL) { + $this->query->add_orderby(NULL, NULL, $order, (!empty($by) ? $by : $this->name_alias)); + } + + /** + * Provide the argument to use to link from the summary to the next level; + * this will be called once per row of a summary, and used as part of + * $view->get_url(). + * + * @param $data + * The query results for the row. + */ + function summary_argument($data) { + return $data->{$this->base_alias}; + } + + /** + * Provides the name to use for the summary. By default this is just + * the name field. + * + * @param $data + * The query results for the row. + */ + function summary_name($data) { + $value = $data->{$this->name_alias}; + if (empty($value) && !empty($this->definition['empty field name'])) { + $value = $this->definition['empty field name']; + } + return check_plain($value); + } + + /** + * Set up the query for this argument. + * + * The argument sent may be found at $this->argument. + */ + function query($group_by = FALSE) { + $this->ensure_my_table(); + $this->query->add_where(0, "$this->table_alias.$this->real_field", $this->argument); + } + + /** + * Get the title this argument will assign the view, given the argument. + * + * This usually needs to be overridden to provide a proper title. + */ + function title() { + return check_plain($this->argument); + } + + /** + * Called by the view object to get the title. This may be set by a + * validator so we don't necessarily call through to title(). + */ + function get_title() { + if (isset($this->validated_title)) { + return $this->validated_title; + } + else { + return $this->title(); + } + } + + /** + * Validate that this argument works. By default, all arguments are valid. + */ + function validate_arg($arg) { + // By using % in URLs, arguments could be validated twice; this eases + // that pain. + if (isset($this->argument_validated)) { + return $this->argument_validated; + } + + if ($this->is_exception($arg)) { + return $this->argument_validated = TRUE; + } + + if ($this->options['validate']['type'] == 'none') { + return $this->argument_validated = $this->validate_argument_basic($arg); + } + + $plugin = $this->get_plugin('argument validator'); + if ($plugin) { + return $this->argument_validated = $plugin->validate_argument($arg); + } + + // If the plugin isn't found, fall back to the basic validation path: + return $this->argument_validated = $this->validate_argument_basic($arg); + } + + /** + * Called by the menu system to validate an argument. + * + * This checks to see if this is a 'soft fail', which means that if the + * argument fails to validate, but there is an action to take anyway, + * then validation cannot actually fail. + */ + function validate_argument($arg) { + $validate_info = $this->default_actions($this->options['validate']['fail']); + if (empty($validate_info['hard fail'])) { + return TRUE; + } + + $rc = $this->validate_arg($arg); + + // If the validator has changed the validate fail condition to a + // soft fail, deal with that: + $validate_info = $this->default_actions($this->options['validate']['fail']); + if (empty($validate_info['hard fail'])) { + return TRUE; + } + + return $rc; + } + + /** + * Provide a basic argument validation. + * + * This can be overridden for more complex types; the basic + * validator only checks to see if the argument is not NULL + * or is numeric if the definition says it's numeric. + */ + function validate_argument_basic($arg) { + if (!isset($arg) || $arg === '') { + return FALSE; + } + + if (!empty($this->definition['numeric']) && !isset($this->options['break_phrase']) && !is_numeric($arg)) { + return FALSE; + } + + return TRUE; + } + + /** + * Set the input for this argument + * + * @return TRUE if it successfully validates; FALSE if it does not. + */ + function set_argument($arg) { + $this->argument = $arg; + return $this->validate_arg($arg); + } + + /** + * Get the value of this argument. + */ + function get_value() { + // If we already processed this argument, we're done. + if (isset($this->argument)) { + return $this->argument; + } + + // Otherwise, we have to pretend to process ourself to find the value. + $value = NULL; + // Find the position of this argument within the view. + $position = 0; + foreach ($this->view->argument as $id => $argument) { + if ($id == $this->options['id']) { + break; + } + $position++; + } + + $arg = isset($this->view->args[$position]) ? $this->view->args[$position] : NULL; + $this->position = $position; + + // Clone ourselves so that we don't break things when we're really + // processing the arguments. + $argument = clone $this; + if (!isset($arg) && $argument->has_default_argument()) { + $arg = $argument->get_default_argument(); + + // remember that this argument was computed, not passed on the URL. + $this->is_default = TRUE; + } + // Set the argument, which will also validate that the argument can be set. + if ($argument->set_argument($arg)) { + $value = $argument->argument; + } + unset($argument); + return $value; + } + + /** + * Export handler for summary export. + * + * Arguments can have styles for the summary view. This special export + * handler makes sure this works properly. + */ + function export_summary($indent, $prefix, $storage, $option, $definition, $parents) { + $output = ''; + $name = $this->options['summary'][$option]; + $options = $this->options['summary_options']; + + $plugin = views_get_plugin('style', $name); + if ($plugin) { + $plugin->init($this->view, $this->view->display_handler->display, $options); + // Write which plugin to use. + $output .= $indent . $prefix . "['summary']['$option'] = '$name';\n"; + + // Pass off to the plugin to export itself. + $output .= $plugin->export_options($indent, $prefix . "['summary_options']"); + } + + return $output; + } + + /** + * Export handler for validation export. + * + * Arguments use validation plugins. This special export handler makes sure + * this works properly. + */ + function export_validation($indent, $prefix, $storage, $option, $definition, $parents) { + $output = ''; + $name = $this->options['validate'][$option]; + $options = $this->options['validate_options']; + + $plugin = views_get_plugin('argument validator', $name); + if ($plugin) { + $plugin->init($this->view, $this->display, $options); + // Write which plugin to use. + $output .= $indent . $prefix . "['validate']['$option'] = '$name';\n"; + + // Pass off to the plugin to export itself. + $output .= $plugin->export_options($indent, $prefix . "['validate_options']"); + } + + return $output; + } + + /** + * Generic plugin export handler. + * + * Since style and validation plugins have their own export handlers, this + * one is currently only used for default argument plugins. + */ + function export_plugin($indent, $prefix, $storage, $option, $definition, $parents) { + $output = ''; + if ($option == 'default_argument_type') { + $type = 'argument default'; + $option_name = 'default_argument_options'; + } + + $plugin = $this->get_plugin($type); + $name = $this->options[$option]; + + if ($plugin) { + // Write which plugin to use. + $output .= $indent . $prefix . "['$option'] = '$name';\n"; + + // Pass off to the plugin to export itself. + $output .= $plugin->export_options($indent, $prefix . "['$option_name']"); + } + + return $output; + } + + /** + * Get the display or row plugin, if it exists. + */ + function get_plugin($type = 'argument default', $name = NULL) { + $options = array(); + switch ($type) { + case 'argument default': + $plugin_name = $this->options['default_argument_type']; + $options_name = 'default_argument_options'; + break; + case 'argument validator': + $plugin_name = $this->options['validate']['type']; + $options_name = 'validate_options'; + break; + case 'style': + $plugin_name = $this->options['summary']['format']; + $options_name = 'summary_options'; + } + + if (!$name) { + $name = $plugin_name; + } + + // we only fetch the options if we're fetching the plugin actually + // in use. + if ($name == $plugin_name) { + $options = $this->options[$options_name]; + } + + $plugin = views_get_plugin($type, $name); + if ($plugin) { + // Style plugins expects different parameters as argument related plugins. + if ($type == 'style') { + $plugin->init($this->view, $this->view->display_handler->display, $options); + } + else { + $plugin->init($this->view, $this, $options); + } + return $plugin; + } + } + + /** + * Return a description of how the argument would normally be sorted. + * + * Subclasses should override this to specify what the default sort order of + * their argument is (e.g. alphabetical, numeric, date). + */ + function get_sort_name() { + return t('Default sort', array(), array('context' => 'Sort order')); + } +} + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_argument_handlers + */ +class views_handler_argument_broken extends views_handler_argument { + function ui_name($short = FALSE) { + return t('Broken/missing handler'); + } + + function ensure_my_table() { /* No table to ensure! */ } + function query($group_by = FALSE) { /* No query to run */ } + function options_form(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '
' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '
', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + function broken() { return TRUE; } +} + +/** + * @} + */ diff --git a/handlers/views_handler_argument_date.inc b/handlers/views_handler_argument_date.inc new file mode 100644 index 00000000..6803c360 --- /dev/null +++ b/handlers/views_handler_argument_date.inc @@ -0,0 +1,101 @@ + t('Current date')); + $form['default_argument_type']['#options'] += array('node_created' => t("Current node's creation time")); + $form['default_argument_type']['#options'] += array('node_changed' => t("Current node's update time")); } + + /** + * Set the empty argument value to the current date, + * formatted appropriately for this argument. + */ + function get_default_argument($raw = FALSE) { + if (!$raw && $this->options['default_argument_type'] == 'date') { + return date($this->arg_format, REQUEST_TIME); + } + else if (!$raw && in_array($this->options['default_argument_type'], array('node_created', 'node_changed'))) { + foreach (range(1, 3) as $i) { + $node = menu_get_object('node', $i); + if (!empty($node)) { + continue; + } + } + + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + } + + if (empty($node)) { + return parent::get_default_argument(); + } + elseif ($this->options['default_argument_type'] == 'node_created') { + return date($this->arg_format, $node->created); + } + elseif ($this->options['default_argument_type'] == 'node_changed') { + return date($this->arg_format, $node->changed); + } + } + + return parent::get_default_argument($raw); + } + + /** + * The date handler provides some default argument types, which aren't argument default plugins, + * so addapt the export mechanism. + */ + function export_plugin($indent, $prefix, $storage, $option, $definition, $parents) { + + // Only use a special behaviour for the special argument types, else just + // use the default behaviour. + if ($option == 'default_argument_type') { + $type = 'argument default'; + $option_name = 'default_argument_options'; + + $plugin = $this->get_plugin($type); + $name = $this->options[$option]; + if (in_array($name, array('date', 'node_created', 'node_changed'))) { + + // Write which plugin to use. + $output = $indent . $prefix . "['$option'] = '$name';\n"; + return $output; + } + } + return parent::export_plugin($indent, $prefix, $storage, $option, $definition, $parents); + } + + + function get_sort_name() { + return t('Date', array(), array('context' => 'Sort order')); + } +} diff --git a/handlers/views_handler_argument_formula.inc b/handlers/views_handler_argument_formula.inc new file mode 100644 index 00000000..76f5991a --- /dev/null +++ b/handlers/views_handler_argument_formula.inc @@ -0,0 +1,63 @@ +definition['formula'])) { + $this->formula = $this->definition['formula']; + } + } + + function get_formula() { + return str_replace('***table***', $this->table_alias, $this->formula); + } + + /** + * Build the summary query based on a formula + */ + function summary_query() { + $this->ensure_my_table(); + // Now that our table is secure, get our formula. + $formula = $this->get_formula(); + + // Add the field. + $this->base_alias = $this->name_alias = $this->query->add_field(NULL, $formula, $this->field); + $this->query->set_count_field(NULL, $formula, $this->field); + + return $this->summary_basics(FALSE); + } + + /** + * Build the query based upon the formula + */ + function query($group_by = FALSE) { + $this->ensure_my_table(); + // Now that our table is secure, get our formula. + $placeholder = $this->placeholder(); + $formula = $this->get_formula() .' = ' . $placeholder; + $placeholders = array( + $placeholder => $this->argument, + ); + $this->query->add_where(0, $formula, $placeholders, 'formula'); + } +} diff --git a/handlers/views_handler_argument_group_by_numeric.inc b/handlers/views_handler_argument_group_by_numeric.inc new file mode 100644 index 00000000..aa522ea0 --- /dev/null +++ b/handlers/views_handler_argument_group_by_numeric.inc @@ -0,0 +1,29 @@ +ensure_my_table(); + $field = $this->get_field(); + $placeholder = $this->placeholder(); + + $this->query->add_having_expression(0, "$field = $placeholder", array($placeholder => $this->argument)); + } + + function ui_name($short = FALSE) { + return $this->get_field(parent::ui_name($short)); + } + + function get_sort_name() { + return t('Numerical', array(), array('context' => 'Sort order')); + } +} diff --git a/handlers/views_handler_argument_many_to_one.inc b/handlers/views_handler_argument_many_to_one.inc new file mode 100644 index 00000000..3446760f --- /dev/null +++ b/handlers/views_handler_argument_many_to_one.inc @@ -0,0 +1,185 @@ +helper = new views_many_to_one_helper($this); + + // Ensure defaults for these, during summaries and stuff: + $this->operator = 'or'; + $this->value = array(); + } + + function option_definition() { + $options = parent::option_definition(); + + if (!empty($this->definition['numeric'])) { + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + } + + $options['add_table'] = array('default' => FALSE, 'bool' => TRUE); + $options['require_value'] = array('default' => FALSE, 'bool' => TRUE); + + if (isset($this->helper)) { + $this->helper->option_definition($options); + } + else { + $helper = new views_many_to_one_helper($this); + $helper->option_definition($options); + } + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // allow + for or, , for and + if (!empty($this->definition['numeric'])) { + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'), + '#default_value' => !empty($this->options['break_phrase']), + '#fieldset' => 'more', + ); + } + + $form['add_table'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple filter values to work together'), + '#description' => t('If selected, multiple instances of this filter can work together, as though multiple values were supplied to the same filter. This setting is not compatible with the "Reduce duplicates" setting.'), + '#default_value' => !empty($this->options['add_table']), + '#fieldset' => 'more', + ); + + $form['require_value'] = array( + '#type' => 'checkbox', + '#title' => t('Do not display items with no value in summary'), + '#default_value' => !empty($this->options['require_value']), + '#fieldset' => 'more', + ); + + $this->helper->options_form($form, $form_state); + } + + /** + * Override ensure_my_table so we can control how this joins in. + * The operator actually has influence over joining. + */ + function ensure_my_table() { + $this->helper->ensure_my_table(); + } + + function query($group_by = FALSE) { + $empty = FALSE; + if (isset($this->definition['zero is null']) && $this->definition['zero is null']) { + if (empty($this->argument)) { + $empty = TRUE; + } + } + else { + if (!isset($this->argument)) { + $empty = TRUE; + } + } + if ($empty) { + parent::ensure_my_table(); + $this->query->add_where(0, "$this->table_alias.$this->real_field", NULL, 'IS NULL'); + return; + } + + if (!empty($this->options['break_phrase'])) { + views_break_phrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + $this->helper->add_filter(); + } + + function title() { + if (!$this->argument) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if (!empty($this->options['break_phrase'])) { + views_break_phrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + // @todo -- both of these should check definition for alternate keywords. + + if (empty($this->value)) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if ($this->value === array(-1)) { + return !empty($this->definition['invalid input']) ? $this->definition['invalid input'] : t('Invalid input'); + } + + return implode($this->operator == 'or' ? ' + ' : ', ', $this->title_query()); + } + + function summary_query() { + $field = $this->table . '.' . $this->field; + $join = $this->get_join(); + + if (!empty($this->options['require_value'])) { + $join->type = 'INNER'; + } + + if (empty($this->options['add_table']) || empty($this->view->many_to_one_tables[$field])) { + $this->table_alias = $this->query->ensure_table($this->table, $this->relationship, $join); + } + else { + $this->table_alias = $this->helper->summary_join(); + } + + // Add the field. + $this->base_alias = $this->query->add_field($this->table_alias, $this->real_field); + + $this->summary_name_field(); + + return $this->summary_basics(); + } + + function summary_argument($data) { + $value = $data->{$this->base_alias}; + if (empty($value)) { + $value = 0; + } + + return $value; + } + + /** + * Override for specific title lookups. + */ + function title_query() { + return $this->value; + } +} diff --git a/handlers/views_handler_argument_null.inc b/handlers/views_handler_argument_null.inc new file mode 100644 index 00000000..5b427282 --- /dev/null +++ b/handlers/views_handler_argument_null.inc @@ -0,0 +1,67 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Override options_form() so that only the relevant options + * are displayed to the user. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['must_not_be'] = array( + '#type' => 'checkbox', + '#title' => t('Fail basic validation if any argument is given'), + '#default_value' => !empty($this->options['must_not_be']), + '#description' => t('By checking this field, you can use this to make sure views with more arguments than necessary fail validation.'), + '#fieldset' => 'more', + ); + + unset($form['exception']); + } + + /** + * Override default_actions() to remove actions that don't + * make sense for a null argument. + */ + function default_actions($which = NULL) { + if ($which) { + if (in_array($which, array('ignore', 'not found', 'empty', 'default'))) { + return parent::default_actions($which); + } + return; + } + $actions = parent::default_actions(); + unset($actions['summary asc']); + unset($actions['summary desc']); + return $actions; + } + + function validate_argument_basic($arg) { + if (!empty($this->options['must_not_be'])) { + return !isset($arg); + } + + return parent::validate_argument_basic($arg); + } + + /** + * Override the behavior of query() to prevent the query + * from being changed in any way. + */ + function query($group_by = FALSE) {} +} diff --git a/handlers/views_handler_argument_numeric.inc b/handlers/views_handler_argument_numeric.inc new file mode 100644 index 00000000..8f36b212 --- /dev/null +++ b/handlers/views_handler_argument_numeric.inc @@ -0,0 +1,116 @@ + FALSE, 'bool' => TRUE); + $options['not'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // allow + for or, , for and + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'), + '#default_value' => !empty($this->options['break_phrase']), + '#fieldset' => 'more', + ); + + $form['not'] = array( + '#type' => 'checkbox', + '#title' => t('Exclude'), + '#description' => t('If selected, the numbers entered for the filter will be excluded rather than limiting the view.'), + '#default_value' => !empty($this->options['not']), + '#fieldset' => 'more', + ); + } + + function title() { + if (!$this->argument) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if (!empty($this->options['break_phrase'])) { + views_break_phrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + if (empty($this->value)) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if ($this->value === array(-1)) { + return !empty($this->definition['invalid input']) ? $this->definition['invalid input'] : t('Invalid input'); + } + + return implode($this->operator == 'or' ? ' + ' : ', ', $this->title_query()); + } + + /** + * Override for specific title lookups. + * @return array + * Returns all titles, if it's just one title it's an array with one entry. + */ + function title_query() { + return $this->value; + } + + function query($group_by = FALSE) { + $this->ensure_my_table(); + + if (!empty($this->options['break_phrase'])) { + views_break_phrase($this->argument, $this); + } + else { + $this->value = array($this->argument); + } + + $placeholder = $this->placeholder(); + $null_check = empty($this->options['not']) ? '' : "OR $this->table_alias.$this->real_field IS NULL"; + + if (count($this->value) > 1) { + $operator = empty($this->options['not']) ? 'IN' : 'NOT IN'; + $this->query->add_where_expression(0, "$this->table_alias.$this->real_field $operator($placeholder) $null_check", array($placeholder => $this->value)); + } + else { + $operator = empty($this->options['not']) ? '=' : '!='; + $this->query->add_where_expression(0, "$this->table_alias.$this->real_field $operator $placeholder $null_check", array($placeholder => $this->argument)); + } + } + + function get_sort_name() { + return t('Numerical', array(), array('context' => 'Sort order')); + } +} diff --git a/handlers/views_handler_argument_string.inc b/handlers/views_handler_argument_string.inc new file mode 100644 index 00000000..dbb98fef --- /dev/null +++ b/handlers/views_handler_argument_string.inc @@ -0,0 +1,274 @@ +definition['many to one'])) { + $this->helper = new views_many_to_one_helper($this); + + // Ensure defaults for these, during summaries and stuff: + $this->operator = 'or'; + $this->value = array(); + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['glossary'] = array('default' => FALSE, 'bool' => TRUE); + $options['limit'] = array('default' => 0); + $options['case'] = array('default' => 'none'); + $options['path_case'] = array('default' => 'none'); + $options['transform_dash'] = array('default' => FALSE, 'bool' => TRUE); + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + + if (!empty($this->definition['many to one'])) { + $options['add_table'] = array('default' => FALSE, 'bool' => TRUE); + $options['require_value'] = array('default' => FALSE, 'bool' => TRUE); + } + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['glossary'] = array( + '#type' => 'checkbox', + '#title' => t('Glossary mode'), + '#description' => t('Glossary mode applies a limit to the number of characters used in the filter value, which allows the summary view to act as a glossary.'), + '#default_value' => $this->options['glossary'], + '#fieldset' => 'more', + ); + + $form['limit'] = array( + '#type' => 'textfield', + '#title' => t('Character limit'), + '#description' => t('How many characters of the filter value to filter against. If set to 1, all fields starting with the first letter in the filter value would be matched.'), + '#default_value' => $this->options['limit'], + '#dependency' => array('edit-options-glossary' => array(TRUE)), + '#fieldset' => 'more', + ); + + $form['case'] = array( + '#type' => 'select', + '#title' => t('Case'), + '#description' => t('When printing the title and summary, how to transform the case of the filter value.'), + '#options' => array( + 'none' => t('No transform'), + 'upper' => t('Upper case'), + 'lower' => t('Lower case'), + 'ucfirst' => t('Capitalize first letter'), + 'ucwords' => t('Capitalize each word'), + ), + '#default_value' => $this->options['case'], + '#fieldset' => 'more', + ); + + $form['path_case'] = array( + '#type' => 'select', + '#title' => t('Case in path'), + '#description' => t('When printing url paths, how to transform the case of the filter value. Do not use this unless with Postgres as it uses case sensitive comparisons.'), + '#options' => array( + 'none' => t('No transform'), + 'upper' => t('Upper case'), + 'lower' => t('Lower case'), + 'ucfirst' => t('Capitalize first letter'), + 'ucwords' => t('Capitalize each word'), + ), + '#default_value' => $this->options['path_case'], + '#fieldset' => 'more', + ); + + $form['transform_dash'] = array( + '#type' => 'checkbox', + '#title' => t('Transform spaces to dashes in URL'), + '#default_value' => $this->options['transform_dash'], + '#fieldset' => 'more', + ); + + if (!empty($this->definition['many to one'])) { + $form['add_table'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple filter values to work together'), + '#description' => t('If selected, multiple instances of this filter can work together, as though multiple values were supplied to the same filter. This setting is not compatible with the "Reduce duplicates" setting.'), + '#default_value' => !empty($this->options['add_table']), + '#fieldset' => 'more', + ); + + $form['require_value'] = array( + '#type' => 'checkbox', + '#title' => t('Do not display items with no value in summary'), + '#default_value' => !empty($this->options['require_value']), + '#fieldset' => 'more', + ); + } + + // allow + for or, , for and + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'), + '#default_value' => !empty($this->options['break_phrase']), + '#fieldset' => 'more', + ); + } + + /** + * Build the summary query based on a string + */ + function summary_query() { + if (empty($this->definition['many to one'])) { + $this->ensure_my_table(); + } + else { + $this->table_alias = $this->helper->summary_join(); + } + + if (empty($this->options['glossary'])) { + // Add the field. + $this->base_alias = $this->query->add_field($this->table_alias, $this->real_field); + $this->query->set_count_field($this->table_alias, $this->real_field); + } + else { + // Add the field. + $formula = $this->get_formula(); + $this->base_alias = $this->query->add_field(NULL, $formula, $this->field . '_truncated'); + $this->query->set_count_field(NULL, $formula, $this->field, $this->field . '_truncated'); + } + + $this->summary_name_field(); + return $this->summary_basics(FALSE); + } + + /** + * Get the formula for this argument. + * + * $this->ensure_my_table() MUST have been called prior to this. + */ + function get_formula() { + return "SUBSTRING($this->table_alias.$this->real_field, 1, " . intval($this->options['limit']) . ")"; + } + + /** + * Build the query based upon the formula + */ + function query($group_by = FALSE) { + $argument = $this->argument; + if (!empty($this->options['transform_dash'])) { + $argument = strtr($argument, '-', ' '); + } + + if (!empty($this->options['break_phrase'])) { + views_break_phrase_string($argument, $this); + } + else { + $this->value = array($argument); + $this->operator = 'or'; + } + + if (!empty($this->definition['many to one'])) { + if (!empty($this->options['glossary'])) { + $this->helper->formula = TRUE; + } + $this->helper->ensure_my_table(); + $this->helper->add_filter(); + return; + } + + $this->ensure_my_table(); + $formula = FALSE; + if (empty($this->options['glossary'])) { + $field = "$this->table_alias.$this->real_field"; + } + else { + $formula = TRUE; + $field = $this->get_formula(); + } + + if (count($this->value) > 1) { + $operator = 'IN'; + $argument = $this->value; + } + else { + $operator = '='; + } + + if ($formula) { + $placeholder = $this->placeholder(); + if ($operator == 'IN') { + $field .= " IN($placeholder)"; + } + else { + $field .= ' = ' . $placeholder; + } + $placeholders = array( + $placeholder => $argument, + ); + $this->query->add_where_expression(0, $field, $placeholders); + } + else { + $this->query->add_where(0, $field, $argument, $operator); + } + } + + function summary_argument($data) { + $value = $this->case_transform($data->{$this->base_alias}, $this->options['path_case']); + if (!empty($this->options['transform_dash'])) { + $value = strtr($value, ' ', '-'); + } + return $value; + } + + function get_sort_name() { + return t('Alphabetical', array(), array('context' => 'Sort order')); + } + + function title() { + $this->argument = $this->case_transform($this->argument, $this->options['case']); + if (!empty($this->options['transform_dash'])) { + $this->argument = strtr($this->argument, '-', ' '); + } + + if (!empty($this->options['break_phrase'])) { + views_break_phrase_string($this->argument, $this); + } + else { + $this->value = array($this->argument); + $this->operator = 'or'; + } + + if (empty($this->value)) { + return !empty($this->definition['empty field name']) ? $this->definition['empty field name'] : t('Uncategorized'); + } + + if ($this->value === array(-1)) { + return !empty($this->definition['invalid input']) ? $this->definition['invalid input'] : t('Invalid input'); + } + + return implode($this->operator == 'or' ? ' + ' : ', ', $this->title_query()); + } + + /** + * Override for specific title lookups. + */ + function title_query() { + return drupal_map_assoc($this->value, 'check_plain'); + } + + function summary_name($data) { + return $this->case_transform(parent::summary_name($data), $this->options['case']); + } + +} diff --git a/handlers/views_handler_field.inc b/handlers/views_handler_field.inc new file mode 100644 index 00000000..b657bd6d --- /dev/null +++ b/handlers/views_handler_field.inc @@ -0,0 +1,1630 @@ + array('table' => tablename, + * 'field' => fieldname); as many fields as are necessary + * may be in this array. + * - click sortable: If TRUE, this field may be click sorted. + * + * @ingroup views_field_handlers + */ +class views_handler_field extends views_handler { + var $field_alias = 'unknown'; + var $aliases = array(); + + /** + * The field value prior to any rewriting. + * + * @var mixed + */ + public $original_value = NULL; + + /** + * @var array + * Stores additional fields which get's added to the query. + * The generated aliases are stored in $aliases. + */ + var $additional_fields = array(); + + /** + * Construct a new field handler. + */ + function construct() { + parent::construct(); + + $this->additional_fields = array(); + if (!empty($this->definition['additional fields'])) { + $this->additional_fields = $this->definition['additional fields']; + } + + if (!isset($this->options['exclude'])) { + $this->options['exclude'] = ''; + } + } + + /** + * Determine if this field can allow advanced rendering. + * + * Fields can set this to FALSE if they do not wish to allow + * token based rewriting or link-making. + */ + function allow_advanced_render() { + return TRUE; + } + + function init(&$view, &$options) { + parent::init($view, $options); + } + + /** + * Called to add the field to a query. + */ + function query() { + $this->ensure_my_table(); + // Add the field. + $params = $this->options['group_type'] != 'group' ? array('function' => $this->options['group_type']) : array(); + $this->field_alias = $this->query->add_field($this->table_alias, $this->real_field, NULL, $params); + + $this->add_additional_fields(); + } + + /** + * Add 'additional' fields to the query. + * + * @param $fields + * An array of fields. The key is an identifier used to later find the + * field alias used. The value is either a string in which case it's + * assumed to be a field on this handler's table; or it's an array in the + * form of + * @code array('table' => $tablename, 'field' => $fieldname) @endcode + */ + function add_additional_fields($fields = NULL) { + if (!isset($fields)) { + // notice check + if (empty($this->additional_fields)) { + return; + } + $fields = $this->additional_fields; + } + + $group_params = array(); + if ($this->options['group_type'] != 'group') { + $group_params = array( + 'function' => $this->options['group_type'], + ); + } + + if (!empty($fields) && is_array($fields)) { + foreach ($fields as $identifier => $info) { + if (is_array($info)) { + if (isset($info['table'])) { + $table_alias = $this->query->ensure_table($info['table'], $this->relationship); + } + else { + $table_alias = $this->table_alias; + } + + if (empty($table_alias)) { + debug(t('Handler @handler tried to add additional_field @identifier but @table could not be added!', array('@handler' => $this->definition['handler'], '@identifier' => $identifier, '@table' => $info['table']))); + $this->aliases[$identifier] = 'broken'; + continue; + } + + $params = array(); + if (!empty($info['params'])) { + $params = $info['params']; + } + + $params += $group_params; + $this->aliases[$identifier] = $this->query->add_field($table_alias, $info['field'], NULL, $params); + } + else { + $this->aliases[$info] = $this->query->add_field($this->table_alias, $info, NULL, $group_params); + } + } + } + } + + /** + * Called to determine what to tell the clicksorter. + */ + function click_sort($order) { + if (isset($this->field_alias)) { + // Since fields should always have themselves already added, just + // add a sort on the field. + $params = $this->options['group_type'] != 'group' ? array('function' => $this->options['group_type']) : array(); + $this->query->add_orderby(NULL, NULL, $order, $this->field_alias, $params); + } + } + + /** + * Determine if this field is click sortable. + */ + function click_sortable() { + return !empty($this->definition['click sortable']); + } + + /** + * Get this field's label. + */ + function label() { + if (!isset($this->options['label'])) { + return ''; + } + return $this->options['label']; + } + + /** + * Return an HTML element based upon the field's element type. + */ + function element_type($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) { + if ($none_supported) { + if ($this->options['element_type'] === '0') { + return ''; + } + } + if ($this->options['element_type']) { + return check_plain($this->options['element_type']); + } + + if ($default_empty) { + return ''; + } + + if ($inline) { + return 'span'; + } + + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + return 'span'; + } + + /** + * Return an HTML element for the label based upon the field's element type. + */ + function element_label_type($none_supported = FALSE, $default_empty = FALSE) { + if ($none_supported) { + if ($this->options['element_label_type'] === '0') { + return ''; + } + } + if ($this->options['element_label_type']) { + return check_plain($this->options['element_label_type']); + } + + if ($default_empty) { + return ''; + } + + return 'span'; + } + + /** + * Return an HTML element for the wrapper based upon the field's element type. + */ + function element_wrapper_type($none_supported = FALSE, $default_empty = FALSE) { + if ($none_supported) { + if ($this->options['element_wrapper_type'] === '0') { + return 0; + } + } + if ($this->options['element_wrapper_type']) { + return check_plain($this->options['element_wrapper_type']); + } + + if ($default_empty) { + return ''; + } + + return 'div'; + } + + /** + * Provide a list of elements valid for field HTML. + * + * This function can be overridden by fields that want more or fewer + * elements available, though this seems like it would be an incredibly + * rare occurence. + */ + function get_elements() { + static $elements = NULL; + if (!isset($elements)) { + $elements = variable_get('views_field_rewrite_elements', array( + '' => t('- Use default -'), + '0' => t('- None -'), + 'div' => 'DIV', + 'span' => 'SPAN', + 'h1' => 'H1', + 'h2' => 'H2', + 'h3' => 'H3', + 'h4' => 'H4', + 'h5' => 'H5', + 'h6' => 'H6', + 'p' => 'P', + 'strong' => 'STRONG', + 'em' => 'EM', + )); + } + + return $elements; + } + + /** + * Return the class of the field. + */ + function element_classes($row_index = NULL) { + $classes = explode(' ', $this->options['element_class']); + foreach ($classes as &$class) { + $class = $this->tokenize_value($class, $row_index); + $class = views_clean_css_identifier($class); + } + return implode(' ', $classes); + } + + /** + * Replace a value with tokens from the last field. + * + * This function actually figures out which field was last and uses its + * tokens so they will all be available. + */ + function tokenize_value($value, $row_index = NULL) { + if (strpos($value, '[') !== FALSE || strpos($value, '!') !== FALSE || strpos($value, '%') !== FALSE) { + $fake_item = array( + 'alter_text' => TRUE, + 'text' => $value, + ); + + // Use isset() because empty() will trigger on 0 and 0 is + // the first row. + if (isset($row_index) && isset($this->view->style_plugin->render_tokens[$row_index])) { + $tokens = $this->view->style_plugin->render_tokens[$row_index]; + } + else { + // Get tokens from the last field. + $last_field = end($this->view->field); + if (isset($last_field->last_tokens)) { + $tokens = $last_field->last_tokens; + } + else { + $tokens = $last_field->get_render_tokens($fake_item); + } + } + + $value = strip_tags($this->render_altered($fake_item, $tokens)); + if (!empty($this->options['alter']['trim_whitespace'])) { + $value = trim($value); + } + } + + return $value; + } + + /** + * Return the class of the field's label. + */ + function element_label_classes($row_index = NULL) { + $classes = explode(' ', $this->options['element_label_class']); + foreach ($classes as &$class) { + $class = $this->tokenize_value($class, $row_index); + $class = views_clean_css_identifier($class); + } + return implode(' ', $classes); + } + + /** + * Return the class of the field's wrapper. + */ + function element_wrapper_classes($row_index = NULL) { + $classes = explode(' ', $this->options['element_wrapper_class']); + foreach ($classes as &$class) { + $class = $this->tokenize_value($class, $row_index); + $class = views_clean_css_identifier($class); + } + return implode(' ', $classes); + } + + /** + * Get the value that's supposed to be rendered. + * + * This api exists so that other modules can easy set the values of the field + * without having the need to change the render method as well. + * + * @param $values + * An object containing all retrieved values. + * @param $field + * Optional name of the field where the value is stored. + */ + function get_value($values, $field = NULL) { + $alias = isset($field) ? $this->aliases[$field] : $this->field_alias; + if (isset($values->{$alias})) { + return $values->{$alias}; + } + } + + /** + * Determines if this field will be available as an option to group the result + * by in the style settings. + * + * @return bool + * TRUE if this field handler is groupable, otherwise FALSE. + */ + function use_string_group_by() { + return TRUE; + } + + function option_definition() { + $options = parent::option_definition(); + + $options['label'] = array('default' => $this->definition['title'], 'translatable' => TRUE); + $options['exclude'] = array('default' => FALSE, 'bool' => TRUE); + $options['alter'] = array( + 'contains' => array( + 'alter_text' => array('default' => FALSE, 'bool' => TRUE), + 'text' => array('default' => '', 'translatable' => TRUE), + 'make_link' => array('default' => FALSE, 'bool' => TRUE), + 'path' => array('default' => ''), + 'absolute' => array('default' => FALSE, 'bool' => TRUE), + 'external' => array('default' => FALSE, 'bool' => TRUE), + 'replace_spaces' => array('default' => FALSE, 'bool' => TRUE), + 'path_case' => array('default' => 'none', 'translatable' => FALSE), + 'trim_whitespace' => array('default' => FALSE, 'bool' => TRUE), + 'alt' => array('default' => '', 'translatable' => TRUE), + 'rel' => array('default' => ''), + 'link_class' => array('default' => ''), + 'prefix' => array('default' => '', 'translatable' => TRUE), + 'suffix' => array('default' => '', 'translatable' => TRUE), + 'target' => array('default' => ''), + 'nl2br' => array('default' => FALSE, 'bool' => TRUE), + 'max_length' => array('default' => ''), + 'word_boundary' => array('default' => TRUE, 'bool' => TRUE), + 'ellipsis' => array('default' => TRUE, 'bool' => TRUE), + 'more_link' => array('default' => FALSE, 'bool' => TRUE), + 'more_link_text' => array('default' => '', 'translatable' => TRUE), + 'more_link_path' => array('default' => ''), + 'strip_tags' => array('default' => FALSE, 'bool' => TRUE), + 'trim' => array('default' => FALSE, 'bool' => TRUE), + 'preserve_tags' => array('default' => ''), + 'html' => array('default' => FALSE, 'bool' => TRUE), + ), + ); + $options['element_type'] = array('default' => ''); + $options['element_class'] = array('default' => ''); + + $options['element_label_type'] = array('default' => ''); + $options['element_label_class'] = array('default' => ''); + $options['element_label_colon'] = array('default' => TRUE, 'bool' => TRUE); + + $options['element_wrapper_type'] = array('default' => ''); + $options['element_wrapper_class'] = array('default' => ''); + + $options['element_default_classes'] = array('default' => TRUE, 'bool' => TRUE); + + $options['empty'] = array('default' => '', 'translatable' => TRUE); + $options['hide_empty'] = array('default' => FALSE, 'bool' => TRUE); + $options['empty_zero'] = array('default' => FALSE, 'bool' => TRUE); + $options['hide_alter_empty'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Performs some cleanup tasks on the options array before saving it. + */ + function options_submit(&$form, &$form_state) { + $options = &$form_state['values']['options']; + $types = array('element_type', 'element_label_type', 'element_wrapper_type'); + $classes = array_combine(array('element_class', 'element_label_class', 'element_wrapper_class'), $types); + + foreach ($types as $type) { + if (!$options[$type . '_enable']) { + $options[$type] = ''; + } + } + + foreach ($classes as $class => $type) { + if (!$options[$class . '_enable'] || !$options[$type . '_enable']) { + $options[$class] = ''; + } + } + + if (empty($options['custom_label'])) { + $options['label'] = ''; + $options['element_label_colon'] = FALSE; + } + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $label = $this->label(); + $form['custom_label'] = array( + '#type' => 'checkbox', + '#title' => t('Create a label'), + '#description' => t('Enable to create a label for this field.'), + '#default_value' => $label !== '', + '#weight' => -103, + ); + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Label'), + '#default_value' => $label, + '#dependency' => array( + 'edit-options-custom-label' => array(1), + ), + '#weight' => -102, + ); + $form['element_label_colon'] = array( + '#type' => 'checkbox', + '#title' => t('Place a colon after the label'), + '#default_value' => $this->options['element_label_colon'], + '#dependency' => array( + 'edit-options-custom-label' => array(1), + ), + '#weight' => -101, + ); + + $form['exclude'] = array( + '#type' => 'checkbox', + '#title' => t('Exclude from display'), + '#default_value' => $this->options['exclude'], + '#description' => t('Enable to load this field as hidden. Often used to group fields, or to use as token in another field.'), + '#weight' => -100, + ); + + $form['style_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Style settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 99, + ); + + $form['element_type_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize field HTML'), + '#default_value' => !empty($this->options['element_type']) || (string) $this->options['element_type'] == '0' || !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_type'] = array( + '#title' => t('HTML element'), + '#options' => $this->get_elements(), + '#type' => 'select', + '#default_value' => $this->options['element_type'], + '#description' => t('Choose the HTML element to wrap around this field, e.g. H1, H2, etc.'), + '#dependency' => array( + 'edit-options-element-type-enable' => array(1), + ), + '#fieldset' => 'style_settings', + ); + + $form['element_class_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Create a CSS class'), + '#dependency' => array( + 'edit-options-element-type-enable' => array(1), + ), + '#default_value' => !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_class'] = array( + '#title' => t('CSS class'), + '#description' => t('You may use token substitutions from the rewriting section in this class.'), + '#type' => 'textfield', + '#default_value' => $this->options['element_class'], + '#dependency' => array( + 'edit-options-element-class-enable' => array(1), + 'edit-options-element-type-enable' => array(1), + ), + '#dependency_count' => 2, + '#fieldset' => 'style_settings', + ); + + $form['element_label_type_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize label HTML'), + '#default_value' => !empty($this->options['element_label_type']) || (string) $this->options['element_label_type'] == '0' || !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_label_type'] = array( + '#title' => t('Label HTML element'), + '#options' => $this->get_elements(FALSE), + '#type' => 'select', + '#default_value' => $this->options['element_label_type'], + '#description' => t('Choose the HTML element to wrap around this label, e.g. H1, H2, etc.'), + '#dependency' => array( + 'edit-options-element-label-type-enable' => array(1), + ), + '#fieldset' => 'style_settings', + ); + $form['element_label_class_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Create a CSS class'), + '#dependency' => array( + 'edit-options-element-label-type-enable' => array(1) + ), + '#default_value' => !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_label_class'] = array( + '#title' => t('CSS class'), + '#description' => t('You may use token substitutions from the rewriting section in this class.'), + '#type' => 'textfield', + '#default_value' => $this->options['element_label_class'], + '#dependency' => array( + 'edit-options-element-label-class-enable' => array(1), + 'edit-options-element-label-type-enable' => array(1), + ), + '#dependency_count' => 2, + '#fieldset' => 'style_settings', + ); + + $form['element_wrapper_type_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Customize field and label wrapper HTML'), + '#default_value' => !empty($this->options['element_wrapper_type']) || (string) $this->options['element_wrapper_type'] == '0' || !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_wrapper_type'] = array( + '#title' => t('Wrapper HTML element'), + '#options' => $this->get_elements(FALSE), + '#type' => 'select', + '#default_value' => $this->options['element_wrapper_type'], + '#description' => t('Choose the HTML element to wrap around this field and label, e.g. H1, H2, etc. This may not be used if the field and label are not rendered together, such as with a table.'), + '#dependency' => array( + 'edit-options-element-wrapper-type-enable' => array(1), + ), + '#fieldset' => 'style_settings', + ); + + $form['element_wrapper_class_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Create a CSS class'), + '#dependency' => array( + 'edit-options-element-wrapper-type-enable' => array(1), + ), + '#default_value' => !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0', + '#fieldset' => 'style_settings', + ); + $form['element_wrapper_class'] = array( + '#title' => t('CSS class'), + '#description' => t('You may use token substitutions from the rewriting section in this class.'), + '#type' => 'textfield', + '#default_value' => $this->options['element_wrapper_class'], + '#dependency' => array( + 'edit-options-element-wrapper-class-enable' => array(1), + 'edit-options-element-wrapper-type-enable' => array(1), + ), + '#dependency_count' => 2, + '#fieldset' => 'style_settings', + ); + + $form['element_default_classes'] = array( + '#type' => 'checkbox', + '#title' => t('Add default classes'), + '#default_value' => $this->options['element_default_classes'], + '#description' => t('Use default Views classes to identify the field, field label and field content.'), + '#fieldset' => 'style_settings', + ); + + $form['alter'] = array( + '#title' => t('Rewrite results'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 100, + ); + + if ($this->allow_advanced_render()) { + $form['alter']['#tree'] = TRUE; + $form['alter']['alter_text'] = array( + '#type' => 'checkbox', + '#title' => t('Rewrite the output of this field'), + '#description' => t('Enable to override the output of this field with custom text or replacement tokens.'), + '#default_value' => $this->options['alter']['alter_text'], + ); + + $form['alter']['text'] = array( + '#title' => t('Text'), + '#type' => 'textarea', + '#default_value' => $this->options['alter']['text'], + '#description' => t('The text to display for this field. You may include HTML. You may enter data from this view as per the "Replacement patterns" below.'), + '#dependency' => array( + 'edit-options-alter-alter-text' => array(1), + ), + ); + + $form['alter']['make_link'] = array( + '#type' => 'checkbox', + '#title' => t('Output this field as a link'), + '#description' => t('If checked, this field will be made into a link. The destination must be given below.'), + '#default_value' => $this->options['alter']['make_link'], + ); + $form['alter']['path'] = array( + '#title' => t('Link path'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['path'], + '#description' => t('The Drupal path or absolute URL for this link. You may enter data from this view as per the "Replacement patterns" below.'), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + '#maxlength' => 255, + ); + $form['alter']['absolute'] = array( + '#type' => 'checkbox', + '#title' => t('Use absolute path'), + '#default_value' => $this->options['alter']['absolute'], + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + $form['alter']['replace_spaces'] = array( + '#type' => 'checkbox', + '#title' => t('Replace spaces with dashes'), + '#default_value' => $this->options['alter']['replace_spaces'], + '#dependency' => array( + 'edit-options-alter-make-link' => array(1) + ), + ); + $form['alter']['external'] = array( + '#type' => 'checkbox', + '#title' => t('External server URL'), + '#default_value' => $this->options['alter']['external'], + '#description' => t("Links to an external server using a full URL: e.g. 'http://www.example.com' or 'www.example.com'."), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + $form['alter']['path_case'] = array( + '#type' => 'select', + '#title' => t('Transform the case'), + '#description' => t('When printing url paths, how to transform the case of the filter value.'), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + '#options' => array( + 'none' => t('No transform'), + 'upper' => t('Upper case'), + 'lower' => t('Lower case'), + 'ucfirst' => t('Capitalize first letter'), + 'ucwords' => t('Capitalize each word'), + ), + '#default_value' => $this->options['alter']['path_case'], + ); + $form['alter']['link_class'] = array( + '#title' => t('Link class'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['link_class'], + '#description' => t('The CSS class to apply to the link.'), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + $form['alter']['alt'] = array( + '#title' => t('Title text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['alt'], + '#description' => t('Text to place as "title" text which most browsers display as a tooltip when hovering over the link.'), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + $form['alter']['rel'] = array( + '#title' => t('Rel Text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['rel'], + '#description' => t('Include Rel attribute for use in lightbox2 or other javascript utility.'), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + $form['alter']['prefix'] = array( + '#title' => t('Prefix text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['prefix'], + '#description' => t('Any text to display before this link. You may include HTML.'), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + $form['alter']['suffix'] = array( + '#title' => t('Suffix text'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['suffix'], + '#description' => t('Any text to display after this link. You may include HTML.'), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + $form['alter']['target'] = array( + '#title' => t('Target'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['target'], + '#description' => t("Target of the link, such as _blank, _parent or an iframe's name. This field is rarely used."), + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + ), + ); + + + // Get a list of the available fields and arguments for token replacement. + $options = array(); + foreach ($this->view->display_handler->get_handlers('field') as $field => $handler) { + $options[t('Fields')]["[$field]"] = $handler->ui_name(); + // We only use fields up to (and including) this one. + if ($field == $this->options['id']) { + break; + } + } + $count = 0; // This lets us prepare the key as we want it printed. + foreach ($this->view->display_handler->get_handlers('argument') as $arg => $handler) { + $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->ui_name())); + $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->ui_name())); + } + + $this->document_self_tokens($options[t('Fields')]); + + // Default text. + $output = t('

You must add some additional fields to this display before using this field. These fields may be marked as Exclude from display if you prefer. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.

'); + // We have some options, so make a list. + if (!empty($options)) { + $output = t('

The following tokens are available for this field. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields. +If you would like to have the characters \'[\' and \']\' please use the html entity codes \'%5B\' or \'%5D\' or they will get replaced with empty space.

'); + foreach (array_keys($options) as $type) { + if (!empty($options[$type])) { + $items = array(); + foreach ($options[$type] as $key => $value) { + $items[] = $key . ' == ' . $value; + } + $output .= theme('item_list', + array( + 'items' => $items, + 'type' => $type + )); + } + } + } + // This construct uses 'hidden' and not markup because process doesn't + // run. It also has an extra div because the dependency wants to hide + // the parent in situations like this, so we need a second div to + // make this work. + $form['alter']['help'] = array( + '#type' => 'fieldset', + '#title' => t('Replacement patterns'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#value' => $output, + '#dependency' => array( + 'edit-options-alter-make-link' => array(1), + 'edit-options-alter-alter-text' => array(1), + 'edit-options-alter-more-link' => array(1), + ), + ); + + $form['alter']['trim'] = array( + '#type' => 'checkbox', + '#title' => t('Trim this field to a maximum length'), + '#description' => t('Enable to trim the field to a maximum length of characters'), + '#default_value' => $this->options['alter']['trim'], + ); + + $form['alter']['max_length'] = array( + '#title' => t('Maximum length'), + '#type' => 'textfield', + '#default_value' => $this->options['alter']['max_length'], + '#description' => t('The maximum number of characters this field can be.'), + '#dependency' => array( + 'edit-options-alter-trim' => array(1), + ), + ); + + $form['alter']['word_boundary'] = array( + '#type' => 'checkbox', + '#title' => t('Trim only on a word boundary'), + '#description' => t('If checked, this field be trimmed only on a word boundary. This is guaranteed to be the maximum characters stated or less. If there are no word boundaries this could trim a field to nothing.'), + '#default_value' => $this->options['alter']['word_boundary'], + '#dependency' => array( + 'edit-options-alter-trim' => array(1), + ), + ); + + $form['alter']['ellipsis'] = array( + '#type' => 'checkbox', + '#title' => t('Add an ellipsis'), + '#description' => t('If checked, a "..." will be added if a field was trimmed.'), + '#default_value' => $this->options['alter']['ellipsis'], + '#dependency' => array( + 'edit-options-alter-trim' => array(1), + ), + ); + + $form['alter']['more_link'] = array( + '#type' => 'checkbox', + '#title' => t('Add a read-more link if output is trimmed.'), + '#description' => t('If checked, a read-more link will be added at the end of the trimmed output'), + '#default_value' => $this->options['alter']['more_link'], + '#dependency' => array( + 'edit-options-alter-trim' => array(1), + ), + ); + + $form['alter']['more_link_text'] = array( + '#type' => 'textfield', + '#title' => t('More link text'), + '#default_value' => $this->options['alter']['more_link_text'], + '#description' => t('The text which will be displayed on the more link. You may enter data from this view as per the "Replacement patterns" above.'), + '#dependency_count' => 2, + '#dependency' => array( + 'edit-options-alter-trim' => array(1), + 'edit-options-alter-more-link' => array(1), + ), + ); + $form['alter']['more_link_path'] = array( + '#type' => 'textfield', + '#title' => t('More link path'), + '#default_value' => $this->options['alter']['more_link_path'], + '#description' => t('The path which is used for the more link. You may enter data from this view as per the "Replacement patterns" above.'), + '#dependency_count' => 2, + '#dependency' => array( + 'edit-options-alter-trim' => array(1), + 'edit-options-alter-more-link' => array(1), + ), + ); + + $form['alter']['html'] = array( + '#type' => 'checkbox', + '#title' => t('Field can contain HTML'), + '#description' => t('If checked, HTML corrector will be run to ensure tags are properly closed after trimming.'), + '#default_value' => $this->options['alter']['html'], + '#dependency' => array( + 'edit-options-alter-trim' => array(1), + ), + ); + + $form['alter']['strip_tags'] = array( + '#type' => 'checkbox', + '#title' => t('Strip HTML tags'), + '#description' => t('If checked, all HTML tags will be stripped.'), + '#default_value' => $this->options['alter']['strip_tags'], + ); + + $form['alter']['preserve_tags'] = array( + '#type' => 'textfield', + '#title' => t('Preserve certain tags'), + '#description' => t('List the tags that need to be preserved during the stripping process. example "<p> <br>" which will preserve all p and br elements'), + '#default_value' => $this->options['alter']['preserve_tags'], + '#dependency' => array( + 'edit-options-alter-strip-tags' => array(1), + ), + ); + + $form['alter']['trim_whitespace'] = array( + '#type' => 'checkbox', + '#title' => t('Remove whitespace'), + '#description' => t('If checked, all whitespaces at the beginning and the end of the output will be removed.'), + '#default_value' => $this->options['alter']['trim_whitespace'], + ); + + $form['alter']['nl2br'] = array( + '#type' => 'checkbox', + '#title' => t('Convert newlines to HTML <br> tags'), + '#description' => t('If checked, all newlines chars (e.g. \n) are converted into HTML <br> tags.'), + '#default_value' => $this->options['alter']['nl2br'], + ); + } + + $form['empty_field_behavior'] = array( + '#type' => 'fieldset', + '#title' => t('No results behavior'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 100, + ); + + $form['empty'] = array( + '#type' => 'textarea', + '#title' => t('No results text'), + '#default_value' => $this->options['empty'], + '#description' => t('Provide text to display if this field contains an empty result. You may include HTML. You may enter data from this view as per the "Replacement patterns" in the "Rewrite Results" section below.'), + '#fieldset' => 'empty_field_behavior', + ); + + $form['empty_zero'] = array( + '#type' => 'checkbox', + '#title' => t('Count the number 0 as empty'), + '#default_value' => $this->options['empty_zero'], + '#description' => t('Enable to display the "no results text" if the field contains the number 0.'), + '#fieldset' => 'empty_field_behavior', + ); + + $form['hide_empty'] = array( + '#type' => 'checkbox', + '#title' => t('Hide if empty'), + '#default_value' => $this->options['hide_empty'], + '#description' => t('Enable to hide this field if it is empty. Note that the field label or rewritten output may still be displayed. To hide labels, check the style or row style settings for empty fields. To hide rewritten content, check the "Hide rewriting if empty" checkbox.'), + '#fieldset' => 'empty_field_behavior', + ); + + $form['hide_alter_empty'] = array( + '#type' => 'checkbox', + '#title' => t('Hide rewriting if empty'), + '#default_value' => $this->options['hide_alter_empty'], + '#description' => t('Do not display rewritten content if this field is empty.'), + '#fieldset' => 'empty_field_behavior', + ); + } + + /** + * Provide extra data to the administration form + */ + function admin_summary() { + return $this->label(); + } + + /** + * Run before any fields are rendered. + * + * This gives the handlers some time to set up before any handler has + * been rendered. + * + * @param $values + * An array of all objects returned from the query. + */ + function pre_render(&$values) { } + + /** + * Render the field. + * + * @param $values + * The values retrieved from the database. + */ + function render($values) { + $value = $this->get_value($values); + return $this->sanitize_value($value); + } + + /** + * Render a field using advanced settings. + * + * This renders a field normally, then decides if render-as-link and + * text-replacement rendering is necessary. + */ + function advanced_render($values) { + if ($this->allow_advanced_render() && method_exists($this, 'render_item')) { + $raw_items = $this->get_items($values); + // If there are no items, set the original value to NULL. + if (empty($raw_items)) { + $this->original_value = NULL; + } + } + else { + $value = $this->render($values); + if (is_array($value)) { + $value = drupal_render($value); + } + $this->last_render = $value; + $this->original_value = $value; + } + + if ($this->allow_advanced_render()) { + $tokens = NULL; + if (method_exists($this, 'render_item')) { + $items = array(); + foreach ($raw_items as $count => $item) { + $value = $this->render_item($count, $item); + if (is_array($value)) { + $value = drupal_render($value); + } + $this->last_render = $value; + $this->original_value = $this->last_render; + + $alter = $item + $this->options['alter']; + $alter['phase'] = VIEWS_HANDLER_RENDER_TEXT_PHASE_SINGLE_ITEM; + $items[] = $this->render_text($alter); + } + + $value = $this->render_items($items); + } + else { + $alter = array('phase' => VIEWS_HANDLER_RENDER_TEXT_PHASE_COMPLETELY) + $this->options['alter']; + $value = $this->render_text($alter); + } + + if (is_array($value)) { + $value = drupal_render($value); + } + // This happens here so that render_as_link can get the unaltered value of + // this field as a token rather than the altered value. + $this->last_render = $value; + } + + if (empty($this->last_render)) { + if ($this->is_value_empty($this->last_render, $this->options['empty_zero'], FALSE)) { + $alter = $this->options['alter']; + $alter['alter_text'] = 1; + $alter['text'] = $this->options['empty']; + $alter['phase'] = VIEWS_HANDLER_RENDER_TEXT_PHASE_EMPTY; + $this->last_render = $this->render_text($alter); + } + } + + return $this->last_render; + } + + /** + * Checks if a field value is empty. + * + * @param $value + * The field value. + * @param bool $empty_zero + * Whether or not this field is configured to consider 0 as empty. + * @param bool $no_skip_empty + * Whether or not to use empty() to check the value. + * + * @return bool + * TRUE if the value is considered empty, FALSE otherwise. + */ + function is_value_empty($value, $empty_zero, $no_skip_empty = TRUE) { + if (!isset($value)) { + $empty = TRUE; + } + else { + $empty = ($empty_zero || ($value !== 0 && $value !== '0')); + } + + if ($no_skip_empty) { + $empty = empty($value) && $empty; + } + return $empty; + } + + /** + * Perform an advanced text render for the item. + * + * This is separated out as some fields may render lists, and this allows + * each item to be handled individually. + */ + function render_text($alter) { + $value = $this->last_render; + + if (!empty($alter['alter_text']) && $alter['text'] !== '') { + $tokens = $this->get_render_tokens($alter); + $value = $this->render_altered($alter, $tokens); + } + + if (!empty($this->options['alter']['trim_whitespace'])) { + $value = trim($value); + } + + // Check if there should be no further rewrite for empty values. + $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->is_value_empty($this->original_value, $this->options['empty_zero']); + + // Check whether the value is empty and return nothing, so the field isn't rendered. + // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE). + // For numeric values you can specify whether "0"/0 should be empty. + if ((($this->options['hide_empty'] && empty($value)) + || ($alter['phase'] != VIEWS_HANDLER_RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty)) + && $this->is_value_empty($value, $this->options['empty_zero'], FALSE)) { + return ''; + } + // Only in empty phase. + if ($alter['phase'] == VIEWS_HANDLER_RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) { + // If we got here then $alter contains the value of "No results text" + // and so there is nothing left to do. + return $value; + } + + if (!empty($alter['strip_tags'])) { + $value = strip_tags($value, $alter['preserve_tags']); + } + + $suffix = ''; + if (!empty($alter['trim']) && !empty($alter['max_length'])) { + $length = strlen($value); + $value = $this->render_trim_text($alter, $value); + if ($this->options['alter']['more_link'] && strlen($value) < $length) { + $tokens = $this->get_render_tokens($alter); + $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : t('more'); + $more_link_text = strtr(filter_xss_admin($more_link_text), $tokens); + $more_link_path = $this->options['alter']['more_link_path']; + $more_link_path = strip_tags(decode_entities(strtr($more_link_path, $tokens))); + + // Take sure that paths which was runned through url() does work as well. + $base_path = base_path(); + // Checks whether the path starts with the base_path. + if (strpos($more_link_path, $base_path) === 0) { + $more_link_path = drupal_substr($more_link_path, drupal_strlen($base_path)); + } + + $more_link = l($more_link_text, $more_link_path, array('attributes' => array('class' => array('views-more-link')))); + + $suffix .= " " . $more_link; + } + } + + if (!empty($alter['nl2br'])) { + $value = nl2br($value); + } + $this->last_render_text = $value; + + if (!empty($alter['make_link']) && !empty($alter['path'])) { + if (!isset($tokens)) { + $tokens = $this->get_render_tokens($alter); + } + $value = $this->render_as_link($alter, $value, $tokens); + } + + return $value . $suffix; + } + + /** + * Render this field as altered text, from a fieldset set by the user. + */ + function render_altered($alter, $tokens) { + // Filter this right away as our substitutions are already sanitized. + $value = filter_xss_admin($alter['text']); + $value = strtr($value, $tokens); + + return $value; + } + + /** + * Trim the field down to the specified length. + */ + function render_trim_text($alter, $value) { + if (!empty($alter['strip_tags'])) { + // NOTE: It's possible that some external fields might override the + // element type so if someone from, say, CCK runs into a bug here, + // this may be why =) + $this->definition['element type'] = 'span'; + } + return views_trim_text($alter, $value); + } + + /** + * Render this field as a link, with the info from a fieldset set by + * the user. + */ + function render_as_link($alter, $text, $tokens) { + $value = ''; + + if (!empty($alter['prefix'])) { + $value .= filter_xss_admin(strtr($alter['prefix'], $tokens)); + } + + $options = array( + 'html' => TRUE, + 'absolute' => !empty($alter['absolute']) ? TRUE : FALSE, + ); + + // $path will be run through check_url() by l() so we do not need to + // sanitize it ourselves. + $path = $alter['path']; + + // strip_tags() removes , so check whether its different to front. + if ($path != '') { + // Use strip tags as there should never be HTML in the path. + // However, we need to preserve special characters like " that + // were removed by check_plain(). + $path = strip_tags(decode_entities(strtr($path, $tokens))); + + if (!empty($alter['path_case']) && $alter['path_case'] != 'none') { + $path = $this->case_transform($path, $this->options['alter']['path_case']); + } + + if (!empty($alter['replace_spaces'])) { + $path = str_replace(' ', '-', $path); + } + } + + // Parse the URL and move any query and fragment parameters out of the path. + $url = parse_url($path); + + // Seriously malformed URLs may return FALSE or empty arrays. + if (empty($url)) { + return $text; + } + + // If the path is empty do not build a link around the given text and return + // it as is. + // http://www.example.com URLs will not have a $url['path'], so check host as well. + if (empty($url['path']) && empty($url['host']) && empty($url['fragment'])) { + return $text; + } + + // If no scheme is provided in the $path, assign the default 'http://'. + // This allows a url of 'www.example.com' to be converted to 'http://www.example.com'. + // Only do this on for external URLs. + if ($alter['external']){ + if (!isset($url['scheme'])) { + // There is no scheme, add the default 'http://' to the $path. + $path = "http://$path"; + // Reset the $url array to include the new scheme. + $url = parse_url($path); + } + } + + if (isset($url['query'])) { + $path = strtr($path, array('?' . $url['query'] => '')); + $query = drupal_get_query_array($url['query']); + // Remove query parameters that were assigned a query string replacement + // token for which there is no value available. + foreach ($query as $param => $val) { + if ($val == '%' . $param) { + unset($query[$param]); + } + } + $options['query'] = $query; + } + if (isset($url['fragment'])) { + $path = strtr($path, array('#' . $url['fragment'] => '')); + // If the path is empty we want to have a fragment for the current site. + if ($path == '') { + $options['external'] = TRUE; + } + $options['fragment'] = $url['fragment']; + } + + $alt = strtr($alter['alt'], $tokens); + // Set the title attribute of the link only if it improves accessibility + if ($alt && $alt != $text) { + $options['attributes']['title'] = decode_entities($alt); + } + + $class = strtr($alter['link_class'], $tokens); + if ($class) { + $options['attributes']['class'] = array($class); + } + + if (!empty($alter['rel']) && $rel = strtr($alter['rel'], $tokens)) { + $options['attributes']['rel'] = $rel; + } + + $target = check_plain(trim(strtr($alter['target'],$tokens))); + if (!empty($target)) { + $options['attributes']['target'] = $target; + } + + // Allow the addition of arbitrary attributes to links. Additional attributes + // currently can only be altered in preprocessors and not within the UI. + if (isset($alter['link_attributes']) && is_array($alter['link_attributes'])) { + foreach ($alter['link_attributes'] as $key => $attribute) { + if (!isset($options['attributes'][$key])) { + $options['attributes'][$key] = strtr($attribute, $tokens); + } + } + } + + // If the query and fragment were programatically assigned overwrite any + // parsed values. + if (isset($alter['query'])) { + // Convert the query to a string, perform token replacement, and then + // convert back to an array form for l(). + $options['query'] = drupal_http_build_query($alter['query']); + $options['query'] = strtr($options['query'], $tokens); + $options['query'] = drupal_get_query_array($options['query']); + } + if (isset($alter['alias'])) { + // Alias is a boolean field, so no token. + $options['alias'] = $alter['alias']; + } + if (isset($alter['fragment'])) { + $options['fragment'] = strtr($alter['fragment'], $tokens); + } + if (isset($alter['language'])) { + $options['language'] = $alter['language']; + } + + // If the url came from entity_uri(), pass along the required options. + if (isset($alter['entity'])) { + $options['entity'] = $alter['entity']; + } + if (isset($alter['entity_type'])) { + $options['entity_type'] = $alter['entity_type']; + } + + $value .= l($text, $path, $options); + + if (!empty($alter['suffix'])) { + $value .= filter_xss_admin(strtr($alter['suffix'], $tokens)); + } + + return $value; + } + + /** + * Get the 'render' tokens to use for advanced rendering. + * + * This runs through all of the fields and arguments that + * are available and gets their values. This will then be + * used in one giant str_replace(). + */ + function get_render_tokens($item) { + $tokens = array(); + if (!empty($this->view->build_info['substitutions'])) { + $tokens = $this->view->build_info['substitutions']; + } + $count = 0; + foreach ($this->view->display_handler->get_handlers('argument') as $arg => $handler) { + $token = '%' . ++$count; + if (!isset($tokens[$token])) { + $tokens[$token] = ''; + } + + // Use strip tags as there should never be HTML in the path. + // However, we need to preserve special characters like " that + // were removed by check_plain(). + $tokens['!' . $count] = isset($this->view->args[$count - 1]) ? strip_tags(decode_entities($this->view->args[$count - 1])) : ''; + } + + // Get flattened set of tokens for any array depth in $_GET parameters. + $tokens += $this->get_token_values_recursive($_GET); + + // Now add replacements for our fields. + foreach ($this->view->display_handler->get_handlers('field') as $field => $handler) { + if (isset($handler->last_render)) { + $tokens["[$field]"] = $handler->last_render; + } + else { + $tokens["[$field]"] = ''; + } + if (!empty($item)) { + $this->add_self_tokens($tokens, $item); + } + + // We only use fields up to (and including) this one. + if ($field == $this->options['id']) { + break; + } + } + + // Store the tokens for the row so we can reference them later if necessary. + $this->view->style_plugin->render_tokens[$this->view->row_index] = $tokens; + $this->last_tokens = $tokens; + + return $tokens; + } + + /** + * Recursive function to add replacements for nested query string parameters. + * + * E.g. if you pass in the following array: + * array( + * 'foo' => array( + * 'a' => 'value', + * 'b' => 'value', + * ), + * 'bar' => array( + * 'a' => 'value', + * 'b' => array( + * 'c' => value, + * ), + * ), + * ); + * + * Would yield the following array of tokens: + * array( + * '%foo_a' => 'value' + * '%foo_b' => 'value' + * '%bar_a' => 'value' + * '%bar_b_c' => 'value' + * ); + * + * @param $array + * An array of values. + * + * @param $parent_keys + * An array of parent keys. This will represent the array depth. + * + * @return + * An array of available tokens, with nested keys representative of the array structure. + */ + function get_token_values_recursive(array $array, array $parent_keys = array()) { + $tokens = array(); + + foreach ($array as $param => $val) { + if (is_array($val)) { + // Copy parent_keys array, so we don't afect other elements of this iteration. + $child_parent_keys = $parent_keys; + $child_parent_keys[] = $param; + // Get the child tokens. + $child_tokens = $this->get_token_values_recursive($val, $child_parent_keys); + // Add them to the current tokens array. + $tokens += $child_tokens; + } + else { + // Create a token key based on array element structure. + $token_string = !empty($parent_keys) ? implode('_', $parent_keys) . '_' . $param : $param; + $tokens['%' . $token_string] = strip_tags(decode_entities($val)); + } + } + + return $tokens; + } + + /** + * Add any special tokens this field might use for itself. + * + * This method is intended to be overridden by items that generate + * fields as a list. For example, the field that displays all terms + * on a node might have tokens for the tid and the term. + * + * By convention, tokens should follow the format of [token-subtoken] + * where token is the field ID and subtoken is the field. If the + * field ID is terms, then the tokens might be [terms-tid] and [terms-name]. + */ + function add_self_tokens(&$tokens, $item) { } + + /** + * Document any special tokens this field might use for itself. + * + * @see add_self_tokens() + */ + function document_self_tokens(&$tokens) { } + + /** + * Call out to the theme() function, which probably just calls render() but + * allows sites to override output fairly easily. + */ + function theme($values) { + return theme($this->theme_functions(), + array( + 'view' => $this->view, + 'field' => $this, + 'row' => $values + )); + } + + function theme_functions() { + $themes = array(); + $hook = 'views_view_field'; + + $display = $this->view->display[$this->view->current_display]; + + if (!empty($display)) { + $themes[] = $hook . '__' . $this->view->name . '__' . $display->id . '__' . $this->options['id']; + $themes[] = $hook . '__' . $this->view->name . '__' . $display->id; + $themes[] = $hook . '__' . $display->id . '__' . $this->options['id']; + $themes[] = $hook . '__' . $display->id; + if ($display->id != $display->display_plugin) { + $themes[] = $hook . '__' . $this->view->name . '__' . $display->display_plugin . '__' . $this->options['id']; + $themes[] = $hook . '__' . $this->view->name . '__' . $display->display_plugin; + $themes[] = $hook . '__' . $display->display_plugin . '__' . $this->options['id']; + $themes[] = $hook . '__' . $display->display_plugin; + } + } + $themes[] = $hook . '__' . $this->view->name . '__' . $this->options['id']; + $themes[] = $hook . '__' . $this->view->name; + $themes[] = $hook . '__' . $this->options['id']; + $themes[] = $hook; + + return $themes; + } + + function ui_name($short = FALSE) { + return $this->get_field(parent::ui_name($short)); + } +} + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_field_handlers + */ +class views_handler_field_broken extends views_handler_field { + function ui_name($short = FALSE) { + return t('Broken/missing handler'); + } + + function ensure_my_table() { /* No table to ensure! */ } + function query($group_by = FALSE) { /* No query to run */ } + function options_form(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '
' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '
', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + function broken() { return TRUE; } +} + +/** + * Render a numeric value as a size. + * + * @ingroup views_field_handlers + */ +class views_handler_field_file_size extends views_handler_field { + function option_definition() { + $options = parent::option_definition(); + + $options['file_size_display'] = array('default' => 'formatted'); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['file_size_display'] = array( + '#title' => t('File size display'), + '#type' => 'select', + '#options' => array( + 'formatted' => t('Formatted (in KB or MB)'), + 'bytes' => t('Raw bytes'), + ), + ); + } + + function render($values) { + $value = $this->get_value($values); + if ($value) { + switch ($this->options['file_size_display']) { + case 'bytes': + return $value; + case 'formatted': + default: + return format_size($value); + } + } + else { + return ''; + } + } +} + +/** + * A handler to run a field through simple XSS filtering. + * + * @ingroup views_field_handlers + */ +class views_handler_field_xss extends views_handler_field { + function render($values) { + $value = $this->get_value($values); + return $this->sanitize_value($value, 'xss'); + } +} + +/** + * @} + */ diff --git a/handlers/views_handler_field_boolean.inc b/handlers/views_handler_field_boolean.inc new file mode 100644 index 00000000..52e02bff --- /dev/null +++ b/handlers/views_handler_field_boolean.inc @@ -0,0 +1,81 @@ + array( + * 'sticky' => array(t('Sticky'), ''), + * ), + * @endcode + * + * @ingroup views_field_handlers + */ +class views_handler_field_boolean extends views_handler_field { + function option_definition() { + $options = parent::option_definition(); + $options['type'] = array('default' => 'yes-no'); + $options['not'] = array('definition bool' => 'reverse'); + + return $options; + } + + function init(&$view, &$options) { + parent::init($view, $options); + + $default_formats = array( + 'yes-no' => array(t('Yes'), t('No')), + 'true-false' => array(t('True'), t('False')), + 'on-off' => array(t('On'), t('Off')), + 'enabled-disabled' => array(t('Enabled'), t('Disabled')), + 'boolean' => array(1, 0), + 'unicode-yes-no' => array('✔', '✖'), + ); + $output_formats = isset($this->definition['output formats']) ? $this->definition['output formats'] : array(); + $this->formats = array_merge($default_formats, $output_formats); + } + + function options_form(&$form, &$form_state) { + foreach ($this->formats as $key => $item) { + $options[$key] = implode('/', $item); + } + + $form['type'] = array( + '#type' => 'select', + '#title' => t('Output format'), + '#options' => $options, + '#default_value' => $this->options['type'], + ); + $form['not'] = array( + '#type' => 'checkbox', + '#title' => t('Reverse'), + '#description' => t('If checked, true will be displayed as false.'), + '#default_value' => $this->options['not'], + ); + parent::options_form($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($this->options['not'])) { + $value = !$value; + } + + if (isset($this->formats[$this->options['type']])) { + return $value ? $this->formats[$this->options['type']][0] : $this->formats[$this->options['type']][1]; + } + else { + return $value ? $this->formats['yes-no'][0] : $this->formats['yes-no'][1]; + } + } +} diff --git a/handlers/views_handler_field_contextual_links.inc b/handlers/views_handler_field_contextual_links.inc new file mode 100644 index 00000000..faeeedc2 --- /dev/null +++ b/handlers/views_handler_field_contextual_links.inc @@ -0,0 +1,102 @@ + array()); + $options['destination'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $all_fields = $this->view->display_handler->get_field_labels(); + // Offer to include only those fields that follow this one. + $field_options = array_slice($all_fields, 0, array_search($this->options['id'], array_keys($all_fields))); + $form['fields'] = array( + '#type' => 'checkboxes', + '#title' => t('Fields'), + '#description' => t('Fields to be included as contextual links.'), + '#options' => $field_options, + '#default_value' => $this->options['fields'], + ); + $form['destination'] = array( + '#type' => 'checkbox', + '#title' => t('Include destination'), + '#description' => t('Include a "destination" parameter in the link to return the user to the original view upon completing the contextual action.'), + '#default_value' => $this->options['destination'], + ); + } + + function pre_render(&$values) { + // Add a row plugin css class for the contextual link. + $class = 'contextual-links-region'; + if (!empty($this->view->style_plugin->options['row_class'])) { + $this->view->style_plugin->options['row_class'] .= " $class"; + } + else { + $this->view->style_plugin->options['row_class'] = $class; + } + } + + /** + * Render the contextual fields. + */ + function render($values) { + $links = array(); + foreach ($this->options['fields'] as $field) { + if (empty($this->view->style_plugin->rendered_fields[$this->view->row_index][$field])) { + continue; + } + $title = $this->view->field[$field]->last_render_text; + $path = ''; + if (!empty($this->view->field[$field]->options['alter']['path'])) { + $path = $this->view->field[$field]->options['alter']['path']; + } + if (!empty($title) && !empty($path)) { + // Make sure that tokens are replaced for this paths as well. + $tokens = $this->get_render_tokens(array()); + $path = strip_tags(decode_entities(strtr($path, $tokens))); + + $links[$field] = array( + 'href' => $path, + 'title' => $title, + ); + if (!empty($this->options['destination'])) { + $links[$field]['query'] = drupal_get_destination(); + } + } + } + + if (!empty($links)) { + $build = array( + '#prefix' => '', + '#theme' => 'links__contextual', + '#links' => $links, + '#attributes' => array('class' => array('contextual-links')), + '#attached' => array( + 'library' => array(array('contextual', 'contextual-links')), + ), + '#access' => user_access('access contextual links'), + ); + return drupal_render($build); + } + else { + return ''; + } + } + + function query() { } +} diff --git a/handlers/views_handler_field_counter.inc b/handlers/views_handler_field_counter.inc new file mode 100644 index 00000000..76213a65 --- /dev/null +++ b/handlers/views_handler_field_counter.inc @@ -0,0 +1,50 @@ + 1); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['counter_start'] = array( + '#type' => 'textfield', + '#title' => t('Starting value'), + '#default_value' => $this->options['counter_start'], + '#description' => t('Specify the number the counter should start at.'), + '#size' => 2, + ); + + parent::options_form($form, $form_state); + } + + function query() { + // do nothing -- to override the parent query. + } + + function render($values) { + // Note: 1 is subtracted from the counter start value below because the + // counter value is incremented by 1 at the end of this function. + $count = is_numeric($this->options['counter_start']) ? $this->options['counter_start'] - 1 : 0; + $pager = $this->view->query->pager; + // Get the base count of the pager. + if ($pager->use_pager()) { + $count += ($pager->get_items_per_page() * $pager->get_current_page() + $pager->get_offset()); + } + // Add the counter for the current site. + $count += $this->view->row_index + 1; + + return $count; + } +} diff --git a/handlers/views_handler_field_custom.inc b/handlers/views_handler_field_custom.inc new file mode 100644 index 00000000..66586de2 --- /dev/null +++ b/handlers/views_handler_field_custom.inc @@ -0,0 +1,55 @@ + TRUE, 'bool' => TRUE); + $options['hide_alter_empty'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Remove the checkbox + unset($form['alter']['alter_text']); + unset($form['alter']['text']['#dependency']); + unset($form['alter']['text']['#process']); + unset($form['alter']['help']['#dependency']); + unset($form['alter']['help']['#process']); + $form['#pre_render'][] = 'views_handler_field_custom_pre_render_move_text'; + } + + function render($values) { + // Return the text, so the code never thinks the value is empty. + return $this->options['alter']['text']; + } +} + +/** + * Prerender function to move the textarea to the top. + */ +function views_handler_field_custom_pre_render_move_text($form) { + $form['text'] = $form['alter']['text']; + $form['help'] = $form['alter']['help']; + unset($form['alter']['text']); + unset($form['alter']['help']); + + return $form; +} diff --git a/handlers/views_handler_field_date.inc b/handlers/views_handler_field_date.inc new file mode 100644 index 00000000..8517f0b5 --- /dev/null +++ b/handlers/views_handler_field_date.inc @@ -0,0 +1,101 @@ + 'small'); + $options['custom_date_format'] = array('default' => ''); + $options['timezone'] = array('default' => ''); + + return $options; + } + + function options_form(&$form, &$form_state) { + + $date_formats = array(); + $date_types = system_get_date_types(); + foreach ($date_types as $key => $value) { + $date_formats[$value['type']] = t('@date_format format', array('@date_format' => $value['title'])) . ': ' . format_date(REQUEST_TIME, $value['type']); + } + + $form['date_format'] = array( + '#type' => 'select', + '#title' => t('Date format'), + '#options' => $date_formats + array( + 'custom' => t('Custom'), + 'raw time ago' => t('Time ago'), + 'time ago' => t('Time ago (with "ago" appended)'), + 'raw time hence' => t('Time hence'), + 'time hence' => t('Time hence (with "hence" appended)'), + 'raw time span' => t('Time span (future dates have "-" prepended)'), + 'inverse time span' => t('Time span (past dates have "-" prepended)'), + 'time span' => t('Time span (with "ago/hence" appended)'), + ), + '#default_value' => isset($this->options['date_format']) ? $this->options['date_format'] : 'small', + ); + $form['custom_date_format'] = array( + '#type' => 'textfield', + '#title' => t('Custom date format'), + '#description' => t('If "Custom", see the
PHP manual for date formats. Otherwise, enter the number of different time units to display, which defaults to 2.', array('@url' => 'http://php.net/manual/function.date.php')), + '#default_value' => isset($this->options['custom_date_format']) ? $this->options['custom_date_format'] : '', + '#dependency' => array('edit-options-date-format' => array('custom', 'raw time ago', 'time ago', 'raw time hence', 'time hence', 'raw time span', 'time span', 'raw time span', 'inverse time span', 'time span')), + ); + $form['timezone'] = array( + '#type' => 'select', + '#title' => t('Timezone'), + '#description' => t('Timezone to be used for date output.'), + '#options' => array('' => t('- Default site/user timezone -')) + system_time_zones(FALSE), + '#default_value' => $this->options['timezone'], + '#dependency' => array('edit-options-date-format' => array_merge(array('custom'), array_keys($date_formats))), + ); + + parent::options_form($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + $format = $this->options['date_format']; + if (in_array($format, array('custom', 'raw time ago', 'time ago', 'raw time hence', 'time hence', 'raw time span', 'time span', 'raw time span', 'inverse time span', 'time span'))) { + $custom_format = $this->options['custom_date_format']; + } + + if ($value) { + $timezone = !empty($this->options['timezone']) ? $this->options['timezone'] : NULL; + $time_diff = REQUEST_TIME - $value; // will be positive for a datetime in the past (ago), and negative for a datetime in the future (hence) + switch ($format) { + case 'raw time ago': + return format_interval($time_diff, is_numeric($custom_format) ? $custom_format : 2); + case 'time ago': + return t('%time ago', array('%time' => format_interval($time_diff, is_numeric($custom_format) ? $custom_format : 2))); + case 'raw time hence': + return format_interval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2); + case 'time hence': + return t('%time hence', array('%time' => format_interval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2))); + case 'raw time span': + return ($time_diff < 0 ? '-' : '') . format_interval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2); + case 'inverse time span': + return ($time_diff > 0 ? '-' : '') . format_interval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2); + case 'time span': + return t(($time_diff < 0 ? '%time hence' : '%time ago'), array('%time' => format_interval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2))); + case 'custom': + if ($custom_format == 'r') { + return format_date($value, $format, $custom_format, $timezone, 'en'); + } + return format_date($value, $format, $custom_format, $timezone); + default: + return format_date($value, $format, '', $timezone); + } + } + } +} diff --git a/handlers/views_handler_field_entity.inc b/handlers/views_handler_field_entity.inc new file mode 100644 index 00000000..d8aaba49 --- /dev/null +++ b/handlers/views_handler_field_entity.inc @@ -0,0 +1,104 @@ +table); + $this->entity_type = $table_data['table']['entity type']; + } + + /** + * Overriden to add the field for the entity id. + */ + function query() { + $this->table_alias = $base_table = $this->view->base_table; + $this->base_field = $this->view->base_field; + + if (!empty($this->relationship)) { + foreach ($this->view->relationship as $relationship) { + if ($relationship->alias == $this->relationship) { + $base_table = $relationship->definition['base']; + $this->table_alias = $relationship->alias; + + $table_data = views_fetch_data($base_table); + $this->base_field = empty($relationship->definition['base field']) ? $table_data['table']['base']['field'] : $relationship->definition['base field']; + } + } + } + + // Add the field if the query back-end implements an add_field() method, + // just like the default back-end. + if (method_exists($this->query, 'add_field')) { + $this->field_alias = $this->query->add_field($this->table_alias, $this->base_field, ''); + } + + $this->add_additional_fields(); + } + + /** + * Load the entities for all rows that are about to be displayed. + */ + function pre_render(&$values) { + if (!empty($values)) { + list($this->entity_type, $this->entities) = $this->query->get_result_entities($values, !empty($this->relationship) ? $this->relationship : NULL, $this->field_alias); + } + } + + /** + * Overridden to return the entity object, or a certain property of the entity. + */ + function get_value($values, $field = NULL) { + if (isset($this->entities[$this->view->row_index])) { + $entity = $this->entities[$this->view->row_index]; + // Support to get a certain part of the entity. + if (isset($field) && isset($entity->{$field})) { + return $entity->{$field}; + } + // Support to get a part of the values as the normal get_value. + elseif (isset($field) && isset($values->{$this->aliases[$field]})) { + return $values->{$this->aliases[$field]}; + } + else { + return $entity; + } + } + return FALSE; + } +} diff --git a/handlers/views_handler_field_machine_name.inc b/handlers/views_handler_field_machine_name.inc new file mode 100644 index 00000000..9f3587fe --- /dev/null +++ b/handlers/views_handler_field_machine_name.inc @@ -0,0 +1,73 @@ +value_options)) { + return; + } + + if (isset($this->definition['options callback']) && is_callable($this->definition['options callback'])) { + if (isset($this->definition['options arguments']) && is_array($this->definition['options arguments'])) { + $this->value_options = call_user_func_array($this->definition['options callback'], $this->definition['options arguments']); + } + else { + $this->value_options = call_user_func($this->definition['options callback']); + } + } + else { + $this->value_options = array(); + } + } + + function option_definition() { + $options = parent::option_definition(); + $options['machine_name'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['machine_name'] = array( + '#title' => t('Output machine name'), + '#description' => t('Display field as machine name.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['machine_name']), + ); + } + + function pre_render(&$values) { + $this->get_value_options(); + } + + function render($values) { + $value = $values->{$this->field_alias}; + if (!empty($this->options['machine_name']) || !isset($this->value_options[$value])) { + $result = check_plain($value); + } + else { + $result = $this->value_options[$value]; + } + + return $result; + } +} diff --git a/handlers/views_handler_field_markup.inc b/handlers/views_handler_field_markup.inc new file mode 100644 index 00000000..b0f1cea9 --- /dev/null +++ b/handlers/views_handler_field_markup.inc @@ -0,0 +1,59 @@ + {$field}) where $field is the field in this table + * used to control the format such as the 'format' field in the node, + * which goes with the 'body' field. + * + * @ingroup views_field_handlers + */ +class views_handler_field_markup extends views_handler_field { + /** + * Constructor; calls to base object constructor. + */ + function construct() { + parent::construct(); + + $this->format = $this->definition['format']; + + $this->additional_fields = array(); + if (is_array($this->format)) { + $this->additional_fields['format'] = $this->format; + } + } + + function render($values) { + $value = $this->get_value($values); + if (is_array($this->format)) { + $format = $this->get_value($values, 'format'); + } + else { + $format = $this->format; + } + if ($value) { + $value = str_replace('', '', $value); + return check_markup($value, $format, ''); + } + } + + function element_type($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) { + if ($inline) { + return 'span'; + } + + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + return 'div'; + } +} diff --git a/handlers/views_handler_field_math.inc b/handlers/views_handler_field_math.inc new file mode 100644 index 00000000..08fba060 --- /dev/null +++ b/handlers/views_handler_field_math.inc @@ -0,0 +1,84 @@ + ''); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['expression'] = array( + '#type' => 'textarea', + '#title' => t('Expression'), + '#description' => t('Enter mathematical expressions such as 2 + 2 or sqrt(5). You may assign variables and create mathematical functions and evaluate them. Use the ; to separate these. For example: f(x) = x + 2; f(2).'), + '#default_value' => $this->options['expression'], + ); + + // Create a place for the help + $form['expression_help'] = array(); + parent::options_form($form, $form_state); + + // Then move the existing help: + $form['expression_help'] = $form['alter']['help']; + unset($form['expression_help']['#dependency']); + unset($form['alter']['help']); + } + + function render($values) { + ctools_include('math-expr'); + $tokens = array_map('floatval', $this->get_render_tokens(array())); + $value = strtr($this->options['expression'], $tokens); + $expressions = explode(';', $value); + $math = new ctools_math_expr; + foreach ($expressions as $expression) { + if ($expression !== '') { + $value = $math->evaluate($expression); + } + } + + // The rest is directly from views_handler_field_numeric but because it + // does not allow the value to be passed in, it is copied. + if (!empty($this->options['set_precision'])) { + $value = number_format($value, $this->options['precision'], $this->options['decimal'], $this->options['separator']); + } + else { + $remainder = abs($value) - intval(abs($value)); + $value = $value > 0 ? floor($value) : ceil($value); + $value = number_format($value, 0, '', $this->options['separator']); + if ($remainder) { + // The substr may not be locale safe. + $value .= $this->options['decimal'] . substr($remainder, 2); + } + } + + // Check to see if hiding should happen before adding prefix and suffix. + if ($this->options['hide_empty'] && empty($value) && ($value !== 0 || $this->options['empty_zero'])) { + return ''; + } + + // Should we format as a plural. + if (!empty($this->options['format_plural']) && ($value != 0 || !$this->options['empty_zero'])) { + $value = format_plural($value, $this->options['format_plural_singular'], $this->options['format_plural_plural']); + } + + return $this->sanitize_value($this->options['prefix'] . $value . $this->options['suffix']); + } + + function query() { } +} diff --git a/handlers/views_handler_field_numeric.inc b/handlers/views_handler_field_numeric.inc new file mode 100644 index 00000000..d10d3d0b --- /dev/null +++ b/handlers/views_handler_field_numeric.inc @@ -0,0 +1,137 @@ + FALSE, 'bool' => TRUE); + $options['precision'] = array('default' => 0); + $options['decimal'] = array('default' => '.', 'translatable' => TRUE); + $options['separator'] = array('default' => ',', 'translatable' => TRUE); + $options['format_plural'] = array('default' => FALSE, 'bool' => TRUE); + $options['format_plural_singular'] = array('default' => '1'); + $options['format_plural_plural'] = array('default' => '@count'); + $options['prefix'] = array('default' => '', 'translatable' => TRUE); + $options['suffix'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + if (!empty($this->definition['float'])) { + $form['set_precision'] = array( + '#type' => 'checkbox', + '#title' => t('Round'), + '#description' => t('If checked, the number will be rounded.'), + '#default_value' => $this->options['set_precision'], + ); + $form['precision'] = array( + '#type' => 'textfield', + '#title' => t('Precision'), + '#default_value' => $this->options['precision'], + '#description' => t('Specify how many digits to print after the decimal point.'), + '#dependency' => array('edit-options-set-precision' => array(TRUE)), + '#size' => 2, + ); + $form['decimal'] = array( + '#type' => 'textfield', + '#title' => t('Decimal point'), + '#default_value' => $this->options['decimal'], + '#description' => t('What single character to use as a decimal point.'), + '#size' => 2, + ); + } + $form['separator'] = array( + '#type' => 'select', + '#title' => t('Thousands marker'), + '#options' => array( + '' => t('- None -'), + ',' => t('Comma'), + ' ' => t('Space'), + '.' => t('Decimal'), + '\'' => t('Apostrophe'), + ), + '#default_value' => $this->options['separator'], + '#description' => t('What single character to use as the thousands separator.'), + '#size' => 2, + ); + $form['format_plural'] = array( + '#type' => 'checkbox', + '#title' => t('Format plural'), + '#description' => t('If checked, special handling will be used for plurality.'), + '#default_value' => $this->options['format_plural'], + ); + $form['format_plural_singular'] = array( + '#type' => 'textfield', + '#title' => t('Singular form'), + '#default_value' => $this->options['format_plural_singular'], + '#description' => t('Text to use for the singular form.'), + '#dependency' => array('edit-options-format-plural' => array(TRUE)), + ); + $form['format_plural_plural'] = array( + '#type' => 'textfield', + '#title' => t('Plural form'), + '#default_value' => $this->options['format_plural_plural'], + '#description' => t('Text to use for the plural form, @count will be replaced with the value.'), + '#dependency' => array('edit-options-format-plural' => array(TRUE)), + ); + $form['prefix'] = array( + '#type' => 'textfield', + '#title' => t('Prefix'), + '#default_value' => $this->options['prefix'], + '#description' => t('Text to put before the number, such as currency symbol.'), + ); + $form['suffix'] = array( + '#type' => 'textfield', + '#title' => t('Suffix'), + '#default_value' => $this->options['suffix'], + '#description' => t('Text to put after the number, such as currency symbol.'), + ); + + parent::options_form($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($this->options['set_precision'])) { + $value = number_format($value, $this->options['precision'], $this->options['decimal'], $this->options['separator']); + } + else { + $remainder = abs($value) - intval(abs($value)); + $value = $value > 0 ? floor($value) : ceil($value); + $value = number_format($value, 0, '', $this->options['separator']); + if ($remainder) { + // The substr may not be locale safe. + $value .= $this->options['decimal'] . substr($remainder, 2); + } + } + + // Check to see if hiding should happen before adding prefix and suffix. + if ($this->options['hide_empty'] && empty($value) && ($value !== 0 || $this->options['empty_zero'])) { + return ''; + } + + // Should we format as a plural. + if (!empty($this->options['format_plural'])) { + $value = format_plural($value, $this->options['format_plural_singular'], $this->options['format_plural_plural']); + } + + return $this->sanitize_value($this->options['prefix'], 'xss') + . $this->sanitize_value($value) + . $this->sanitize_value($this->options['suffix'], 'xss'); + } +} diff --git a/handlers/views_handler_field_prerender_list.inc b/handlers/views_handler_field_prerender_list.inc new file mode 100644 index 00000000..00a571aa --- /dev/null +++ b/handlers/views_handler_field_prerender_list.inc @@ -0,0 +1,158 @@ +items + * + * @ingroup views_field_handlers + */ +class views_handler_field_prerender_list extends views_handler_field { + /** + * Stores all items which are used to render the items. + * It should be keyed first by the id of the base table, for example nid. + * The second key is the id of the thing which is displayed multiple times + * per row, for example the tid. + * + * @var array + */ + var $items = array(); + + function option_definition() { + $options = parent::option_definition(); + + $options['type'] = array('default' => 'separator'); + $options['separator'] = array('default' => ', '); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['type'] = array( + '#type' => 'radios', + '#title' => t('Display type'), + '#options' => array( + 'ul' => t('Unordered list'), + 'ol' => t('Ordered list'), + 'separator' => t('Simple separator'), + ), + '#default_value' => $this->options['type'], + ); + + $form['separator'] = array( + '#type' => 'textfield', + '#title' => t('Separator'), + '#default_value' => $this->options['separator'], + '#dependency' => array('radio:options[type]' => array('separator')), + ); + parent::options_form($form, $form_state); + } + + /** + * Render the field. + * + * This function is deprecated, but left in for older systems that have not + * yet or won't update their prerender list fields. If a render_item method + * exists, this will not get used by advanced_render. + */ + function render($values) { + $field = $this->get_value($values); + if (!empty($this->items[$field])) { + if ($this->options['type'] == 'separator') { + return implode($this->sanitize_value($this->options['separator']), $this->items[$field]); + } + else { + return theme('item_list', + array( + 'items' => $this->items[$field], + 'title' => NULL, + 'type' => $this->options['type'] + )); + } + } + } + + /** + * Render all items in this field together. + * + * When using advanced render, each possible item in the list is rendered + * individually. Then the items are all pasted together. + */ + function render_items($items) { + if (!empty($items)) { + if ($this->options['type'] == 'separator') { + return implode($this->sanitize_value($this->options['separator'], 'xss_admin'), $items); + } + else { + return theme('item_list', + array( + 'items' => $items, + 'title' => NULL, + 'type' => $this->options['type'] + )); + } + } + } + + /** + * Return an array of items for the field. + * + * Items should be stored in the result array, if possible, as an array + * with 'value' as the actual displayable value of the item, plus + * any items that might be found in the 'alter' options array for + * creating links, such as 'path', 'fragment', 'query' etc, such a thing + * is to be made. Additionally, items that might be turned into tokens + * should also be in this array. + */ + function get_items($values) { + // Only the parent get_value returns a single field. + $field = parent::get_value($values); + if (!empty($this->items[$field])) { + return $this->items[$field]; + } + + return array(); + } + + /** + * Get the value that's supposed to be rendered. + * + * @param $values + * An object containing all retrieved values. + * @param $field + * Optional name of the field where the value is stored. + * @param $raw + * Use the raw data and not the data defined in pre_render + */ + function get_value($values, $field = NULL, $raw = FALSE) { + if ($raw) { + return parent::get_value($values, $field); + } + $item = $this->get_items($values); + $item = (array) $item; + if (isset($field) && isset($item[$field])) { + return $item[$field]; + } + return $item; + } + + /** + * Determine if advanced rendering is allowed. + * + * By default, advanced rendering will NOT be allowed if the class + * inheriting from this does not implement a 'render_items' method. + */ + function allow_advanced_render() { + // Note that the advanced render bits also use the presence of + // this method to determine if it needs to render items as a list. + return method_exists($this, 'render_item'); + } +} diff --git a/handlers/views_handler_field_serialized.inc b/handlers/views_handler_field_serialized.inc new file mode 100644 index 00000000..1579fce7 --- /dev/null +++ b/handlers/views_handler_field_serialized.inc @@ -0,0 +1,65 @@ + 'unserialized'); + $options['key'] = array('default' => ''); + return $options; + } + + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['format'] = array( + '#type' => 'select', + '#title' => t('Display format'), + '#description' => t('How should the serialized data be displayed. You can choose a custom array/object key or a print_r on the full output.'), + '#options' => array( + 'unserialized' => t('Full data (unserialized)'), + 'serialized' => t('Full data (serialized)'), + 'key' => t('A certain key'), + ), + '#default_value' => $this->options['format'], + ); + $form['key'] = array( + '#type' => 'textfield', + '#title' => t('Which key should be displayed'), + '#default_value' => $this->options['key'], + '#dependency' => array('edit-options-format' => array('key')), + ); + } + + function options_validate(&$form, &$form_state) { + // Require a key if the format is key. + if ($form_state['values']['options']['format'] == 'key' && $form_state['values']['options']['key'] == '') { + form_error($form['key'], t('You have to enter a key if you want to display a key of the data.')); + } + } + + function render($values) { + $value = $values->{$this->field_alias}; + + if ($this->options['format'] == 'unserialized') { + return check_plain(print_r(unserialize($value), TRUE)); + } + elseif ($this->options['format'] == 'key' && !empty($this->options['key'])) { + $value = (array) unserialize($value); + return check_plain($value[$this->options['key']]); + } + + return $value; + } +} diff --git a/handlers/views_handler_field_time_interval.inc b/handlers/views_handler_field_time_interval.inc new file mode 100644 index 00000000..e6063af1 --- /dev/null +++ b/handlers/views_handler_field_time_interval.inc @@ -0,0 +1,37 @@ + 2); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['granularity'] = array( + '#type' => 'textfield', + '#title' => t('Granularity'), + '#description' => t('How many different units to display in the string.'), + '#default_value' => $this->options['granularity'], + ); + } + + function render($values) { + $value = $values->{$this->field_alias}; + return format_interval($value, isset($this->options['granularity']) ? $this->options['granularity'] : 2); + } +} diff --git a/handlers/views_handler_field_url.inc b/handlers/views_handler_field_url.inc new file mode 100644 index 00000000..4a76548e --- /dev/null +++ b/handlers/views_handler_field_url.inc @@ -0,0 +1,46 @@ + TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link to the page being visited. + */ + function options_form(&$form, &$form_state) { + $form['display_as_link'] = array( + '#title' => t('Display as link'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['display_as_link']), + ); + parent::options_form($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($this->options['display_as_link'])) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = $value; + $text = !empty($this->options['text']) ? $this->sanitize_value($this->options['text']) : $this->sanitize_value($value, 'url'); + return $text; + } + else { + return $this->sanitize_value($value, 'url'); + } + } +} diff --git a/handlers/views_handler_filter.inc b/handlers/views_handler_filter.inc new file mode 100644 index 00000000..f5db70f9 --- /dev/null +++ b/handlers/views_handler_filter.inc @@ -0,0 +1,1410 @@ +operator = $this->options['operator']; + $this->value = $this->options['value']; + $this->group_info = $this->options['group_info']['default_group']; + + // Compatibility: The new UI changed several settings. + if (!empty($options['exposed']) && !empty($options['expose']['optional']) && !isset($options['expose']['required'])) { + $this->options['expose']['required'] = !$options['expose']['optional']; + } + if (!empty($options['exposed']) && !empty($options['expose']['single']) && !isset($options['expose']['multiple'])) { + $this->options['expose']['multiple'] = !$options['expose']['single']; + } + if (!empty($options['exposed']) && !empty($options['expose']['operator']) && !isset($options['expose']['operator_id'])) { + $this->options['expose']['operator_id'] = $options['expose']['operator_id'] = $options['expose']['operator']; + } + + if ($this->multiple_exposed_input()) { + $this->group_info = array_filter($options['group_info']['default_group_multiple']); + $this->options['expose']['multiple'] = TRUE; + } + + // If there are relationships in the view, allow empty should be true + // so that we can do IS NULL checks on items. Not all filters respect + // allow empty, but string and numeric do and that covers enough. + if ($this->view->display_handler->get_option('relationships')) { + $this->definition['allow empty'] = TRUE; + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['operator'] = array('default' => '='); + $options['value'] = array('default' => ''); + $options['group'] = array('default' => '1'); + $options['exposed'] = array('default' => FALSE, 'bool' => TRUE); + $options['expose'] = array( + 'contains' => array( + 'operator_id' => array('default' => FALSE), + 'label' => array('default' => '', 'translatable' => TRUE), + 'description' => array('default' => '', 'translatable' => TRUE), + 'use_operator' => array('default' => FALSE, 'bool' => TRUE), + 'operator' => array('default' => ''), + 'identifier' => array('default' => ''), + 'required' => array('default' => FALSE, 'bool' => TRUE), + 'remember' => array('default' => FALSE, 'bool' => TRUE), + 'multiple' => array('default' => FALSE, 'bool' => TRUE), + 'remember_roles' => array('default' => array( + DRUPAL_AUTHENTICATED_RID => DRUPAL_AUTHENTICATED_RID, + )), + ), + ); + + // A group is a combination of a filter, an operator and a value + // operating like a single filter. + // Users can choose from a select box which group they want to apply. + // Views will filter the view according to the defined values. + // Because it acts as a standard filter, we have to define + // an identifier and other settings like the widget and the label. + // This settings are saved in another array to allow users to switch + // between a normal filter and a group of filters with a single click. + $options['is_grouped'] = array('default' => FALSE, 'bool' => TRUE); + $options['group_info'] = array( + 'contains' => array( + 'label' => array('default' => '', 'translatable' => TRUE), + 'description' => array('default' => '', 'translatable' => TRUE), + 'identifier' => array('default' => ''), + 'optional' => array('default' => TRUE, 'bool' => TRUE), + 'widget' => array('default' => 'select'), + 'multiple' => array('default' => FALSE, 'bool' => TRUE), + 'remember' => array('default' => 0), + 'default_group' => array('default' => 'All'), + 'default_group_multiple' => array('default' => array()), + 'group_items' => array('default' => array()), + ), + ); + + return $options; + } + + /** + * Display the filter on the administrative summary + */ + function admin_summary() { + return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value); + } + + /** + * Determine if a filter can be exposed. + */ + function can_expose() { return TRUE; } + + /** + * Determine if a filter can be converted into a group. + * Only exposed filters with operators available can be converted into groups. + */ + function can_build_group() { + return $this->is_exposed() && (count($this->operator_options()) > 0); + } + + /** + * Returns TRUE if the exposed filter works like a grouped filter. + */ + function is_a_group() { + return $this->is_exposed() && !empty($this->options['is_grouped']); + } + + /** + * Provide the basic form which calls through to subforms. + * If overridden, it is best to call through to the parent, + * or to at least make sure all of the functions in this form + * are called. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + if ($this->can_expose()) { + $this->show_expose_button($form, $form_state); + } + if ($this->can_build_group()) { + $this->show_build_group_button($form, $form_state); + } + $form['clear_markup_start'] = array( + '#markup' => '
', + ); + if ($this->is_a_group()) { + if ($this->can_build_group()) { + $form['clear_markup_start'] = array( + '#markup' => '
', + ); + // Render the build group form. + $this->show_build_group_form($form, $form_state); + $form['clear_markup_end'] = array( + '#markup' => '
', + ); + } + } + else { + // Add the subform from operator_form(). + $this->show_operator_form($form, $form_state); + // Add the subform from value_form(). + $this->show_value_form($form, $form_state); + $form['clear_markup_end'] = array( + '#markup' => '
', + ); + if ($this->can_expose()) { + // Add the subform from expose_form(). + $this->show_expose_form($form, $form_state); + } + } + } + + /** + * Simple validate handler + */ + function options_validate(&$form, &$form_state) { + $this->operator_validate($form, $form_state); + $this->value_validate($form, $form_state); + if (!empty($this->options['exposed']) && !$this->is_a_group()) { + $this->expose_validate($form, $form_state); + } + if ($this->is_a_group()) { + $this->build_group_validate($form, $form_state); + } + } + + /** + * Simple submit handler + */ + function options_submit(&$form, &$form_state) { + unset($form_state['values']['expose_button']); // don't store this. + unset($form_state['values']['group_button']); // don't store this. + if (!$this->is_a_group()) { + $this->operator_submit($form, $form_state); + $this->value_submit($form, $form_state); + } + if (!empty($this->options['exposed'])) { + $this->expose_submit($form, $form_state); + } + if ($this->is_a_group()) { + $this->build_group_submit($form, $form_state); + } + } + + /** + * Shortcut to display the operator form. + */ + function show_operator_form(&$form, &$form_state) { + $this->operator_form($form, $form_state); + $form['operator']['#prefix'] = '
'; + $form['operator']['#suffix'] = '
'; + } + + /** + * Options form subform for setting the operator. + * + * This may be overridden by child classes, and it must + * define $form['operator']; + * + * @see options_form() + */ + function operator_form(&$form, &$form_state) { + $options = $this->operator_options(); + if (!empty($options)) { + $form['operator'] = array( + '#type' => count($options) < 10 ? 'radios' : 'select', + '#title' => t('Operator'), + '#default_value' => $this->operator, + '#options' => $options, + ); + } + } + + /** + * Provide a list of options for the default operator form. + * Should be overridden by classes that don't override operator_form + */ + function operator_options() { return array(); } + + /** + * Validate the operator form. + */ + function operator_validate($form, &$form_state) { } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function operator_submit($form, &$form_state) { } + + /** + * Shortcut to display the value form. + */ + function show_value_form(&$form, &$form_state) { + $this->value_form($form, $form_state); + if (empty($this->no_operator)) { + $form['value']['#prefix'] = '
' . (isset($form['value']['#prefix']) ? $form['value']['#prefix'] : ''); + $form['value']['#suffix'] = (isset($form['value']['#suffix']) ? $form['value']['#suffix'] : '') . '
'; + } + } + + /** + * Options form subform for setting options. + * + * This should be overridden by all child classes and it must + * define $form['value'] + * + * @see options_form() + */ + function value_form(&$form, &$form_state) { $form['value'] = array(); } + + /** + * Validate the options form. + */ + function value_validate($form, &$form_state) { } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function value_submit($form, &$form_state) { } + + /** + * Shortcut to display the exposed options form. + */ + function show_build_group_form(&$form, &$form_state) { + if (empty($this->options['is_grouped'])) { + return; + } + + $this->build_group_form($form, $form_state); + + // When we click the expose button, we add new gadgets to the form but they + // have no data in $_POST so their defaults get wiped out. This prevents + // these defaults from getting wiped out. This setting will only be TRUE + // during a 2nd pass rerender. + if (!empty($form_state['force_build_group_options'])) { + foreach (element_children($form['group_info']) as $id) { + if (isset($form['group_info'][$id]['#default_value']) && !isset($form['group_info'][$id]['#value'])) { + $form['group_info'][$id]['#value'] = $form['group_info'][$id]['#default_value']; + } + } + } + } + + /** + * Shortcut to display the build_group/hide button. + */ + function show_build_group_button(&$form, &$form_state) { + + $form['group_button'] = array( + '#prefix' => '
', + '#suffix' => '
', + // Should always come after the description and the relationship. + '#weight' => -190, + ); + + $grouped_description = t('Grouped filters allow a choice between predefined operator|value pairs.'); + $form['group_button']['radios'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('js-only')), + ); + $form['group_button']['radios']['radios'] = array( + '#title' => t('Filter type to expose'), + '#description' => $grouped_description, + '#type' => 'radios', + '#options' => array( + t('Single filter'), + t('Grouped filters'), + ), + ); + + if (empty($this->options['is_grouped'])) { + $form['group_button']['markup'] = array( + '#markup' => '
' . $grouped_description . '
', + ); + $form['group_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Grouped filters'), + '#submit' => array('views_ui_config_item_form_build_group'), + ); + $form['group_button']['radios']['radios']['#default_value'] = 0; + } + else { + $form['group_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Single filter'), + '#submit' => array('views_ui_config_item_form_build_group'), + ); + $form['group_button']['radios']['radios']['#default_value'] = 1; + } + } + /** + * Shortcut to display the expose/hide button. + */ + function show_expose_button(&$form, &$form_state) { + $form['expose_button'] = array( + '#prefix' => '
', + '#suffix' => '
', + // Should always come after the description and the relationship. + '#weight' => -200, + ); + + // Add a checkbox for JS users, which will have behavior attached to it + // so it can replace the button. + $form['expose_button']['checkbox'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('js-only')), + ); + $form['expose_button']['checkbox']['checkbox'] = array( + '#title' => t('Expose this filter to visitors, to allow them to change it'), + '#type' => 'checkbox', + ); + + // Then add the button itself. + if (empty($this->options['exposed'])) { + $form['expose_button']['markup'] = array( + '#markup' => '
' . t('This filter is not exposed. Expose it to allow the users to change it.') . '
', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Expose filter'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 0; + } + else { + $form['expose_button']['markup'] = array( + '#markup' => '
' . t('This filter is exposed. If you hide it, users will not be able to change it.') . '
', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Hide filter'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 1; + } + } + + /** + * Options form subform for exposed filter options. + * + * @see options_form() + */ + function expose_form(&$form, &$form_state) { + $form['#theme'] = 'views_ui_expose_filter_form'; + // #flatten will move everything from $form['expose'][$key] to $form[$key] + // prior to rendering. That's why the pre_render for it needs to run first, + // so that when the next pre_render (the one for fieldsets) runs, it gets + // the flattened data. + array_unshift($form['#pre_render'], 'views_ui_pre_render_flatten_data'); + $form['expose']['#flatten'] = TRUE; + + if (empty($this->always_required)) { + $form['expose']['required'] = array( + '#type' => 'checkbox', + '#title' => t('Required'), + '#default_value' => $this->options['expose']['required'], + ); + } + else { + $form['expose']['required'] = array( + '#type' => 'value', + '#value' => TRUE, + ); + } + $form['expose']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + + $form['expose']['description'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['description'], + '#title' => t('Description'), + '#size' => 60, + ); + + if (!empty($form['operator']['#type'])) { + // Increase the width of the left (operator) column. + $form['operator']['#prefix'] = '
'; + $form['operator']['#suffix'] = '
'; + $form['value']['#prefix'] = '
'; + $form['value']['#suffix'] = '
'; + + $form['expose']['use_operator'] = array( + '#type' => 'checkbox', + '#title' => t('Expose operator'), + '#description' => t('Allow the user to choose the operator.'), + '#default_value' => !empty($this->options['expose']['use_operator']), + ); + $form['expose']['operator_id'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['operator_id'], + '#title' => t('Operator identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this operator.'), + '#dependency' => array( + 'edit-options-expose-use-operator' => array(1) + ), + '#fieldset' => 'more', + ); + } + else { + $form['expose']['operator_id'] = array( + '#type' => 'value', + '#value' => '', + ); + } + + if (empty($this->always_multiple)) { + $form['expose']['multiple'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple selections'), + '#description' => t('Enable to allow users to select multiple items.'), + '#default_value' => $this->options['expose']['multiple'], + ); + } + $form['expose']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember the last selection'), + '#description' => t('Enable to remember the last selection made by the user.'), + '#default_value' => $this->options['expose']['remember'], + ); + + $role_options = array_map('check_plain', user_roles()); + $form['expose']['remember_roles'] = array( + '#type' => 'checkboxes', + '#title' => t('User roles'), + '#description' => t('Remember exposed selection only for the selected user role(s). If you select no roles, the exposed data will never be stored.'), + '#default_value' => $this->options['expose']['remember_roles'], + '#options' => $role_options, + '#dependency' => array( + 'edit-options-expose-remember' => array(1), + ), + ); + + $form['expose']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['identifier'], + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + } + + /** + * Validate the options form. + */ + function expose_validate($form, &$form_state) { + if (empty($form_state['values']['options']['expose']['identifier'])) { + form_error($form['expose']['identifier'], t('The identifier is required if the filter is exposed.')); + } + + if (!empty($form_state['values']['options']['expose']['identifier']) && $form_state['values']['options']['expose']['identifier'] == 'value') { + form_error($form['expose']['identifier'], t('This identifier is not allowed.')); + } + + if (!$this->view->display_handler->is_identifier_unique($form_state['id'], $form_state['values']['options']['expose']['identifier'])) { + form_error($form['expose']['identifier'], t('This identifier is used by another handler.')); + } + } + + /** + * Validate the build group options form. + */ + function build_group_validate($form, &$form_state) { + if (!empty($form_state['values']['options']['group_info'])) { + if (empty($form_state['values']['options']['group_info']['identifier'])) { + form_error($form['group_info']['identifier'], t('The identifier is required if the filter is exposed.')); + } + + if (!empty($form_state['values']['options']['group_info']['identifier']) && $form_state['values']['options']['group_info']['identifier'] == 'value') { + form_error($form['group_info']['identifier'], t('This identifier is not allowed.')); + } + + if (!$this->view->display_handler->is_identifier_unique($form_state['id'], $form_state['values']['options']['group_info']['identifier'])) { + form_error($form['group_info']['identifier'], t('This identifier is used by another handler.')); + } + } + + if (!empty($form_state['values']['options']['group_info']['group_items'])) { + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + + // Check if the title is defined but value wasn't defined. + if (!empty($group['title'])) { + if ((!is_array($group['value']) && trim($group['value']) == "") || + (is_array($group['value']) && count(array_filter($group['value'], '_views_array_filter_zero')) == 0)) { + form_error($form['group_info']['group_items'][$id]['value'], + t('The value is required if title for this item is defined.')); + } + } + + // Check if the value is defined but title wasn't defined. + if ((!is_array($group['value']) && trim($group['value']) != "") || + (is_array($group['value']) && count(array_filter($group['value'], '_views_array_filter_zero')) > 0)) { + if (empty($group['title'])) { + form_error($form['group_info']['group_items'][$id]['title'], + t('The title is required if value for this item is defined.')); + } + } + } + } + } + } + + /** + * Save new group items, re-enumerates and remove groups marked to delete. + */ + function build_group_submit($form, &$form_state) { + $groups = array(); + uasort($form_state['values']['options']['group_info']['group_items'], 'drupal_sort_weight'); + // Filter out removed items. + + // Start from 1 to avoid problems with #default_value in the widget. + $new_id = 1; + $new_default = 'All'; + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + // Don't store this. + unset($group['remove']); + unset($group['weight']); + $groups[$new_id] = $group; + + if ($form_state['values']['options']['group_info']['default_group'] === $id) { + $new_default = $new_id; + } + } + $new_id++; + } + if ($new_default != 'All') { + $form_state['values']['options']['group_info']['default_group'] = $new_default; + } + $filter_default_multiple = array_filter($form_state['values']['options']['group_info']['default_group_multiple']); + $form_state['values']['options']['group_info']['default_group_multiple'] = $filter_default_multiple; + + $form_state['values']['options']['group_info']['group_items'] = $groups; + } + + /** + * Provide default options for exposed filters. + */ + function expose_options() { + $this->options['expose'] = array( + 'use_operator' => FALSE, + 'operator' => $this->options['id'] . '_op', + 'identifier' => $this->options['id'], + 'label' => $this->definition['title'], + 'description' => NULL, + 'remember' => FALSE, + 'multiple' => FALSE, + 'required' => FALSE, + ); + } + + /** + * Provide default options for exposed filters. + */ + function build_group_options() { + $this->options['group_info'] = array( + 'label' => $this->definition['title'], + 'description' => NULL, + 'identifier' => $this->options['id'], + 'optional' => TRUE, + 'widget' => 'select', + 'multiple' => FALSE, + 'remember' => FALSE, + 'default_group' => 'All', + 'default_group_multiple' => array(), + 'group_items' => array(), + ); + } + + /** + * Build a form containing a group of operator | values to apply as a + * single filter. + */ + function group_form(&$form, &$form_state) { + if (!empty($this->options['group_info']['optional']) && !$this->multiple_exposed_input()) { + + $old_any = $this->options['group_info']['widget'] == 'select' ? '' : '<Any>'; + $any_label = variable_get('views_exposed_filter_any_label', 'new_any') == 'old_any' ? $old_any : t('- Any -'); + $groups = array('All' => $any_label); + } + foreach ($this->options['group_info']['group_items'] as $id => $group) { + if (!empty($group['title'])) { + $groups[$id] = $id != 'All' ? t($group['title']) : $group['title']; + } + } + + if (count($groups)) { + $value = $this->options['group_info']['identifier']; + + $form[$value] = array( + '#type' => $this->options['group_info']['widget'], + '#default_value' => $this->group_info, + '#options' => $groups, + ); + if (!empty($this->options['group_info']['multiple'])) { + if (count($groups) < 5) { + $form[$value]['#type'] = 'checkboxes'; + } + else { + $form[$value]['#type'] = 'select'; + $form[$value]['#size'] = 5; + $form[$value]['#multiple'] = TRUE; + } + unset($form[$value]['#default_value']); + if (empty($form_state['input'])) { + $form_state['input'][$value] = $this->group_info; + } + } + + $this->options['expose']['label'] = ''; + } + } + + + /** + * Render our chunk of the exposed filter form when selecting + * + * You can override this if it doesn't do what you expect. + */ + function exposed_form(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + // Build the exposed form, when its based on an operator. + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id'])) { + $operator = $this->options['expose']['operator_id']; + $this->operator_form($form, $form_state); + $form[$operator] = $form['operator']; + + if (isset($form[$operator]['#title'])) { + unset($form[$operator]['#title']); + } + + $this->exposed_translate($form[$operator], 'operator'); + + unset($form['operator']); + } + + // Build the form and set the value based on the identifier. + if (!empty($this->options['expose']['identifier'])) { + $value = $this->options['expose']['identifier']; + $this->value_form($form, $form_state); + $form[$value] = $form['value']; + + if (isset($form[$value]['#title']) && !empty($form[$value]['#type']) && $form[$value]['#type'] != 'checkbox') { + unset($form[$value]['#title']); + } + + $this->exposed_translate($form[$value], 'value'); + + if (!empty($form['#type']) && ($form['#type'] == 'checkboxes' || ($form['#type'] == 'select' && !empty($form['#multiple'])))) { + unset($form[$value]['#default_value']); + } + + if (!empty($form['#type']) && $form['#type'] == 'select' && empty($form['#multiple'])) { + $form[$value]['#default_value'] = 'All'; + } + + if ($value != 'value') { + unset($form['value']); + } + } + } + + /** + * Build the form to let users create the group of exposed filters. + * This form is displayed when users click on button 'Build group' + */ + function build_group_form(&$form, &$form_state) { + if (empty($this->options['exposed']) || empty($this->options['is_grouped'])) { + return; + } + $form['#theme'] = 'views_ui_build_group_filter_form'; + + // #flatten will move everything from $form['group_info'][$key] to $form[$key] + // prior to rendering. That's why the pre_render for it needs to run first, + // so that when the next pre_render (the one for fieldsets) runs, it gets + // the flattened data. + array_unshift($form['#pre_render'], 'views_ui_pre_render_flatten_data'); + $form['group_info']['#flatten'] = TRUE; + + if (!empty($this->options['group_info']['identifier'])) { + $identifier = $this->options['group_info']['identifier']; + } + else { + $identifier = 'group_' . $this->options['expose']['identifier']; + } + $form['group_info']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $identifier, + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + $form['group_info']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + + $form['group_info']['optional'] = array( + '#type' => 'checkbox', + '#title' => t('Optional'), + '#description' => t('This exposed filter is optional and will have added options to allow it not to be set.'), + '#default_value' => $this->options['group_info']['optional'], + ); + $form['group_info']['multiple'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple selections'), + '#description' => t('Enable to allow users to select multiple items.'), + '#default_value' => $this->options['group_info']['multiple'], + ); + $form['group_info']['widget'] = array( + '#type' => 'radios', + '#default_value' => $this->options['group_info']['widget'], + '#title' => t('Widget type'), + '#options' => array( + 'radios' => t('Radios'), + 'select' => t('Select'), + ), + '#description' => t('Select which kind of widget will be used to render the group of filters'), + ); + $form['group_info']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember'), + '#description' => t('Remember the last setting the user gave this filter.'), + '#default_value' => $this->options['group_info']['remember'], + ); + + if (!empty($this->options['group_info']['identifier'])) { + $identifier = $this->options['group_info']['identifier']; + } + else { + $identifier = 'group_' . $this->options['expose']['identifier']; + } + $form['group_info']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $identifier, + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + $form['group_info']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + $form['group_info']['description'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['description'], + '#title' => t('Description'), + '#size' => 60, + ); + $form['group_info']['optional'] = array( + '#type' => 'checkbox', + '#title' => t('Optional'), + '#description' => t('This exposed filter is optional and will have added options to allow it not to be set.'), + '#default_value' => $this->options['group_info']['optional'], + ); + $form['group_info']['widget'] = array( + '#type' => 'radios', + '#default_value' => $this->options['group_info']['widget'], + '#title' => t('Widget type'), + '#options' => array( + 'radios' => t('Radios'), + 'select' => t('Select'), + ), + '#description' => t('Select which kind of widget will be used to render the group of filters'), + ); + $form['group_info']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember'), + '#description' => t('Remember the last setting the user gave this filter.'), + '#default_value' => $this->options['group_info']['remember'], + ); + + $groups = array('All' => '- Any -'); // The string '- Any -' will not be rendered see @theme_views_ui_build_group_filter_form + + // Provide 3 options to start when we are in a new group. + if (count($this->options['group_info']['group_items']) == 0) { + $this->options['group_info']['group_items'] = array_fill(1, 3, array()); + } + + // After the general settings, comes a table with all the existent groups. + $default_weight = 0; + foreach ($this->options['group_info']['group_items'] as $item_id => $item) { + if (!empty($form_state['values']['options']['group_info']['group_items'][$item_id]['remove'])) { + continue; + } + // Each rows contains three widgets: + // a) The title, where users define how they identify a pair of operator | value + // b) The operator + // c) The value (or values) to use in the filter with the selected operator + + // In each row, we have to display the operator form and the value from + // $row acts as a fake form to render each widget in a row. + $row = array(); + $groups[$item_id] = ''; + $this->operator_form($row, $form_state); + // Force the operator form to be a select box. Some handlers uses + // radios and they occupy a lot of space in a table row. + $row['operator']['#type'] = 'select'; + $row['operator']['#title'] = ''; + $this->value_form($row, $form_state); + + // Fix the dependencies to update value forms when operators + // changes. This is needed because forms are inside a new form and + // their ids changes. Dependencies are used when operator changes + // from to 'Between', 'Not Between', etc, and two or more widgets + // are displayed. + $without_children = TRUE; + foreach (element_children($row['value']) as $children) { + if (isset($row['value'][$children]['#dependency']['edit-options-operator'])) { + $row['value'][$children]['#dependency']["edit-options-group-info-group-items-$item_id-operator"] = $row['value'][$children]['#dependency']['edit-options-operator']; + unset($row['value'][$children]['#dependency']['edit-options-operator']); + $row['value'][$children]['#title'] = ''; + + if (!empty($this->options['group_info']['group_items'][$item_id]['value'][$children])) { + $row['value'][$children]['#default_value'] = $this->options['group_info']['group_items'][$item_id]['value'][$children]; + } + } + $without_children = FALSE; + } + + if ($without_children) { + if (!empty($this->options['group_info']['group_items'][$item_id]['value'])) { + $row['value']['#default_value'] = $this->options['group_info']['group_items'][$item_id]['value']; + } + } + + if (!empty($this->options['group_info']['group_items'][$item_id]['operator'])) { + $row['operator']['#default_value'] = $this->options['group_info']['group_items'][$item_id]['operator']; + } + + $default_title = ''; + if (!empty($this->options['group_info']['group_items'][$item_id]['title'])) { + $default_title = $this->options['group_info']['group_items'][$item_id]['title']; + } + + // Per item group, we have a title that identifies it. + $form['group_info']['group_items'][$item_id] = array( + 'title' => array( + '#type' => 'textfield', + '#size' => 20, + '#default_value' => $default_title, + ), + 'operator' => $row['operator'], + 'value' => $row['value'], + 'remove' => array( + '#type' => 'checkbox', + '#id' => 'views-removed-' . $item_id, + '#attributes' => array('class' => array('views-remove-checkbox')), + '#default_value' => 0, + ), + 'weight' => array( + '#type' => 'weight', + '#delta' => 10, + '#default_value' => $default_weight++, + '#attributes' => array('class' => array('weight')), + ), + ); + } + // From all groups, let chose which is the default. + $form['group_info']['default_group'] = array( + '#type' => 'radios', + '#options' => $groups, + '#default_value' => $this->options['group_info']['default_group'], + '#required' => TRUE, + '#attributes' => array( + 'class' => array('default-radios'), + ) + ); + // From all groups, let chose which is the default. + $form['group_info']['default_group_multiple'] = array( + '#type' => 'checkboxes', + '#options' => $groups, + '#default_value' => $this->options['group_info']['default_group_multiple'], + '#attributes' => array( + 'class' => array('default-checkboxes'), + ) + ); + + $form['group_info']['add_group'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#type' => 'submit', + '#value' => t('Add another item'), + '#submit' => array('views_ui_config_item_form_add_group'), + ); + + $js = array(); + $js['tableDrag']['views-filter-groups']['weight'][0] = array( + 'target' => 'weight', + 'source' => NULL, + 'relationship' => 'sibling', + 'action' => 'order', + 'hidden' => TRUE, + 'limit' => 0, + ); + if (!empty($form_state['js settings']) && is_array($js)) { + $form_state['js settings'] = array_merge($form_state['js settings'], $js); + } + else { + $form_state['js settings'] = $js; + } + } + + + /** + * Make some translations to a form item to make it more suitable to + * exposing. + */ + function exposed_translate(&$form, $type) { + if (!isset($form['#type'])) { + return; + } + + if ($form['#type'] == 'radios') { + $form['#type'] = 'select'; + } + // Checkboxes don't work so well in exposed forms due to GET conversions. + if ($form['#type'] == 'checkboxes') { + if (empty($form['#no_convert']) || empty($this->options['expose']['multiple'])) { + $form['#type'] = 'select'; + } + if (!empty($this->options['expose']['multiple'])) { + $form['#multiple'] = TRUE; + } + } + if (empty($this->options['expose']['multiple']) && isset($form['#multiple'])) { + unset($form['#multiple']); + $form['#size'] = NULL; + } + + // Cleanup in case the translated element's (radios or checkboxes) display value contains html. + if ($form['#type'] == 'select') { + $this->prepare_filter_select_options($form['#options']); + } + + if ($type == 'value' && empty($this->always_required) && empty($this->options['expose']['required']) && $form['#type'] == 'select' && empty($form['#multiple'])) { + $any_label = variable_get('views_exposed_filter_any_label', 'new_any') == 'old_any' ? t('') : t('- Any -'); + $form['#options'] = array('All' => $any_label) + $form['#options']; + $form['#default_value'] = 'All'; + } + + if (!empty($this->options['expose']['required'])) { + $form['#required'] = TRUE; + } + } + + + + /** + * Sanitizes the HTML select element's options. + * + * The function is recursive to support optgroups. + */ + function prepare_filter_select_options(&$options) { + foreach ($options as $value => $label) { + // Recurse for optgroups. + if (is_array($label)) { + $this->prepare_filter_select_options($options[$value]); + } + // FAPI has some special value to allow hierarchy. + // @see _form_options_flatten + elseif (is_object($label)) { + $this->prepare_filter_select_options($options[$value]->option); + } + else { + $options[$value] = strip_tags(decode_entities($label)); + } + } + } + + /** + * Tell the renderer about our exposed form. This only needs to be + * overridden for particularly complex forms. And maybe not even then. + * + * @return array|null + * For standard exposed filters. An array with the following keys: + * - operator: The $form key of the operator. Set to NULL if no operator. + * - value: The $form key of the value. Set to NULL if no value. + * - label: The label to use for this piece. + * For grouped exposed filters. An array with the following keys: + * - value: The $form key of the value. Set to NULL if no value. + * - label: The label to use for this piece. + */ + function exposed_info() { + if (empty($this->options['exposed'])) { + return; + } + + if ($this->is_a_group()) { + return array( + 'value' => $this->options['group_info']['identifier'], + 'label' => $this->options['group_info']['label'], + 'description' => $this->options['group_info']['description'], + ); + } + + return array( + 'operator' => $this->options['expose']['operator_id'], + 'value' => $this->options['expose']['identifier'], + 'label' => $this->options['expose']['label'], + 'description' => $this->options['expose']['description'], + ); + } + + /* + * Transform the input from a grouped filter into a standard filter. + * + * When a filter is a group, find the set of operator and values + * that the choosed item represents, and inform views that a normal + * filter was submitted by telling the operator and the value selected. + * + * The param $selected_group_id is only passed when the filter uses the + * checkboxes widget, and this function will be called for each item + * choosed in the checkboxes. + */ + function convert_exposed_input(&$input, $selected_group_id = NULL) { + if ($this->is_a_group()) { + // If it is already defined the selected group, use it. Only valid + // when the filter uses checkboxes for widget. + if (!empty($selected_group_id)) { + $selected_group = $selected_group_id; + } + else { + $selected_group = $input[$this->options['group_info']['identifier']]; + } + if ($selected_group == 'All' && !empty($this->options['group_info']['optional'])) { + return NULL; + } + if ($selected_group != 'All' && empty($this->options['group_info']['group_items'][$selected_group])) { + return FALSE; + } + if (isset($selected_group) && isset($this->options['group_info']['group_items'][$selected_group])) { + $input[$this->options['expose']['operator']] = $this->options['group_info']['group_items'][$selected_group]['operator']; + + // Value can be optional, For example for 'empty' and 'not empty' filters. + if (!empty($this->options['group_info']['group_items'][$selected_group]['value'])) { + $input[$this->options['expose']['identifier']] = $this->options['group_info']['group_items'][$selected_group]['value']; + } + $this->options['expose']['use_operator'] = TRUE; + + $this->group_info = $input[$this->options['group_info']['identifier']]; + return TRUE; + } + else { + return FALSE; + } + } + } + + /** + * Returns the options available for a grouped filter that users checkboxes + * as widget, and therefore has to be applied several times, one per + * item selected. + */ + function group_multiple_exposed_input(&$input) { + if (!empty($input[$this->options['group_info']['identifier']])) { + return array_filter($input[$this->options['group_info']['identifier']]); + } + return array(); + } + + /** + * Returns TRUE if users can select multiple groups items of a + * grouped exposed filter. + */ + function multiple_exposed_input() { + return $this->is_a_group() && !empty($this->options['group_info']['multiple']); + } + + /** + * If set to remember exposed input in the session, store it there. + * This function is similar to store_exposed_input but modified to + * work properly when the filter is a group. + */ + function store_group_input($input, $status) { + if (!$this->is_a_group() || empty($this->options['group_info']['identifier'])) { + return TRUE; + } + + if (empty($this->options['group_info']['remember'])) { + return; + } + + // Figure out which display id is responsible for the filters, so we + // know where to look for session stored values. + $display_id = ($this->view->display_handler->is_defaulted('filters')) ? 'default' : $this->view->current_display; + + // false means that we got a setting that means to recuse ourselves, + // so we should erase whatever happened to be there. + if ($status === FALSE && isset($_SESSION['views'][$this->view->name][$display_id])) { + $session = &$_SESSION['views'][$this->view->name][$display_id]; + + if (isset($session[$this->options['group_info']['identifier']])) { + unset($session[$this->options['group_info']['identifier']]); + } + } + + if ($status !== FALSE) { + if (!isset($_SESSION['views'][$this->view->name][$display_id])) { + $_SESSION['views'][$this->view->name][$display_id] = array(); + } + + $session = &$_SESSION['views'][$this->view->name][$display_id]; + + $session[$this->options['group_info']['identifier']] = $input[$this->options['group_info']['identifier']]; + } + } + + /** + * Check to see if input from the exposed filters should change + * the behavior of this filter. + */ + function accept_exposed_input($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) { + $this->operator = $input[$this->options['expose']['operator_id']]; + } + + if (!empty($this->options['expose']['identifier'])) { + $value = $input[$this->options['expose']['identifier']]; + + // Various ways to check for the absence of non-required input. + if (empty($this->options['expose']['required'])) { + if (($this->operator == 'empty' || $this->operator == 'not empty') && $value === '') { + $value = ' '; + } + + if ($this->operator != 'empty' && $this->operator != 'not empty') { + if ($value == 'All' || $value === array()) { + return FALSE; + } + } + + if (!empty($this->always_multiple) && $value === '') { + return FALSE; + } + } + + + if (isset($value)) { + $this->value = $value; + if (empty($this->always_multiple) && empty($this->options['expose']['multiple'])) { + $this->value = array($value); + } + } + else { + return FALSE; + } + } + + return TRUE; + } + + function store_exposed_input($input, $status) { + if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) { + return TRUE; + } + + if (empty($this->options['expose']['remember'])) { + return; + } + + // Check if we store exposed value for current user. + global $user; + $allowed_rids = empty($this->options['expose']['remember_roles']) ? array() : array_filter($this->options['expose']['remember_roles']); + $intersect_rids = array_intersect_key($allowed_rids, $user->roles); + if (empty($intersect_rids)) { + return; + } + + // Figure out which display id is responsible for the filters, so we + // know where to look for session stored values. + $display_id = ($this->view->display_handler->is_defaulted('filters')) ? 'default' : $this->view->current_display; + + // shortcut test. + $operator = !empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']); + + // false means that we got a setting that means to recuse ourselves, + // so we should erase whatever happened to be there. + if (!$status && isset($_SESSION['views'][$this->view->name][$display_id])) { + $session = &$_SESSION['views'][$this->view->name][$display_id]; + if ($operator && isset($session[$this->options['expose']['operator_id']])) { + unset($session[$this->options['expose']['operator_id']]); + } + + if (isset($session[$this->options['expose']['identifier']])) { + unset($session[$this->options['expose']['identifier']]); + } + } + + if ($status) { + if (!isset($_SESSION['views'][$this->view->name][$display_id])) { + $_SESSION['views'][$this->view->name][$display_id] = array(); + } + + $session = &$_SESSION['views'][$this->view->name][$display_id]; + + if ($operator && isset($input[$this->options['expose']['operator_id']])) { + $session[$this->options['expose']['operator_id']] = $input[$this->options['expose']['operator_id']]; + } + + $session[$this->options['expose']['identifier']] = $input[$this->options['expose']['identifier']]; + } + } + + /** + * Add this filter to the query. + * + * Due to the nature of fapi, the value and the operator have an unintended + * level of indirection. You will find them in $this->operator + * and $this->value respectively. + */ + function query() { + $this->ensure_my_table(); + $this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", $this->value, $this->operator); + } + + /** + * Can this filter be used in OR groups? + * + * Some filters have complicated where clauses that cannot be easily used + * with OR groups. Some filters must also use HAVING which also makes + * them not groupable. These filters will end up in a special group + * if OR grouping is in use. + * + * @return bool + */ + function can_group() { + return TRUE; + } +} + + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_filter_handlers + */ +class views_handler_filter_broken extends views_handler_filter { + function ui_name($short = FALSE) { + return t('Broken/missing handler'); + } + + function ensure_my_table() { /* No table to ensure! */ } + function query($group_by = FALSE) { /* No query to run */ } + function options_form(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '
' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '
', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + function broken() { return TRUE; } +} + +/** + * Filter by no empty values, though allow to use "0". + * @param $var + * @return bool + */ +function _views_array_filter_zero($var) { + return trim($var) != ""; +} + + +/** + * @} + */ diff --git a/handlers/views_handler_filter_boolean_operator.inc b/handlers/views_handler_filter_boolean_operator.inc new file mode 100644 index 00000000..56365e19 --- /dev/null +++ b/handlers/views_handler_filter_boolean_operator.inc @@ -0,0 +1,179 @@ + 0. + * This might be helpful for performance reasons. + * + * @ingroup views_filter_handlers + */ +class views_handler_filter_boolean_operator extends views_handler_filter { + // exposed filter options + var $always_multiple = TRUE; + // Don't display empty space where the operator would be. + var $no_operator = TRUE; + // Whether to accept NULL as a false value or not + var $accept_null = FALSE; + + function construct() { + $this->value_value = t('True'); + if (isset($this->definition['label'])) { + $this->value_value = $this->definition['label']; + } + if (isset($this->definition['accept null'])) { + $this->accept_null = (bool) $this->definition['accept null']; + } + else if (isset($this->definition['accept_null'])) { + $this->accept_null = (bool) $this->definition['accept_null']; + } + $this->value_options = NULL; + parent::construct(); + } + + /** + * Return the possible options for this filter. + * + * Child classes should override this function to set the possible values + * for the filter. Since this is a boolean filter, the array should have + * two possible keys: 1 for "True" and 0 for "False", although the labels + * can be whatever makes sense for the filter. These values are used for + * configuring the filter, when the filter is exposed, and in the admin + * summary of the filter. Normally, this should be static data, but if it's + * dynamic for some reason, child classes should use a guard to reduce + * database hits as much as possible. + */ + function get_value_options() { + if (isset($this->definition['type'])) { + if ($this->definition['type'] == 'yes-no') { + $this->value_options = array(1 => t('Yes'), 0 => t('No')); + } + if ($this->definition['type'] == 'on-off') { + $this->value_options = array(1 => t('On'), 0 => t('Off')); + } + if ($this->definition['type'] == 'enabled-disabled') { + $this->value_options = array(1 => t('Enabled'), 0 => t('Disabled')); + } + } + + // Provide a fallback if the above didn't set anything. + if (!isset($this->value_options)) { + $this->value_options = array(1 => t('True'), 0 => t('False')); + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['value']['default'] = FALSE; + + return $options; + } + + function operator_form(&$form, &$form_state) { + $form['operator'] = array(); + } + + function value_form(&$form, &$form_state) { + if (empty($this->value_options)) { + // Initialize the array of possible values for this filter. + $this->get_value_options(); + } + if (!empty($form_state['exposed'])) { + // Exposed filter: use a select box to save space. + $filter_form_type = 'select'; + } + else { + // Configuring a filter: use radios for clarity. + $filter_form_type = 'radios'; + } + $form['value'] = array( + '#type' => $filter_form_type, + '#title' => $this->value_value, + '#options' => $this->value_options, + '#default_value' => $this->value, + ); + if (!empty($this->options['exposed'])) { + $identifier = $this->options['expose']['identifier']; + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value; + } + // If we're configuring an exposed filter, add an option. + if (empty($form_state['exposed']) || empty($this->options['expose']['required'])) { + $any_label = variable_get('views_exposed_filter_any_label', 'new_any') == 'old_any' ? '' : t('- Any -'); + if ($form['value']['#type'] != 'select') { + $any_label = check_plain($any_label); + } + $form['value']['#options'] = array('All' => $any_label) + $form['value']['#options']; + } + } + } + + function value_validate($form, &$form_state) { + if ($form_state['values']['options']['value'] == 'All' && !empty($form_state['values']['options']['expose']['required'])) { + form_set_error('value', t('You must select a value unless this is an non-required exposed filter.')); + } + } + + function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + if (empty($this->value_options)) { + $this->get_value_options(); + } + // Now that we have the valid options for this filter, just return the + // human-readable label based on the current value. The value_options + // array is keyed with either 0 or 1, so if the current value is not + // empty, use the label for 1, and if it's empty, use the label for 0. + return $this->value_options[!empty($this->value)]; + } + + function expose_options() { + parent::expose_options(); + $this->options['expose']['operator_id'] = ''; + $this->options['expose']['label'] = $this->value_value; + $this->options['expose']['required'] = TRUE; + } + + function query() { + $this->ensure_my_table(); + $field = "$this->table_alias.$this->real_field"; + + if (empty($this->value)) { + if ($this->accept_null) { + $or = db_or() + ->condition($field, 0, '=') + ->condition($field, NULL, 'IS NULL'); + $this->query->add_where($this->options['group'], $or); + } + else { + $this->query->add_where($this->options['group'], $field, 0, '='); + } + } + else { + if (!empty($this->definition['use equal'])) { + $this->query->add_where($this->options['group'], $field, 1, '='); + } + else { + $this->query->add_where($this->options['group'], $field, 0, '<>'); + } + } + } +} diff --git a/handlers/views_handler_filter_boolean_operator_string.inc b/handlers/views_handler_filter_boolean_operator_string.inc new file mode 100644 index 00000000..b49dde94 --- /dev/null +++ b/handlers/views_handler_filter_boolean_operator_string.inc @@ -0,0 +1,35 @@ +ensure_my_table(); + $where = "$this->table_alias.$this->real_field "; + + if (empty($this->value)) { + $where .= "= ''"; + if ($this->accept_null) { + $where = '(' . $where . " OR $this->table_alias.$this->real_field IS NULL)"; + } + } + else { + $where .= "<> ''"; + } + $this->query->add_where($this->options['group'], $where); + } +} diff --git a/handlers/views_handler_filter_combine.inc b/handlers/views_handler_filter_combine.inc new file mode 100644 index 00000000..915924b8 --- /dev/null +++ b/handlers/views_handler_filter_combine.inc @@ -0,0 +1,170 @@ + array()); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $this->view->init_style(); + + // Allow to choose all fields as possible. + if ($this->view->style_plugin->uses_fields()) { + $options = array(); + foreach ($this->view->display_handler->get_handlers('field') as $name => $field) { + $options[$name] = $field->ui_name(TRUE); + } + if ($options) { + $form['fields'] = array( + '#type' => 'select', + '#title' => t('Choose fields to combine for filtering'), + '#description' => t("This filter doesn't work for very special field handlers."), + '#multiple' => TRUE, + '#options' => $options, + '#default_value' => $this->options['fields'], + ); + } + else { + form_set_error('', t('You have to add some fields to be able to use this filter.')); + } + } + } + + function query() { + $this->view->_build('field'); + $fields = array(); + // Only add the fields if they have a proper field and table alias. + foreach ($this->options['fields'] as $id) { + $field = $this->view->field[$id]; + // Always add the table of the selected fields to be sure a table alias + // exists. + $field->ensure_my_table(); + if (!empty($field->field_alias) && !empty($field->field_alias)) { + $fields[] = "$field->table_alias.$field->real_field"; + } + } + if ($fields) { + $count = count($fields); + $separated_fields = array(); + foreach ($fields as $key => $field) { + $separated_fields[] = $field; + if ($key < $count - 1) { + $separated_fields[] = "' '"; + } + } + $expression = implode(', ', $separated_fields); + $expression = "CONCAT_WS(' ', $expression)"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($expression); + } + } + } + + // By default things like op_equal uses add_where, that doesn't support + // complex expressions, so override all operators. + function op_equal($field) { + $placeholder = $this->placeholder(); + $operator = $this->operator(); + $this->query->add_where_expression($this->options['group'], "$field $operator $placeholder", array($placeholder => $this->value)); + } + + function op_contains($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field LIKE $placeholder", array($placeholder => '%' . db_like($this->value) . '%')); + } + + function op_word($field) { + $where = $this->operator == 'word' ? db_or() : db_and(); + + // Don't filter on empty strings. + if (empty($this->value)) { + return; + } + + preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' ' . $this->value, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $phrase = FALSE; + // Strip off phrase quotes. + if ($match[2]{0} == '"') { + $match[2] = substr($match[2], 1, -1); + $phrase = TRUE; + } + $words = trim($match[2], ',?!();:-'); + $words = $phrase ? array($words) : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY); + $placeholder = $this->placeholder(); + foreach ($words as $word) { + $where->where($field . " LIKE $placeholder", array($placeholder => '%' . db_like(trim($word, " ,!?")) . '%')); + } + } + + if (!$where) { + return; + } + + // Previously this was a call_user_func_array() but that's unnecessary + // as views will unpack an array that is a single arg. + $this->query->add_where($this->options['group'], $where); + } + + function op_starts($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field LIKE $placeholder", array($placeholder => db_like($this->value) . '%')); + } + + function op_not_starts($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field NOT LIKE $placeholder", array($placeholder => db_like($this->value) . '%')); + } + + function op_ends($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field LIKE $placeholder", array($placeholder => '%' . db_like($this->value))); + } + + function op_not_ends($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field NOT LIKE $placeholder", array($placeholder => '%' . db_like($this->value))); + } + + function op_not($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field NOT LIKE $placeholder", array($placeholder => '%' . db_like($this->value) . '%')); + } + + function op_regex($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "$field RLIKE $placeholder", array($placeholder => $this->value)); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where_expression($this->options['group'], "$field $operator"); + } +} diff --git a/handlers/views_handler_filter_date.inc b/handlers/views_handler_filter_date.inc new file mode 100644 index 00000000..4ef61b47 --- /dev/null +++ b/handlers/views_handler_filter_date.inc @@ -0,0 +1,183 @@ + 'radios', + '#title' => t('Value type'), + '#options' => array( + 'date' => t('A date in any machine readable format. CCYY-MM-DD HH:MM:SS is preferred.'), + 'offset' => t('An offset from the current time such as "!example1" or "!example2"', array('!example1' => '+1 day', '!example2' => '-2 hours -30 minutes')), + ), + '#default_value' => !empty($this->value['type']) ? $this->value['type'] : 'date', + ); + } + parent::value_form($form, $form_state); + } + + function options_validate(&$form, &$form_state) { + parent::options_validate($form, $form_state); + + if (!empty($this->options['exposed']) && empty($form_state['values']['options']['expose']['required'])) { + // Who cares what the value is if it's exposed and non-required. + return; + } + + $this->validate_valid_time($form['value'], $form_state['values']['options']['operator'], $form_state['values']['options']['value']); + } + + function exposed_validate(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + if (empty($this->options['expose']['required'])) { + // Who cares what the value is if it's exposed and non-required. + return; + } + + $value = &$form_state['values'][$this->options['expose']['identifier']]; + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id'])) { + $operator = $form_state['values'][$this->options['expose']['operator_id']]; + } + else { + $operator = $this->operator; + } + + $this->validate_valid_time($this->options['expose']['identifier'], $operator, $value); + + } + + /** + * Validate that the time values convert to something usable. + */ + function validate_valid_time(&$form, $operator, $value) { + $operators = $this->operators(); + + if ($operators[$operator]['values'] == 1) { + $convert = strtotime($value['value']); + if (!empty($form['value']) && ($convert == -1 || $convert === FALSE)) { + form_error($form['value'], t('Invalid date format.')); + } + } + elseif ($operators[$operator]['values'] == 2) { + $min = strtotime($value['min']); + if ($min == -1 || $min === FALSE) { + form_error($form['min'], t('Invalid date format.')); + } + $max = strtotime($value['max']); + if ($max == -1 || $max === FALSE) { + form_error($form['max'], t('Invalid date format.')); + } + } + } + + /** + * Validate the build group options form. + */ + function build_group_validate($form, &$form_state) { + // Special case to validate grouped date filters, this is because the + // $group['value'] array contains the type of filter (date or offset) + // and therefore the number of items the comparission has to be done + // against 'one' instead of 'zero'. + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + // Check if the title is defined but value wasn't defined. + if (!empty($group['title'])) { + if ((!is_array($group['value']) && empty($group['value'])) || (is_array($group['value']) && count(array_filter($group['value'])) == 1)) { + form_error($form['group_info']['group_items'][$id]['value'], t('The value is required if title for this item is defined.')); + } + } + + // Check if the value is defined but title wasn't defined. + if ((!is_array($group['value']) && !empty($group['value'])) || (is_array($group['value']) && count(array_filter($group['value'])) > 1)) { + if (empty($group['title'])) { + form_error($form['group_info']['group_items'][$id]['title'], t('The title is required if value for this item is defined.')); + } + } + } + } + } + + + function accept_exposed_input($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + // Store this because it will get overwritten. + $type = $this->value['type']; + $rc = parent::accept_exposed_input($input); + + // Don't filter if value(s) are empty. + $operators = $this->operators(); + if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id'])) { + $operator = $input[$this->options['expose']['operator_id']]; + } + else { + $operator = $this->operator; + } + + if ($operators[$operator]['values'] == 1) { + if ($this->value['value'] == '') { + return FALSE; + } + } + else { + if ($this->value['min'] == '' || $this->value['max'] == '') { + return FALSE; + } + } + + // restore what got overwritten by the parent. + $this->value['type'] = $type; + return $rc; + } + + function op_between($field) { + $a = intval(strtotime($this->value['min'], 0)); + $b = intval(strtotime($this->value['max'], 0)); + + if ($this->value['type'] == 'offset') { + $a = '***CURRENT_TIME***' . sprintf('%+d', $a); // keep sign + $b = '***CURRENT_TIME***' . sprintf('%+d', $b); // keep sign + } + // This is safe because we are manually scrubbing the values. + // It is necessary to do it this way because $a and $b are formulas when using an offset. + $operator = strtoupper($this->operator); + $this->query->add_where_expression($this->options['group'], "$field $operator $a AND $b"); + } + + function op_simple($field) { + $value = intval(strtotime($this->value['value'], 0)); + if (!empty($this->value['type']) && $this->value['type'] == 'offset') { + $value = '***CURRENT_TIME***' . sprintf('%+d', $value); // keep sign + } + // This is safe because we are manually scrubbing the value. + // It is necessary to do it this way because $value is a formula when using an offset. + $this->query->add_where_expression($this->options['group'], "$field $this->operator $value"); + } +} diff --git a/handlers/views_handler_filter_entity_bundle.inc b/handlers/views_handler_filter_entity_bundle.inc new file mode 100644 index 00000000..c46cd0b8 --- /dev/null +++ b/handlers/views_handler_filter_entity_bundle.inc @@ -0,0 +1,122 @@ +get_entity_type(); + } + + /** + * Set and returns the entity_type. + * + * @return string + * The entity type on the filter. + */ + function get_entity_type() { + if (!isset($this->entity_type)) { + $data = views_fetch_data($this->table); + if (isset($data['table']['entity type'])) { + $this->entity_type = $data['table']['entity type']; + } + + // If the current filter is under a relationship you can't be sure that the + // entity type of the view is the entity type of the current filter + // For example a filter from a node author on a node view does have users as entity type. + if (!empty($this->options['relationship']) && $this->options['relationship'] != 'none') { + $relationships = $this->view->display_handler->get_option('relationships'); + if (!empty($relationships[$this->options['relationship']])) { + $options = $relationships[$this->options['relationship']]; + $data = views_fetch_data($options['table']); + $this->entity_type = $data['table']['entity type']; + } + } + } + + return $this->entity_type; + } + + + function get_value_options() { + if (!isset($this->value_options)) { + $info = entity_get_info($this->entity_type); + $types = $info['bundles']; + $this->value_title = t('@entity types', array('@entity' => $info['label'])); + + $options = array(); + foreach ($types as $type => $info) { + $options[$type] = t($info['label']); + } + asort($options); + $this->value_options = $options; + } + } + + /** + * All entity types beside comment and taxonomy terms have a proper implement + * bundle, though these two need an additional join to node/vocab table + * to work as required. + */ + function query() { + $this->ensure_my_table(); + + // Adjust the join for the comment case. + if ($this->entity_type == 'comment') { + $join = new views_join(); + $def = array( + 'table' => 'node', + 'field' => 'nid', + 'left_table' => $this->table_alias, + 'left_field' => 'nid', + ); + $join->definition = $def; + $join->construct(); + $join->adjusted = TRUE; + $this->table_alias = $this->query->add_table('node', $this->relationship, $join); + $this->real_field = 'type'; + + // Replace the value to match the node type column. + foreach ($this->value as &$value) { + $value = str_replace('comment_node_', '', $value); + } + } + elseif ($this->entity_type == 'taxonomy_term') { + $join = new views_join(); + $def = array( + 'table' => 'taxonomy_vocabulary', + 'field' => 'vid', + 'left_table' => $this->table_alias, + 'left_field' => 'vid', + ); + $join->definition = $def; + $join->construct(); + $join->adjusted = TRUE; + $this->table_alias = $this->query->add_table('taxonomy_vocabulary', $this->relationship, $join); + $this->real_field = 'machine_name'; + } + else { + $entity_info = entity_get_info($this->entity_type); + $this->real_field = $entity_info['bundle keys']['bundle']; + } + parent::query(); + } +} diff --git a/handlers/views_handler_filter_equality.inc b/handlers/views_handler_filter_equality.inc new file mode 100644 index 00000000..e045c7e3 --- /dev/null +++ b/handlers/views_handler_filter_equality.inc @@ -0,0 +1,45 @@ + t('Is equal to'), + '!=' => t('Is not equal to'), + ); + } + + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Value'), + '#size' => 30, + '#default_value' => $this->value, + ); + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + if (!isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value; + } + } + } +} diff --git a/handlers/views_handler_filter_group_by_numeric.inc b/handlers/views_handler_filter_group_by_numeric.inc new file mode 100644 index 00000000..2b265bef --- /dev/null +++ b/handlers/views_handler_filter_group_by_numeric.inc @@ -0,0 +1,56 @@ +ensure_my_table(); + $field = $this->get_field(); + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + function op_between($field) { + $placeholder_min = $this->placeholder(); + $placeholder_max = $this->placeholder(); + if ($this->operator == 'between') { + $this->query->add_having_expression($this->options['group'], "$field >= $placeholder_min", array($placeholder_min => $this->value['min'])); + $this->query->add_having_expression($this->options['group'], "$field <= $placeholder_max", array($placeholder_max => $this->value['max'])); + } + else { + $this->query->add_having_expression($this->options['group'], "$field <= $placeholder_min OR $field >= $placeholder_max", array($placeholder_min => $this->value['min'], $placeholder_max => $this->value['max'])); + } + } + + function op_simple($field) { + $placeholder = $this->placeholder(); + $this->query->add_having_expression($this->options['group'], "$field $this->operator $placeholder", array($placeholder => $this->value['value'])); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_having_expression($this->options['group'], "$field $operator"); + } + + function ui_name($short = FALSE) { + return $this->get_field(parent::ui_name($short)); + } + + function can_group() { return FALSE; } +} diff --git a/handlers/views_handler_filter_in_operator.inc b/handlers/views_handler_filter_in_operator.inc new file mode 100644 index 00000000..fc2700bd --- /dev/null +++ b/handlers/views_handler_filter_in_operator.inc @@ -0,0 +1,426 @@ +value_title = t('Options'); + $this->value_options = NULL; + } + + /** + * Child classes should be used to override this function and set the + * 'value options', unless 'options callback' is defined as a valid function + * or static public method to generate these values. + * + * This can use a guard to be used to reduce database hits as much as + * possible. + * + * @return + * Return the stored values in $this->value_options if someone expects it. + */ + function get_value_options() { + if (isset($this->value_options)) { + return; + } + + if (isset($this->definition['options callback']) && is_callable($this->definition['options callback'])) { + if (isset($this->definition['options arguments']) && is_array($this->definition['options arguments'])) { + $this->value_options = call_user_func_array($this->definition['options callback'], $this->definition['options arguments']); + } + else { + $this->value_options = call_user_func($this->definition['options callback']); + } + } + else { + $this->value_options = array(t('Yes'), t('No')); + } + + return $this->value_options; + } + + function expose_options() { + parent::expose_options(); + $this->options['expose']['reduce'] = FALSE; + } + + function expose_form(&$form, &$form_state) { + parent::expose_form($form, $form_state); + $form['expose']['reduce'] = array( + '#type' => 'checkbox', + '#title' => t('Limit list to selected items'), + '#description' => t('If checked, the only items presented to the user will be the ones selected here.'), + '#default_value' => !empty($this->options['expose']['reduce']), // safety + ); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['operator']['default'] = 'in'; + $options['value']['default'] = array(); + $options['expose']['contains']['reduce'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * This kind of construct makes it relatively easy for a child class + * to add or remove functionality by overriding this function and + * adding/removing items from this array. + */ + function operators() { + $operators = array( + 'in' => array( + 'title' => t('Is one of'), + 'short' => t('in'), + 'short_single' => t('='), + 'method' => 'op_simple', + 'values' => 1, + ), + 'not in' => array( + 'title' => t('Is not one of'), + 'short' => t('not in'), + 'short_single' => t('<>'), + 'method' => 'op_simple', + 'values' => 1, + ), + ); + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + + return $operators; + } + + /** + * Build strings from the operators() for 'select' options + */ + function operator_options($which = 'title') { + $options = array(); + foreach ($this->operators() as $id => $info) { + $options[$id] = $info[$which]; + } + + return $options; + } + + function operator_values($values = 1) { + $options = array(); + foreach ($this->operators() as $id => $info) { + if (isset($info['values']) && $info['values'] == $values) { + $options[] = $id; + } + } + + return $options; + } + + function value_form(&$form, &$form_state) { + $form['value'] = array(); + $options = array(); + + if (empty($form_state['exposed'])) { + // Add a select all option to the value form. + $options = array('all' => t('Select all')); + } + + $this->get_value_options(); + $options += $this->value_options; + $default_value = (array) $this->value; + + $which = 'all'; + if (!empty($form['operator'])) { + $source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator'; + } + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { + // exposed and locked. + $which = in_array($this->operator, $this->operator_values(1)) ? 'value' : 'none'; + } + else { + $source = 'edit-' . drupal_html_id($this->options['expose']['operator_id']); + } + + if (!empty($this->options['expose']['reduce'])) { + $options = $this->reduce_value_options(); + + if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) { + $default_value = array(); + } + } + + if (empty($this->options['expose']['multiple'])) { + if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) { + $default_value = 'All'; + } + elseif (empty($default_value)) { + $keys = array_keys($options); + $default_value = array_shift($keys); + } + else { + $copy = $default_value; + $default_value = array_shift($copy); + } + } + } + + if ($which == 'all' || $which == 'value') { + $form['value'] = array( + '#type' => $this->value_form_type, + '#title' => $this->value_title, + '#options' => $options, + '#default_value' => $default_value, + // These are only valid for 'select' type, but do no harm to checkboxes. + '#multiple' => TRUE, + '#size' => count($options) > 8 ? 8 : count($options), + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $default_value; + } + + if ($which == 'all') { + if (empty($form_state['exposed']) && (in_array($this->value_form_type, array('checkbox', 'checkboxes', 'radios', 'select')))) { + $form['value']['#prefix'] = '
'; + $form['value']['#suffix'] = '
'; + } + $form['value']['#dependency'] = array($source => $this->operator_values(1)); + } + } + } + + /** + * When using exposed filters, we may be required to reduce the set. + */ + function reduce_value_options($input = NULL) { + if (!isset($input)) { + $input = $this->value_options; + } + + // Because options may be an array of strings, or an array of mixed arrays + // and strings (optgroups) or an array of objects, we have to + // step through and handle each one individually. + $options = array(); + foreach ($input as $id => $option) { + if (is_array($option)) { + $options[$id] = $this->reduce_value_options($option); + continue; + } + elseif (is_object($option)) { + $keys = array_keys($option->option); + $key = array_shift($keys); + if (isset($this->options['value'][$key])) { + $options[$id] = $option; + } + } + elseif (isset($this->options['value'][$id])) { + $options[$id] = $option; + } + } + return $options; + } + + function accept_exposed_input($input) { + // A very special override because the All state for this type of + // filter could have a default: + if (empty($this->options['exposed'])) { + return TRUE; + } + + // If this is non-multiple and non-required, then this filter will + // participate, but using the default settings, *if* 'limit is true. + if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) { + $identifier = $this->options['expose']['identifier']; + if ($input[$identifier] == 'All') { + return TRUE; + } + } + + return parent::accept_exposed_input($input); + } + + function value_submit($form, &$form_state) { + // Drupal's FAPI system automatically puts '0' in for any checkbox that + // was not set, and the key to the checkbox if it is set. + // Unfortunately, this means that if the key to that checkbox is 0, + // we are unable to tell if that checkbox was set or not. + + // Luckily, the '#value' on the checkboxes form actually contains + // *only* a list of checkboxes that were set, and we can use that + // instead. + + $form_state['values']['options']['value'] = $form['value']['#value']; + } + + function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + $info = $this->operators(); + + $this->get_value_options(); + + if (!is_array($this->value)) { + return; + } + + $operator = check_plain($info[$this->operator]['short']); + $values = ''; + if (in_array($this->operator, $this->operator_values(1))) { + // Remove every element which is not known. + foreach ($this->value as $value) { + if (!isset($this->value_options[$value])) { + unset($this->value[$value]); + } + } + // Choose different kind of ouput for 0, a single and multiple values. + if (count($this->value) == 0) { + $values = t('Unknown'); + } + else if (count($this->value) == 1) { + // If any, use the 'single' short name of the operator instead. + if (isset($info[$this->operator]['short_single'])) { + $operator = check_plain($info[$this->operator]['short_single']); + } + + $keys = $this->value; + $value = array_shift($keys); + if (isset($this->value_options[$value])) { + $values = check_plain($this->value_options[$value]); + } + else { + $values = ''; + } + } + else { + foreach ($this->value as $value) { + if ($values !== '') { + $values .= ', '; + } + if (drupal_strlen($values) > 8) { + $values .= '...'; + break; + } + if (isset($this->value_options[$value])) { + $values .= check_plain($this->value_options[$value]); + } + } + } + } + + return $operator . (($values !== '') ? ' ' . $values : ''); + } + + function query() { + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}(); + } + } + + function op_simple() { + if (empty($this->value)) { + return; + } + $this->ensure_my_table(); + + // We use array_values() because the checkboxes keep keys and that can cause + // array addition problems. + $this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", array_values($this->value), $this->operator); + } + + function op_empty() { + $this->ensure_my_table(); + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", NULL, $operator); + } + + function validate() { + $this->get_value_options(); + $errors = array(); + + // If the operator is an operator which doesn't require a value, there is + // no need for additional validation. + if (in_array($this->operator, $this->operator_values(0))) { + return array(); + } + + if (!in_array($this->operator, $this->operator_values(1))) { + $errors[] = t('The operator is invalid on filter: @filter.', array('@filter' => $this->ui_name(TRUE))); + } + if (is_array($this->value)) { + if (!isset($this->value_options)) { + // Don't validate if there are none value options provided, for example for special handlers. + return $errors; + } + if ($this->options['exposed'] && !$this->options['expose']['required'] && empty($this->value)) { + // Don't validate if the field is exposed and no default value is provided. + return $errors; + } + + // Some filter_in_operator usage uses optgroups forms, so flatten it. + $flat_options = form_options_flatten($this->value_options, TRUE); + + // Remove every element which is not known. + foreach ($this->value as $value) { + if (!isset($flat_options[$value])) { + unset($this->value[$value]); + } + } + // Choose different kind of ouput for 0, a single and multiple values. + if (count($this->value) == 0) { + $errors[] = t('No valid values found on filter: @filter.', array('@filter' => $this->ui_name(TRUE))); + } + } + elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) { + $errors[] = t('The value @value is not an array for @operator on filter: @filter', array('@value' => views_var_export($this->value), '@operator' => $this->operator, '@filter' => $this->ui_name(TRUE))); + } + return $errors; + } +} diff --git a/handlers/views_handler_filter_many_to_one.inc b/handlers/views_handler_filter_many_to_one.inc new file mode 100644 index 00000000..f3847961 --- /dev/null +++ b/handlers/views_handler_filter_many_to_one.inc @@ -0,0 +1,125 @@ +helper = new views_many_to_one_helper($this); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['operator']['default'] = 'or'; + $options['value']['default'] = array(); + + if (isset($this->helper)) { + $this->helper->option_definition($options); + } + else { + $helper = new views_many_to_one_helper($this); + $helper->option_definition($options); + } + + return $options; + } + + function operators() { + $operators = array( + 'or' => array( + 'title' => t('Is one of'), + 'short' => t('or'), + 'short_single' => t('='), + 'method' => 'op_helper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + 'and' => array( + 'title' => t('Is all of'), + 'short' => t('and'), + 'short_single' => t('='), + 'method' => 'op_helper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + 'not' => array( + 'title' => t('Is none of'), + 'short' => t('not'), + 'short_single' => t('<>'), + 'method' => 'op_helper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + ); + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + + return $operators; + } + + var $value_form_type = 'select'; + function value_form(&$form, &$form_state) { + parent::value_form($form, $form_state); + + if (empty($form_state['exposed'])) { + $this->helper->options_form($form, $form_state); + } + } + + /** + * Override ensure_my_table so we can control how this joins in. + * The operator actually has influence over joining. + */ + function ensure_my_table() { + // Defer to helper if the operator specifies it. + $info = $this->operators(); + if (isset($info[$this->operator]['ensure_my_table']) && $info[$this->operator]['ensure_my_table'] == 'helper') { + return $this->helper->ensure_my_table(); + } + + return parent::ensure_my_table(); + } + + function op_helper() { + if (empty($this->value)) { + return; + } + $this->helper->add_filter(); + } +} diff --git a/handlers/views_handler_filter_numeric.inc b/handlers/views_handler_filter_numeric.inc new file mode 100644 index 00000000..982abd87 --- /dev/null +++ b/handlers/views_handler_filter_numeric.inc @@ -0,0 +1,325 @@ + array( + 'min' => array('default' => ''), + 'max' => array('default' => ''), + 'value' => array('default' => ''), + ), + ); + + return $options; + } + + function operators() { + $operators = array( + '<' => array( + 'title' => t('Is less than'), + 'method' => 'op_simple', + 'short' => t('<'), + 'values' => 1, + ), + '<=' => array( + 'title' => t('Is less than or equal to'), + 'method' => 'op_simple', + 'short' => t('<='), + 'values' => 1, + ), + '=' => array( + 'title' => t('Is equal to'), + 'method' => 'op_simple', + 'short' => t('='), + 'values' => 1, + ), + '!=' => array( + 'title' => t('Is not equal to'), + 'method' => 'op_simple', + 'short' => t('!='), + 'values' => 1, + ), + '>=' => array( + 'title' => t('Is greater than or equal to'), + 'method' => 'op_simple', + 'short' => t('>='), + 'values' => 1, + ), + '>' => array( + 'title' => t('Is greater than'), + 'method' => 'op_simple', + 'short' => t('>'), + 'values' => 1, + ), + 'between' => array( + 'title' => t('Is between'), + 'method' => 'op_between', + 'short' => t('between'), + 'values' => 2, + ), + 'not between' => array( + 'title' => t('Is not between'), + 'method' => 'op_between', + 'short' => t('not between'), + 'values' => 2, + ), + ); + + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + + // Add regexp support for MySQL. + if (Database::getConnection()->databaseType() == 'mysql') { + $operators += array( + 'regular_expression' => array( + 'title' => t('Regular expression'), + 'short' => t('regex'), + 'method' => 'op_regex', + 'values' => 1, + ), + ); + } + + return $operators; + } + + /** + * Provide a list of all the numeric operators + */ + function operator_options($which = 'title') { + $options = array(); + foreach ($this->operators() as $id => $info) { + $options[$id] = $info[$which]; + } + + return $options; + } + + function operator_values($values = 1) { + $options = array(); + foreach ($this->operators() as $id => $info) { + if ($info['values'] == $values) { + $options[] = $id; + } + } + + return $options; + } + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + $form['value']['#tree'] = TRUE; + + // We have to make some choices when creating this as an exposed + // filter form. For example, if the operator is locked and thus + // not rendered, we can't render dependencies; instead we only + // render the form items we need. + $which = 'all'; + if (!empty($form['operator'])) { + $source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator'; + } + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { + // exposed and locked. + $which = in_array($this->operator, $this->operator_values(2)) ? 'minmax' : 'value'; + } + else { + $source = 'edit-' . drupal_html_id($this->options['expose']['operator_id']); + } + } + + if ($which == 'all') { + $form['value']['value'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Value') : '', + '#size' => 30, + '#default_value' => $this->value['value'], + '#dependency' => array($source => $this->operator_values(1)), + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['value'])) { + $form_state['input'][$identifier]['value'] = $this->value['value']; + } + } + elseif ($which == 'value') { + // When exposed we drop the value-value and just do value if + // the operator is locked. + $form['value'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Value') : '', + '#size' => 30, + '#default_value' => $this->value['value'], + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value['value']; + } + } + + if ($which == 'all' || $which == 'minmax') { + $form['value']['min'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('Min') : '', + '#size' => 30, + '#default_value' => $this->value['min'], + ); + $form['value']['max'] = array( + '#type' => 'textfield', + '#title' => empty($form_state['exposed']) ? t('And max') : t('And'), + '#size' => 30, + '#default_value' => $this->value['max'], + ); + if ($which == 'all') { + $dependency = array( + '#dependency' => array($source => $this->operator_values(2)), + ); + $form['value']['min'] += $dependency; + $form['value']['max'] += $dependency; + } + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['min'])) { + $form_state['input'][$identifier]['min'] = $this->value['min']; + } + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['max'])) { + $form_state['input'][$identifier]['max'] = $this->value['max']; + } + + if (!isset($form['value'])) { + // Ensure there is something in the 'value'. + $form['value'] = array( + '#type' => 'value', + '#value' => NULL + ); + } + } + } + + function query() { + $this->ensure_my_table(); + $field = "$this->table_alias.$this->real_field"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + + function op_between($field) { + if ($this->operator == 'between') { + $this->query->add_where($this->options['group'], $field, array($this->value['min'], $this->value['max']), 'BETWEEN'); + } + else { + $this->query->add_where($this->options['group'], db_or()->condition($field, $this->value['min'], '<=')->condition($field, $this->value['max'], '>=')); + } + } + + function op_simple($field) { + $this->query->add_where($this->options['group'], $field, $this->value['value'], $this->operator); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where($this->options['group'], $field, NULL, $operator); + } + + function op_regex($field) { + $this->query->add_where($this->options['group'], $field, $this->value, 'RLIKE'); + } + + function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + + $options = $this->operator_options('short'); + $output = check_plain($options[$this->operator]); + if (in_array($this->operator, $this->operator_values(2))) { + $output .= ' ' . t('@min and @max', array('@min' => $this->value['min'], '@max' => $this->value['max'])); + } + elseif (in_array($this->operator, $this->operator_values(1))) { + $output .= ' ' . check_plain($this->value['value']); + } + return $output; + } + + /** + * Do some minor translation of the exposed input + */ + function accept_exposed_input($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + // rewrite the input value so that it's in the correct format so that + // the parent gets the right data. + if (!empty($this->options['expose']['identifier'])) { + $value = &$input[$this->options['expose']['identifier']]; + if (!is_array($value)) { + $value = array( + 'value' => $value, + ); + } + } + + $rc = parent::accept_exposed_input($input); + + if (empty($this->options['expose']['required'])) { + // We have to do some of our own checking for non-required filters. + $info = $this->operators(); + if (!empty($info[$this->operator]['values'])) { + switch ($info[$this->operator]['values']) { + case 1: + if ($value['value'] === '') { + return FALSE; + } + break; + case 2: + if ($value['min'] === '' && $value['max'] === '') { + return FALSE; + } + break; + } + } + } + + return $rc; + } +} diff --git a/handlers/views_handler_filter_string.inc b/handlers/views_handler_filter_string.inc new file mode 100644 index 00000000..c50eff44 --- /dev/null +++ b/handlers/views_handler_filter_string.inc @@ -0,0 +1,338 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * This kind of construct makes it relatively easy for a child class + * to add or remove functionality by overriding this function and + * adding/removing items from this array. + */ + function operators() { + $operators = array( + '=' => array( + 'title' => t('Is equal to'), + 'short' => t('='), + 'method' => 'op_equal', + 'values' => 1, + ), + '!=' => array( + 'title' => t('Is not equal to'), + 'short' => t('!='), + 'method' => 'op_equal', + 'values' => 1, + ), + 'contains' => array( + 'title' => t('Contains'), + 'short' => t('contains'), + 'method' => 'op_contains', + 'values' => 1, + ), + 'word' => array( + 'title' => t('Contains any word'), + 'short' => t('has word'), + 'method' => 'op_word', + 'values' => 1, + ), + 'allwords' => array( + 'title' => t('Contains all words'), + 'short' => t('has all'), + 'method' => 'op_word', + 'values' => 1, + ), + 'starts' => array( + 'title' => t('Starts with'), + 'short' => t('begins'), + 'method' => 'op_starts', + 'values' => 1, + ), + 'not_starts' => array( + 'title' => t('Does not start with'), + 'short' => t('not_begins'), + 'method' => 'op_not_starts', + 'values' => 1, + ), + 'ends' => array( + 'title' => t('Ends with'), + 'short' => t('ends'), + 'method' => 'op_ends', + 'values' => 1, + ), + 'not_ends' => array( + 'title' => t('Does not end with'), + 'short' => t('not_ends'), + 'method' => 'op_not_ends', + 'values' => 1, + ), + 'not' => array( + 'title' => t('Does not contain'), + 'short' => t('!has'), + 'method' => 'op_not', + 'values' => 1, + ), + 'shorterthan' => array( + 'title' => t('Length is shorter than'), + 'short' => t('shorter than'), + 'method' => 'op_shorter', + 'values' => 1, + ), + 'longerthan' => array( + 'title' => t('Length is longer than'), + 'short' => t('longer than'), + 'method' => 'op_longer', + 'values' => 1, + ), + ); + // if the definition allows for the empty operator, add it. + if (!empty($this->definition['allow empty'])) { + $operators += array( + 'empty' => array( + 'title' => t('Is empty (NULL)'), + 'method' => 'op_empty', + 'short' => t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => t('Is not empty (NOT NULL)'), + 'method' => 'op_empty', + 'short' => t('not empty'), + 'values' => 0, + ), + ); + } + // Add regexp support for MySQL. + if (Database::getConnection()->databaseType() == 'mysql') { + $operators += array( + 'regular_expression' => array( + 'title' => t('Regular expression'), + 'short' => t('regex'), + 'method' => 'op_regex', + 'values' => 1, + ), + ); + } + + return $operators; + } + + /** + * Build strings from the operators() for 'select' options + */ + function operator_options($which = 'title') { + $options = array(); + foreach ($this->operators() as $id => $info) { + $options[$id] = $info[$which]; + } + + return $options; + } + + function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + + $options = $this->operator_options('short'); + $output = ''; + if(!empty($options[$this->operator])) { + $output = check_plain($options[$this->operator]); + } + if (in_array($this->operator, $this->operator_values(1))) { + $output .= ' ' . check_plain($this->value); + } + return $output; + } + + function operator_values($values = 1) { + $options = array(); + foreach ($this->operators() as $id => $info) { + if (isset($info['values']) && $info['values'] == $values) { + $options[] = $id; + } + } + + return $options; + } + + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + // We have to make some choices when creating this as an exposed + // filter form. For example, if the operator is locked and thus + // not rendered, we can't render dependencies; instead we only + // render the form items we need. + $which = 'all'; + if (!empty($form['operator'])) { + $source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator'; + } + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { + // exposed and locked. + $which = in_array($this->operator, $this->operator_values(1)) ? 'value' : 'none'; + } + else { + $source = 'edit-' . drupal_html_id($this->options['expose']['operator_id']); + } + } + + if ($which == 'all' || $which == 'value') { + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Value'), + '#size' => 30, + '#default_value' => $this->value, + ); + if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $this->value; + } + + if ($which == 'all') { + $form['value'] += array( + '#dependency' => array($source => $this->operator_values(1)), + ); + } + } + + if (!isset($form['value'])) { + // Ensure there is something in the 'value'. + $form['value'] = array( + '#type' => 'value', + '#value' => NULL + ); + } + } + + function operator() { + return $this->operator == '=' ? 'LIKE' : 'NOT LIKE'; + } + + /** + * Add this filter to the query. + * + * Due to the nature of fapi, the value and the operator have an unintended + * level of indirection. You will find them in $this->operator + * and $this->value respectively. + */ + function query() { + $this->ensure_my_table(); + $field = "$this->table_alias.$this->real_field"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + + function op_equal($field) { + $this->query->add_where($this->options['group'], $field, $this->value, $this->operator()); + } + + function op_contains($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value) . '%', 'LIKE'); + } + + function op_word($field) { + $where = $this->operator == 'word' ? db_or() : db_and(); + + // Don't filter on empty strings. + if (empty($this->value)) { + return; + } + + preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' ' . $this->value, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $phrase = false; + // Strip off phrase quotes + if ($match[2]{0} == '"') { + $match[2] = substr($match[2], 1, -1); + $phrase = true; + } + $words = trim($match[2], ',?!();:-'); + $words = $phrase ? array($words) : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY); + foreach ($words as $word) { + $placeholder = $this->placeholder(); + $where->condition($field, '%' . db_like(trim($word, " ,!?")) . '%', 'LIKE'); + } + } + + if (!$where) { + return; + } + + // previously this was a call_user_func_array but that's unnecessary + // as views will unpack an array that is a single arg. + $this->query->add_where($this->options['group'], $where); + } + + function op_starts($field) { + $this->query->add_where($this->options['group'], $field, db_like($this->value) . '%', 'LIKE'); + } + + function op_not_starts($field) { + $this->query->add_where($this->options['group'], $field, db_like($this->value) . '%', 'NOT LIKE'); + } + + function op_ends($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value), 'LIKE'); + } + + function op_not_ends($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value), 'NOT LIKE'); + } + + function op_not($field) { + $this->query->add_where($this->options['group'], $field, '%' . db_like($this->value) . '%', 'NOT LIKE'); + } + + function op_shorter($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "LENGTH($field) < $placeholder", array($placeholder => $this->value)); + } + + function op_longer($field) { + $placeholder = $this->placeholder(); + $this->query->add_where_expression($this->options['group'], "LENGTH($field) > $placeholder", array($placeholder => $this->value)); + } + + function op_regex($field) { + $this->query->add_where($this->options['group'], $field, $this->value, 'RLIKE'); + } + + function op_empty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->add_where($this->options['group'], $field, NULL, $operator); + } + +} diff --git a/handlers/views_handler_relationship.inc b/handlers/views_handler_relationship.inc new file mode 100644 index 00000000..695082b6 --- /dev/null +++ b/handlers/views_handler_relationship.inc @@ -0,0 +1,186 @@ +definition['relationship table'])) { + $this->table = $this->definition['relationship table']; + } + if (isset($this->definition['relationship field'])) { + // Set both real_field and field so custom handler + // can rely on the old field value. + $this->real_field = $this->field = $this->definition['relationship field']; + } + } + + /** + * Get this field's label. + */ + function label() { + if (!isset($this->options['label'])) { + return $this->ui_name(); + } + return $this->options['label']; + } + + function option_definition() { + $options = parent::option_definition(); + + + // Relationships definitions should define a default label, but if they aren't get another default value. + if (!empty($this->definition['label'])) { + $label = $this->definition['label']; + } + else { + $label = !empty($this->definition['field']) ? $this->definition['field'] : $this->definition['base field']; + } + + $options['label'] = array('default' => $label, 'translatable' => TRUE); + $options['required'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Default options form that provides the label widget that all fields + * should have. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Identifier'), + '#default_value' => isset($this->options['label']) ? $this->options['label'] : '', + '#description' => t('Edit the administrative label displayed when referencing this relationship from filters, etc.'), + '#required' => TRUE, + ); + + $form['required'] = array( + '#type' => 'checkbox', + '#title' => t('Require this relationship'), + '#description' => t('Enable to hide items that do not contain this relationship'), + '#default_value' => !empty($this->options['required']), + ); + } + + /** + * Called to implement a relationship in a query. + */ + function query() { + // Figure out what base table this relationship brings to the party. + $table_data = views_fetch_data($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensure_my_table(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $base_field; + $def['left_table'] = $this->table_alias; + $def['left_field'] = $this->real_field; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + if (!empty($this->definition['extra'])) { + $def['extra'] = $this->definition['extra']; + } + + if (!empty($def['join_handler']) && class_exists($def['join_handler'])) { + $join = new $def['join_handler']; + } + else { + $join = new views_join(); + } + + $join->definition = $def; + $join->options = $this->options; + $join->construct(); + $join->adjusted = TRUE; + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship); + + // Add access tags if the base table provide it. + if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) { + $access_tag = $table_data['table']['base']['access query tag']; + $this->query->add_tag($access_tag); + } + } + + /** + * You can't groupby a relationship. + */ + function use_group_by() { + return FALSE; + } +} + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_relationship_handlers + */ +class views_handler_relationship_broken extends views_handler_relationship { + function ui_name($short = FALSE) { + return t('Broken/missing handler'); + } + + function ensure_my_table() { /* No table to ensure! */ } + function query() { /* No query to run */ } + function options_form(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '
' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '
', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + function broken() { return TRUE; } +} + +/** + * @} + */ diff --git a/handlers/views_handler_relationship_groupwise_max.inc b/handlers/views_handler_relationship_groupwise_max.inc new file mode 100644 index 00000000..23198c60 --- /dev/null +++ b/handlers/views_handler_relationship_groupwise_max.inc @@ -0,0 +1,382 @@ + NULL); + // Descending more useful. + $options['subquery_order'] = array('default' => 'DESC'); + $options['subquery_regenerate'] = array('default' => FALSE, 'bool' => TRUE); + $options['subquery_view'] = array('default' => FALSE); + $options['subquery_namespace'] = array('default' => FALSE); + + return $options; + } + + /** + * Extends the relationship's basic options, allowing the user to pick + * a sort and an order for it. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Get the sorts that apply to our base. + $sorts = views_fetch_fields($this->definition['base'], 'sort'); + foreach ($sorts as $sort_id => $sort) { + $sort_options[$sort_id] = "$sort[group]: $sort[title]"; + } + $base_table_data = views_fetch_data($this->definition['base']); + + $form['subquery_sort'] = array( + '#type' => 'select', + '#title' => t('Representative sort criteria'), + // Provide the base field as sane default sort option. + '#default_value' => !empty($this->options['subquery_sort']) ? $this->options['subquery_sort'] : $this->definition['base'] . '.' . $base_table_data['table']['base']['field'], + '#options' => $sort_options, + '#description' => theme('advanced_help_topic', array('module' => 'views', 'topic' => 'relationship-representative')) . + t("The sort criteria is applied to the data brought in by the relationship to determine how a representative item is obtained for each row. For example, to show the most recent node for each user, pick 'Content: Updated date'."), + ); + + $form['subquery_order'] = array( + '#type' => 'radios', + '#title' => t('Representative sort order'), + '#description' => t("The ordering to use for the sort criteria selected above."), + '#options' => array('ASC' => t('Ascending'), 'DESC' => t('Descending')), + '#default_value' => $this->options['subquery_order'], + ); + + $form['subquery_namespace'] = array( + '#type' => 'textfield', + '#title' => t('Subquery namespace'), + '#description' => t('Advanced. Enter a namespace for the subquery used by this relationship.'), + '#default_value' => $this->options['subquery_namespace'], + ); + + + // WIP: This stuff doens't work yet: namespacing issues. + // A list of suitable views to pick one as the subview. + $views = array('' => ''); + $all_views = views_get_all_views(); + foreach ($all_views as $view) { + // Only get views that are suitable: + // - base must the base that our relationship joins towards + // - must have fields. + if ($view->base_table == $this->definition['base'] && !empty($view->display['default']->display_options['fields'])) { + // TODO: check the field is the correct sort? + // or let users hang themselves at this stage and check later? + if ($view->type == 'Default') { + $views[t('Default Views')][$view->name] = $view->name; + } + else { + $views[t('Existing Views')][$view->name] = $view->name; + } + } + } + + $form['subquery_view'] = array( + '#type' => 'select', + '#title' => t('Representative view'), + '#default_value' => $this->options['subquery_view'], + '#options' => $views, + '#description' => t('Advanced. Use another view to generate the relationship subquery. This allows you to use filtering and more than one sort. If you pick a view here, the sort options above are ignored. Your view must have the ID of its base as its only field, and should have some kind of sorting.'), + ); + + $form['subquery_regenerate'] = array( + '#type' => 'checkbox', + '#title' => t('Generate subquery each time view is run.'), + '#default_value' => $this->options['subquery_regenerate'], + '#description' => t('Will re-generate the subquery for this relationship every time the view is run, instead of only when these options are saved. Use for testing if you are making changes elsewhere. WARNING: seriously impairs performance.'), + ); + } + + /** + * Helper function to create a pseudo view. + * + * We use this to obtain our subquery SQL. + */ + function get_temporary_view() { + views_include('view'); + $view = new view(); + $view->vid = 'new'; // @todo: what's this? + $view->base_table = $this->definition['base']; + $view->add_display('default'); + return $view; + } + + /** + * When the form is submitted, take sure to clear the subquery string cache. + */ + function options_form_submit(&$form, &$form_state) { + $cid = 'views_relationship_groupwise_max:' . $this->view->name . ':' . $this->view->current_display . ':' . $this->options['id']; + cache_clear_all($cid, 'cache_views_data'); + } + + /** + * Generate a subquery given the user options, as set in the options. + * These are passed in rather than picked up from the object because we + * generate the subquery when the options are saved, rather than when the view + * is run. This saves considerable time. + * + * @param $options + * An array of options: + * - subquery_sort: the id of a views sort. + * - subquery_order: either ASC or DESC. + * @return + * The subquery SQL string, ready for use in the main query. + */ + function left_query($options) { + // Either load another view, or create one on the fly. + if ($options['subquery_view']) { + $temp_view = views_get_view($options['subquery_view']); + // Remove all fields from default display + unset($temp_view->display['default']->display_options['fields']); + } + else { + // Create a new view object on the fly, which we use to generate a query + // object and then get the SQL we need for the subquery. + $temp_view = $this->get_temporary_view(); + + // Add the sort from the options to the default display. + // This is broken, in that the sort order field also gets added as a + // select field. See http://drupal.org/node/844910. + // We work around this further down. + $sort = $options['subquery_sort']; + list($sort_table, $sort_field) = explode('.', $sort); + $sort_options = array('order' => $options['subquery_order']); + $temp_view->add_item('default', 'sort', $sort_table, $sort_field, $sort_options); + } + + // Get the namespace string. + $temp_view->namespace = (!empty($options['subquery_namespace'])) ? '_'. $options['subquery_namespace'] : '_INNER'; + $this->subquery_namespace = (!empty($options['subquery_namespace'])) ? '_'. $options['subquery_namespace'] : 'INNER'; + + // The value we add here does nothing, but doing this adds the right tables + // and puts in a WHERE clause with a placeholder we can grab later. + $temp_view->args[] = '**CORRELATED**'; + + // Add the base table ID field. + $views_data = views_fetch_data($this->definition['base']); + $base_field = $views_data['table']['base']['field']; + $temp_view->add_item('default', 'field', $this->definition['base'], $this->definition['field']); + + // Add the correct argument for our relationship's base + // ie the 'how to get back to base' argument. + // The relationship definition tells us which one to use. + $temp_view->add_item( + 'default', + 'argument', + $this->definition['argument table'], // eg 'term_node', + $this->definition['argument field'] // eg 'tid' + ); + + // Build the view. The creates the query object and produces the query + // string but does not run any queries. + $temp_view->build(); + + // Now take the SelectQuery object the View has built and massage it + // somewhat so we can get the SQL query from it. + $subquery = $temp_view->build_info['query']; + + // Workaround until http://drupal.org/node/844910 is fixed: + // Remove all fields from the SELECT except the base id. + $fields =& $subquery->getFields(); + foreach (array_keys($fields) as $field_name) { + // The base id for this subquery is stored in our definition. + if ($field_name != $this->definition['field']) { + unset($fields[$field_name]); + } + } + + // Make every alias in the subquery safe within the outer query by + // appending a namespace to it, '_inner' by default. + $tables =& $subquery->getTables(); + foreach (array_keys($tables) as $table_name) { + $tables[$table_name]['alias'] .= $this->subquery_namespace; + // Namespace the join on every table. + if (isset($tables[$table_name]['condition'])) { + $tables[$table_name]['condition'] = $this->condition_namespace($tables[$table_name]['condition']); + } + } + // Namespace fields. + foreach (array_keys($fields) as $field_name) { + $fields[$field_name]['table'] .= $this->subquery_namespace; + $fields[$field_name]['alias'] .= $this->subquery_namespace; + } + // Namespace conditions. + $where =& $subquery->conditions(); + $this->alter_subquery_condition($subquery, $where); + // Not sure why, but our sort order clause doesn't have a table. + // TODO: the call to add_item() above to add the sort handler is probably + // wrong -- needs attention from someone who understands it. + // In the meantime, this works, but with a leap of faith... + $orders =& $subquery->getOrderBy(); + foreach ($orders as $order_key => $order) { + // But if we're using a whole view, we don't know what we have! + if ($options['subquery_view']) { + list($sort_table, $sort_field) = explode('.', $order_key); + } + $orders[$sort_table . $this->subquery_namespace . '.' . $sort_field] = $order; + unset($orders[$order_key]); + } + + // The query we get doesn't include the LIMIT, so add it here. + $subquery->range(0, 1); + + // Extract the SQL the temporary view built. + $subquery_sql = $subquery->__toString(); + + // Replace the placeholder with the outer, correlated field. + // Eg, change the placeholder ':users_uid' into the outer field 'users.uid'. + // We have to work directly with the SQL, because putting a name of a field + // into a SelectQuery that it does not recognize (because it's outer) just + // makes it treat it as a string. + $outer_placeholder = ':' . str_replace('.', '_', $this->definition['outer field']); + $subquery_sql = str_replace($outer_placeholder, $this->definition['outer field'], $subquery_sql); + + return $subquery_sql; + } + + /** + * Recursive helper to add a namespace to conditions. + * + * Similar to _views_query_tag_alter_condition(). + * + * (Though why is the condition we get in a simple query 3 levels deep???) + */ + function alter_subquery_condition(QueryAlterableInterface $query, &$conditions) { + foreach ($conditions as $condition_id => &$condition) { + // Skip the #conjunction element. + if (is_numeric($condition_id)) { + if (is_string($condition['field'])) { + $condition['field'] = $this->condition_namespace($condition['field']); + } + elseif (is_object($condition['field'])) { + $sub_conditions =& $condition['field']->conditions(); + $this->alter_subquery_condition($query, $sub_conditions); + } + } + } + } + + /** + * Helper function to namespace query pieces. + * + * Turns 'foo.bar' into 'foo_NAMESPACE.bar'. + */ + function condition_namespace($string) { + return str_replace('.', $this->subquery_namespace . '.', $string); + } + + /** + * Called to implement a relationship in a query. + * This is mostly a copy of our parent's query() except for this bit with + * the join class. + */ + function query() { + // Figure out what base table this relationship brings to the party. + $table_data = views_fetch_data($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensure_my_table(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $base_field; + $def['left_table'] = $this->table_alias; + $def['left_field'] = $this->field; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + if ($this->options['subquery_regenerate']) { + // For testing only, regenerate the subquery each time. + $def['left_query'] = $this->left_query($this->options); + } + else { + // Get the stored subquery SQL string. + $cid = 'views_relationship_groupwise_max:' . $this->view->name . ':' . $this->view->current_display . ':' . $this->options['id']; + $cache = cache_get($cid, 'cache_views_data'); + if (isset($cache->data)) { + $def['left_query'] = $cache->data; + } + else { + $def['left_query'] = $this->left_query($this->options); + cache_set($cid, $def['left_query'], 'cache_views_data'); + } + } + + if (!empty($def['join_handler']) && class_exists($def['join_handler'])) { + $join = new $def['join_handler']; + } + else { + $join = new views_join_subquery(); + } + + $join->definition = $def; + $join->construct(); + $join->adjusted = TRUE; + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship); + } +} diff --git a/handlers/views_handler_sort.inc b/handlers/views_handler_sort.inc new file mode 100644 index 00000000..99574e4d --- /dev/null +++ b/handlers/views_handler_sort.inc @@ -0,0 +1,240 @@ +ensure_my_table(); + // Add the field. + $this->query->add_orderby($this->table_alias, $this->real_field, $this->options['order']); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['order'] = array('default' => 'ASC'); + $options['exposed'] = array('default' => FALSE, 'bool' => TRUE); + $options['expose'] = array( + 'contains' => array( + 'label' => array('default' => '', 'translatable' => TRUE), + ), + ); + return $options; + } + + /** + * Display whether or not the sort order is ascending or descending + */ + function admin_summary() { + if (!empty($this->options['exposed'])) { + return t('Exposed'); + } + switch ($this->options['order']) { + case 'ASC': + case 'asc': + default: + return t('asc'); + break; + case 'DESC'; + case 'desc'; + return t('desc'); + break; + } + } + + /** + * Basic options for all sort criteria + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + if ($this->can_expose()) { + $this->show_expose_button($form, $form_state); + } + $form['op_val_start'] = array('#value' => '
'); + $this->show_sort_form($form, $form_state); + $form['op_val_end'] = array('#value' => '
'); + if ($this->can_expose()) { + $this->show_expose_form($form, $form_state); + } + } + + /** + * Shortcut to display the expose/hide button. + */ + function show_expose_button(&$form, &$form_state) { + $form['expose_button'] = array( + '#prefix' => '
', + '#suffix' => '
', + // Should always come first + '#weight' => -1000, + ); + + // Add a checkbox for JS users, which will have behavior attached to it + // so it can replace the button. + $form['expose_button']['checkbox'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('js-only')), + ); + $form['expose_button']['checkbox']['checkbox'] = array( + '#title' => t('Expose this sort to visitors, to allow them to change it'), + '#type' => 'checkbox', + ); + + // Then add the button itself. + if (empty($this->options['exposed'])) { + $form['expose_button']['markup'] = array( + '#markup' => '
' . t('This sort is not exposed. Expose it to allow the users to change it.') . '
', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Expose sort'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 0; + } + else { + $form['expose_button']['markup'] = array( + '#markup' => '
' . t('This sort is exposed. If you hide it, users will not be able to change it.') . '
', + ); + $form['expose_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Hide sort'), + '#submit' => array('views_ui_config_item_form_expose'), + ); + $form['expose_button']['checkbox']['checkbox']['#default_value'] = 1; + } + } + + /** + * Simple validate handler + */ + function options_validate(&$form, &$form_state) { + $this->sort_validate($form, $form_state); + if (!empty($this->options['exposed'])) { + $this->expose_validate($form, $form_state); + } + + } + + /** + * Simple submit handler + */ + function options_submit(&$form, &$form_state) { + unset($form_state['values']['expose_button']); // don't store this. + $this->sort_submit($form, $form_state); + if (!empty($this->options['exposed'])) { + $this->expose_submit($form, $form_state); + } + } + + /** + * Shortcut to display the value form. + */ + function show_sort_form(&$form, &$form_state) { + $options = $this->sort_options(); + if (!empty($options)) { + $form['order'] = array( + '#type' => 'radios', + '#options' => $options, + '#default_value' => $this->options['order'], + ); + } + } + + function sort_validate(&$form, &$form_state) { } + + function sort_submit(&$form, &$form_state) { } + + /** + * Provide a list of options for the default sort form. + * Should be overridden by classes that don't override sort_form + */ + function sort_options() { + return array( + 'ASC' => t('Sort ascending'), + 'DESC' => t('Sort descending'), + ); + } + + function expose_form(&$form, &$form_state) { + // #flatten will move everything from $form['expose'][$key] to $form[$key] + // prior to rendering. That's why the pre_render for it needs to run first, + // so that when the next pre_render (the one for fieldsets) runs, it gets + // the flattened data. + array_unshift($form['#pre_render'], 'views_ui_pre_render_flatten_data'); + $form['expose']['#flatten'] = TRUE; + + $form['expose']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['expose']['label'], + '#title' => t('Label'), + '#required' => TRUE, + '#size' => 40, + '#weight' => -1, + ); + } + + /** + * Provide default options for exposed sorts. + */ + function expose_options() { + $this->options['expose'] = array( + 'order' => $this->options['order'], + 'label' => $this->definition['title'], + ); + } +} + +/** + * A special handler to take the place of missing or broken handlers. + * + * @ingroup views_sort_handlers + */ +class views_handler_sort_broken extends views_handler_sort { + function ui_name($short = FALSE) { + return t('Broken/missing handler'); + } + + function ensure_my_table() { /* No table to ensure! */ } + function query($group_by = FALSE) { /* No query to run */ } + function options_form(&$form, &$form_state) { + $form['markup'] = array( + '#markup' => '
' . t('The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.') . '
', + ); + } + + /** + * Determine if the handler is considered 'broken' + */ + function broken() { return TRUE; } +} + + +/** + * @} + */ diff --git a/handlers/views_handler_sort_date.inc b/handlers/views_handler_sort_date.inc new file mode 100644 index 00000000..b37cc41c --- /dev/null +++ b/handlers/views_handler_sort_date.inc @@ -0,0 +1,74 @@ + 'second'); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['granularity'] = array( + '#type' => 'radios', + '#title' => t('Granularity'), + '#options' => array( + 'second' => t('Second'), + 'minute' => t('Minute'), + 'hour' => t('Hour'), + 'day' => t('Day'), + 'month' => t('Month'), + 'year' => t('Year'), + ), + '#description' => t('The granularity is the smallest unit to use when determining whether two dates are the same; for example, if the granularity is "Year" then all dates in 1999, regardless of when they fall in 1999, will be considered the same date.'), + '#default_value' => $this->options['granularity'], + ); + } + + /** + * Called to add the sort to a query. + */ + function query() { + $this->ensure_my_table(); + switch ($this->options['granularity']) { + case 'second': + default: + $this->query->add_orderby($this->table_alias, $this->real_field, $this->options['order']); + return; + case 'minute': + $formula = views_date_sql_format('YmdHi', "$this->table_alias.$this->real_field"); + break; + case 'hour': + $formula = views_date_sql_format('YmdH', "$this->table_alias.$this->real_field"); + break; + case 'day': + $formula = views_date_sql_format('Ymd', "$this->table_alias.$this->real_field"); + break; + case 'month': + $formula = views_date_sql_format('Ym', "$this->table_alias.$this->real_field"); + break; + case 'year': + $formula = views_date_sql_format('Y', "$this->table_alias.$this->real_field"); + break; + } + + // Add the field. + $this->query->add_orderby(NULL, $formula, $this->options['order'], $this->table_alias . '_' . $this->field . '_' . $this->options['granularity']); + } +} diff --git a/handlers/views_handler_sort_group_by_numeric.inc b/handlers/views_handler_sort_group_by_numeric.inc new file mode 100644 index 00000000..4a521c15 --- /dev/null +++ b/handlers/views_handler_sort_group_by_numeric.inc @@ -0,0 +1,38 @@ +handler = views_get_handler($options['table'], $options['field'], 'sort'); + $this->handler->init($view, $options); + } + + /** + * Called to add the field to a query. + */ + function query() { + $this->ensure_my_table(); + + $params = array( + 'function' => $this->options['group_type'], + ); + + $this->query->add_orderby($this->table_alias, $this->real_field, $this->options['order'], NULL, $params); + } + + function ui_name($short = FALSE) { + return $this->get_field(parent::ui_name($short)); + } +} diff --git a/handlers/views_handler_sort_menu_hierarchy.inc b/handlers/views_handler_sort_menu_hierarchy.inc new file mode 100644 index 00000000..dacb1fee --- /dev/null +++ b/handlers/views_handler_sort_menu_hierarchy.inc @@ -0,0 +1,54 @@ + FALSE); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['sort_within_level'] = array( + '#type' => 'checkbox', + '#title' => t('Sort within each hierarchy level'), + '#description' => t('Enable this to sort the items within each level of the hierarchy by weight and title. Warning: this may produce a slow query.'), + '#default_value' => $this->options['sort_within_level'], + ); + } + + function query() { + $this->ensure_my_table(); + $max_depth = isset($this->definition['max depth']) ? $this->definition['max depth'] : MENU_MAX_DEPTH; + for ($i = 1; $i <= $max_depth; ++$i) { + if ($this->options['sort_within_level']) { + $join = new views_join(); + $join->construct('menu_links', $this->table_alias, $this->field . $i, 'mlid'); + $menu_links = $this->query->add_table('menu_links', NULL, $join); + $this->query->add_orderby($menu_links, 'weight', $this->options['order']); + $this->query->add_orderby($menu_links, 'link_title', $this->options['order']); + } + + // We need this even when also sorting by weight and title, to make sure + // that children of two parents with the same weight and title are + // correctly separated. + $this->query->add_orderby($this->table_alias, $this->field . $i, $this->options['order']); + } + } +} diff --git a/handlers/views_handler_sort_random.inc b/handlers/views_handler_sort_random.inc new file mode 100644 index 00000000..eaaaf790 --- /dev/null +++ b/handlers/views_handler_sort_random.inc @@ -0,0 +1,22 @@ +query->add_orderby('rand'); + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['order']['#access'] = FALSE; + } +} diff --git a/help/about.html b/help/about.html new file mode 100644 index 00000000..8ba77f86 --- /dev/null +++ b/help/about.html @@ -0,0 +1,62 @@ +The views module allows administrators and site designers to create, manage, and display lists of content. Each list managed by the views module is known as a "view", and the output of a view is known as a "display". Displays are provided in either block or page form, and a single view may have multiple displays. Optional navigation aids, including a system path and menu item, can be set for each page-based display of a view. By default, views may be created that list content (a Node view type), content revisions (a Node revisions view type) or users (a User view type). A view may be restricted to members of specific user roles, and may be added, edited or deleted at the views administration page. + +For more technical users, views can be understood as a user interface to compose SQL-queries and to pull information (Content, Users, etc.) from the database and show it on a screen as desired. + +The "building block" design of the views system provides power and flexibility, allowing parameters to be specified only when needed. While an advanced view may use all of available parameters to create complex and highly interactive applications, a simple content listing may specify only a few options. All views rely on a conceptual framework that includes: + +
    +
  • Fields, or the individual pieces of data being displayed. Adding the fields Node: Title, Node: Type, and Node: Post date to a node view, for example, includes the title, content type and creation date in the displayed results
  • + +
  • Relationships, or information about how data elements relate to one another. If relationship data is available, like that provided by a CCK nodereference field, items from a related node may be included in the view
  • + +
  • Arguments, or additional parameters that dynamically refine the view results, passed as part of the path. Adding an argument of Node: Type to a node view with a path of "content", for example, dynamically filters the displayed items by content type. In this example (shown with Clean URLs enabled), accessing the view through the path "http://www.example.com/content/page" displays all posts of the type "page", the path "http://www.example.com/content/story" displays all posts of the type "story", and "http://www.example.com/content" displays all posts regardless of type)
  • + +
  • Sort criteria, which determine the order of items displayed in the view results. Adding the sort criteria Node: Post date (in descending order) to a node view, for example, sorts the displayed posts in descending order by creation date
  • + +
  • Filters, which limit items displayed in the results. Adding the filter Node: Published (and setting it equal to "Published") to a node view, for example, prevents unpublished items from being displayed
  • + +
  • Displays, which control where the output will be seen. Every view has a default display, which doesn't actually display the view anywhere, but is used to hold the default settings for the view, and is used when the view is called programmatically if another display is not specified. Much more useful to users are the page display, which gives a view a path and allows it to be the primary content of a page, or the block display which allows it to appear as secondary content on other pages.
  • + +
  • Header, which allow you to add by default one or more text area above the views output.
  • + +
  • Footer, which allow you to add by default one or more text area beneath the views output.
  • + +
  • The Emtpy Text content will be displayed, when you choose in the Arguments Section "Action to take if argument is not present" the option "Display empty text".
  • + + +
+ +Parallels between the components in the Views interface and an sql query: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sql QueryViews Component
SELECT n.title, u.namefields
FROM {node} n base tableview type
INNER JOIN {users} u ON n.uid = u.uidrelationship
WHERE n.status = 1filter
AND u.uid = arg(1)argument
ORDER BY n.changed DESCsort
diff --git a/help/advanced-settings.html b/help/advanced-settings.html new file mode 100644 index 00000000..6a168590 --- /dev/null +++ b/help/advanced-settings.html @@ -0,0 +1,43 @@ +In the category Other you have different options to set Advanced configurations in your View. + +Machine Name: +You can change the default machine name of the view. + +Comment: No comment +You can Use the comment option to write comments for your Views, the comments are only shown in the Views UI. Comment your Display for other Maintainer + +Use AJAX: No +If set, this view will use an AJAX mechanism for paging, table sorting and exposed filters. This prevents the entire page from refreshing. It is not recommended that you use this if this view is the main content of the page as it will prevent deep linking to specific pages, but it is very useful for side content. Block displays require this setting to be ON if exposed filters are being used. + +Hide attachments in summary: No + +Use aggregation: No +All fields that are selected for grouping will be collapsed to one record per distinct value. Other fields which are selected for aggregation will have the function run on them. For example, you can group nodes on title and count the number of nids in order to get a list of duplicate titles. +For more Information how aggregation work see the "Use Aggregation" Help Page + +Query settings: Settings + +Here can you set advanced Settings for the SQL Settings +
    +
  • Disable SQL rewriting
  • + +
  • Distinct: No
  • + This will make the view display only distinct items. If there are multiple identical items, each will be displayed only once. You can use this to try and remove duplicates from a view, though it does not always work. Note that this can slow queries down, so use it with caution. + +
  • Use Slave Server
  • + +
  • Query Comment
  • +
+Field Language: Current user's language + +Caching: None +You can choose a "Time-based" Caching if you want. With it you get the option to choose the length of time raw query results should be cached and "The length of time rendered HTML output should be cached." + +Link display: Page + +CSS class: None +You can define some own CSS Classes for your View + +Theme: Information +Clicking on the "Theme: Information" link provides you with a listing of all posiible theming files. The highlighted files are the ones Views is currently using. All other filenames are suggested templates. +For more Information see the "Theme information" Page diff --git a/help/advanced-style-settings.html b/help/advanced-style-settings.html new file mode 100644 index 00000000..9c90c0f7 --- /dev/null +++ b/help/advanced-style-settings.html @@ -0,0 +1,30 @@ +In Views 3 you can set Advanced Style Settings, they help you to insert markup of your own from the Views UI, so that you can fairly easily override the default markup without having to restyle via templates. + + +
    +
  • Customize field HTML
  • +With Customize field HTML you can generate html tags around the field. + +
  • Customize label HTML
  • +Here you can generate html tags around the label of a field. + +
  • Customize field and label wrapper HTML
  • +Here you can generate html tags around the wrapper of the label and field +
+ + + +Usage example + +In a view with a field: +
    +
  1. Configure the field. (Click on the field.)
  2. + +
  3. In the modal that opens, scroll down to Style Settings.
  4. + +
  5. Choose one or more of the three Customize options. This will reveal a dropdown menu where you can choose from one or more HTML tags to use on that field and allow you to add a CSS class specific to that field should you desire.
  6. + +
  7. Decide if you want to keep the Views default classes. Unchecking this box means your markup is *it*.
  8. +
+ +In Views 2 you needed for Style Settings the Semantic Views Modul, now the Semantic Views module has been mostly incorporated into Views 3.x. Semantic Views is still around for people who need it, though. For some details on how the original module is different from the Views implementation, please see this issue. diff --git a/help/aggregation.html b/help/aggregation.html new file mode 100644 index 00000000..83ad880d --- /dev/null +++ b/help/aggregation.html @@ -0,0 +1 @@ +See: Group by diff --git a/help/alter-exposed-filter.html b/help/alter-exposed-filter.html new file mode 100644 index 00000000..454994f7 --- /dev/null +++ b/help/alter-exposed-filter.html @@ -0,0 +1,31 @@ +Modifying default values of a views exposed form is tricky, because FAPI was not designed to work with GET forms. One consequence is that it often can't tell if textfields (there are others) were submitted or not. + +As a consequence, it *always* thinks the value was submitted, even if it was not. To fix that, Views modifies $form_state['input'][$identifier] with the default value if $form_state['input'][$identifier] was not set. In order to modify the default value in an alter, you need to do this: + +
+<?php
+if (empty($form_state['view']->exposed_input[$identifier])) .
+		{ $form_state['input'][$identifier] = $default_value; }
+?>
+
+ +where $identifier is the particular filter for which you want to change the default value, and $default_value is the new default value you want to set. + +If you use a hook_form_FORM_ID_alter or hook_form_alter, you can modify exposed filters on the fly based on information that is external to Views. For example, I modified the exposed filter of a form to set a taxonomy term ID based on the user's GeoIP. + +To do this, I used the following function, where geoip_redirect_get_tid() loads the relevant term id based on the user's current ip_address(): + +
+<?php
+function MODULENAME_form_views_exposed_form_alter(&$form, $form_state) {
+  if(strpos($form['#id'], 'volunteer-directory') !== FALSE) {
+    $city_tid = geoip_redirect_get_tid();
+    if(is_numeric($city_tid) && $city_tid != 7660) {
+      if (empty($form_state['view']->exposed_input['tid']))  {
+        $form_state['input']['tid'] = $city_tid;
+      }
+    }
+  }
+}
+?>
+
diff --git a/help/analyze-theme.html b/help/analyze-theme.html new file mode 100644 index 00000000..290dc074 --- /dev/null +++ b/help/analyze-theme.html @@ -0,0 +1,24 @@ +

Clicking on the "Theme: Information" link provides you with a listing of all posiible theming files. The highlighted files are the ones Views is currently using. All other filenames are suggested templates.

+

You may use any of the following possible theme files to modify individual parts of your view. In total, there are four parts to theming a view.

+
    +
  • The display theme is usually views-view.tpl.php and it largely controls the decorations around a view; where the header, footer, pager, more link, feed icon, etc, will be placed.
  • + +
  • The style will control how all of the results of the display are put together. It may be as simple as just displaying all of the rows, or it may be a complex table generator or something in between.
  • + +
  • The row style controls each individual row; not all styles utilize the row style (notably the table), but most others do. + +
  • Finally, field themes allow you to override the look and even the data of each individual field, if the style uses fields. The actual template the system will use should be hilighted in bold.
  • +
+ +
+ +A breakdown of View output +
+ +

The link to the left of each type will give you information about the default template used for that type. You may cut and paste this and place it in your theme with the appropriate template, or you may copy the base file from the views/theme directory (or, if provided by a module, from the module's directory). It is important that you clear the theme registry cache every time you add a new template, or the new template will not be picked up.

+ +

Important note: You place your custom template files in your theme directory, not views/theme. This is always true of theming with Drupal. + +

In addition to this tool, the very useful devel module contains a tool called the "Theme developer" which does a good job of visually showing you which areas of your site use which themes. Be careful with it, though, as the theme developer causes the Views edit page to break.

+ +

Also, this feature will only work properly with Drupal 6.3 and later; prior to Drupal 6.3 this patch will be required.

diff --git a/help/api-default-views.html b/help/api-default-views.html new file mode 100644 index 00000000..f676ed86 --- /dev/null +++ b/help/api-default-views.html @@ -0,0 +1,103 @@ +Views can be stored in the database, which is typical of smaller sites and hobby sites. However, Views may also be stored directly in the code as "default" views, (which simply means they're available by default). Modules often come with views that are specific to the module data, but it's also possible -- and highly recommended -- that sites which have separate "development" and "production" sites export their views into default views in a site-specific module. This makes it very easy to transfer views from dev to production without making database changes. + +

Creating a module

+First, create a directory in sites/all/modules for your new module. Call it whatever you like, but for this example we will call it mymodule. + +In this directory, create a mymodule.module file. It can be empty for now, but it should at least contain an opening PHP tag: +
<?php
+
+ +It should not contain a closing ?> tag, as the closing ?> tag is not required and anything AFTER the closing tag, such as a space or a linefeed, will be displayed directly to the browser and can potentially cause problems. + +The .module file will contain functions and drupal hooks. Hooks are specially named functions that Drupal will call in order to get your module's response at certain times while generating pages. The only function you will need for this exercise is the 'views_api' hook that tells Views that this module supports the Views API and what version: + +
function mymodule_views_api() {
+  return array('api' => 2.0);
+}
+
+ +For other uses you may well add additional functions. + +Second, you need to create a mymodule.info file: + +
name = My module
+description = My site specific module.
+core = 6.x
+
+ +Once you have these two files set up, you should be able to activate your new module at the Administer >> Modules page. +

Exporting your views

+ +The easiest way to do this is to activate the 'views_export' module, and navigate to Administer >> Structure >> Views >> Tools >> Bulk export Place a check next to each view that you want in your module, type the module name into the text field, and click export. This will create the entire hook_views_default_views() function for you. + +You can also export individual views. If you do this, keep in mind that this export does not include the line that adds the exported $view into the larger $views array: + +
$views[$view->name] = $view
+ +To place this into your hook_views_default_views() you will need to place that after the view, and make sure the function returns $views at the end. + +

Placing your exported views into your module

+Cut and paste the entire output of the bulk export tool into mymodule.views_default.inc -- and be sure to put a <?php at the top of the file so that the webserver knows that it's PHP code! Then visit the Views tools page and clear the Views cache. Your views should now be listed as Overridden on the view list page. If you revert these views, they will be removed from the database, but will remain in code. + +

Theming your views in your module

+You can theme these views in the module and not need to rely on the theme to do this at all; and in fact, the theme can continue to override these just like it ordinarily would, even if your module provides a theme. This is very useful for distributing a module where the view needs to look "just so." + +To do this, you need to implement hook_theme() in your module: +
function mymodule_theme($existing) {
+  return array(
+    'views_view__viewname__displayid' => array (
+      'arguments' => array('view' => NULL),
+      'template' => 'views-view--viewname--displayid',
+      'base hook' => 'views_view',
+      'path' => drupal_get_path('module', 'mymodule'),
+    ),
+  );
+}
+
+ +There are a small number of gotchas in doing this that you must be aware of. + +
    +
  1. When referring to a template filename, you always use dashes in the name. i.e, views-view--viewname--displayid.tpl.php. However, when referring to the hook or function names, you use underscores instead of dashes. i.e, views_view and views_view__viewname__displayid
  2. + +
  3. The 'arguments' change based upon which of the 3 types you're overriding. There's the 'display', the 'style' and the 'row' style. The above code is assuming the display, which is usually just views_view. Here are the possibilities: + +
    display: array('view_array' => array(), 'view' => NULL),
    +style: array('view' => NULL, 'options' => NULL, 'rows' => NULL, 'title' => NULL),
    +row: array('view' => NULL, 'options' => NULL, 'row' => NULL, 'field_alias' => NULL),
    +field: array('view' => NULL, 'field' => NULL, 'row' => NULL),
    +
    + +Be sure to use the right arguments line or the theme system will not properly translate. +
  4. +
  5. The 'template' line should never include the extension, so drop the .tpl.php from it.
  6. + +
  7. You need to make sure that the Views preprocess functions get registered. The 'base hook' line in the definition does that, but it can only do it if it comes after the Views registration, which actually happens very late in theme building. 99% of the time, your module will come before Views. You have two choices to deal with this: +
      +
    1. Set your module's weight to 11 or higher in the database. Views' weight is 10. You can make this happen automatically when the module is first installed by creating a mymodule.install file and using this code: +
      function mymodule_install() {
      +  db_query("UPDATE {system} SET weight = 11 WHERE name = 'mymodule'");
      +}
      +
      + If you use this method, the base hook should be set to the name of the original template being used. i.e, if this is a variate of views-view-list.tpl.php, this should be 'views_view_list'. +
    2. +
    3. You can also just force it to list the preprocessors without actually having to detect them. This doesn't require modifying your module's weight, which is not always possible, you can insert this code into the array: +
            'preprocess functions' => array(
      +        'template_preprocess',
      +        'template_preprocess_views_view',
      +        'mymodule_preprocess_views_view__viewname_displayid',
      +      ),
      +
      + + The first one is the global 'template_preprocess' function which all templates utilize. It does some basic things such as setting up $zebra and a few other items. See api.drupal.org for specifics. + + The second one is the plugin specific preprocess. Like 'base hook' it should conform to the name used by the original template. i.e, if the original template was views-view-list.tpl.php then that preprocess function would be named template_preprocess_views_view_list. + + The third one is your module's preprocess function, if it needs one. In general, you probably will not need one, and you should only attempt to use one if you are reasonably familiar with the concept of preprocess functions and Drupal's theme system in general. See Drupal's theme documentation for more information. +
    4. +
    +
  8. +
  9. + If you leave the path blank the template file will be searched for in "./" which is the Drupal install base path. +
  10. +
diff --git a/help/api-example.html b/help/api-example.html new file mode 100644 index 00000000..682fe87f --- /dev/null +++ b/help/api-example.html @@ -0,0 +1,179 @@ + +For the new table defined by the Node example module to be understood by the views module you need to create a node_example.views.inc file that describes the table and its relationships to the rest of the database. In order for views to know that this file is to be loaded you need to implement hook_views_api. This is done by adding the following function into your node_example.module file + +
+<?php
+/**
+ * Implements hook_views_api().
+ *
+ * This tells drupal that there is Views integration file named
+ * module-name.views.inc
+ */
+function node_example_views_api() {
+  // Note that you can include 'path' in this array so that your views.inc
+  // file can be stored in a different location.
+  return array(
+    'api' => 2.0
+  );
+}
+?>
+
+ +Below is the contents of a simple node_example.views.inc file that allows you to create views that include the new color and quantity information. + +
+<?php
+
+/**
+ * This file is used to tell the views module about the new node_example table.
+ *
+ * Database definition:
+ * @code
+ *   CREATE TABLE node_example (
+ *     vid int(10) unsigned NOT NULL default '0',
+ *     nid int(10) unsigned NOT NULL default '0',
+ *     color varchar(255) NOT NULL default '',
+ *     quantity int(10) unsigned NOT NULL default '0',
+ *     PRIMARY KEY (vid, nid),
+ *     KEY `node_example_nid` (nid)
+ *   )
+ * @endcode
+ */
+
+function node_example_views_data()  {
+  // Basic table information.
+
+  // ----------------------------------------------------------------
+  // node_example table
+  //  New group within Views called 'Example'
+  //  The group will appear in the UI in the dropdown tha allows you
+  //  to narrow down which fields and filters are available.
+
+  $data = array();
+  $data['node_example']['table']['group']  = t('Example');
+
+  // Let Views know that our example table joins to the 'node'
+  // base table. This means it will be available when listing
+  // nodes and automatically make its fields appear.
+  //
+  // We also show up for node revisions.
+  $data['node_example']['table']['join'] = array(
+    'node_revisions' => array(
+      'left_field' => 'vid',
+      'field' => 'vid',
+    ),
+    'node' => array(
+      'left_field' => 'vid',
+      'field' => 'vid',
+    ),
+  );
+
+  // quantity
+  $data['node_example']['quantity'] = array(
+    'title' => t('Quantity'),
+    'help' => t('Quantity of items.'),
+    'field' => array(
+      'handler' => 'views_handler_field_numeric',
+      'click sortable' => TRUE,
+     ),
+    'filter' => array(
+      'handler' => 'views_handler_filter_numeric',
+    ),
+    'sort' => array(
+      'handler' => 'views_handler_sort',
+    ),
+  );
+
+  // Color
+  $data['node_example']['color'] = array(
+    'title' => t('Color'),
+    'help' => t('Color of item.'),
+
+    'field' => array(
+      'handler' => 'views_handler_field',
+      'click sortable' => TRUE,
+     ),
+     'filter' => array(
+      'handler' => 'views_handler_filter_string',
+     ),
+     'argument' => array(
+       'handler' => 'views_handler_argument_string',
+     ),
+     'sort' => array(
+      'handler' => 'views_handler_sort',
+     ),
+  );
+
+  return $data;
+}
+
+?>
+
+ +Some notes on usage: + +Within Views, click on the Add tab. You have a number of type options here. Normally you would select either 'Node' (if you only want to display information on current nodes) or 'Node revision' (if you want to display information on all revisions of the nodes) + +With this configuration you always pull out of the database, data for every single node, whether or not it has color and quantity information. To display information on just those nodes that have color and quantity information you can use a filter so that only nodes which don't have a NULL color or a NULL quantity are displayed. + +

Type/relationship extension

+ +When your tables have first class data, you will often need to have own View types and View relationships defined. With the current node_example table this isn't required although I try to justify it below on an efficiency basis. See [[http://groups.drupal.org/node/17236#comment-58980|this discussion]] as to why it isn't justified. + +Pulling data out of the database for every node when you only want data for the new Example node type is inefficient. To reduce the initial data extraction to just that relating to the new Example nodes requires that you make the node_example table the base table. This can be done by adding the following code into the node_example.views.inc file just before the 'return $data;' + +
+<?php
+
+//  **** Begin optional extra for type and relationships ****
+
+  //  Use node_example as a new base table
+  //     by creating a new views type called 'Node example'
+  //  This allows it to be selected as the 'view type'
+  //          when you initially add a new view.
+  $data['node_example']['table']['base'] = array(
+    'field' => 'vid',
+    'title' => t('Node example'),
+    'help' => t("Node example type with color and quantity information."),
+    'weight' => -9,
+  );
+
+  // When using the new 'Node example' type you need to use relationships
+  //   to access fields in other tables.
+
+  // Relationship to the 'Node revision' table
+  $data['node_example']['vid'] = array(
+    'title' => t('Node revision'),
+    'help' => t('The particular node revision the color and quantity is attached to'),
+    'relationship' => array(
+      'label' => t('Node revision'),
+      'base' => 'node_revisions',
+      'base field' => 'vid',
+      // This allows us to not show this relationship if the base is already
+      // node_revisions so users won't create circular relationships.
+      'skip base' => array('node', 'node_revisions'),
+    ),
+  );
+
+  // Relationship to the 'Node' table
+  $data['node_example']['nid'] = array(
+    'title' => t('Node'),
+    'help' => t('The particular node the color and quantity is attached to'),
+    'relationship' => array(
+      'label' => t('Node'),
+      'base' => 'node',
+      'base field' => 'nid',
+      // This allows us to not show this relationship if the base is already
+      // node so users won't create circular relationships.
+      'skip base' => array('node', 'node_revisions'),
+    ),
+  );
+
+//  **** End optional extra for type and relationships ****
+
+?>
+
+ +The above code adds a new 'Node example' to the view types that can be selected within the Add tab window of views. Selecting this sets the node_example table to be the base table. + +If you select 'Node example' as view type, when you initially go into the edit window of views you will find the only fields available are the color and quantity fields. To get fields from other tables you need to add a relationship. Relationships may be found at the top in the same column as the fields. diff --git a/help/api-forms.html b/help/api-forms.html new file mode 100644 index 00000000..27729091 --- /dev/null +++ b/help/api-forms.html @@ -0,0 +1,88 @@ +Views allows handlers to output form elements, wrapping them automatically in a form, and handling validation / submission. +The form is multistep by default, allowing other modules to add additional steps, such as confirmation screens. + +

Implementation

+A views handler outputs a special placeholder in render(), while the real form with matching structure gets added in views_form(). +When the View is being preprocessed for the theme file, all placeholders get replaced with the rendered form elements. + +The views handler can also implement views_form_validate() and views_form_submit(). +
+  function render($values) {
+    return '<!--form-item-' . $this->options['id'] . '--' . $this->view->row_index . '-->';
+  }
+
+  function form_element_name() {
+    // Make sure this value is unique for all the view fields
+    return $this->options['id'];
+  }
+
+  function form_element_row_id($row_id) {
+    // You could use values from $this->view->result[$row_id]
+    // to provide complex row ids.
+    return $row_id;
+  }
+
+  function views_form(&$form, &$form_state) {
+    // The view is empty, abort.
+    if (empty($this->view->result)) {
+      return;
+    }
+
+    $field_name = $this->form_element_name();
+    $form[$field_name] = array(
+      '#tree' => TRUE,
+    );
+    // At this point, the query has already been run, so we can access the results
+    foreach ($this->view->result as $row_id => $row) {
+      $form_element_row_id = $this->form_element_row_id($row_id);
+      $form[$field_name][$form_element_row_id] = array(
+        '#type' => 'textfield',
+        '#title' => t('Your name'),
+        '#default_value' => '',
+      );
+    }
+  }
+
+  // Optional validate function.
+  function views_form_validate($form, &$form_state) {
+    $field_name = $this->form_element_name();
+    foreach ($form_state['values'][$field_name] as $row_id => $value) {
+      if ($value == 'Drupal') {
+        form_set_error($field_name . '][' . $row_id, "You can't be named Drupal. That's my name.");
+      }
+    }
+  }
+
+  // Optional submit function.
+  function views_form_submit($form, &$form_state) {
+    // Do something here
+  }
+
+ +The form is multistep by default, with one step: 'views_form_views_form'. +A "form_example" module could add a confirmation step by setting: +
+ $form_state['step'] = 'form_example_confirmation';
+
+in form_example_views_form_submit(). +Then, views_form would call form_example_confirmation($form, $form_state, $view, $output) to get that step. + +Important: You can fetch the Views object in form_alter and validate / submit hooks from the form state: +
+  $view = $form_state['build_info']['args'][0];
+
+ +

Relevant Views functions

+
    +
  • template_preprocess_views_view()
  • +
  • views_form()
  • +
  • views_form_views_form()
  • +
  • views_form_views_form_validate()
  • +
  • views_form_views_form_submit()
  • +
  • theme_views_form_views_form()
  • +
+ +

Hooks

+
    +
  • hook_views_form_substitutions()
  • +
diff --git a/help/api-handler-area.html b/help/api-handler-area.html new file mode 100644 index 00000000..ad8ec563 --- /dev/null +++ b/help/api-handler-area.html @@ -0,0 +1,45 @@ +In Views areas (header, footer, empty-text) are pluggable, this means you can write your own php logic to place whatever you want. + +

Requirements

+You should have read API and Tables API to get a basic knowledge +how to extend views. + +

Create your own area handler

+ +The first step is to tell views: Hey i want to add a new area handler. +Therefore you have to implement hook_views_data and add a new one. For example: + +
+function yourmodule_views_data() {
+  $data['views']['collapsible_area'] = array(
+    'title' => t('Collabsible Text area'),
+    'help' => t('Provide collabsible markup text for the area.'),
+    'area' => array(
+      'handler' => 'yourmodule_handler_collapsible_area_text',
+    ),
+  );
+}
+
+ +The second step is to write this handler. Therefore create a file called yourmodule_handler_collapsible_area_text.inc and +add it to the .info file of your module. + +Then add content to your area file like this: +
+class yourmodule_handler_collapsible_area_text extends views_handler_area_text {
+  function render($empty = FALSE) {
+    // Here you just return a string of your content you want.
+    if ($render = parent::render($empty)) {
+      $element = array(
+        '#type' => 'fieldset',
+        '#title' => t('Title'),
+        '#value' => $render,
+      );
+      $output = theme('fieldset', $element);
+      return $output;
+    }
+  }
+}
+
+ +As on every handler you can add options so you can configure the behavior. If the area isn't shown yet in the views interface, please clear the cache :) diff --git a/help/api-tables.html b/help/api-tables.html new file mode 100644 index 00000000..cafbbab4 --- /dev/null +++ b/help/api-tables.html @@ -0,0 +1,262 @@ +Tables are described to Views by implementing hook_views_data(). This should be placed in MODULENAME.views.inc and it will be autoloaded (see Views' API). The hook implementation should return an array of table information, keyed by the name of the table. For example, if your module is describing three tables, 'foo', 'bar' and 'baz', your hook will look like this: +
  /**
+   * Implements hook_views_data().
+   */
+  function MODULENAME_views_data() {
+    $data = array(
+      'foo' => array(
+        // ...info about the table named 'foo'...
+      ),
+      'bar' => array(
+        // ...info about the table named 'bar'...
+      ),
+      'baz' => array(
+        // ...info about the table named 'baz'...
+      ),
+    );
+    return $data;
+  }
+
+ +The key should be the actual database name of the table (not including prefix), but it can be an alias as long as the join information (explained later) contains the real name of the table. + +Each item in the array should be a field in the table, with the exception of a special information section called 'table'. Example: + +
$data['foo'] = array(
+  'table' => array(
+    // ... info about the table, described later ...
+  ),
+  'bar' => array(
+    // ... info about the field named 'bar', i.e, foo.bar,
+  ),
+  'baz' => array(
+    // ... info about the field named 'baz', i.e, foo.baz,
+  ),
+);
+
+ +Once you get down to an array that contains actual data, that piece of the array will often be referred to as the definition. + +

The 'table' section

+Each table should have a 'table' section in it, which is used to set default information for the table, such as the group, as well as the very important joins and whether or not this is a base table. + +First, there are several items that are actually for fields but can be placed here so that all fields within the table inherit them: +
+
group
+
The name of the group this item will be with. In the UI, this is displayed as Group: Title. For example, "Node: Node ID", "Taxonomy: Term description", etc. It is important to be consistent with groups, because the UI sorts by group, and allows filtering by group to find fields as well.
+
title
+
The actual name of the field; it should be concise and descriptive.
+
help
+
A longer description to help describe what the field is or does. It should try to be only a line or two so as not to clutter the UI.
+
+ +In general, having 'title' and 'help' at the table level doesn't make a lot of sense, but usually every item in a table is in the same group. Thus it is very common to define the 'group': + +
+  $data['foo']['table']['group'] = t('Foo');
+
+ +The other items in the 'table' section are described in the following sections. + +

'base': Base table

+If your table is a base table -- meaning it can be the primary, central table for a View to use, you can declare it to be a base table. This primarily provides UI information so that it can be selected. +For example: +
+  // Advertise this table as a possible base table
+  $data['node']['table']['base'] = array(
+    'field' => 'nid',
+    'title' => t('Node'),
+    'help' => t("Nodes are a Drupal site's primary content."),
+    'weight' => -10,
+  );
+
+ +The following items are available in the base section: +
+
field
+
The primary key field for this table. For Views to treat any table as a base table, it must have a primary field. For node this is the 'nid', for users this is the 'uid', etc. Without a single primary key field (i.e. not a composite key), Views will not be able to utilize the table as a base table. If your table does not have a primary key field, it is not too difficult to just add a serial field to it, usually.
+
title
+
The title of this table in the UI. It should be singular and describe the object that this table contains from the perspective of the user.
+
help
+
A short piece of text to describe what object this table contains.
+
database
+
If this table is held in a different database from your Drupal database, specify it as a string in the exact same format as the settings.php file. This is a special purpose variable that will probably be only used in site specific code, and it must be the same database type as your Drupal database. Also, don't try to join it to any table that isn't in the same database. That'll just create all kinds of silly errors. For example: +
+  // In settings.php for your site
+  // Your drupal (site) database needs to be called 'default'
+  $db_url['default'] = 'mysqli://user:pass@host/drupal_db';
+  $db_url['budget'] = 'mysqli://user:pass@host/other_db';
+
+Then when you are describing the external database in your base table you would write something like this: +
+  $data[$table]['table']['base'] = array(
+    'field' => 'Primary key',
+    'title' => t('Field name'),
+    'help' => t('Field description'),
+    'database' => 'budget',
+    'weight' => -10,
+    );
+
+
+
+ +

'join': Linking your table to existing base tables

+For Views to use your table, it has to either be a base table, or know how to link to an existing base table. Or sometimes both. Views uses this information to create a path to the base table; when the table is added to the query, Views will walk along this path, adding all tables required into the query. + +
+ +How taxonomy_term_data joins to node +
+ +In the above example, to use these with 'node' as the base table, both 'taxonomy_term_data' and 'term_node' need to be defined, and they each need a join handler for node: + +
+$data['taxonomy_term_data']['table']['join']['node'] = array(
+  'left_table' => 'term_node',
+  'left_field' => 'tid',
+  'field' => 'tid',
+);
+
+ +The above can be read as "In order to join to the node table, the taxonomy_term_data table must first link to the term_node table, and they join on the 'tid' field.". When adding this table to the query for a node view, Views will look at this and then look for the term_node table. + +
+$data['term_node']['table']['join']['node'] = array(
+  'left_field' => 'nid',
+  'field' => 'nid',
+);
+
+ +Above, the fact that 'left_table' is left out lets us know that term_node links directly to the node table, using the 'nid' field on both sides of the join. + +Quite a few more fields are available in this definition: +
+
handler
+
The name of the handler object to use. Defaults to 'views_join'. You may create custom join handlers that may or may not use any of the data below, as they see fit.
+
table
+
Table to join. This is optional, and should only be used if the table being referenced is an alias.
+
field
+
Field to join on. This is required.
+
left_table
+
The next step toward the final destination. If this is the final destination it may be omitted.
+
left_field
+
The field to join to on the left side. This is required.
+
type
+
Either LEFT (default) or INNER.
+
extra
+
Either a string that's directly added, or an array of items. Each item is, itself, an array: +
+
table
+
table should not be set in most cases, as it would be filled with the right table alias. Set it to NULL if you want to use a formular in "field"
+
field
+
Field or formula, therein "%alias" can be used to reference the right table.
+
operator
+
Similar to filters, this is the operator, such as >, <, =, etc. Defaults to = or IN.
+
value
+
Must be set. If an array, operator will be defaulted to IN.
+
numeric
+
If true, the value will not be surrounded in quotes, and %d will be used for its placeholder.
+
+
+
extra type
+
How all the extras will be combined. Either AND or OR. Defaults to AND.
+
+ +

Describing fields on tables

+Aside from the special table tag, each table can also have an unlimited number of field designations; these correspond roughly to fields on the table, though it is very common to use non-fields to display data that isn't directly in a field, such as data arrived from formulae, or special links related to the object the table is part of. + +Each field is described in the view data with an array, keyed to the database name of the field. This array may contain some information fields, plus an entry in each of the five types of items Views has per field: argument, field, filter, relationship, sort. For example: + +
+$data['node']['nid'] = array(
+  'title' => t('Nid'),
+  'help' => t('The node ID of the node.'), // The help that appears on the UI,
+  // Information for displaying the nid
+  'field' => array(
+    'handler' => 'views_handler_field_node',
+    'click sortable' => TRUE,
+  ),
+  // Information for accepting a nid as an argument
+  'argument' => array(
+    'handler' => 'views_handler_argument_node_nid',
+    'name field' => 'title', // the field to display in the summary.
+    'numeric' => TRUE,
+    'validate type' => 'nid',
+  ),
+  // Information for accepting a nid as a filter
+  'filter' => array(
+    'handler' => 'views_handler_filter_numeric',
+  ),
+  // Information for sorting on a nid.
+  'sort' => array(
+    'handler' => 'views_handler_sort',
+  ),
+);
+
+ +The above example describes the 'nid' field on the 'node' table, providing 4 of the 5 handlers. Note that while field is normally expected to be the database name of the field, it doesn't have to be; you can use an alias (which is how you get multiple handlers per field) or something completely made up for items that aren't tied to the database. For example: + +
+$data['node']['edit_node'] = array(
+  'field' => array(
+    'title' => t('Edit link'),
+    'help' => t('Provide a simple link to edit the node.'),
+    'handler' => 'views_handler_field_node_link_edit',
+  ),
+);
+
+ +The above handler definition an edit link to a node, but this isn't a field in and of itself. For aliased fields, here is another example: + +
+$data['users']['uid_current'] = array(
+  'real field' => 'uid',
+  'title' => t('Current'),
+  'help' => t('Filter the view to the currently logged in user.'),
+  'filter' => array(
+    'handler' => 'views_handler_filter_user_current',
+  ),
+);
+
+ +The above definition provides an alternate filter handler on the uid field for the current user. + +The following items are allowed in the field definition: + +
+
group, title, help
+
As above, these fields are for the UI. If placed here, any of these fields will override a setting on the base table.
+
real field
+
If this field is an alias, the "real field" may be placed here, and the handler will never know the difference.
+ +
field
+
A handler definition for the "Field" section, which is a field that may be displayed in a view. The definition is an array; the contents of the array are completely up to the handler, other than the 'handler' definition. If omitted, handler will default to 'views_handler_field'.
+
filter
+
A handler definition for the "Filters" section, which will be used to apply WHERE clauses to the view. The definition is an array; the contents of the array are completely up to the handler, other than the 'handler' definition. If omitted, handler will default to 'views_handler_filter'.
+
sort
+
A handler definition for the "Sort criteria" section, which will be used to add an ORDER BY clause to the view. The definition is an array; the contents of the array are completely up to the handler, other than the 'handler' definition. If omitted, handler will default to 'views_handler_sort'.
+
relationship
+
A handler definition for the "Field" section, which is a way to bring in new or alternative base tables in the view. The definition is an array; the contents of the array are completely up to the handler, other than the 'handler' definition. If omitted, handler will default to 'views_handler_relationship'. All relationships need the 'base' to be set. The basic relationship handler also requires 'base field' to be set. 'base' and 'base field' represent the "right" half of the join that will use this field as the left side.
+
argument
+
A handler definition for the "Field" section, which is method of accepting user input from the URL or some other source. The definition is an array; the contents of the array are completely up to the handler, other than the 'handler' definition. If omitted, handler will default to 'views_handler_argument'.
+
+ +For more information about what handlers need/use what data, visit the Views API site and check out the available handlers. + +

Support old tabledata

+If you need to rename some tables/fields you can create some references in the views data to be able to continue to work. +Therefore you create the whole table structure of the current views data. + +If you have to rename a single table you need to specify +
+$data['oldtable']['moved to'] = 'newtable';
+
+ +If you have to rename/move a single a field to another table you specify +
+$data['oldtable']['oldfield']['field']['moved to'] = array('newtable', 'newfield');
+
+or +
+$data['oldtable']['oldfield']['moved to'] = array('newtable', 'newfield');
+
diff --git a/help/api-upgrading.html b/help/api-upgrading.html new file mode 100644 index 00000000..158f8091 --- /dev/null +++ b/help/api-upgrading.html @@ -0,0 +1,224 @@ +In order to take advantage of the changes in Drupal 7, Views has gone through several API changes. +Here's what you should know. + +

Handler registry

+ +Views now uses Drupal's dynamic-loading code registry. +You can safely remove your implementations of hook_views_handlers(), since they are no longer used. + +Please remember to specify the handlers in your module's .info file. For example: +
+name = Example module
+description = "Gives an example of a module."
+core = 7.x
+files[] = example.module
+files[] = example.install
+
+; Views handlers
+files[] = includes/views/handlers/example_handler_argument_string.inc
+
+ +

Removed handlers

+ +Note that views_handler_filter_float has been removed. +This functionality is now handled by views_handler_filter_numeric. +There's no need for having a special handler any more, thanks to the new DB layer in Drupal 7. + +views_handler_sort_formula has been removed. +Everyone who used it can extend from views_handler_sort, too. + +

Ctools dependency

+Views requires ctools now, so it can use the dependency system of ctools. +The only thing you have to do is to remove views_process_dependency. + +

Changed add_where api

+If your field is a plain sql field: +
+$this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field " . $this->operator . " '%s'", $this->value);
+
+has to be converted to +
+$this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", $this->value, $this->operator);
+
+If your field is a complex where condition: +
+$this->query->add_where($this->options['group'], "$upper($field) NOT LIKE $upper('%%%s')", $this->value);
+
+has to be converted to +
+$placeholder = $this->placeholder();
+$this->query->add_where_expression($this->options['group'], "$field LIKE $placeholder", array($placeholder => '%' . db_like($this->value)));
+
+placeholder() generates a automatic unique placeholder for you. + +add_where with operator 'formula' can be converted to add_where_expression. +add_having with operator 'formula' can be converted to add_having_expression. + +

Changed place for display specific settings

+In the new ui the new place for display settings is at the top of the second column. +Therefore use something like this code in your display plugin: +
+$categories['block'] = array(
+  'title' => t('Block settings'),
+  'column' => 'second',
+  'build' => array(
+    '#weight' => -10,
+  ),
+);
+
+ +

Changed filter settings and associated class variables

+'optional' and 'single' are now 'required' and 'multiple', the logic is now opposite. +Also, the 'no_single' and 'no_optional' class variables (known as "object flags" in the API docs) +are now 'always_multiple' and 'always_required'. + +

Changed argument settings

+See the init() function in views_handler_argument for an overview of everything +that changed. + +1. The default actions 'summary asc', 'summary desc', 'summary asc by count', 'summary asc by count' +have been replaced by a single 'summary' action (which takes the sort order and type as options). +2. Wildcards are now called exceptions. +
+$this->options['exception']['value'] = $options['wildcard'];
+$this->options['exception']['title'] = $options['wildcard_substitution'];
+
+3. Summary plugin options are now stored in 'summary_options' instead of 'style_options' +
+$this->options['summary_options'] = $options['style_options'];
+
+4. The selected summary plugin is no longer stored in 'style_plugin'. +
+$this->options['summary']['format'] = $options['style_plugin'];
+
+5. The validator options have been moved. +
+$options['validate']['type'] = $options['validate_type'];
+$options['validate']['fail'] = $options['validate_fail'];
+
+6. The validator settings have been moved from $form['argument_validate'] to ['validate_options'] +This means that dependent code in validate plugins needs to change. +Example change for views_plugin_argument_validate_user: +
+    $form['roles'] = array(
+       '#dependency' => array(
+-        'edit-options-argument-validate-user-restrict-roles' => array(1),
++        'edit-options-validate-options-user-restrict-roles' => array(1),
+       ),
+
+ +

The introduction of get_value() and sanitize_value()

+The views_handler class got two new functions: +
+/**
+ * Get the value that's supposed to be rendered.
+ *
+ * @param $values
+ *   An object containing all retrieved values.
+ * @param $field
+ *   Optional name of the field where the value is stored.
+ */
+function get_value($values, $field = NULL) {
+  $alias = isset($field) ? $this->aliases[$field] : $this->field_alias;
+  if (isset($values->{$alias})) {
+    return $values->{$alias};
+  }
+}
+
+/**
+ * Sanitize the value for output.
+ *
+ * @param $value
+ *   The value being rendered.
+ * @param $type
+ *   The type of sanitization needed. If not provided, check_plain() is used.
+ */
+function sanitize_value($value, $type = NULL) {
+  switch ($type) {
+    case 'xss':
+      $value = filter_xss($value);
+      break;
+    case 'url':
+      $value = check_url($value);
+      break;
+    default:
+      $value = check_plain($value);
+      break;
+  }
+  return $value;
+}
+
+These functions are meant to be used in the render() functions of field handlers, +for fetching data (usually by alias) from the $values object, and for sanitizing values. + +The abstraction of fetching data from rendering data is important because +different query backends have different ways of storing data in $values, and the field alias +is a SQL specific thing. So instead of overriding the whole render() function and copying +all of the logic there (as well as having to constantly keep up with upstream Views changes), +the backend can just override get_values(), which is significantly less code. + +Of course, different ways of fetching and displaying data might require different +ways of sanitizing it, hence the usage of the sanitize_value() function. + +Examples of converting render() field handler implementations: +
+// This
+$value = $values->{$this->field_alias};
+// Becomes this
+$value = $this->get_value($values);
+
+// And this
+$format = $values->{$this->aliases['format']};
+// Becomes this
+$format = $this->get_values($values, 'format');
+
+// Instead of this:
+return check_plain($value);
+// We write:
+return $this->sanitize_value($value);
+
+// Since sanitize_value() supports different sanitization functions, this:
+return filter_xss($value);
+// Can become:
+return $this->sanitize_value($value, 'xss');
+
+ + +

Changed views_get_page_view

+In contrast to 6.x views_get_page_view now does stores the current view, not the current page display. + +

Removed views-view-row-node

+Due to changes in comment.module there is no extra views-view-row-node template needed to display the comments. If you do some custom stuff there you should now be able to do everything in your node.tpl.php. + +

Entity type Key on Base tables

+During the development of the drupal7 version of views the entity type associated with a table got added to $data['name']['table']['base']['entity type']. It should be moved to $data['name']['table']['entity type']. + +

Changed views_plugin_style::render_grouping()

+The parameters as well as the structure of the methods return have changed. +The method now accepts a third optional parameter called "$group_rendered". +This parameter defines whether to use the rendered or the raw field value for grouping. +Intention for adding the parameter was that the grouping could have been acted +unexpected if the rendered field contained unique values e.g. by using drupal_html_id(). +
+
New return structure
+
+{grouping value} is the value affected by the new parameter. +
+  array (
+    {grouping value} => array(
+      'group' => {rendered_value of the grouping field},
+      'rows' => array({group rows}),
+    ),
+  );
+
+
+
Old return structure
+
+If the new parameter isn't explicitly set or its value is NULL the structure of the return will be the same as in D6! +
+  array (
+    {rendered_value of the grouping field} => array({group rows}),
+  );
+
+
+
diff --git a/help/api.html b/help/api.html new file mode 100644 index 00000000..2b743a75 --- /dev/null +++ b/help/api.html @@ -0,0 +1,24 @@ +Views allows modules to describe their tables relationships to each other, as well as fields, filters, sort criteria and arguments via hook_views_data(). Whenever Views deems it necessary, this hook is called, the data aggregated together and cached. hook_views_data_alter() may also be used to modify existing data, changing other module's handlers or adding handlers to other module's tables. + +Views also allows modules to create new display types, style types, row styles, argument default handlers and argument validators via hook_views_handlers() and hook_views_plugins(). + +These hooks are kept in a file named MODULENAME.views.inc. This file is automatically included upon need, so there is no need to try and include this in hook_init or any other method of including .inc files. This file should store hook_views_data, hook_views_data_alter(), hook_views_plugins(), hook_views_handlers(), as well as any other hooks and subsidiary data that will only be used by your module when Views is active. All handlers and plugins provided by your module should be in separate .inc files, and must be referenced in your module's .info file with the files[] directive. + +There are two similar files, MODULENAME.views_default.inc and MODULENAME.views_convert.inc which contain default views and views 1 to views 2 convert helpers, respectively. + +

hook_views_api()

+In order for your files to be included, your module must first implement hook_views_api() in the main .module file. This module should return array of information. The following items may be returned: + +
+
api
+
This must appear; it should be the oldest API version that your module can work with. If Views is currently running an older version of the API, it will ignore your module's views integration. This is a good thing, as it will prevent code crashes, at the expense of your module's functionality disappearing. +
+You may find the current Views API version by calling views_api_version() which is implemented at the top of views.module. This version numbering starts at 2.0. Every time changes are made to the Views handlers and plugins or other aspects of the Views API, the number will tick up (by either .001, .01 .1 or 1 depending upon how major the changes are). Note that views_api_version() was introduced in Views 2.0-rc2 and may not exist prior to that version. You may use drupal_function_exists() to test to see if this function is there. +
+Often these versions are basically compatible with each other and Views won't care if your module implements 2.000, 2.001, 2.002, etc. Your module can request that it won't work with any version older than a given version, however. Views will determine, itself, if a newer version will work. +
+
path
+
If your *.views*.inc files are not in the same directory as the .module file, then return the full path here. You should probably use something like drupal_get_path('module', 'yourmodulename') . '/includepath'.
+
template path
+
A path where the module has stored it's views template files. When you have specificed this key views automatically uses the template files for the views. You can use the same naming conventions like for normal views template files.
+
diff --git a/help/argument.html b/help/argument.html new file mode 100644 index 00000000..da67e353 --- /dev/null +++ b/help/argument.html @@ -0,0 +1,106 @@ +Contextual Filters (known in previous versions of Views as "Arguments") are input. While they often come from the URL, they don't always. Don't be shocked when they don't. Each display type may have its own source for contextual filters. Block displays have no source of contextual filters at all; they cannot pull contextual filters from the URL, and often require use of PHP code as a default contextual filter. Various default values can be applied against contextual filters to use filters in a block view. See "Provide default value," under "When the filter value is NOT in the URL" below. + +In general, contextual filters are used to filter the view, and in that sense they have a very close relationship to filters. However, this isn't necessarily true for every contextual filter. Contextual filters can be used for any purpose, really; the extent of what the contextual filter does is up to the developer of the contextual filter. + +A typical use of an contextual filter might be to reduce a view to a single node, a single user, or nodes with a given taxonomy term. + +

When the filter value is NOT in the URL

+ +
+
Display all results for the specified field
+
If this option is selected, the contextual filter is removed from the view as though it weren't there and all results will be displayed.
+
Provide default value
+
If no contextual filter is given, a default contextual filter can be selected. You may choose from a number of different default filter options. See "Default contextual filters" below.
+
Hide view
+
The view will remove itself entirely if the contextual filter is not present; for a block this means it simply won't appear. For page views the view will return a 404 and present a "Page not found" error.
+
Display a summary
+
The view will attempt to display a summary of contextual filters available, based upon the view, and provide links to the view with those contextual filters. Summaries have their own style handlers as well as options. The default summary style simply displays a list of contextual filters with a count of entries found per contextual filter. This special mode is a very powerful part of Views.
+
Display contents of "No results found"
+
If this option is selected, the value specified under "No results behavior" on the main view page will be displayed when there is no filter value in the URL.
+
+ +An exception value can be set under the "Exceptions" menu. If this exception value is received, any filter value specified under "When the filter value is NOT in the URL" will be ignored. This is a literal value: if you enter the word "everything" here, the exception will apply only if the value "everything" is received. + +

Default contextual filters

+ +The Default contextual filter selection is available only if the action to take is "Provide default value" under "When the filter value is NOT in the URL." When that option is selected, a new fieldset will appear that will allow you to choose default contextual filters. Views provides the following default selectors by default (but other modules may add more): + +
+
Content ID from URL
+
This will attempt to find a Node ID from the URL, and is primarily useful for the Node: ID contextual filter (though other contextual filters that use Node IDs, such as the CCK Node Reference contextual filter, will find it useful, too). For example, when visiting the path "node/1" or "node/1/edit" it will know that the "1" in that path is a node ID and use it.
+
Fixed value
+
You may directly enter what the contextual filter will be. This is not a variable, it will always be the same contextual filter.
+
PHP Code
+
Arbitrary php code inserted here will run whenever a contextual filter is not present in the URL.
+
Taxonomy term ID from URL
+
This default filter will attempt to return a taxonomy term from the current path. Selecting this option gives you the choice to also load default filters from terms.
+
User ID from URL
+
Like Node ID and Taxonomy term ID from URL, this will attempt to find a user ID from the path. It also includes an option to look for a node and use the node author as the user ID.
+
User ID from logged in user
+
If a user is currently logged in and accessing this view, their user ID will be returned as the contextual filter.
+
Current date
+
This option simply returns today's date.
+
Current node's creation time
+
Select this to return a node's creation time as a contextual filter.
+
Current node's update time
+
Not surprisingly, this filter returns the current node's update time
+
+ +Please bear in mind that the selection of a default contextual filter happens only if a contextual filter is not provided. If you are using a display that has no contextual filter source, such as a block, the contextual filter value selected here will always be used. However, if using a display that reads contextual filters from the URL, the options selected here will only be applied if the URL did not contain an contextual filter. + +The "Skip default argument for view URL" option gives you the choice of ignoring the default argument totally. This is useful for certain applications, like the creation of feeds. + +

When the filter value IS in the URL or a default is provided

+ +
+
Override title
+
Selecting this option will allow you to replace the default view title with a piece of arbitrary text. Argument substitutions in the form of %1, %2, etc. may be used here.
+
Override breadcrumb
+
This option will allow you to overwrite the view name in breadcrumbs. The same substitution values as in "Override title" may be used.
+
Specify validation criteria
+
Contextual filters can have validators, which are pluggable systems used to ensure that contextual filters fit within certain parameters. When a validator is chosen, it may provide some specific settings, including the action to take if an contextual filter is presented, but it fails to validate. These actions are generally the same as the default actions above, excluding the ability to provide another default filter. See "Contextual filter validation" below for a detailed description.
+
+ +

Contextual filter validation

+ +Note: If a view provides a menu option, such as a tab, the tab will not appear if the contextual filter does not validate. + +This sytem can have other validators plugged in; by default, Views provides: + +
+
Basic validation
+
This validator ensures that the contextual filter is present. A PHP NULL value (from eg. PHP default contextual filter code) will invalidate.
+
Content
+
This validator ensures that the contextual filter is a valid Node ID. You may select which node types the validator will accept.
+
Numeric
+
This validator ensures that the contextual filter is a valid number.
+
PHP Code
+
You may enter arbitrary PHP code here, similar to the "PHP code" option under "Provide default value" in "When the filter value is NOT in the URL" above, to determine if the contextual filter is valid or not.
+
Taxonomy term
+
Ensures that the contextual filter is a valid taxonomy term. This includes options to limit to specific vocabularies and can transform the contextual filter to the right type depending upon the actual contextual filter. Set the contextual filter value type option to the actual type of data that the contextual filter being used is expecting.
+
User
+
Ensures that the contextual filter refers to a valid user. If you select this validator, additional options are available to accept numeric user IDs, usernames or both, as well as to consider a user's role when filtering the view.
+
+ +If you select the "Specify validation criteria" option to select a specific validator, you will have the option to select an action to take if the filter value does not validate. + +
+
Display all results for the specified field
+
Selecting this option will return all values if the filter value does not validate, similar to "Display all results for the specified field" under "When the filter value is not in the URL" above.
+
Hide View
+
Similar to "Hide view" under "When the filter value is NOT in the URL" above, if this option is selected and the selector does not validate, the view will hide itself. If the view is a block, nothing will appear. If it is a page, it will throw a 404 and present a "Page not found" error.
+
Return Summary
+
If you select this option and the filter does not validate, a summary of all values that are valid will be returned, as in the option of the same name under "When the filter value is NOT in the URL above."
+
Display contents of "No results found"
+
If this option is selected and the selector does not validate, the value specified under "No results behavior" on the main view page will be displayed.
+
+ +

Adminstrative title

+ +Located under the "More" group at the bottom of the "Contextual filters" menu, this option allows you to specify a custom name for the contextual filter. This may be particularly useful if you use the same contextual filter twice and you'd like to distinguish between the two filters. + +

Grouping of contextual filters

+ +Even though contextual filters do not appear in the "and/or" user interface for sorting and grouping regular filters, contextual filters are always added to the first group of filters. Thus the order of the groups can cause the contextual filter to have entirely different effects on the results of a view that has contextual filters. Even though differences might not be apparent through the user interface. + +Multiple contextual filters are therefore always in the same "and/or" group of filters, and can not be placed in different groups. There is an effort to add this feature. diff --git a/help/basic-settings.html b/help/basic-settings.html new file mode 100644 index 00000000..be2cadc1 --- /dev/null +++ b/help/basic-settings.html @@ -0,0 +1,20 @@ +You choose the name of the current display. +This title will be displayed with the view, wherever titles are normally displayed; i.e, as the page title, block title, etc. + +When you use have many items to display, you have the choice to display them in different variants. +
+
Display a specified number of items
+
Specify the number of items to display per page and an offset. The offset is the number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed. +
Display all items
+
All items will display, but you can choose an offset. The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.
+
Paged output, full pager
+
A Pager can be used to display items, with the possibility to select the next page and also the first and last page. When you have many items the query is very expensive. To avoid this, you can choose the mini pager. +You can also choose the number of items per page. If you enter 0, then there is no limit. Pagers also will respect an offset, if present. If multiple pagers are present on one page you may need to set this number to a higher value so as not to conflict within the ?page= array. Large values will add commas to your URLs, so avoid this if possible. Unless you're experiencing problems with pagers related to this view, you should leave this at 0. Enter the total number of pages to limit the number of values. When you leave the field empty all pages will show. +The Exposed options allow users to define their values in a exposed form when view is displayed. +You can choose "Expose items per page". With this option the user can determine how many items per page show in a view. Options for which label should display and what numberic options are also available. +Furthermore, you can choose "Expose Offset". When checked, users can determine how many items should be skipped at the beginning. You can define a label. +
+
Paged output, mini pager
+
The pager optiona are the same as for the "Paged output, full pager" but you have no possibility to jump to the last or first items.
+
+Normally, all views are created with unrestricted access. This means any site visitor can see the views. Please consider this when you make a view with a menu link and private data as output. You have by default two options: "Permission" and "Role". If you choose permission, you get a list of all permissions. Only users with the selected permission flag will be able to access this display. If you choose role, you get all roles as checkboxes. Only the checked roles will be able to access this display. Note that users with "access all views" can see any view, regardless of role. diff --git a/help/demo-video.html b/help/demo-video.html new file mode 100644 index 00000000..43c02403 --- /dev/null +++ b/help/demo-video.html @@ -0,0 +1,5 @@ +

Here are some links to demonstration videos. It will get you started working with Views.

+ +

Views 2 (Drupal 6) Demonstration Video - Drupal Handbook

+ +

NodeOne's initial screencasts on the Views 3 UI. diff --git a/help/display-attachment.html b/help/display-attachment.html new file mode 100644 index 00000000..3dbdf88f --- /dev/null +++ b/help/display-attachment.html @@ -0,0 +1 @@ +Attachment displays are 'attached' to another display in the same view. When the display is visited, the attached display will also be rendered and may be placed before, after or both before and after the original display. Attachment displays are often useful for displaying an argument summary view along with a page display that accepts arguments. This can be used to provide a kind of glossary. diff --git a/help/display-block.html b/help/display-block.html new file mode 100644 index 00000000..9ac6d315 --- /dev/null +++ b/help/display-block.html @@ -0,0 +1,11 @@ +Block displays will show up on your blocks administration page. Once a block display is created and saved, it can be enabled and positioned in your theme by visiting administer >> site building >> blocks and selecting it from the list. + +Blocks do not accept arguments from any source; the only way to get arguments to a block is to provide defaults to it, possibly via the PHP Code default setting. + +

    +
  • Edit the argument in question; you may want to override this argument if you have multiple displays using it.
  • +
  • Change the "Action to take if argument is not present" to "Provide default argument". This will bring up a new box called "Provide default argument options".
  • +
  • The most common default argument type used for blocks is Node from URL, where it attempts to determine if the URL refers to a node, for example if visiting 'node/1' or 'node/1/edit'. User ID from URL is also very common.
  • +
  • If you change the default argument type to 'PHP Code' (note: You must have permission to use PHP code on your site) you can enter PHP to define the argument needed. Simply return the argument.
  • +
  • If you are using a more link with a block, you must have a page display for that more link to attach to. The more link will not print if there is no place (no display) for it to link to.
  • +
diff --git a/help/display-default.html b/help/display-default.html new file mode 100644 index 00000000..b619981d --- /dev/null +++ b/help/display-default.html @@ -0,0 +1,3 @@ +The default display is primarily a display to store settings, and isn't actually used anywhere within the Views system. It is possible for external programs to use the default display, but if they do they will (hopefully) tell you which display they will be working with. The default display is also a convenient display to use to embed into your site using PHP snippets; this is useful, for example, in node content, but this is something that should generally only be done by administrators. + +In general, you probably want to add either a page display or a block display. diff --git a/help/display-feed.html b/help/display-feed.html new file mode 100644 index 00000000..beddd008 --- /dev/null +++ b/help/display-feed.html @@ -0,0 +1 @@ +A feed display allows you to attach an RSS feed to a view. diff --git a/help/display-page.html b/help/display-page.html new file mode 100644 index 00000000..9ab84995 --- /dev/null +++ b/help/display-page.html @@ -0,0 +1,7 @@ +Page displays have a path and an optional menu component. Page displays will be the primary content for the page, meaning they will be displayed in the main content area when you visit the URL that corresponds to the path. + +Page displays take their arguments from the URL. You can embed arguments into the URL using %; in previous versions of Views, this was '$arg'. For example, 'node/%/foo' will accept URLs such as 'node/1/foo'. + +Please remember that using a % placeholder makes the argument required. If you wish to have an optional argument, simply omit the % from the path. I.e. using "page/%" as the path requires an argument and visiting 'http://www.example.com/page' will not trigger the view. + +If you intend to embed a view manually into another view, it is recommended that the page display not be used for embedding. Select a different display type to embed. diff --git a/help/display.html b/help/display.html new file mode 100644 index 00000000..48b2475c --- /dev/null +++ b/help/display.html @@ -0,0 +1,13 @@ +Displays tell Views where the output should go. By adding a display to a View, you can have your view appear as a page, or as a block, or even as an attachment to a different display on the view. + +Displays tell Views where the output should go. By adding a display to a View, you can have your view appear as a page, or as a block, or even as an attachment to a different display on the view. You must create at least one display in order to place a View on your site. + +Each display can have its own settings, but when created, a display will take all of its basic settings from the default display which all Views must have. For most settings, there is an override button that will override that single setting for the current display. Overridden settings will have a mark in the summary for that display. All 'default display settings' are shown in the other displays in 'italic'. When you override a setting, then it is shown 'normal'. + +Please keep in mind that when you are editing a setting on a display that is not overridden, then by default you are editing that for all displays. + +Overriding fields, arguments, sorts, filters and relationships, can only be done by overriding the entire group or none of them. To do this, click on the header for the filters or the rearrange button. Once you override, the display will then have its own copies of the fields/filters/etc and changes to the defaults will not be reflected on your display. + +With the Reorder button you can organize the order of your displays. +With the Analysis button the system checks the view and may give you suggestions if something is wrong. +You can clone a display by using the link in the header of the display. diff --git a/help/drush.html b/help/drush.html new file mode 100644 index 00000000..689ff339 --- /dev/null +++ b/help/drush.html @@ -0,0 +1,13 @@ +There are some Drush commands available for Views, initially added in Drush command to revert views: + +
    +
  • views-dev (vd) - Setup the views settings to a more developer oriented value
  • +
  • views-revert (vr) - Revert overridden views to their default state. Make backups first!
  • +
+ +Examples: +drush views-revert +[prompts the user with a list of overridden views to choose from, or to revert all] + +drush views-revert archive myview2 +[reverts the two specified views] diff --git a/help/embed.html b/help/embed.html new file mode 100644 index 00000000..e39fbf5c --- /dev/null +++ b/help/embed.html @@ -0,0 +1,24 @@ +You can easily embed the results of a view into other parts of your site; +either with code as a module, or in nodes or blocks as snippets. The +easiest way is to use the function views_embed_view(): + +/** + * Embed a view using a PHP snippet. + * + * This function is meant to be called from PHP snippets, should one wish to + * embed a view in a node or something. It's meant to provide the simplest + * solution and doesn't really offer a lot of options, but breaking the function + * apart is pretty easy, and this provides a worthwhile guide to doing so. + * + * @param $name + * The name of the view to embed. + * @param $display_id + * The display id to embed. If unsure, use 'default', as it will always be + * valid. But things like 'page' or 'block' should work here. + * @param ... + * Any additional parameters will be passed as arguments. + */ +function views_embed_view($name, $display_id = 'default') { + + +To figure out the id of a display, hover your mouse over the tab to select that display. Everything after the '#views-tab-' is the id of that display. This ID is guaranteed never to change unless you delete the display and create a new one. diff --git a/help/empty-text.html b/help/empty-text.html new file mode 100644 index 00000000..0a085938 --- /dev/null +++ b/help/empty-text.html @@ -0,0 +1,3 @@ +The Emtpy Text content will be displayed when you choose the option Display empty text under the Arguments labelled Action to take if argument is not present. + +By default you can choose one or more text areas. diff --git a/help/example-author-block.html b/help/example-author-block.html new file mode 100644 index 00000000..41ff071d --- /dev/null +++ b/help/example-author-block.html @@ -0,0 +1,77 @@ +

In this example you will create a context-sensitive block that shows the titles of recent blog entries by an author when viewing one of their posts. This will demonstrate using Views arguments to dynamically filter a view's contents at display time.

+ +

Before working through this example, enable the Blog module and populate some entries from a few different users.

+ +

Creating the View

+

The first step is creating a view for our recent blog entries block. Because the block will show the titles of blog entries, this view is considered a "Node" type. Go to add new view, enter the following properties, and click Next:

+ +
+
View name
+
recent_blog_entries
+
View description
+
List of recent blog entries for a given author.
+
View tag
+
blog
+
View type
+
Node
+
+ +

Generating a list of blog entries

+

It will be much easier to see the view progress if we can see it doing something. In this section, we will create a basic view displaying blog entry titles.

+ +
    +
  1. In the third column, locate the Fields area. Generally speaking, fields are the pieces of information that you want to display in the view (in this case, node title). Click the + icon to add a field.
  2. +
  3. Scroll down to Defaults: Add fields, below the settings table. A large selection of fields will be available.
  4. +
  5. In the Groups drop-down menu, select Node. This will limit the list to only the default fields exposed by Node module.
  6. +
  7. Scroll down the list, select the Node: Title field, and click Add.
  8. +
  9. You will now be presented with settings for the Node: Title field. Delete the label from the Label field, so that each individual node title is not prefixed with the word "Title." Additionally, check the Link this field to its node box so that visitors who see an interesting title can click directly on it to read the blog entry to which it belongs.
  10. +
  11. When finished, click Update. If you scroll down to the Live Preview section, you should now see a list of several node titles; however both blog entries and other node types will be in the list. Let's fix that.
  12. +
  13. In the fourth column, locate the Filters area. Filters limit the results displayed in the view, and we can use this to our advantage here by showing node titles only from blog entries and not every type of node. Click the + icon to add a filter.
  14. +
  15. As before, scroll down to the Defaults: Add filters section, select Node from the Groups select box to limit the list of options to only those exposed by Node module.
  16. +
  17. Scroll down and select the Node: Type field and click Add. In the settings page that appears, leave Operator as Is one of and select Blog entry under Node type. Click Update when finished.
  18. +
  19. Now, by scrolling down to Live preview, you'll see that the list only shows blog entries.
  20. +
+ +

Adding context with arguments

+

While filters are very useful for limiting the results of a view when the condition is always consistent (for example, a view of blog entry nodes should always be filtered by the blog entry type), something filters can't do is smart decision-making based on the page context. In our case, we want the view to display a different list of blog entries when looking at a post by user 'admin' than we do when looking at a post by user 'member', and filters won't be able to help.

+ +

Luckily, there's another way to filter a view's content: arguments. Through arguments, Views are able to obtain additional context (typically via dynamic URLs with IDs in them) and can take this context into consideration when displaying the view.

+ +

Let's walk through adding and configuring an argument to our view so that we can change its contents based on post author.

+ +
    +
  1. In the third column, locate the Arguments area. Click the + icon to add an argument.
  2. +
  3. Because we are basing the view around content authors, this time under Groups select User. Check User: Uid and click Add.
  4. +
  5. The Defaults: Configure Argument User: Uid settings page has a lot going on, but only a few things that need our attention.
  6. +
  7. The Title field here, unlike the Title field under Basic Settings, can be based upon the context that the view is being displayed in. Change the title to 'Recent entries by %1.' %1 will later be expanded to the user's name (based on the User: Uid argument) when the view is displayed.
  8. +
  9. Under Action to take if argument is not present, there are a variety of options, ranging from displaying a 404 or a 403 page to simply displaying all values in the view. In our case, if an argument isn't specified (which it won't be, since this view will be displayed in a sidebar block, not as a page with its own URL), we want to give it a default one to act on. Select Provide default argument.
  10. +
  11. Assuming JavaScript is enabled in your browser, you should now get another selection for Default argument type. Select User ID from URL, which will then provide a new option, Also look for a node and use the node author. Select it. This will cause Views to first see if it can figure out a user ID from the current URL (for example, user/1). If it can't, it will instead check to see if the current page is a node page (such as node/42) and, if so, take the user ID from the node's author field instead.
  12. +
  13. Validator options provide a useful way to control what kind of arguments your view will accept. Select User as the Validator. By default, changing this setting will check the incoming argument and ensure it's a valid user ID; if not, the view will be hidden from the page.
  14. +
  15. Once you have changed the argument's title, default argument, and validator options, click Update to save your changes.
  16. +
  17. You'll notice that now the Live preview no longer shows anything. Did we just break the view? Fortunately, no. It's merely abiding by our wishes to hide itself if there is no valid user ID given to it. Try entering a '1' in the Arguments box and clicking Preview. You should now see a list of only user 1's blog entries.
  18. +
+ +

Creating the block

+

So the live preview is now showing basically what we want. There's just one problem: we have no way to stick what we've done so far into a sidebar block! Let's fix that by adding a new Display.

+ +
    +
  1. In the first column, under Defaults, there is a select box containing entries such as Page, Feed, and, yes, Block! Select Block and click Add display.
  2. +
  3. There's not much else to do here as far as Views is concerned. Under Block settings, click the None link next to Admin and fill in a description for the block in the administrative interface, such as: 'Recent blog entries by author.' and click Update.
  4. +
  5. Save your work by clicking the Save button at the bottom of the Views interface. You should receive a message that the view has been saved.
  6. +
  7. Next, navigate to the blocks interface and drag the 'Recent blog entries by author' block to the right sidebar region (or similar) and click Save blocks.
  8. +
  9. You'll notice this appeared to do nothing. No block shows in the sidebar. But remember, we are looking at an adminitrative page; we are not looking at a page that would provide a user ID context. Navigate to the main blog listing and click on an entry there. You should now see a sidebar block, titled something like "Recent entries by admin," with a list of blog entries beneath it.
  10. +
+ +

Finishing touches

+

There are still a few remaining things to do before our view is complete. For example, we said that the block was to show recent blog entries, but instead it's showing them in the order they were entered, with oldest on top. Additionally, even unpublished entries are showing in the list currently.

+ +
    +
  1. Return to the recent_blog_entries view edit page.
  2. +
  3. Add an additional filter by clicking the + icon in the Filters section in the fourth column.
  4. +
  5. Change Groups to Node and select Node: Published. Click Add.
  6. +
  7. Under the Published selection, choose Yes and click Update.
  8. +
  9. To handle sorting, locate the Sort criteria area, just above filters, and click the + icon there.
  10. +
  11. Under Groups, again select Node. From the list of options, check Node: Post date and click Add.
  12. +
  13. In the settings page, change Sort order to Descending. This will place the newer posts on top of the older ones. Click Update when finished.
  14. +
  15. Finally, Save the view for your new settings to take effect.
  16. +
diff --git a/help/example-filter-by-current-user.html b/help/example-filter-by-current-user.html new file mode 100644 index 00000000..b7fdd05d --- /dev/null +++ b/help/example-filter-by-current-user.html @@ -0,0 +1,46 @@ +

In this example you will create a page that displays a list of the content authored by the current logged-in user. This will demonstrate using Views filters and relationships to dynamically filter the view's contents at display time.

+ +

For this example, we are assuming you have a content type "Blog Post".

+ +

Creating the View

+

The first step is creating a view for our content list page. Because the page will show the titles of content, this view is considered a "Content" type. Go to add new view, enter the following properties, and click Next:

+ +
+
View name
+
content_by_current_user
+
Description
+
List of content authored by the current user.
+
+ +

Choose Show Content of type Blog Post. You can choose any way you wish to sort the content.

+ +

Creating the page

+

Tick the box next to Create a page. Enter a page title and a path. For our purposes here, the default settings for the rest of this page are sufficient.

+ +

Click on Continue & edit.

+ +

Creating the relationship

+

In order to have access to the author of the content, it is important to create a relationship between the current content type, and users.

+ +

Under Advanced in the right culumn, select add next to Relationships.

+

Select Content: Author and click on Add and configure relationships. Leave the settings as they are and click on Apply (all displays).

+ +

You now have access to the user data related to the content you are viewing.

+ +

Filtering the view

+

Now you need to filter the view to display only content authored by the current user. This data is now available for the content because you have created the relationship in the step above.

+ +

Next to Filter criteria click on add to add a new filter to your view.

+ +

Filter the list of fields by selecting User next to Filter at the top. You now have more fields than before due to the relationship you created.

+ +

Select User: Current from the list and click on Add and configure filter criteria.

+ +

Since this field is only visible due to the relationship you created, author will already be selected under Relationship. This shows that the relationship you created is being used for the filter field.

+

Select Yes under Is the logged in user, and click on Apply (all displays).

+ +

If you have authored content of the type Blog Post, you should now see a list of those posts under the preview section at the bottom.

+ +

Saving & testing the view

+

Click on Save to save the view.

+

You can test the view by going to the path you entered in the first part of this example.

diff --git a/help/example-recent-stories.html b/help/example-recent-stories.html new file mode 100644 index 00000000..7e21324d --- /dev/null +++ b/help/example-recent-stories.html @@ -0,0 +1,57 @@ +In this example you will create a list of nodes of the content type "story", to be shown in a block. Through this step-by-step process, you will become familiar with some basic steps in creating a view, and familiarize yourself with the Views User Interface. + +
    +
  1. Creating a new view

    +

    Go to add new view. Give your new view the name 'recent_stories', description 'Recent Stories', tag 'story', type 'Node' and click Next.

  2. +
  3. About the interface

    +

    You have been brought to Views User Interface. As you start, you are editing the "Default" options for the view. In the 1st column on the left you can see the drop-down menu offers 'block', for example, to select settings specific only to block views. In the remaining columns, you will be able to add or change options by clicking on links or icons. These options will then appear below this main area. Most likely, you will need to scroll a bit to see the options appear.

  4. +
  5. Selecting the fields to display

    +
      +
    1. In 3rd column locate the Fields options. Click the + icon to add fields.
    2. +
    3. Scroll down to Defaults: Add fields. In the Groups drop-down menu select 'Node', then check the following two fields: Node: Post date, Node: Title. Then click Add.
    4. +
    5. You will be taken through the fields you added one at a time. Make the changes specified below. +
        +
      • For the Post date field: Delete the 'Post date' label. Change the Date format to Custom, and the Custom date format to 'F j, Y, g:i a' (do not type the single quotes; for the meaning of these letter codes, click on the PHP docs link under that box to arrive at the explanation). Click Update.
      • +
      • For the Title field: Delete the 'Title' label. Select Link this field to its node. Click Update.
      • +
      +
    6. +
    7. Scroll back up to Fields and click the ↑↓ icon to rearrange fields.
    8. +
    9. Drag the four-sided arrow next to Node: Title so that it appears above Node: Post date. Click Update to save the new field order.
    10. +
    +
  6. +
  7. Filtering to story nodes only

    +
      +
    1. Click the + icon next to Filters.
    2. +
    3. In the Groups drop-down menu select 'Node', then check the Node: Published and Node: Type filters, and click Add.
    4. +
    5. Select the Published checkbox. Click Update
    6. +
    7. Select Is one of and check Story in the Node Type field. Click Update.
    8. +
    +
  8. +
  9. Sorting to show most recent first

    +
      +
    1. Scroll up to Sort criteria and click the + icon.
    2. +
    3. In the Groups drop-down menu below, select 'Node', then check Node: Post date, and click Add. Alternatively, you may instead check Node: Last comment time, or Node: Updated/commented date, or Node: Updated date.
    4. +
    5. Select Descending Sort order. Click Update.
    6. +
    +
  10. +
  11. Refining the basic settings

    +
      +
    • In 1st column under Basic settings locate these options: +
        +
      • Items to Display setting, click 10. Change the '10' to '4'. Click Update
      • +
      • Style setting, click Unformatted. Change to List. Click Update.
      • +
      +
    • +
    +
  12. +
  13. Adding a block display for custom options

    +
      +
    1. In the dropdown on the left, ensure that Block is selected, and click Add Display.
    2. +
    3. Under Block settings, click the None link next to the Admin setting. Change Block: Block admin description to 'Recent Stories'.
    4. +
    +
  14. +
  15. Saving the view

    +

    Click Save to save your work.

  16. +
  17. Instructing Drupal to show the block

    +

    Finally, you should tell Drupal to show this block. Configure your block by going to admin/structure/block. Locate the block in the list: it is labeled Recent Stories. Place this block in a region and click Save. You may click Configure to set a different title, to determine which roles can view the block, and on which pages it appears; If you want your block on the front page only, enter '<front>'.

  18. +
diff --git a/help/example-slideshow-thumb-pager.html b/help/example-slideshow-thumb-pager.html new file mode 100644 index 00000000..1c83984d --- /dev/null +++ b/help/example-slideshow-thumb-pager.html @@ -0,0 +1,54 @@ +

In this example you will create a views block that displays images in a slideshow using thumbnails of the images as a pager underneath the slideshow. This will demonstrate using Views Slideshow and Image Styles to display images.

+ +

For this example, we are going to display a single image from each content item of the type "Photos", which we assume you have already set up with an image field. We are also assuming that Views Slideshow and at least one plugin is installed and activated.

+ +

Creating the image styles

+ +

The first step is creating the right image styles to display the images from the node. We will create one for the slideshow image, and one for the pagers. Go to Image Styles and create the following two styles:

+ +
+
Style name
+
slideshow_image
+
Effects
+
Scale and crop: 600px wide, 400px high
+
+
+
Style name
+
slideshow_thumbnail
+
Effects
+
Scale and crop: 30px wide, 20px high
+
+ +

Creating the View and block

+ +

The next step is creating a view for the slideshow. Because the block will show the images in content, this view is considered a "Content" view. Go to add new view, enter the following properties, and click Next:

+
+
View name
+
Photo Slideshow
+
Description
+
Slideshow of images from Photos.
+
+ +

Choose Show Content of type Photos. You can choose any way you wish to sort the content.

+

Untick the box next to Create a page and tick the box next to Create a block. Enter a block title and choose Slideshow from the Display format select box. Select fields from the other select box. Leave the remaining settings as they are.

+

Click on Continue & edit.

+ +

Editing the view settings

+ +

Turn off the pager by clicking on Display a specified number of items in the middle column and selecting Display all items in the next screen, and applying the settings.

+

Enter a Block name by clicking on None at the top of the middle column.

+

Next, remove the Content: Title field from the fields list in the left column by blicking on rearrage under the arrow.

+

Next we have to add the thumbnail image field. Click on Add under the fields section and select your image field from the list. In the next screen, turn off the label, select Exclude from display and select slideshow_thumbnail from the Image Style select box. Under MORE, enter Thumbnail under Administrative Title.

+

Click on Apply (all displays).

+

Now we have to add the image field to display in the slideshow. Clcik on Add under the fields section and select your image field from the list. In the next screen, turn off the label and select slideshow_image from the Image Style select box. Under MORE, enter Display Image under Administrative Title.

+

Click on Apply (all displays).

+ +

Editing the slideshow settings

+ +

Click on Settings next to Format: Slideshow in the first column. In the screen that opens we can choose the options for our slideshow.

+

For the purpose of this example, we will only add the thumbnails as a pager, and leave the remaining slideshow settings as they are. Select the tick box next to Pager under Bottom Widgets. Select Fields from the Pager Type select box. Select the tick box next to Thumbnail.

+

Click on Apply (all displays).

+ +

Saving & testing the view

+

Click on Save to save the view.

+

You can test the view by adding the block you have created to your theme.

diff --git a/help/example-user-feed.html b/help/example-user-feed.html new file mode 100644 index 00000000..64b2b42a --- /dev/null +++ b/help/example-user-feed.html @@ -0,0 +1,73 @@ +

In this example you will create a Feed display to show nodes by individual users, dynamically selected through the URL. You will become familiar with the Views 2 interface, as well as learn how to use an argument to pull in a user name and use it in a dynamically created path.

+

A feed is a data format that places your site's content into a file that can be read and displayed by news reader programs. When visiting a site, you may notice a small RSS transmission icon, whereby clicking on it, you can subscribe to the site's most recent content. This makes it easier for your visitors to keep up to date with your website. You can also use this format to aggregate information into other sites. For more information, please watch a video from Common Craft about RSS in plain English.

+

Note, Drupal automatically creates a feed for your website, but you may want to create feeds with specific information. In this case, a list per user.

+
    +
  1. +

    Creating a new view

    +
      +
    1. Go to add new view. Give it the name 'user_feed', description 'A feed of user nodes.', tag 'users', type 'Node' and click Next.
    2. +
    +
  2. +
  3. About the Interface. You have been brought to the Views User Interface. As you start, you are editing the "Default" options for the view. In the 1st column on the left- you can see the pull-down menu offers 'Feed', for example, to select settings specific only to RSS views. In the remaining columns, you will be able to add or change options by clicking on links or icons. These options appear below this main area. Most likely, you will need to scroll to see the options appear. As you make changes, these options will appear in bold until you save your view.
  4. +
  5. +

    Change default display

    +
      +
    1. Under Basic Settings in the 2nd column, click Row style: Fields
    2. +
    3. A menu loads below, Defaults: How should each row in this view be styled, check the Node option, and click Update.
    4. +
    5. This loads another options menu, Defaults: Row style options click Update.
    6. +
    +
  6. +
  7. +

    Create the RSS view

    +
      +
    1. In the 1st column, select 'Feed' in the drop-down menu, and click Add Display.
    2. +
    3. Under Basic Settings in the 2nd column, click Row style:Missing style plugin
    4. +
    5. Note, options appear below the Views Interface, you may need to scroll to see Feed: How should each row in this view be styled
      + tick Node, then Update
    6. +
    7. This loads the next options menu- Display type: select "Use default RSS settings", click Update.
    8. +
    +
  8. +
  9. +

    Set the path for accessing your feed

    +
      +
    1. In the 2nd column under Feed settings, click Path: None
    2. +
    3. In options below Feed: The menu path or URL of this view enter in the path with an argument feeds/%/rss.xml
    4. +
    5. Click Update
    6. +
    +
  10. +
  11. +

    Set up your arguments to say which user's nodes to display

    +
      +
    1. To the right of Arguments, click the + sign to add and argument
    2. +
    3. In the Feed: Add arguments menu that loads below, select User in the pull-down menu
    4. +
    5. Check the box User: Name, click Add
    6. +
    7. Scroll down to options to find Case in path: select Lower case
    8. +
    9. Check the box Transform spaces to dashes in URL
    10. +
    11. Click Update default display
    12. +
    +
  12. +
  13. +

    Sort to show most recent at top of feed

    +
      +
    1. Scroll up to Sort criteria in the right most column and click the + icon.
    2. +
    3. In the Groups drop-down menu below, select 'Node', then check Node: Post date, and click Add.
    4. +
    5. Select Descending Sort order. Click Update.
    6. +
    +
  14. +
  15. +

    Set filters to hide unpublished entries

    +
      +
    1. Click the + icon next to Filters. In the options below, select Node under Groups drop-down menu, choose the Node: Published filter, and click Add.
    2. +
    3. Check the box Published. Click Update default display
    4. +
    +
  16. +
  17. +

    Test

    +
      +
    1. Click Save
    2. +
    3. Under Live preview type in the name of a user, in lowercase, replacing spaces with dashes, click Preview.
    4. +
    5. You should test and find your feeds at URLs like http://yoursite.com/feeds/user-name/rss.xml
    6. +
    7. You can use this path for aggregating on another site. You can also attach the RSS feed to another display of view to make the feed link appear on that display.
    8. +
    +
  18. +
diff --git a/help/example-users-by-role.html b/help/example-users-by-role.html new file mode 100644 index 00000000..f5228be3 --- /dev/null +++ b/help/example-users-by-role.html @@ -0,0 +1,47 @@ +In this example you will create a page view listing users on your site. Through this step-by-step process, you will become familiar with some basic steps in creating a view, and familiarize yourself with the Views User Interface. + +
    +
  1. Creating a new view

    +

    Go to add new view. Give your new view the name 'user_list', description 'A simple user listing.', tag 'users', type 'User' and click Next.

  2. +
  3. About the Interface

    +

    You have been brought to the Views User Interface. As you start, you are editing the "Default" options for the view. In the 1st column on the left you can see the drop-down menu offers 'block', for example, to select settings specific only to block views. In the remaining columns, you will be able to add or change options by clicking on links or icons. These options will then appear below this main area. Most likely, you will need to scroll to see the options appear. As you make changes, these options will appear in bold until you save your view.

  4. +
  5. Creating a page display; choosing a URL and creating a menu link

    +
      +
    1. In the 1st column, ensure that 'Page' is selected in the drop-down menu, and click Add Display.
    2. +
    3. Next we'll define the path for this page. A page must have a path, and we define it early so that Views doesn't warn us "Display Page uses path but path is undefined." Locate the Page settings in the 2nd column, and click the None link next to the Path setting. In the options editing area that appears below, set the path to 'user_list' (or something Implement you prefer) and click Update.
    4. +
    5. Next to Menu setting, Click the No menu link. In the options which appear below, select Normal menu entry, and set the title to 'User list' and click Update.
    6. +
    7. Scroll up to Basic settings, in that same 2nd column, and click the No link next to Use pager. Below, in the options, select Full pager and click Update default display.
    8. +
    +
  6. +
  7. Selecting the fields to display

    +
      +
    1. In 3rd column locate the Fields options. Click the + icon to add fields.
    2. +
    3. Scroll down to Defaults: Add fields. In the Groups drop-down menu select 'User', then check the following fields: User: Created date, User: Delete link, User: Edit link, User: Last access, User: Name and User: Picture. Then click Add.
    4. +
    5. You will be taken through the fields you added one at a time. Click Update default display to go to each next field. Leave the default options on all fields except Delete link; change that field's label to 'Operations'.
    6. +
    7. Scroll back up to Fields and click the ↑↓ icon to rearrange fields. Down below, drag the Name field, by dragging its four-sided arrow, to the top. Drag the Delete link (Operations) field to the bottom, and the Edit link field just above it. Then click Update.
    8. +
    +
  8. +
  9. Seeing what we've done so far

    +

    At this point, you have done enough to create a valid view. If you scroll down, you will see a preview of your view. If it doesn't show already, click the Preview button; but generally this display updates automatically whenever you finish working in one of the mini forms.

  10. +
  11. Styling the view as a table; combining related fields into columns

    +
      +
    1. Under Basic settings, in the 1st column, click the Unformatted link next to the Style setting. In the options below, under Page: How should this view be styled, choose Table and click Update default display.
    2. +
    3. You will be taken to a Page: Style options form to edit the table settings. Locate our Edit link field in this mini form, and notice the Column drop-down. Change this drop-down to show Operations. In the Separator column next to the Operations field, type ' | ' (note the spaces around the | symbol). Check all of the Sortable checkboxes, and set Default sort to Name. When finished, click Update default display.
    4. +
    +
  12. +
  13. Filtering the user list to exclude unwanted entries

    +
      +
    1. Click the + icon next to Filters.
    2. +
    3. In the Groups drop-down menu select 'User', then check the User: Name filter, and click Add.
    4. +
    5. Select Is not one of and enter 'Anonymous' in the Usernames box. Click Update default display.
    6. +
    +
  14. +
  15. Adding an argument to list users by role dynamically

    +
      +
    1. Scroll up to Arguments, and click its + icon.
    2. +
    3. Check the User: Roles argument, and click Add. Set the title to '%1' (don't type the quotes), and under Action to take if argument is not present select Summary, sorted ascending. Leave the other settings as they are. Click Update default display, and click Update through the prompts that follow to accept their default values.
    4. +
    +
  16. +
  17. Saving the view

    +

    Finally, click the Save button to save your work. At the very top, click View "Page" to go to your new view!

  18. +
diff --git a/help/exposed-form.html b/help/exposed-form.html new file mode 100644 index 00000000..ca06e9fb --- /dev/null +++ b/help/exposed-form.html @@ -0,0 +1,24 @@ +This is used when you want to position the exposed form in the sidebar or anywhere else, but not with the view. Instead, a block will be made available to the Drupal block administration system, and the exposed form will appear there. Note that this block must be enabled manually, Views will not enable it for you. +To do this select "Exposed form in block: Yes" and choose one option from "Exposed form style". +
+
Basic
+
When you expose a form and the view is loaded, no filter is selected and all items will displayed.
+
Input required
+
When you expose a form and the view is loaded, only the filter settings are shown. After you select one filter and hit the apply button, the items willi be shown.
+
+ +You have several options to customize the appearance of the exposed forms: +
+
Submit button text
+
Text to display in the submit button of the exposed form.
+
Include reset button
+
If checked the exposed form will provide a button to reset all the applied exposed filters
+
Reset button label
+
Text to display in the reset button of the exposed form.
+
Exposed sorts label
+
Text to display as the label of the exposed sort select box.
+
Ascending
+
Text to use when exposed sort is ordered ascending.
+
Descending
+
Text to use when exposed sort is ordered descending.
+
diff --git a/help/field.html b/help/field.html new file mode 100644 index 00000000..e9d9e609 --- /dev/null +++ b/help/field.html @@ -0,0 +1,27 @@ +Fields are the individual pieces of data being displayed. Adding the fields Node: Title, Node: Type, and Node: Post date to a node view, for example, includes the title, content type and creation date in the displayed results). + +Fields may not appear on every display, because not all style plugins actually use fields. For example, the 'node' row plugin simply displays the node through Drupal's normal mechanisms, and fields are not involved. +For the most part, the field settings should be self explanatory. Fields will appear in the order that they are arranged in, and they will usually appear with the label they are given. + +If you add new cck fields you will find them under the Group "Content". Search for the field name. With new modules the list of groups will grow. Modules can add new items with the hook_views_data() hook. + +If you do not find a field, consider whether or not you need a Relationship. + +You can override the entire field section - see here for more information. + +When a field is added, the "Configure field" modal opens. It has a dropdown at the top that lets you choose what display this field configuration is valid for (ie, this display, or all displays.) + +You start by configuring three checkboxes: +
    +
  • Create a label: when checked, this opens a textbox that can be filled out to label the field.
  • +
  • Exclude from display: Loads the field, but hides it from general view. This is useful for grouping fields by hiding the group from the user's view.
  • +
  • Link this field to the original piece of content: overrides any default link set up.
  • +
+ +Style settings give you several options for wrapping an HTML element around a field; title, for example, can be wrapped in an H1-H6, a SPAN, DIV, etc. It can also be given a particular identifying CSS class of its own here. You can do the same with the label or do them both at the same time. This is part of the Semantic Views integration. Alternately, you can leave the default Views classes to identify the field and content. + +If you have No Results, you can customize that in this modal also. You have the options to count 0 as empty, or to hide the entire field if it's empty. + +Next up is the Rewrite results: all the options you need to rewrite the output of the field with tokens, custom text, a link, etc. If your field is output as a link using the Output this field as a link option, you can include the "Alt" text for a link hover in a Title attribute. This is important for accessibility. + +The last section is the administrative title, which just gives you a place to give the field a special name on the admin screen in case you have more than one copy. diff --git a/help/filter.html b/help/filter.html new file mode 100644 index 00000000..cb84f8a8 --- /dev/null +++ b/help/filter.html @@ -0,0 +1,35 @@ +Filters are used to reduce the data set that Views provides. That is to say, without any filters applied, Views will return all of your content. You don't want that, so at least some filters must be used. + +Some very commonly used filters: +
    +
  • The 'Node: Published' filter is used to restrict a node View to only nodes that are are have the 'published' box checked. This can be very important to prevent users from viewing content they should not have access to.
  • +
  • The 'Node: Promoted to front page' filter can be used to show only nodes that have the 'promote to front page' turned on.
  • +
  • The 'Node: Type' filter is useful for showing only certain types of nodes. Let's say you wanted users to see only nodes that were 'book' nodes, or a combination of 'book' nodes and 'staff-blog' nodes. This filter allows you to select exactly that.
  • +
  • The 'User: Current' filter will show only nodes that the logged in user has authored.
  • +
  • The 'Node: Post date' filter can be used to show only nodes posted before, after, or between a range of dates.
  • +
+ +The above list is only a tiny fraction of the filters available in Views, referenced here to give an idea of the kinds of tasks filters can accomplish. When you do not find a filter type, you may need to choose a Relationship before the expected filter will show, or to install a new module that contains the requested filter. + +When you click the Rearrange Icon you can first rearrange your filters, easily delete filters and select an operator: "AND" or "Or". By default the "AND" operator is selcted. At the lower right of the window is the new button "Add new group". When you click on it, you can drag and drop an individual filter to the new group "Group 1". For this new group and the default group you can select the "Group operator": "And" or "Or". To remove a group, remove all filters and click the "Remove group 1" button. + +When you want that the user to select their own filter, you can expose the filter. A selection box will show for the user and they will be able to select one item. After that the view will reload and only the selected item will be displayed. You can also choose to expose the selection to a block, see here. + +For exposed filters, you can create a grouped filter. When filters are in a group, each item of the group represents a set of operators and values. The following table illustrates how this feature works. The values of the first column of the table are displayed as options of a single select box: + + + + + + + + + + +
What the user seeWhat views does
Is lower than 10Operator: Is Lower than. Value: 10
Is between 10 and 20Operator: Is between. Value: 10 and 20
Is greater than 20Operator: Is Greater. Value: 20
+ +Please note: When using grouped filters with the option: 'Enable to allow users to select multiple items' checked, you probably may want to to place the filter in a separated group and define the operator of the group as 'OR'. This may be neccesary because in order to use multiple times the same filter, all options have to be applied using the OR operator, if not, probably you will get nothing listed since usually items in a group are mutually exclusive. + +Taxonomy filters have been significantly altered in Views 7.x-3.x. D7 significantly re-organized taxonomy, there was a lot of duplicate taxonomy related fields and filters. Some of them were removed to try and reduce confusion between them. Implicit relationships to taxonomy have been removed, in favor of explicit relationships. If the filters you can find don't do what you need, try adding either the related taxonomy terms relationship, or a relationship on the specific taxonomy field. That will give you the term specific filters. + +You can override the complete filter section - see here for more information. diff --git a/help/get-total-rows.html b/help/get-total-rows.html new file mode 100644 index 00000000..623b35fe --- /dev/null +++ b/help/get-total-rows.html @@ -0,0 +1,16 @@ +The flag $view->get_total_rows is used to force the query of the view to calculate the total number of results of the set. + +This parameter is TRUE by default in views that get all the results (no limit) or those which have a pager, so you always have the $view->total_rows variable populated in those cases. +But when you have a view that gets only a given number of results and no pager, the count query is not executed by default so you have to force it, i.e. in the hook_views_pre_execute so you have $view->total_rows populated for later use. + +This code will help you do that. + +
+<?php
+function my_module_views_pre_execute(&$view) {
+  if ($view->name == 'my_view' && $view->current_display == 'my_display') {
+    $view->get_total_rows = TRUE;
+  }
+}
+?>
+
diff --git a/help/getting-started.html b/help/getting-started.html new file mode 100644 index 00000000..e05af40b --- /dev/null +++ b/help/getting-started.html @@ -0,0 +1,23 @@ +For those new to Views, it can be a complex system that appears totally overwhelming. The good news is that the UI is designed to compartmentalize everything; this means that for the most part, you can ignore the parts you're not interested in. Start small and build your way up. + +Because of this, the edit UI can seem overwhelming at first, but there are really just a few things you have to know. The rest you can find through exploration. The Views Edit UI image, below, gives an overview of what you'll find on the edit UI. + +
+ +The Views Edit UI +
+ +Notes: +1) Every view has a number of displays which represent where output will be placed. If you're familiar with the original Views 1, you could set a view to be a 'page', with a URL (path), or a block that could show up in a sidebar. With Views 2, you can add as many displays as you like. In addition, you have the default display which contains the basic settings, but doesn't actually show up anywhere. + +2) When you click on the link for an item, a form will open up. For browsers with smaller resolutions, you may have to scroll down a little to see this form. If a form is open, the item its attached to will be highlighted. + +3) Overrides mean that a particular display is not using default settings. When you create a new display, many of its settings will start off using default values. This will be indicated by italics and a lighter color. If you change these values without first overriding them, you will change the default value for all displays that use them. + +4) Some items, particularly styles, have additional settings. Ordinarily when you update a style, if it has additional settings you will automatically see that form next. Often, you will need to go directly to those settings. + +5) You can safely leave a view page to go and do other things. If you come back, the view will still be there, stored in a cache. Keep in mind, however, that while you do this, that view is locked, meaning another user cannot edit this view without breaking the lock. Breaking the lock will discard your changes. + +6) Don't forget permissions. Views installs with two default permissions. Users with access all views permissions will have access to all views. Users with administer views permissions will be able to edit and change views. If you are trying to restrict access based on role, make sure that the role does not have access all views checked. + +It helps to have something particular in mind that you want to accomplish when using Views. Here are a couple of ideas and a brief sketch of how to accomplish what you want. diff --git a/help/group-by.html b/help/group-by.html new file mode 100644 index 00000000..871fb536 --- /dev/null +++ b/help/group-by.html @@ -0,0 +1,17 @@ +This is another major new feature for Views that has been long requested. It incorporates another module that has seen a relatively wide amount of use: views_groupby. This feature provides multiple new options for manipulating data. First, it includes the important “group” SQL functionality, and then enables aggregation functions for Views, such as SUM and COUNT. +
+ +Where to set aggregation to get group settings +
+ +Grouping is available for sorts and filters. To use grouping, “Use grouping” must be enabled on a per-view basis in the Views UI. This is toggled in the Advanced settings box by clicking the link next to “Use grouping”. Once you activate this checkbox (be sure to read the notes in the UI!), functions for aggregating particular fields will become available. The gear icon that should be familiar to users with any amount of Views experience will now appear next to any sorted or filtered field with aggregation capabilities. Choosing that icon will open up the configuration for the aggregation functions. As an example, this could be used to count things like number of posts in a day, or number of published posts. This could also be used to sum the values of a row, instead of everything in the view. + +Setting only the "Use aggregation" turned on itself does nothing. It only gives the possibility to set Aggregation types. + +
+ +Different aggregation possibilities +
+ + +It should be noted that modules that are providing data to Views are responsible for noting whether a field supports aggregation or not; modules that do not provide this information may not have all of their fields available to Views if this data is not in place. diff --git a/help/header.html b/help/header.html new file mode 100644 index 00000000..bc31b490 --- /dev/null +++ b/help/header.html @@ -0,0 +1,3 @@ +In this section you can choose one or more areas, by default a text area, which will display above the view output. + +You can override the complete header section - see here for more information. diff --git a/help/images/node-term_node-term_data-large.png b/help/images/node-term_node-term_data-large.png new file mode 100644 index 0000000000000000000000000000000000000000..4fcd19146f92075a6fac35bc9f3eb3e429fe7b99 GIT binary patch literal 4141 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 literal 0 HcmV?d00001 diff --git a/help/images/overview-ui-large.png b/help/images/overview-ui-large.png new file mode 100644 index 0000000000000000000000000000000000000000..04fbe90d1485ab7b9121237dcb80559099108425 GIT binary patch literal 83826 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∋7l9DmsW2%&89KcrJIOc@l+Lofw9PB37iK`(`a5J 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

5iG)jFQL)s;ZxXG1s}j*y=rh}Ma~^q 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 Y0S!Zc4;{&>J^%m!07*qoM6N<$f;kKr!~g&Q literal 0 HcmV?d00001 diff --git a/help/images/overview-ui-small.png b/help/images/overview-ui-small.png new file mode 100644 index 0000000000000000000000000000000000000000..ed7595f26b81039f74adc44e2ac70ac5c0c887b0 GIT binary patch literal 44890 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%0A+bA0=C&kv(o$~Ld0>%)8@jJ`L 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 literal 0 HcmV?d00001 diff --git a/help/images/views1-admin.png b/help/images/views1-admin.png new file mode 100644 index 0000000000000000000000000000000000000000..398c145b90e440c3a3b63dfde8278e40e75a9225 GIT binary patch literal 24372 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 literal 0 HcmV?d00001 diff --git a/help/images/views1-changeviewtype-large.png b/help/images/views1-changeviewtype-large.png new file mode 100644 index 0000000000000000000000000000000000000000..5c58d81377074cac4b1cd35c8e5fb4a2b2c18e69 GIT binary patch literal 37394 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 literal 0 HcmV?d00001 diff --git a/help/images/views2-addaview-large.png b/help/images/views2-addaview-large.png new file mode 100644 index 0000000000000000000000000000000000000000..bbad1b18fefef095764c98b71bc5a915fca90478 GIT binary patch literal 46121 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 literal 0 HcmV?d00001 diff --git a/help/images/views2-adddisplay-large.png b/help/images/views2-adddisplay-large.png new file mode 100644 index 0000000000000000000000000000000000000000..51c367551a19d7fe693205a5eacd957145d8c8cd GIT binary patch literal 43413 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 w3um>7vS-!?cmeZ} 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 XWFy;YSW1HA00000NkvXXu0mjfIsccz literal 0 HcmV?d00001 diff --git a/help/images/views2-addfields-large.png b/help/images/views2-addfields-large.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c1ba58c2339a28772927f64cff85353dc088bb GIT binary patch literal 29487 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 S`By&x0000DEhbbE(-=rZ0zpr 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 W$qzw9-O0@W0000A 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 literal 0 HcmV?d00001 diff --git a/help/images/views2-admin.png b/help/images/views2-admin.png new file mode 100644 index 0000000000000000000000000000000000000000..c2733638179d986ed12262aa85596acbe91964ff GIT binary patch literal 19994 zcmV(-K-|BHP)K6~^ofz2;Nj$xi-eTA)MT>c^z!5^hrFeonU#)?yw~L2*36sAbbN%Cylk%2dVGXbqSQ;Llezd#3#JuM8e2T0ulGM-2#E!n)WV+<2zSMknY~9erG?vr!?Bu?+ ztE#ueyU_ZFetATq+(?A12U?F@qRh3fs+N+HCVsT3%G`^-2eq_WZuZ%&*1l&ENc>s=SAdlvJwZdB5Z| zjojt#{5+Y|hPmAJ^Xw;y#Jtx0f`o*=yu1v6#H_yTByyxouH?qb)R~ry5O%ayvh176 z?9t}@RIcP~rp$zfl)JvnG?LuR%FLX;+$5URjGoN1vaHPN{M_E$l$xx{;q=Vm?61-6 z*X#J)=Jnj-?9%4+%%=>;ud(y2^>l1XHbRcd z;MWSX?T6$OX$^Au{nr2j1qVz=6wXeI)mkkgkI|w4QCy<4nNK+7Ohzv1+10!HF z2wd19Vg;#dWRnU%b9P#u{kRRs(ZBO{jDB?Ie);Gw_2(B45)SVVe-k|uwvLjBQDIOv zGzIl)^dzG@%MflPHH>YqF9I7vkLoyA9%s5(UhQ>XGi&qe0g`Zq2)i$1G+Lw+Wkb7y z_1hNnGX#+2Ft)vh_Lt7OJ9Cte_xV>SpR7~SiN!Ds%LLu%M(_@|%`G5*-uG>!c@9z= zk%$rs AGmRvo`;}RgTmLBM=(ugqJ2Ek!Gr3kMAK4y0Wmk9L0A&sa~pgFNVj`bE18BGF3<=sFmyZHQPr|NN<2ZE>%5ODx50n&6xskho2&~Szv z!lFdVSEl%i>9(Yzk?(@8{=grUBO6pWAdA!VVsCc=%ekz-C z*jheQ=#uwieqZRaYwWTgqHDor<-sqXYw7W|;PhkO)$8tMKFsr6{@07IKlleh;;;-` z`;Xn*1AUGI?efpVAxI$xfjG|SNa6P@rMM!-Q6D^p@H`omMv>F{48sdsm-kT|kl{&Q z9-vtv8M;;2-rOℜ_9^w@c+`6&grTZ;$}QmW!-OR#^2Gjp*u8o2o6Iwi84Li+g!u zP-_8!xY1s;sA9G`n^qFvNVQ#MBJunP7)S7fDf)aYv^V z|6TDlZ=N5IQeO@vOd$^cBp|| zi6NYAR%j81`%p!$EwfegiDG6l>3VMj97gVPgz+eMo}LqBa?ma!>U|wIZ!94?`H-#ik;F%7g&s$m<`3j; z_aYy#T)zbCTn09Sy8ZfWK1mQHbj>=}#^(&8Eoe^_M}#@C;9U=o8j67UT>-?bAM0mq z!-Jy8QJfu{yb1Wi;HYRD;I0-zb;DPX?00M{;m$Rnt4LoR93RnH?*1r0*OUY|Yt&|| zJJ?(PIeXVy)m!Vy`1gb3)79Svv_|WNSq>fk$5MNu!gAMC)DFL(MD3j~=9a5SaC6p@ zMvXv54p0EJH6Yo+>q^=j8}t2RT7Gzn|Dgm*fJ_&P0{*(+*7x}`iBOq+XfbtYX48m_2 zC2-9J4Z@=}f1v~zgjx;3C>HBmNTm#yK4A5^NA%JplNiifv5(+oN(u9+OvW*aN({et z+M-v5WQj8hXfa|#$KgZzmp(U1l9;>ijY)o9SYSo7#%!InN8{oBNm6Y;Y901IV|&`} zXj45MC11>e6Gr6O7MV3av{fLIEJ8nMW+Mu0w-^?HW%$s+&^FP}xJ%o24$>XiHweN#lJWSJ@!C<#b5urT$ z{o!(Yj;Al)eYt!)cz$txxXI0zhYx1cZ_|s7-fZ?&D6D`hCqDXMm1QB--O?;v0VRty za#Wz3pRzo;n2~I;4kBldwtjA$-``GO9q(U%d}5e#8w&qVHbib?rMC}-XEUs4BNwl)4hZ_AmRfkV)tul<4G$;T@0%Tuo23o0qaLe zrRGgkHA8BhH$Nq62{VTlpwQs+JCMoillIQ1d&k?mg=eoB>)lfYnwmMtd(gU;2c-(TM09ll;{flQ0V%vk;nU68+Ast=vc7kLfPn2Tf@)2 zSwOCcYWVw3-FQ~%J$s;FSa%?g$)={uR@fItq2Ur0|lIlJPvte1mtqfE><)1X68LwSU=foJdhms`}jrr zn3)9Fn=uihi+Zm|gpFiM)WK+{p0ZiL}TvAy6u<%mQGWceP45xpeny2iV| zs-ABB+y}eZ2qbekTR#^zI4FbNz%5G zKYiy>^<)yEdb`Y_?{7rCI*9SG626v%nY#weI6fetON-bbAChSn`2i)Vz#MmJ5YvWi zUKML1;*R4(35#8pIf4y-u@(&}O*ScSaU=i()|9iHO0huk-@B+DkzSc9vR>#!<`qqD z8yV5wS1Jl1iDYq#;tD}}jhE0IUHjl_OY~mq> zC{qnQ=7Z`q5zSAgy((kOlWUFLKb^QWtGmYC8RxDSqh3tQ-s#~&?)q$A?5dz#+!LBH z?)gg5`%QSk?V4kWwIQ(zDH6sHk!pOEUl8t?NV7WRk9{=~0b)13`E^j#A*>JWAc)0u0{9oKrwbcdn zpHp1^{!2ThwLTMZ2y>Wu4jh=n4NDA;kY_B+7E_Jow#;a(36exedE)|kFZwMnz;dQ^ z{y<6Q07Nkpoi#3{wE43*y!CJwLLXA)=1 zVOt|^*zlqz;7T@Piq5TIUWgP+?0M;P?>Qt=1dz~yHC9bswt%)!AxoFg`^!4S4&uG zNJ$AIOy>Rae-c7J0@!>5#N$w2ZbyL5$wKN#ayI}qES%}o9ShkW#!~hWJDXpOat^ZQTM6zVmtssnpVMeiKCBTcHQ*9t&gg78h z_=zO&m=`|kbBF#94jNtQPpLVMe;2?pcIgPi_!>!Fr0z=(uN z)QFNODmIdYRFo@U!=oMO)mOO;QH)IiZP{HAt z;)pE)_=dZWws+d!pB!wqAD^`Ms;`?z?e?I0)S=U$eOf~1o$XK?{)AWBw08Gj9K3n{ zc;n5pmq$${00Y5ZF^{G$O2b}x6c+Qw)X2B{D;<1%rPlEu2!E(s)5N-wRwL_-4$oq4 z5pv)Dc4l1cf@?LB#s#awcrJOx#vuddq4%;tJ}@;0+uPajMX7f5$6#%zy7S>d^Yq)D zYVB$1{GfAk`sjnoM)h!r37IQFf6-*VvWS2G?BcKIgZ`F93Mv~)cAC0tL!MlZ~JQF0w?%suAFB){gxJkOKl%)+;7 z%UB)P{{>WXZ+i^Eh1pzy{G7nE^|abKY;N{nXU&bT&j)MGCl{Um=6PrD;G)vs_~+eJ z(HIa%GF1GrHm$!a$Nl{;j{86NcdIw>vEPPKF)6EtO4V33qGD9j+^6Kn2fh5&G}@j4 z(8?Z6dyOQ@UuEB1m!~Ke`j4F})^Xg3p{iV@He9IL(m(=2?UjZXYIV3Km5@J3bNZp} z{0&i&G&X{?!F|E~fuxxwPg0RbYDSPTxLmsimZUh3@fsnk^3BWZu zm5{t_a`icqd3`)wcJq1OLF?nO{e&!T&-8JB?ur37g7#OeT>sOHyY86=+=+$#Ki!Uo zec!*}GxA4MBgdV(`*}x{@d_g{wV(^3`d5YluF-~)kPaL22bK0cvhhGB2A+=fvA)!o z1IPe5$DL)%X3+*Pu1HgYNJdhDQXC7o9g`0zxorq{k%YT3X6m9C+6EG!-$sC5to>dh zDFtD6-xvEUV{TuifO>>{knK&iNW}IPcH5qZ6wHUd83U<+5U-_Bjz@H3z+%TFUITOD z5&O_J@&eQ;C*)2KzIe% z#)kY*-Zx}sacqb5Qp=jXP+((YWGhuw4g z_vH!>tQRapy)4R}Ph|h)v*^7=S8hvbdRs{gn+~(+($0{48>OfVppTxp@;X91Uf6NP z3Nj;U*%3J08rcY|czi{T+F<}8x9joqe7jb+?&RVoM33@<0u^{zM< z_3djt+?YPKmtX4oQ0(fN7_2#u7EgFQDE7{IHbUa(28$=~3KG2SNaFM@sU{Y>-b^X# zvLQ~lKnGrd38ld}$Q-0voYurmBo?4~V`cN_3>L^nD9$8skg0cJGpuAmkP(h7CU96O@(uuxVxcLrB5-Jf z$?4a0US(pCNJ+$&zbEQDW-SRx4`kR79jAn03qfIk%Z3Q4b5+n72rWUlVThpcYhj~- z4JJg?mm2=*KaF8bcZ|TTR-4-8V`+xfgUfRbo z#3Zz>o|+nYRWwy`;Tl5&6u}t#fk}3;EtM8!m;@ksZ(3{%z_FN{Od^W2`3`$I7OOr6 z03*E<$1Jmp3yX9B7^or8x2fKU#~Rg)v7j@iHCLIF6;<1)R8}!476a>}Lz2-IE&u)* z@9RySpT6vFrN+xkM+ahOC;IY>r;7RB`sIP%iD{bX|HH3hi=(6Gb`2ew-kw>1dv@fF z?-s}I^bCG6J<-1}l|NV4yNe-V2^V`fEX0s3T&kxbiH(sYz^fUKZA?XS#)Y`iT-gvt zBzI%oarPL7-K=bh25k`*3bj*rP9MH~c=Y^4`Vj%KKbq`&;po5nCeM$(JaBum?~S)p zG;#2)+HYPtpa1J*zJ7oEd)?`ipB$}UyZ7F+r%&C!oj-YdU~EF`s(|Qr3p18&$DW0J zt+7ShfJ>XH#EmFC2sXdo`_5N3e90Ow&HUp=$tacH`D&e!r`*+dkF*$aF!TAq+2K<^ zF1lL*IGA6m*LU?#j`rq<`UgKc&-=$DLteA`{ zq2X&M7Gud+moX$lOz_D;I4XwVnLbv{qGSw(lw@jHhSQvim!j}ExW`y&Sh-8hJx(+p zpNhw$71#pY=#m~35?iz}lNU!PI1dcIDnL)0c@I=(#tK3NQ+@p2rfH_ZZQCM%bh@d5 zkFb&l&zWh#%`RgIYY^brQ6nsoAz7%6h|;YzaA_jC&d%{ zXJuQyjrpvLl95d5MN!PZjsbgFA-O;(;h+9<#G#ka`730$eCR55!Zhn5qA@F z$8A(y=x{Qb24PljB@#DTyVGovqf%U@a>oL3}1yiY?kYEJlH_L#qb-z^0Lgz7~@{u40JC&5>m7RC<3sInhEa z0RbU~K$l|g50v}P-3R>iMB?~7xxt{EGLL4i4^5=D0x+JKpU0312JY$NiQ2uTiFWvD zqeM3Z#CibG0qg)B;;J~H(Jd>BDhTM5?5Oc*W0kS>_1tQNtwyW}vm08reK~C>RZ?7^ zA^V5KuU@%xc}!d%l{UWl+=t&FIV@J|xx0!LGz6NWgGPD7 z0*OMg#)>;%mu}>s`#M)SQ;L_OH&z;q$z=i^i5${Vr{M#iA?e<5EY=enOZFDH#B2PU zoAWj(+!Mv6Vm_9SEfuHJ=H0}r^YiMKRfdP~vsddw6WGoUJ+^ffzyMZB^r!xhyPPGMP2xP13BCAP7!)c0&W;i!4ME3sHh#DnlChEMn zGBd@IR2I#WBuxetx_}`dMA9R1lmbvle zIbsN|EnDV@D0gzGI)7`fD*~cF&=G6s5Ob#Z@DF%Qcc^>!gspd4bmp2uLCc?>ic%o&ORjNCLi zfwrai?k>7xW9`$lQI~wm@(46Nt0U&nK$0b7A&}})hXJ!1jHC@ZQyy95*fu!h%;0gzinBo{HXt;Ns6#yP~2BQv%MmG4Z3v>`8A5*T8sDCqKjBxfjiX;0Ux+;FD8j7JS8 zw>NqKjs`}X+^C${`;JpGqHw+I}W!^v>CfDOgqp`8~HjSIEW7 zGh~L9WK#%Jl0gYYIr|SSV5KR!iv|upL)h$SaZqqg2uu?};p5|T$B_y`q}gXSH(e;O}e0;m-%_`)a9L`MVt%5IR+i;I;AtA&U$_6F{x`^x*#QCxB2zCl;tM z`CGskom&gZk7*Kswb}lwgTte-p{uVAkLhgzx%t5d^bTGZ#|o#1>fbA#BtH1?&2gMx zs%f(a#}<2^$?xik4fpcdoyibkNs0h?sCO{L5*%^$>7mg@krL<%RmB1kr-9rr0>hczLvbq#n!PX`_ffe(~a*3yRP#r;f$Kbw%RwH!sc=P&ri7-p}9biCxXt zdv+ZRYr8+*p$t*Hz@U&y3;{dcz9i1n$$`ZfPH>%5W~oX>F`A+!lt5SmC()?;4B?6) zWpDOK>ieWu6sd4KTj!UTkINh)jwSaGG5_1d<>MJtzN_snmVY-KD?T$gQmmI}cVF3g z0pY|D|3^gHatPtCay3}}$XIN5l5xQu)&$TS)y=!ivZ~rjRagyrb-%&RG(^OytaJ5g z;9-W?`1ajnYxtVo_5A9(na#~jWC+i!d256ATAR6mj&Hwp<;wF=slE05m47YmK!&JH z!8O#{a|qe#g^2J|IZf&5G!Vixj2UO8xyLb#4PBgx8oB71lHtTlXN-8ssN^>6iVU{@ zDn&3qg9k@Le*XUgQl9eKp*cjTeB6;k=);wy351JAmY_NMaVyhU$dw{DDw}`)$}+`^ zIpcy6JutO$!7vUOE6tgb^`PS40#$-%KMmp2!WlxP(s5@F(FlAEDoHuUA!TUZWRX?l zl2fspmF7Y-63H}Io9obD0_jZUUfNvXxKC#ed0HOtIRr{)4)LGVN!L(P+=3?&#w|L5 z0fJpxZ+&yfWe=QbM*NS)Du+B?K%O$>f9zaOYa2%tFNw&(y_68UbSvsZO=Gu=gu#N% zMRuHSK-i3`BIGnE3vThjVh;kR)uE6N&>VVqFTw@;5Nzm0!GgZSNXC&1CAeP5UUJB# zq@@R0j{ht=^v%qh?FxT^3i4*=&3kVLgZJ_C?0auK>b1uaLVE~>6J$p2XrJj^JPs(S zs-U_8qol8Mgc4nr$TYerT11K?!~t+rljK}#mj!2VJ+atAtr%@cxI+qA%**-0AjB~U z0rQnrB2h!aGB+Qabwrk`ZW$U34&hDI2q1R6q`4R^K8=62!~ zH9LW`@1m$aE8ub1nPYMi0#j*?lsSLCZ*Bj6>3Oqsb?2&51U3tD^$-2qBh4 z2y#n0MJhOBBV0I)Q#Pf8Xk#jnabk(a5(=c!?O0S$G!6-wKlgl$tM@30qo_TA0{4|F zWi1ea;MmcI0~JMF4gjMvF*L+*fG;?yoMVp%h7|M-AVg_EA6j(Fx3VxUD~4;$cK^KF zVa@#$d~kfoy6pH+{pH;ukcp* z5P*pcstjC|!Dq^2AZh94T9RAf18_ePbaJ_l2;Kt$iKM-`sF#VMBCzwPK2BlYzYspS zf*hh`vm&F8&x-AT85y>xl( zHyq`=>5=(%zwRin8-)0$AAN9jdF=-;@LECjb+VPK%*;;R+VEOmx~Y4W09x+S!Tq3x z`IQ{lJNE-G8Qfa0Osr0AWEQ^lA#+fh-gvNcJ-2cDVJ3UM@x! zjg?1w^XX>2fI<86hwRRYf?2eOaM=6%k2z?Z-d^{n4>qSi&Wtt=68|h@y(_bejcWEy zYqU7?`RMAs(M1=YOisS|{FBq{)7jbTFOx5x7C|iKQkChI(eIyBtCiJM5mMpsA%w&G z1^edX!e8%Rj2~_7&51#TSX4|RfY2U-P9l{Oi+X_cj@4y1&U)whdiTbD?{2-{t#|u) zwzR*W@4Ug#Fn`kRsv9)>^)pu1`LYNhAjC0;#Dupw6C{J|DrRp}8;KxO4T?*}nPL*Q zS296kDwQhQubr9o?Ci^*yy=zf?^nM5bHbYv@a!P7((p3g&PrpUkt!O5z)&^+V(TKE z{_5yx>+$&Rcql|H4$)yDlpNVmHVC2e5+$_MPEH766#sP`?2IudMje_WiJj=!#UR9` zKlrWKM=h+(8KluU#i zId*agfx?PP=oTRa@R5lKamW=WG67{Sfj%LjJ%lqhNfvQX1Do`*vXDbKXpev%pap5s zveKf*@h?PeAw9AVZI3ZAh!CCbrMd_q1Z8rErJdoxe}M>4u?o2~fqI}HQnpz85jjd! zqB00ch|Bbr;s~**AVP;1OfHL(yJ(jnv813sjnPNiTaq&qyVHJeYKp2$0OAa20JDbx zIG5-xSultYDj7N`0mUJ7zyzcNq6)3#Ol7Lz+)?MPb@IePoibl)H8SNyi}oiu!N2TX zKZ_ej8&7e#NOQWNSAk{0MXD)A*H|#s!8tsJ1+hXeVGU<_I~Q40IC--{0ugeu#s& z%dVIM(Xdd$rKTh}CfaL+)LOl3ECgXlfx>d$e?<@H_m=bxJ-?xE?oFql-a86e>LbuX zO}m#CA+m;z3T*eT3n0Rm-F^R}4{ZU95WQcp=ADQwgw*K_Q1hTp&o3!Z?DxKJP2aBi z7geh8>%+mrB@7%x;!P70D>#^iv6 zXo?eaKm>?+fQTG3A`lM*LX5V^J|F{3h3sT|-KrI#U63%2@F_wx{nqk5KS9Wvlx2{k z^f?tWayw}C=Bx9IpZcxl`Mm$=?IRdSTMB|;4GFFJ?a&|u1RfXpX`G)8>7I`=@aZ3a z!w$OO#}x0jJYmA^lK6xZR-BYEPF%=NyeQ6RB8^8BI|X4LPYD@~36CJl)29g;@6WP= z7sQJa$m6ucn0tg5e8dSkS>!Yu#ZjY}g>KQIX~%_0QIIfN6l5HY_}?Hz7`}<#z-@$> z0G4`Tl((UhUOTI6c;EZjyDq2A{%Q_t1q3xMOHPA$e04cG`mTMsp9oBko?Sg= zKeY4q)!Bpp-!V)kr{YEAby)0r9Wwra-nfJg@d|=*!@B}34w%b4H!OI}NlGC)URo>vjBbErU z6e2W25x53 zho^TgvKmRqNT*R%Nijpouw)@yFd=xz0GXeZaU-N55B-o+^jN~WF`o$*mV4EMy!0rB z3&z80A!DKMbBZ4_&o5ELjS1t7L*|zgpI0CFoF%2$M#xMAb%Iv4*2+@F@_2-*jo1qG zIFJL?4*-ng7&ung$)OHW+VHz#kcJDv9gzr3xL-XqqmW_fhkW)IV|NGmxqJp@^`3mQ_I6vf3R$>21`Rhbpp_7m2E2vfBo^@@YpS) zvwe!#cJq^cul@8)+gLk<)Vz1GX=T|NF!rxWV$rI`!Nuz0;d%3y>EL|uwl|;k@4vkN zv;0}0X{+D5F&TPgNZ+!uvSD%5jvoK%{IQRi{Eu|rJ&!J*x9JWc2$uKmXx5MMLyVXb z%4~&CxW5!uH_K^rIe2w%zFNJ0ICWn4-aqP9q|)odd2?<;z_M0{A}gyyk~WFrQJA`X zJjA^Q7DMmBFm3E~h-|g@t#?hQZMP7j%_v8$GIs`5-=+jr(L_>5);I%WkAcdRfHsOL z+>Z7pRBQIQyR))XnHfK1=IADH0*erxDXgl%$Q}(LxkOW~)_Fo&Ev|2*)2i0St^%En z@7D0-00La|FL3rcW` zghExc$=q@dH3gsROHsg=q8=siVG3iJSUTiVFIX2FbW0!@S9C=;I&@~<&WsSWm#PIp z-~PQfv%80RezWh*`<@IzMzq{3qdILN2~jE~>u;ses9d3nU(_-^ND$Ej$8JRMgtQPJ zjs24o;59){P4ZIKP5K_mX-W4_jrp&LXbX{Rff^PEId!N5K_EuR4w1T9E9Bdw%wVy=PABgj z<&U0rjtZIeWc&Ko{5p%``)_gISFI0^TS$ZYqgw)OouB}rmQxLIoPd`wYsn~b;TEwA z!RN?%jbonnZvHFEkZ5bd4uyV)A99e14`m?=2Bd3s2>i_ht`q{p!u?dzE`9+E%G_mG5O$9 zn-G&p)NIr#;q9amCWxp&Krpix%LvS2gn^%dvQmk@UoDBv6oll>n@gB&Y>Zs#-Y!H^7odo~)hbNzNcKN_{K3&RK`0T-gc z)|3Vx+<)>F`lJgErh^{_rSHI z*M{Y4B!qtu4RV2SmU9BE5;S>sH}9qw6>r3tebb^*}B_k zoi=yN%~E{Ms2x;SyO-HdZl^A*@%_~K`F?u&&h3gve9LHBt$K4|H&u$4nsuX>ptD5e zMj^MC{OIvJFZa$~E`_f8hUokfkc`yzT*Id`ag`}@iJAipsR~4#A!i;xB9md-&jah>vmadacz;mr_GLXWl(f+p#ILd2IA z0@b!^*{P%A5Q4y^0zDx44H1z#GHZ+d49kDy*U{^ChIzT8!nM$iRFDo@K(o9H{mI_- zw8oKKaS?tBTOG_`k9V}p&LLgZRb9x@W*rjHY+^O4eqGZeK^3pw zzu&Lw15hU)D~4voj0J8(a%{7`9;#JeF6=hBcKF%nJK6r7ry2e2{cP30_jxTDvTX}^ zaBxWLYWYYV|Ch$`zjjEkuc_XV2@sGNqRd)_vCXA4_9AJGQvqd(GbwHjjW3R)xcqtTF2f#oI3i3mw~G|qX+VKf-V z$a4_GV#Yc-BZ9~&Vj&bSMb5ckGJruy2-__3^N6yU2nFM0woG6-AqFi<8H=$ja7jfV zxV(g+yh{YDz7Ln>ro;{=O`Cd%-vh`J5w0adOuYnCbW5Fn@)~lTtb^5iHpFZ1d-82h zh!UaXvS2c1T`oJJN+*}wyw~9%7f;5rD0|bTVwW(9q03ot6n@z`oGu`gBqW_sUc!j8 zVmMt4ry)|{@;L8=ESktrGC7tZlt;bdd;u&tl$|2ENkdXQ-j;>il0P?Wh)!)N*wPDI zAF8b(M&?cX;{z63P4@|S4+AQvVFi2`1EugJZumOIIw0pJVG@)H;gWL}&KRG_2qZ~( z7y)4{%ul!D`WA#pWyzCC$P^E`T0&4pn|wwH!-<-4$(bxP^m?Q)EN7}hvLHMpGJyIk zJm+*XKFv9&H*1I)UdNwmH%s0?hkbDTP4s3ywY*w$H7IO%DvVPJ)1=PxH7|G@^5^)A z!NJMFuP@6591Nyvs$P#DbstN0!d?%im$x`6hz{C{bxYAa_znQ~IaalRlvi#f&3Ui7 zM|aBr=w<{Wvft7d(p-N8=nOpGXhS@9+VerMavEZR)V|*^p-do7Eyj+z39{Pb?1ts< z+G$BmhhtD3p3DXZKku=}Na=8V*kPZB)9I7o>%aWr7vj^CQ+o_vK&(O{lwNv))+QL;KZK6cKWG$fAiKqE#837BHd504o zK6}VjM?M;;#qIjB=J($NfcsayaQ&IyIQrjbLtMhw2-h{lmUgL;>c;N|l%4%O%j(MD zYQEZ91RK|HawG%d?B!i?8}Gt|+=B3T^iV^l`}g9_&0} zs*oMd1~U@&6tICdh0hdY^^vhnr&T9=n1*y2A<*`z;P*K6TDwX1g0Ss8k zfvFzP&s9NwatiD&p}Z`ZBr_GjkQFoS5DXST|JjB#CSBX}_iTtW4i0rx-K3R%*F0B_Q)Lwc z-$R|~I3$l6nE+tCMvMV1%JTGzrZHSSKV5600!rFm$Tu^)Y!(W^=!fS;TZ`2RsFCKo@{b_uj z0{yH|=f9hMee%h29fsXBw~Kyvhq0voJP@lPTJnWm>Q4$m&sn#ZbJF}wSrP-5tLPs{N8MwEzwv)Q~=f?#rUzzq;zrv)#H~(FafW=c_E+n)hGMvu{^T%FxVGF=kwLUVaK14(QLEQDUwsAypA4Dni zqJ!c)}RUBhk(@zdMG`XhJJzcQgW~lK|R{k zhgTR!ST?kjkU6La2XZL5r5Jnb7OOt=Y5wfnU8oevhR)2snc10TBn?k*X7>Hwlglg8 zWC-b)f`{ydaytHE%ADWY*aN98VI$+^r(ol3+3y>a70~N=GJ?q=>eC<*sEO`SuBmG( zbXO(QIt1O&2UNY&4(VD-u*==BZ z&upxvnq8v;B#A~`kqn6&8U-TJDANehme)|!M3vNy1RWe-ZhY#jb7AT(o%SlpVPr+z zZJx4o<%g$?R?IB|s2w`onj?A?Zv=3!=SJcT|(=a0y9~^K)p0=GEA6_ zl+-6uLu36vDAU6v8s$y>eFhJuyRz`BGMV!f)0RlIeH!V>fr)sE z5wj42DpczsY=}BsqyrW0;@;Tp#pHw9dSFFE(k;a|$~T3t*@jlmD2`~A)wFqc@)aRH zO~^S>6ck$kPlcR2!SJtDfMqE?{-s|L0F!Z;kuz`P1&MI_kB8;;xOk0-2a#4#NmR#qZs{dtLCILmR-m zV+k9)G_NxUfLk+26oLvsU~(F6uf*3-CxY(@7u(pO|3Tg5hqfi4xyn5U)HID8iAVbP zV*MJ298Yw!X(2dN{(>SCaYM>VNPyDvRdvH5p)x6`GhfW@3StbwT{xb{mxWJ)VzB)p z8w7!SI)5Cr=3Cze=fQ4hAR7!g#3Fo1N}T-i(Y-g^$2GL_w^KkpnTof0-+R8_@%Evu z5mX2D($SIQt*&i8^rq0}p0{)J>WnvaGtk~HL=u@G>^|7N!fER~XnlHI&Z4J^ zR{4j?y>e?Cjm8G3<=c`gW4VB%=kOvAJMaAdW!C*8eQSDgZZUWBYP-5wt0O+A*2zDb zMFiDXOB1&zR;q>B9)jvd{-^b~A5O1cd$T&Xwe|HXhvhqn>{+??V{Wccs@4k4O!tDm z5p-||FdDh~iAUKq?0_P>@yW(U_Vd>kdI+lbIE!*f{6HP4f{Oev5+K25(3rs`3(_NaT(%jQLdHdkXmDMri|?-AZr;B z6Y(ksS+Ozsa@~bB0xgDnmMDjS6li>>1dW-Bv!1@?IXwhb%4w8fyIqS zI&joEYvj_6+#Li=t=B!qz^ge^%&RkRwNh$0jm*KNV5b;-@g^bXhYmx31pLG5b`d{y zcEVWEZnC6qubvF^$2CUM+zK%TE}g|=B)|cd;Q0}}O7AoHK&d9tbF2qwTicR5&ub*d^&f)pK@hBk*&#FoV!%SB<2hvN$D8M7ce5R` zNNdt>G^MI(nT2_L{ObMsJjxiN2mLy@eZhg^YP` z0o;2}$H}#%aCe<_AWtGc((N0usx$1Rjx58E+F?>lQq_+|46>0ItKUJS58};@{?ewJ zjEnB6coBye zuLvO-@yeO+($-~>drr-sSb55jQ$s&D8yv)g&7c{Lq%7RBeA)9OKOFBf;fu)ilgJJI zP)6`@o5gJz%|#Itue0yL0BdHIT?C@e;7KVC>nubK3r2PZ+LJ|k%kx<*G z(QJt#2U%4v8UP?43F1VWoVxL{ZQ`AhSS%asd@W;lIDTeRxpXC?Nie9Bq8|d(8$k!U zy8LhZA=ryRCZ1}04SW8 z8jkHQRM2Uw@|&NZeEsq6$2ULOZQgqOmx2*p3YrtMAe{|jj}Wroe;~xzbJfumTBsSJ z;yNLsN{b0Xh%8jl8Nd7FlaJqCfAUFld-vw=HlxphDj)5IiyU_jG_I07^`3axUOHOZ zyuJA#z7TKiHlyv6=A(_MIq0(rAqE}B*|VT9)EeY*7sB!qR^R@7``2IJ`t#A&``dr> zZ-GP|BAUowq?@rUaGk8)Z8hJz)l9b~z-LDT@1y@e|I_b(G3EIaP)$d`uZ#+`D5tJJ zBC;%N^8GO^oJl4*W6WV19p*i|aFO?qk+5Qa$c?t>_XmA) zPQ&45OSddLSdzYEX{ctH<|4)LFZTCfSC4hofzKGrOZg!cLNL6Jz8of*YO{nO$1KMz zeaLYJTUMmxpgDRHuhVEqp7lds42KFK9Bhc4T09XUFblYYoO$J72sbk7!!@{}yBe}( zVP5MQCWqw3`lxIOhMG8Vq7bbioe$-%&^-HNrJ$VU-$2H3?vYvrV^oLbWY(pgWsDV~)IA=XmcnmI%WT68X{5tp0F#Ux|m%j0j4S)Pq2Y-czbPV(V+QlHEN zv)(->QZRf{%osjurp>q7Pd49wDvD645Q33F$I!)av;{RxIZ41dhhQ7mld)3P^1F@2 zV3dsx4~M~|GkWK^)19gaI7ah9YZ(!e75K9DA6?wroKEfjXaJ%h6++Cnje0;942uv$ z<%7)%$a1Y038Uf8`urELxb~N!8XK|}1PAr0f>@@70{3b$6y#*oWhTPdy}ji>!xc~= z1n){x|Bypb<2JG3P`2FqES#GORE+IRmpZSr=_|X)8Dm4t1d}?5^NDPTmc$p&N`(+q z$1HO}2%#K_5ot^edg9Um2=ogZ)v1&hEjn>Nm0~5W5aJXlmDi5)Ov8c5A%9H86M^a5oW~*N*RPyG9%wdppd7I zN2@4&n6c28857-aM0fJ;+UV2c&Zh?uoE}f+gWzE8un{ZAs zvxU}%%=*g#6{8O5(sNiot~bvAv3_{iSZoZPVANUbE_bHh`oZ5j!5Ox*OU;}Dr4dp| zA$d#LRzwKVUSTY2);`TI^4!TU^ZfGc2bP@;FHi5BF}1&(t_E)$Ga`0QRT+g;2qCII zj%6d5Q=g?o0CVe>2t^xp0N{{;;Uu*)?g8CHdn1z+lILX*QV9_6E5^$?B18)!1aKOg z7OD9hOc+ISr6EmDs#aug1nlbNXo*VPvMK>|7QmACg8?K((7z#p*#){zt7>odvu_2WJ&QC=~ z2lVM`w!;nMy~yyV9TDwcFrGPQ0Kgdl%Rq8)z(l+-hL1LgVp%1mGU~(~j`v)rkW=TV zDP)NaeP4we|GFbajmy*Xd@?*=AF{^q>?}XiP@|$CLWeq+R7hpi@p}8?CePaJri99p zSa+=lOJ?W)w>AlWHQ!weg8AUM`$i*ZeA~UmREU`qMvZPxIfPUK7=5>?BrJtPgcy}L zR8?x30VFI>^)Pwo__sSju)N%ubc0T#a{$&sw=~#5#H8h8S(T?=s5`_uhnm$6pd~kc z3FfD`lV>a+JKAu3Vbn}rB{p&7Xn|DfG_m$ltoD8hfpz=7Bj)3R`~xIdLkAA zr~pKd0f#}4M16r(iaPV_L?)WLAS7c9L~m~yCpIG1Bvq{7gC=-*C8 zSfNYTmDvzToMa3?R)w^)ME98hGaNEh4_%mxgme`NB=p9l05}mzB4ZGu7s=2UadwsJ z!_+JxL?d8^5Hc4u!4r$K$G*#S>hN)%v*8c(@{*nA^LHdwHrT+{mEN*s>m-aJc0)Oi zeIdYGfSB(m<8&O!xpopuA^bfzVaW-I2)wr9(~x19?73kKiE2R~$kdJY9td$`yX6hS zz7!%-|Mk@<1Pub&5cLard-Wc;rQM~bb&V?)`pSaLgVPy(c@PAh`EL)_x|}K-IAtQQ z&<|PtPOSL-LAvXCTS7Q{(cECX=cRqN>c;IjZ42Q)^jb%A56}1ShkFqSXgPCR@zPPh zDSZ$ER@`*pr{kVf%z^`N*A-%~-%i_CcFUs76qWU8Po1u;WHzi>$NB}yq5HTVj1KGF zPIoPsbORHbw-u*|5a$YGSv?7E)|1kAq%VZLFQp5K?>!Ae9~?-S5D;e4g;1)63C$}p zmWg_#5CUSKooUDxk)_HFd)H{=)vooBolfxn9w#wR&O2EXU zEJ$e0YG@-~xMCY&uy_yzhyPp*I)6A={$tSSj@AyljqYLtJsiE3wjmWlSoWN6TT_j{ z{70Ga(WE{Kf`ilJPH-^4b}$*8?u_1eZ?t~!1rp5R-^1&~`3kRf3ip{Gg^&`j%mm&u z#xe$%*>WRe-$<5C&vLpmh6Z0(YePaO2_@-9C}!8h(Ffmzvc!jN(Q}oXSm>_nwbjE2 zVmI;NdaB=__f>0zlz7ol189+e>$05ZylPoVkr}6jXB_u- zCAnIJB=^MXg|`(qH^Tc&fY8XAGYw{{n?-tqQCWL-zmx002ovPDHLkV1l|r)VKfu literal 0 HcmV?d00001 diff --git a/help/images/views2-changedisplaystyle-large.png b/help/images/views2-changedisplaystyle-large.png new file mode 100644 index 0000000000000000000000000000000000000000..09925df87fb64afbdfc3b2d44b31c70fc19015ad GIT binary patch literal 43090 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 z

tjo5B&0El@}qJK07v!iUS--43#k=y%i34gd3k4Z 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 wjEaO@W_+x-w64kG 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

5(y*}WABszh~QI0cyF-53X$we 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 KMNUMnLSTXgr>Y_V literal 0 HcmV?d00001 diff --git a/help/images/views2-newview-large.png b/help/images/views2-newview-large.png new file mode 100644 index 0000000000000000000000000000000000000000..498627a90940b9fd7958ceab239aada9d6b1298b GIT binary patch literal 36263 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[wDHT>?(2tcDiVS!pCiFXond>N(;(WJH52 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%0DujBUWJ_gVQb{1{82 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 literal 0 HcmV?d00001 diff --git a/help/images/views2-rearrangefields.png b/help/images/views2-rearrangefields.png new file mode 100644 index 0000000000000000000000000000000000000000..562df08d6858b4d33b99debf0df480ab0dd70c3d GIT binary patch literal 19129 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 IM6N<$g2xFM!vFvP literal 0 HcmV?d00001 diff --git a/help/images/views2-tablestyle-large.png b/help/images/views2-tablestyle-large.png new file mode 100644 index 0000000000000000000000000000000000000000..67e9e6b9cc0b8a4bb92d0c2aedede6d91e071571 GIT binary patch literal 38890 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 O0000n|xXT literal 0 HcmV?d00001 diff --git a/help/images/views2-tablestyle.png b/help/images/views2-tablestyle.png new file mode 100644 index 0000000000000000000000000000000000000000..f8997403ac2d266e1898c1b760b218b81a78ff4b GIT binary patch literal 20917 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⪙eqT74x41PI0t>-6AqnflhYfJ8pz(#lh;1g`6Is=4HR!Gfpj?0Y zbo-HJSN>oNOA2CGi4js(?rVv+0p;?* z@Ef=hvP)e!fMf356=((rhYzL-c4`jpfYo^p(l^o-V#EM!37p&H<;##H{gaCAS>QG> zs%V_l+$DCuQ|bOkC52TPt+nYEPUsiF1wB1I0jNe8#%}BE zEF~ZXkgH-q=11ZfP%uqJP+oX@u;y z1cdsyZ61_B0ZZi2`g2>S`Iw)|6f&dblYoG79LoQmA_NpZ!0`Pt(W6ZUI^ zbYS@TXJpVpI6+e4-@V%dxE5LUV1xxQ=Oc$D8JHBj{mkNSAv5rDi0f?4Ldw^#UqgvW zg@r{9Pq}PUfQV}fJ6U2gfmfm6tM5^N1L+1^#Q#Y!&Fm(s1M~E$WTY{NRiU>az6w}0(p)gb1;+N62gk|A4VGEz)gXX7MKJ9 z-!pPe*v~I1x;ZIail&~22-^S2_kQzauU25+rG-FjLP1#DvPMO_^Wx{bTf^`~5iH0) z2$3lBOH1#Hi#g#ohrEx#E}f`LHPs)gvzrq!>xpXtMH@s>0R-sC2uQ8>KyS^g$MT<- zgYYnv9ipk+d;RmFGwkTor}`lqtKs3{@}>E3?GE;DT}xrI3QFC9@)zW&91IGPig@yY zDKjX7lB%NDp^=qlwmnr zCNSVEL@I(GadXKt;?L}x&`@l+j?N=@(5jIWcL2f>u!sYN2BMTgu8-&Hyh?|(b##!v z(Cf`{Ul2=xQJ8|d@rjI^SyK;w$bc`AHc?d`0~hQTNlazTm%=4 zdN%(2nI_}>v1k?YDnrK`7)qtE=>1MKc&O8^pimH0zCNyp3c&g74C4TDi;0V`jpRlj zaf73Ietuqbw+N4Y;#!^xkT;YD5WB&QNrAAW(~*{w!+;SuJ^}TJH$j??p(!;^YUu<0 zFsT9~LIG)vn^WbX;ARbF$P;U0=sRQxmpOo(+S)3GXR+7G!N`6$ANCb)9A?pCVq!=V z5)#I1+#CzUH5QhOeXhI(oPMWx=?}L3foGV$7nesp#+1EY=m+KNkwyQ#*o{KNFYK(K z7$QgatgIgRLd3me%msS@Mv5Mpv_|};d;;DEQ547HVAs+$S@d`9@lxpk*|DsxNfV^n zDSdHFbu4vPli_Ivc=oZZOyhS4-z*ZJGx`M3(bM1LbW|NCGQ-0G)I$R5kT}7N{a*GXEv`D~x&(k)OGx^~HY%U)qe-ZW#TOWVKv$kL zI1zE%havqQ=vYY33{@WZIxZDe@~RV=9JD^AE!=R9~%+%&wSBFz$L0&Gw{O8t zYs-o0>Q2l5SWU~y@`s)X>hV670kGOMT5atP*8A^Vi5x~gBYCc?NOcQP099R%XTdmP zHP=0^|JR=a9!L9HBEZm*gusUdYHDos1(gx(tVWpQuy=BTs!a9<^w?*$?#}QKUXZBc)o0?Ly%YfFxD^iiR6i^D<8xS(t7;`?&_c41E^rYAV&is4w3C2tcjNCX(3I$ ze_TXFqr_)tL$Z5)zLuXpeL~Ix17vL)9K-{@08dVrdZ2*xihSI_X>&ZxWzH)aCL))f z8;qiuaU`4Hd1zv2n8I58kWM`W9#%h-Kq&19#ksjM7W!J=zI}VV)`J&pzBE9Z;Dzu_ zVZQ{BKnhZ7G`qnH4Ck_bXCwUvW2jCCx<1nV#pX)0UdMV{4O}3EcN|@U^A*EwO$5j; z4RRKNoUWX!_c&$))4d7od8h>-WyP7lR9$(@9g8~O{+L?~eS(Hj@;%m5d{WW?C_G>V zK{+hWZa|xa|0-Y}`TqjI6bBM6f~W#d>_YVUA^muOGe+*lf9D7Cd*CfN<+@fyIZN10O`j$LQR_LQbWA8K$jhFDBL$%>Ni<=E~ z*2*c%kTeaQ literal 0 HcmV?d00001 diff --git a/help/images/views3-group-aggregation.png b/help/images/views3-group-aggregation.png new file mode 100644 index 0000000000000000000000000000000000000000..ef93bba66e1683101dc6f9f039f32ca7e8a9be3c GIT binary patch literal 55825 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 CirHKM literal 0 HcmV?d00001 diff --git a/help/images/views3-semanticviews.png b/help/images/views3-semanticviews.png new file mode 100644 index 0000000000000000000000000000000000000000..e01163a794a1ae8caa70bba4e7674a20beea2612 GIT binary patch literal 5611 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 literal 0 HcmV?d00001 diff --git a/help/images/views3-views-all.png b/help/images/views3-views-all.png new file mode 100644 index 0000000000000000000000000000000000000000..27ce88ed40ad4f019ac10d02458731f388ec3c48 GIT binary patch literal 38583 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 M!l`#8qkI2n0K%^A6951J literal 0 HcmV?d00001 diff --git a/help/menu.html b/help/menu.html new file mode 100644 index 00000000..bb818a39 --- /dev/null +++ b/help/menu.html @@ -0,0 +1,21 @@ +Page displays can hook into the Drupal menu system and provide menu links that will appear in the Navigation system as well as tabs that can be used to keep Views next to each other. + +For simple menu links, there is very little you need to do; simply select 'Normal menu entry' and fill in the text for the title. This will appear in the Navigation menu by default; you will need to visit the menu administration page to move this to another menu. + +Tabs are not quite so simple; there are some complex rules for using tabs in Drupal. +

    +
  1. All tabs must have a parent, which is the next level up in the path hierarchy. For example, if the view path is 'example/taba' then the parent must be 'example'. +
  2. All tabs must have one and only one default tab; this is usually the same content as the parent. +
  3. If a parent does not exist, when creating the 'default' tab, Views will allow you to also create a parent item. It will automatically set up the URL for you when it does this. +
  4. Tab weight is used to control what order the tabs are displayed in. Lower numbers will display more to the left. For tabs whose numbers are the same, they will be displayed alphabetically. +
  5. Drupal only supports 2 levels of tabs, so be careful about trying to put tabs within tabs within tabs. That won't work. +
+ +For example, if you have two views that you want to be tabs, you could set it up like this: +
    +
  • In the first view, set the path to 'tabs/tab1'. Set it to be the 'default tab', set the title to 'Tab 1' and the weight to 0. +
  • Click update and you will be taken to a form that lets you define the parent. Since 'tabs' doesn't already exist in the system, select 'Normal menu item', and set the title to 'Tabs'. +
  • On the second view, set the path to 'tabs/tab2'; set it to be a 'Menu tab', and set the title to 'Tab 2'. +
+ +With this done, you will now have a Navigation link named 'Tabs' and when you click on it, you will go to the tabs, with 'Tab 1' being the default tab that appears. You can then click between Tab 1 and Tab 2. diff --git a/help/misc-notes.html b/help/misc-notes.html new file mode 100644 index 00000000..8efc1f9e --- /dev/null +++ b/help/misc-notes.html @@ -0,0 +1,11 @@ +

Image Assist & ImageField Assist

+Under certain conditions these modules can block access to the header input section of the admin. +They need to be disabled on certain views paths. To do this go to +Image assist ->Access settings +check "NOT on specific paths": +and enter + +Drupal 7.x has different paths +admin/structure/views/ajax/display/* +admin/structure/views/ajax/* +For futher reference please see
this issue for more information diff --git a/help/new.html b/help/new.html new file mode 100644 index 00000000..bee52e9f --- /dev/null +++ b/help/new.html @@ -0,0 +1,131 @@ +Views 3 is the newest major release of Views and is coded for Drupal 6. Views 3 retains all of the core functionality of previous releases, together with a completely revamped user interface and a large set of new features. + +Major new features (these need help files): +
    +
  1. Views Or has been incorporated.
  2. +
  3. Semantic views have been incorporated.
  4. +
  5. Group by possibility.
  6. +
  7. The Jump Menu style has been added.
  8. +
  9. The Panel Fields style has been added.
  10. +
  11. Pluggable back ends
  12. +
  13. Pluggable text areas, pagers, and forms
  14. +
  15. Translation plugin
  16. +
  17. Exposed sorts
  18. +
  19. Reliance on CTools
  20. +
+ +Everything below this point is subject to change based on the new Views wizard and UI in 7.x. + +

Note: Only UID 1 or users with the "Use PHP code" permission from the phpfilter module will be able to import views.

+ +

Admin interface

+The first thing that pops out after you install Views 2 is the radically different admin interface: +
+ +Views 2 admin interface +
+ +compared to the old comfy Views 1 interface: + +
+ +Views 1 admin interface +
+ +The new admin interface performs the same functions as the old -- listing all the views in the system, providing links to add or import views and a link to Views Tools -- but has been compacted, with each view displayed as a paragraph style-row compared to the table of Views 1 and set of filters on top to ease locating views among a large list. + +Context-help is available by clicking the small blue question-mark icon. Context-help in Views 2 is provided by the Advanced Help module, so make sure to install that together with installing Views 2. The small blue help icons will be available in various parts of the Views UI. In particular, look for them as part of the description of a display, when setting style options, and in various editing sections such as path, menu and the like. + +Several new attributes of each view are visible in the filter header: +
    +
  1. Tag - This is just another label for organizing and sorting views. Tags can be any text. Views that are provided by modules will often be tagged together to make it easy to find them. Tags are also added to your template suggestions, so take care what you set here. For example setting the tag Page will give all your views the Page template.
  2. +
  3. Display - In Views 1 each view query was tied to its display; in other words your fields, sorts, filters, and arguments could only be displayed in the single page or block display provided in the view definition. In Views 2, view displays have been decoupled from view queries - it is now possible to have multiple page, block, and feed displays from a single view. More on view displays later.
  4. +
  5. Type - Views 2 view types are radically different from Views 1 types. Views 1 types basically defined how the node list displays were styled - you had Full Nodes, Teaser List, Table View, and so on. In Views 2 view display styles have been broken out into the separate Style attribute. View types now refer to the primary table on which information for the query will be retrieved which controls what arguments, fields, sort criteria and filters are available. Views 2 view types are discussed later. +
  6. +
+ +

Adding a view

+So let's jump in and add a view. For this example, we're going to create a user view, which will display a list of users. +
+ +Adding a view +
+ +The first step in adding a view is simply entering a name (only alphanumeric characters, no spaces) a description, tag, and the view type. To get the user view, we selected the User radio button. + +
+ +Configuring the new view +
+ +This might be the 2nd whoa moment as the interface here is also completely revamped from Views 1.x. The best way to summarize is to say all the pieces from the Views 1.x interface are still there...just in different places. Fields, arguments, sort critera and filters are all still there there, just in new AJAXY-flavours. + +Let's start by adding some fields: + +
+ +Adding fields +
+ +Clicking on the [+] icon next to the word Fields unfurls a section beneath the view information with all the available fields grouped by Comment, File, Node, Node revision, Taxonomy and User, and probably a few others. This is a general paradigm for the Views 2 interface -- clicking on a widget or link unfurls a section beneath the view information with the relevant interface. Usually, what is being edited will be hilited in yellow, as well. + +When adding items, you can use the Groups drop-down box to show only a subset of the fields available according to the above groups, or select All to see all fields available, which is what was selected when the section unfurled. For our example, we're selecting the 'User' group and adding the User: E-mail, User: Name and User: Picture fields. + +
+ +Adding fields +
+ +Once we add our fields they show up in the Fields section of the interface. We will be walked through each field we added, so keep clicking update, even if you don't make changes to the field and you will see the next one. + +The fields we added can be rearranged by clicking the up/down icon, right next to the add icon we used earlier. We can also remove a field using the same interface. + +
+ +Rearranging fields +
+ +From here, the fields can be dragged up and down by grabbing the little drag handle on the left and moving them where you like. Making a change to any part of the view by clicking update usually triggers a refresh of the view preview which is conveniently located right below the main interface. + +
+ +Views preview +
+ +Now that we have some fields set up we can turn our attention to Basic Settings for the view. + +It's important to note that all the interface elements pertain to the current Display selected for the view. As mentioned before a view can have multiple displays. The first time you create a view you'll be manipulating the Default display. You can add displays using the Add Display button, whose Basic Settings are completely different from each other; this lets you have as many displays of a view as you would like all sharing items such as Sort Criteria, Filters and Arguments but different display settings like Title, Style, Fields, and Pager settings. Also, any display you add automatically inherits display settings from the default display initially, so you can keep a core of common settings in your default display and add additional settings for every other display. + +
+ +Adding a Page display +
+Let's stick with the Default display and twiddle some settings. We can set the Title to "User View 1" and the Style to Table. As mentioned earlier, view styles in Views 2 correspond more to view types in Views 1 (remember, List, Table, Teasers, Full nodes). + +
+ +Selecting a Views 1 View Type +
+ +In Views 2, view styles control how a view display looks. These styles are significantly different from the Types in Views 1; in particular, types have been 'broken up'; there is now the style as well as the row style which focus on different parts of the output. + +
+ +A breakdown of View output +
+ +We change the style by clicking on the current style on the left hand side of the View information area. + +
+ +Selecting a Views 2 Display Style +
+ +We're given the style options of Grid, List, Table and Unformatted. Additional display styles can be added by modules which have Views style plugins. Choosing a style reveals a "settings" button which you can click to configure the style you've chosen. In the shot below we've selected and are configuring the Table style, which we're using to produce a more compact output than we had earlier. + +
+ +Selecting and configuring the table style +
+ +... TODO: Finish this document ... diff --git a/help/other-help.html b/help/other-help.html new file mode 100644 index 00000000..a2100f39 --- /dev/null +++ b/help/other-help.html @@ -0,0 +1,9 @@ +There are many tutorials, podcasts, and a few books on Views that you can turn to for further help. + +Books: +Drupal Building Blocks is the Views' author book; check the Drupal.org books page for a fairly comprehensive list. + +Videos: +See this page. + +Google. Many Drupal shops put together helpful tutorials and publish them to Drupal Planet, not to mention the countless users of Views who do it for the community. diff --git a/help/overrides.html b/help/overrides.html new file mode 100644 index 00000000..08fb356b --- /dev/null +++ b/help/overrides.html @@ -0,0 +1,6 @@ + +If an item is using defaults then it is using values from the default display. IMPORTANT NOTE: If you modify this value, you are modifying the default display and thus modifying for all displays that are using default values. + +If that is not what you intend, you must click the override button. Once overridden, that display now has its own version of the value; modifying it will not modify it for other displays. You can override in the settings of the non-default display when you are clicking on the header of the section or on the rearrange button. + +For relationships, arguments, fields, sort criterias, and filters, each of these must be overridden as a group! In other words, you cannot override a single filter, but instead must override all filters. A message will appear on the item to let you know what its status is, but you can only change the status by clicking on the header or the rearrange button for that item. diff --git a/help/path.html b/help/path.html new file mode 100644 index 00000000..eb44de79 --- /dev/null +++ b/help/path.html @@ -0,0 +1,7 @@ +If a display has a path that means that it can be retrieved directly by calling a URL as a first class page on your Drupal site. Any items after the path will be passed into the view as arguments. For example, if the path is foo/bar and a user visits http://www.example.com/foo/bar/baz/beta, 'baz' and 'beta' will be given as arguments to the view. These can be handled by adding items to the arguments section. + +You may also use placeholders in your path to represent arguments that come in the middle. For example, the path node/%/someview would expect the first argument to be the second part of the path. For example, node/21/someview would have an argument of '21'. + +Note: Views 1 used $arg for this kind of thing. $arg is no longer allowed as part of the path. You must use % instead. + +If multiple displays within the same view have the same path, the user will get the first display they have access to. This means you can create successfuly less restricted displays in order to give administrators and privileged users different content at the same path. diff --git a/help/performance-views-vs-displays.html b/help/performance-views-vs-displays.html new file mode 100644 index 00000000..9f02a5a9 --- /dev/null +++ b/help/performance-views-vs-displays.html @@ -0,0 +1,5 @@ +

Multiple displays in one view is appropriate when they are fundamentally similar and are sharing quite a fair amount of data. For example, showing upcoming events in either blocks or page and sorting it according to either event name or date of the event will be appropriate to be implemented by four displays (two blocks and two pages) in one view.

+ +

In the other hand, having multiple displays in one view that contains mostly overrides will be a burden to the system and will be hard to maintain. The effect of having only couple of such displays has negligible performance difference. It will be magnified when the number of displays in one view is large.

+ +

Another consideration is the use of Views, especially when using multiple views in conjunction with other modules such as Panels. It is entirely possible to have many views on a page due to Panels' ability to contain a view in each pane. Some people have mistaken this as a problem on Panels' or Views' part, but realistically it is likely to be the sheer number of queries that are being run during page render.

diff --git a/help/performance.html b/help/performance.html new file mode 100644 index 00000000..e393452e --- /dev/null +++ b/help/performance.html @@ -0,0 +1 @@ +

Views module is optimized for certain practices. To ensure Views has its best performance, please follow these best practices for Views.

diff --git a/help/relationship-representative.html b/help/relationship-representative.html new file mode 100644 index 00000000..cf7feb5b --- /dev/null +++ b/help/relationship-representative.html @@ -0,0 +1,14 @@ +A representative relationship obtains just one object from the linked in objects. + +This is best explained with an example. Suppose you have a term view that shows you the terms in your vocabulary: Horse, Cat, Aardvark. +In addition to the term names, you want to show the title of the most recent node in that term. This would give you a view that shows: Horse - latest horse node title, Cat - latest cat node title, Aardvark - latest aardvark node title. +Each of these is the title of the representative node for that term, chosen by creation time. + +Any sort criterion can be used to choose the representative node. You might instead want to show the node with the most comments for each term, the first node in alphabetical order, or if you have a voting module installed, the most popular node. + +The options for a representative relationship let you choose a sort criterion and a sort order. This determines how the representative object is chosen: the first object returned by the sort is shown. For example, choose 'Node: title' and 'Ascending' to get the first node by title as the representative; or 'Node: comment count' and 'Descending' to get the node with the most comments. + +

Performance

+These relationships require a correlated subquery. This can be slow to run on large amounts of data, as the subquery must be run for every row of the main query. + +For more on this topic, see the MySQL tutorial on group-wise maximum queries. diff --git a/help/relationship.html b/help/relationship.html new file mode 100644 index 00000000..2e3c09ca --- /dev/null +++ b/help/relationship.html @@ -0,0 +1,17 @@ +Relationships allow you to expand the query to include objects other than the base query. This is actually made more difficult to understand by the fact that Views actually includes a few relationships by default, and doesn't tell you they're there. For historical reasons, it would be inconvenient to remove these default relationships. When relationships are present, all fields (including relationships) will gain a new form item to let you select which relationship they will use. They will default to using no relationship at all. + +The main example of the relationship that is there by default is the node --> user relationship; every node has an author, and if a node is in the query, the user who wrote that node is automatically made available. [Note: the author considers it an error that this relationship is automatic, but by the time it was realized this was in error, it was too late to change it.] + +A similar relationship that is not automatically made available is for node revisions. Each revision has its own author, which is the user who made the revision. By adding the "Node revision: User" relationship, all of the 'user' fields, sorts, filters and arguments available to a user will now be available for the revision author. + +When a relationship is added to the view, all applicable items will gain a "Relationship" select box, where you can choose which version of that particular item you wish to use. This can be illustrated with an example: + +A 'comment' view contains the relationships 'Comment: node' and 'Comment: user'. This means that all the fields for the node that a comment is attached to are available, and all the user fields for that node author also become available. The other relationship makes fields for the author of the comment available -- very often not the author of the node! + +When you add the "User: name" field, you will be presented with a select box. Either the node relationship or the user relationship must be selected, because there are two possible user names in the view to choose from. + +Another example of relationships involves the Files table. In Drupal, files are related to users, but files are not necessarily related to nodes. However, the upload.module allows some files to be attached to nodes. The only way for Views to deal with this discrepancy is with relationships. When creating a 'node' view, it's possible to add an uploaded files relationship to get file data for nodes that were attached with the upload module. It is also possible to go the other way; from a files view you may add a relationship via the Upload table to view information about the node. + +Drupal 7 made significant changes to Taxonomy. Because of this, many taxonomy functions Views can perform are now part of relationships. If you can't find the filter you need, add either the related taxonomy terms relationship, or a relationship on the specific taxonomy field. + +You can override the complete relationship section - see overrides for more information. diff --git a/help/reports.html b/help/reports.html new file mode 100644 index 00000000..2a608735 --- /dev/null +++ b/help/reports.html @@ -0,0 +1,3 @@ +Visit admin/reports/views-fields to see an overview of fields usage across all the views. + +It's useful to check for unused or misused fields. diff --git a/help/select-multple-nids-contextual-filters.html b/help/select-multple-nids-contextual-filters.html new file mode 100644 index 00000000..433d751c --- /dev/null +++ b/help/select-multple-nids-contextual-filters.html @@ -0,0 +1,28 @@ +We assume that you have a properly configured view at this point. You should have a page or block display with fields/node to be displayed in it. + +
    +
  1. In the views administration screen add a new 'contextual filter' using 'add'. You will see a configuration screen appear as a modal.
  2. + +
  3. In the 'Filter' jump menu that you now see select 'Content'. A list will appear below the jump menu.
  4. + +
  5. In the list look for 'Content: Nid', and check it.
  6. + +
  7. Press the 'Add and configure contextual filters' button. You will now get the configuration screen for 'Content: Nid'.
  8. + +
  9. Under 'When the filter value is NOT in the URL' select 'Provide default argument', and leave it set at 'Fixed value'.
  10. + +
  11. In the field 'Fixed value' add the Nids of the nodes separated by a '+' that you want to show by default.
  12. + +
  13. In 'When the filter value IS in the URL or a default is provided' choose 'Specify validation criteria'. Now select the content types you want, or leave blank for all nodes.
  14. + +
  15. Check the 'Validate user has access to the node'. This will check if a user has permission to view nodes, that are going to be displayed
  16. + +
  17. For the 'Filter value format' select 'Node IDs separated by , or +'.
  18. + +
  19. Select the MORE link.
  20. + +
  21. Check 'Allow Multiple Terms per Argument' and press 'Apply (all displays +)' (or choose a particular display in the dropdown at the top and Apply (this display)).
  22. +
+ +Preview should now show nodes, that you selected as "default argument". You can now pass arguments to your view, to select custom node IDs. If you provide no argument, default will be used. Save the view when ready. diff --git a/help/semantic-views.html b/help/semantic-views.html new file mode 100644 index 00000000..ef645c6d --- /dev/null +++ b/help/semantic-views.html @@ -0,0 +1,18 @@ +The Semantic Views module has been mostly incorporated into Views 3.x. Semantic Views is still around for people who need it, though. For some details on how the original module is different from the Views implementation, please see this issue. + +Semantic views help you insert markup of your own from the Views UI, so that you can fairly easily override the default markup without having to restyle via templates. + +As a usage example, + +In a view with a field: +
    +
  1. Configure the field. (Click on the field.)
  2. + +
  3. In the modal that opens, scroll down to Style Settings.
  4. + +
  5. Choose one or more of the three Customize options. This will reveal a dropdown menu where you can choose from one or more HTML tags to use on that field and allow you to add a CSS class specific to that field should you desire.
  6. + +
  7. Decide if you want to keep the Views default classes. Unchecking this box means your markup is *it*.
  8. +
+ + diff --git a/help/sort.html b/help/sort.html new file mode 100644 index 00000000..322537ed --- /dev/null +++ b/help/sort.html @@ -0,0 +1,28 @@ +Sort criteria determine what order the records are retrieved from the database and displayed in; generally, all you need to do is pick a field and choose ascending (1, 2, 3, 4) or descending (4, 3, 2, 1) and it will be done. If you have multiple sort criteria, the second (and later) items only come into play if the first item is the same. + +In Views 3.x, sorts may be exposed just as filters are. Note that if only one item is exposed for sorting, the dropdown menu created by the exposed sort will only have one item in it. Some users may find this odd. + +Different data types sort just a little bit differently from others: +
+
Number fields
+
Number fields sort like you would expect. 1 comes before 2 which comes before 10 which comes before 100 which comes before 200, etc.
+
Text fields
+
Text fields always sort alphabetically, even if the text contains numbers. This can have some odd effects if you have numbers stored in text, because the values 1, 3, 7, 10, 12, 20, 100, 120 will sort like this: +
    +
  • 1
  • +
  • 10
  • +
  • 100
  • +
  • 12
  • +
  • 120
  • +
  • 200
  • +
  • 3
  • +
  • 7
  • +
+ +This is because these fields sort purely by characters, and not numeric value. i.e, comparing 200 and 3, the '2' comes before the '3', therefore, '200' is "smaller" than '3'. +
+
Date fields
+
Date fields often can have a 'granularity', which is a way of making similar dates actually be the same date. Take two dates that are close to each other: May 1, 2007 5:30 am and May 1, 2007 9:45am. Without granularity, the two dates are compared and the first date comes before the second date. However, if the granularity is set to 'day' it only looks at the parts of the date up to the day: May 1, 2007 and May 1, 2007. At that point, they are the same, and the sort would move on to the next sort criterion.
+
+ +You can override the complete sort criteria section - see
here for more information. diff --git a/help/style-comment-rss.html b/help/style-comment-rss.html new file mode 100644 index 00000000..5ce1dcd0 --- /dev/null +++ b/help/style-comment-rss.html @@ -0,0 +1 @@ +This row style is only available to RSS styles. It produces XML necessary for an RSS feed for the comment. diff --git a/help/style-fields.html b/help/style-fields.html new file mode 100644 index 00000000..4d8d077b --- /dev/null +++ b/help/style-fields.html @@ -0,0 +1,16 @@ +The fields row style displays each field defined in the view, one after another. Each field defines its own output. + +By default, each field is put in a <div> unless it is selected to be inline. If it is inline, it is put in a <span>. Two items in <div>s will be displayed one after another, with the second one below the first. Two items in <span>s will be displayed on the same line. One item in a <span> next to <div>s is the same as two items in <div>s. This means that for the inline setting to do anything, at least two consecutive items must be set inline. + +You may define a separator which will be placed between each item. This separator may be html. You can use &nbsp; to print blank space. + +If the view's row style is set to "fields", fields must be added to the View. If there are no fields, you may receive validation errors such as: + +* Display "Defaults" uses fields but there are none defined for it or all are excluded. +* Display "Page" uses fields but there are none defined for it or all are excluded. + +This is because the row style "fields" expects at least one field for display. + +There is also an option to hide empty fields, so empty fields, along with their labels or markup will not be displayed. + +There is also an option to hide empty fields, so empty fields, along with their labels or markup will not be displayed. diff --git a/help/style-grid.html b/help/style-grid.html new file mode 100644 index 00000000..463e2bdc --- /dev/null +++ b/help/style-grid.html @@ -0,0 +1,22 @@ +The grid style will display each row of your view within a grid. You may customize the number of columns, though it defaults to 4. A grid looks like this: + + + + + + +
row 1row 2row 3row 4
row 5row 6row 7row 8
row 9row 10row 11row 12
row 13row 14row 15row 16
+ +The above uses the horizontal alignment, where rows are added into the grid from left to right. + +With a vertical alignment, rows will be placed from top to bottom, like this (your row results will appear as columns): + + + + + +
row 1row 5row 9row 13
row 2row 6row 10row 14
row 3row 7row 11row 15
row 4row 8row 12row 16
+ +You can also choose to group a field from the Fields Section. This grouping field will be displayed as a header, and all rows will be displayed beneath it. + +This style uses a row style to determine what each row will look like. diff --git a/help/style-grouping.html b/help/style-grouping.html new file mode 100644 index 00000000..8b0c9a7a --- /dev/null +++ b/help/style-grouping.html @@ -0,0 +1,7 @@ +Many styles can be grouped. For styles that can, there will be a 'grouping field' option; pick one of the fields to group by. This grouping field will be displayed as a header, and all rows will be displayed beneath it. + +If the style is not grouped then the corresponding style output template will by invoked once when displaying the view. If the style is grouped, then the style output template will be invoked once per group. The text if the grouping filed is passed to the template in the $title variable. + +Views is very naive about grouping, and it can only group on one field. It doesn't understand anything about which object (node, comment, etc.) the field belongs to, and doesn't make any smart grouping choices based on the parent object of the grouping field. The grouping field does not modify the views SQL query - grouping is handed after records are retrieved from the database. + +The devel_themer module is known to break grouping. If grouping is not working, please check and make sure the devel modules are disabled. diff --git a/help/style-jump.html b/help/style-jump.html new file mode 100644 index 00000000..dc8532f8 --- /dev/null +++ b/help/style-jump.html @@ -0,0 +1,48 @@ +With the jump menu style can you create selectbox menus for the content of your site. + + + +The jump menu style will display each row of your view within a jump menu. This style requires that your view's row style is set to fields. + +To properly configure a jump menu, you must select one field that will represent the path to utilize. In most cases, you will need to rewrite the output of this field. You should set that field to exclude from display. All other displayed fields will be part of the menu. Please note that all HTML will be stripped from this output as select boxes cannot show HTML. + +Some examples of how this might be useful: + + +

Jump to a node

+
    +
  1. Create a new Node view
  2. +
  3. Select the Node: Path and Node: Title fields
  4. +
  5. Configure the Node: Path field to "exclude from display" and check "Use absolute link"
  6. +
  7. Configure the Node: Title field by removing the "Label" and unchecking "Link this field to its node"
  8. +
  9. Set the view style to jump menu
  10. +
  11. In the style settings, set the "Path field" to Node: Path
  12. +
+ +Your view will now display with a select list and a Go button. If you select an item in the list and hit the Go button you will see the selected node's page in the browser. + +

Jump to a node's edit page

+
    +
  1. Create a new Node view
  2. +
  3. Select the Node: Nid and Node: Title fields
  4. +
  5. Configure the Node: Nid field to "exclude from display" and check "Rewrite the output of this field"
  6. +
  7. In the text field that appears for rewriting the output of this field, enter node/[nid]/edit
  8. +
  9. Configure the Node: Title field by removing the "Label" and unchecking "Link this field to its node"
  10. +
  11. Set the view style to jump menu
  12. +
  13. In the style settings, set the "Path field" to Node: Nid
  14. +
+ +Your view will now display with a select list and a Go button. If you select an item in the list and hit the Go button you will see the selected node's edit page in the browser. Please note that users without rights to the node's edit page will see an access denied message. + +

Jump to a user profile

+
    +
  1. Create a new User view
  2. +
  3. Select the User: Uid and User: Name fields
  4. +
  5. Configure the User: Uid field to "exclude from display" and check "Rewrite the output of this field"
  6. +
  7. In the text field that appears for rewriting the output of this field, enter user/[uid]
  8. +
  9. Configure the User: Name field by removing the "Label" and unchecking "Link this field to its user"
  10. +
  11. Set the view style to jump menu
  12. +
  13. In the style settings, set the "Path field" to User: Uid
  14. +
+ +Your view will now display with a select list and a Go button. If you select an item in the list and hit the Go button you will see the selected user's profile page in the browser. diff --git a/help/style-list.html b/help/style-list.html new file mode 100644 index 00000000..86355a51 --- /dev/null +++ b/help/style-list.html @@ -0,0 +1,20 @@ +The List view style will display every row of the view as part of an HTML list construct. For example: +
    +
  • Row 1
  • +
  • Row 2
  • +
  • Row 3
  • +
  • Row 4
  • +
+ +You may select whether or not the list is ordered which just means whether or not it uses numbers instead of the bullet: + +
    +
  1. Row 1
  2. +
  3. Row 2
  4. +
  5. Row 3
  6. +
  7. Row 4
  8. +
+ +The list style also uses a row style which means that it doesn't care what the actual output for each row of the view is. + +If you need information about using CSS to style list views, you may find this A list apart guide to styling lists useful. diff --git a/help/style-node-rss.html b/help/style-node-rss.html new file mode 100644 index 00000000..7a97bdd5 --- /dev/null +++ b/help/style-node-rss.html @@ -0,0 +1 @@ +This row style is only available to RSS styles. It produces XML necessary for an RSS feed for the node record. diff --git a/help/style-node.html b/help/style-node.html new file mode 100644 index 00000000..928980f2 --- /dev/null +++ b/help/style-node.html @@ -0,0 +1,11 @@ +The node row style will display each item of the view through Drupal's standard node_view() function. Views has very little control over this output, except for the options you see. You can choose from different Build modes. By default there are "Teaser" and "Full node". You can also decide if you want to "Display links" and/or "Display node comments". + +Because the output is run through the standard node template mechanism (typically node.tpl.php or a variant thereof), any decisions about what is output may be done there. + +Views does add an extra 'suggestion' to the list of possible node templates: node--view--VIEWNAME.tpl.php -- you may use this to theme a node specifically for the view. This can be handy for creating very small teasers and the like. + +You may opt to display the full node body or the node teaser, and you may add the node links (such as the 'comment' links that appear after a node) or not. + +Because of this behavior, the node row style does not utilize fields and the Fields section will not be displayed. + +Please note that this row style performs a node_load() for every row, and as such can produce a lot of extra queries. Sometimes this is necessary, but it can have a negative impact on your site's performance! diff --git a/help/style-row.html b/help/style-row.html new file mode 100644 index 00000000..a8406d35 --- /dev/null +++ b/help/style-row.html @@ -0,0 +1,11 @@ +A row style is an individual style to display only an individual record within a view. You can choose between node or fields. For example, a node type view would display one node per row; a user type view would display one user per row. + +When you choose fields, you must have entries in the Fields section to produce an output. To set some options for fields, read here. + +Some row styles use fields which means you may select from the available fields to display. Others row styles do not - they are able to use the base type and create a display. Usually, row styles that do not use fields produce less efficient (slower) views, so bear this in mind when contemplating the performance of your site. + +When you choose node the complete node will display through Drupal's standard node_view() function. You can choose some options. + +When styling views, it's important to realize that the unformatted styles will take the majority of styling from more specific templates. In an unformatted style, the row style will provide theming. For instance, in a view that uses fields as its primary data, the views-view-fields.tpl.php will provide the theming. + +Mustardseed created a videocast on row theming that can be viewed here. diff --git a/help/style-rss.html b/help/style-rss.html new file mode 100644 index 00000000..e67bd309 --- /dev/null +++ b/help/style-rss.html @@ -0,0 +1,5 @@ +The RSS output style is only available for Feed display types. It will display the view as an RSS feed, which is a specialized XML output. This output is not user visible, but can be parsed by feed readers for aggregation. + +You may supply a description for the RSS feed; most feed readers will display this description along with the contents of the feed. You may also select to use the site's mission statement for the description. + +Please note that when using RSS views only comes with the one RSS style. There's no style override for RSS. Modules can add more. diff --git a/help/style-settings.html b/help/style-settings.html new file mode 100644 index 00000000..63133d81 --- /dev/null +++ b/help/style-settings.html @@ -0,0 +1,3 @@ +Views includes many options for theming or styling. + +TODO: add more here. diff --git a/help/style-summary-unformatted.html b/help/style-summary-unformatted.html new file mode 100644 index 00000000..bd14f249 --- /dev/null +++ b/help/style-summary-unformatted.html @@ -0,0 +1,3 @@ +The unformatted summary style is only available for summary styles, which are when an argument has been set to provide a summary if it was not provided with a value. This summary provides the possible candidates for the argument one after another with no special formatting. If inline is selected, the summary items will be enclosed within <span> tags. Otherwise the items will be in <div> tags. + +You can also elect to display the number of matching records for the argument, plus change the number of items per page for the summary. This is often useful because summary views are often quite small, but other views quite space intensive. It is very common to have far more records available in the summary view than in the more normal view. diff --git a/help/style-summary.html b/help/style-summary.html new file mode 100644 index 00000000..0c3845fa --- /dev/null +++ b/help/style-summary.html @@ -0,0 +1,3 @@ +The list summary style is only available for summary styles, which are when an argument has been set to provide a summary if it was not provided with a value. This summary provides a list of possible candidates for the argument in a standard HTML list. Like the normal list style, you may set this list to be ordered or not. + +You can also elect to display the number of matching records for the argument, plus change the number of items per page for the summary. This is often useful because summary views are often quite small, but other views quite space intensive. It is very common to have far more records available in the summary view than in the more normal view. diff --git a/help/style-table.html b/help/style-table.html new file mode 100644 index 00000000..9fc8175e --- /dev/null +++ b/help/style-table.html @@ -0,0 +1,13 @@ +The table style will display the View results as a table; each row of the table will correspond to a row from the view result. + +When setting the table options, each field in the view will be presented with some information next to each field: +
+
Column
+
By default, each field is its own column. However, you can place multiple fields in the same column. To do this, pick which field you want to represent the column, then pick another field and set the 'column' value to that field. You can place as many fields as you like in a single column, but only the main field in a column can be click-sorted.
+
Separator
+
If you have multiple fields in the same column, the separator will be placed between each one. At the very least, &nbsp; should be used, as without the separator the fields will be placed very close to each other. Common separators are a bullet, the | symbol, and a comma. If there are no other fields in the column, the separator will have no effect.
+
Sortable
+
If checked, the header for the column will be clickable, and the user may re-sort the table by clicking on this to sort by that field. At this time Views does not support click-sorting to sort by multiple columns at the same time.
+
Default sort
+
You may select a column which will be sorted by default when the table is first viewed. This column will be highlighted to the user. You may also select whether the default sort is ascending or descending.
+
diff --git a/help/style-unformatted.html b/help/style-unformatted.html new file mode 100644 index 00000000..99fa3de7 --- /dev/null +++ b/help/style-unformatted.html @@ -0,0 +1 @@ +The unformatted output style simply places each row of the view, one after another, with no additional formatting. diff --git a/help/style.html b/help/style.html new file mode 100644 index 00000000..d32a2eda --- /dev/null +++ b/help/style.html @@ -0,0 +1,15 @@ +The Views' style system is how you customize the output produced by your view. A view style is basically a smart theme template that processes the view data and then outputs it. All styles in Views can be overridden by placing copies of the templates in your theme directory and then modifying them. See the theme: information link available on all views to get hints for which templates a given view is using. + +
+ +A breakdown of View output +
+By default, the style is unformatted, which means that there is very little style actually used; the records are simply displayed one after another, enclosed in a <div> tag so that you can use CSS to manipulate the view. + +Some styles use a separate row style to determine how each row of the View looks. This is useful for mixing and matching styles to more readily produce exactly the kind of output you need. + +Many styles can be grouped. For styles that can, there will be a 'grouping field' option; pick one of the fields to group by. Please see Grouping in styles for more information. + +Each style is its own entity. + +If you want your fields to be templatable, you need to use values that are acceptable in function names. That's actually more strict, since that basically limits you to alphanumeric and _. Even though Views may be able to query datastores that use special characters, naming is still important for use in templates. diff --git a/help/taxonomy-page-override.html b/help/taxonomy-page-override.html new file mode 100644 index 00000000..f6762347 --- /dev/null +++ b/help/taxonomy-page-override.html @@ -0,0 +1,41 @@ +NOTE: This page has not been updated for 7.x-3.x +Views 2 provides a way to override the taxonomy pages of any term. The view is included by default but is disabled. This page covers three minor aspects that some users could use to control the aspect of their taxonomy pages. + +

Background

+The Taxonomy module provides a way to organize content through the site. It also presents a page with nodes that belong to some term. The style can't be changed easily and taxonomy only displays node teasers. + +

Force All

+When using vocabularies with multiple term levels, a top level won't include the nodes that belongs to the levels inside. Let's clarify this with an example, let's assume the site have this vocabulary: + + +Vocab + - Term 1 + -- Term 2 + -- Term 3 + - Term 4 + -- Term 5 + -- Term 6 + + +If you go to taxonomy/term/1, then you see all nodes in Term 1 listed, but not nodes in Term 2 and Term 3. If you want to see all nodes in Term 1, Term 2 and Term 3 in a single page, you have to go to + + +taxonomy/term/1/all + + +Views 2 allows you to show all terms inside another term. + +

Step 1. Enabling taxonomy_term

+The first step we need to do is to enable the "taxonomy term" view that views provides for us. After we've done that we need to edit it. + +

Step 2. Changing minor aspects

+On the Display: Page, we can change the Style to any of the styles that views provides for us, it could be a table or a list. If we do this it's necessary to add some fields to the view. + +The most important thing we're going to change it's the argument Taxonomy: Term ID: (with depth). At the bottom of the screen, you'll have 2 options, the first one to change it's the depth, if we want to do what the Taxonomy Force All modules does, you'll have to set it up for at least 1. + +The next thing you have to do it's check the Set the breadcrumb for the term parents option. This will allow your view to show the breadcrumb with all the term levels in your taxonomy. + +Save the changes and try. Next you could do whatever you want, like adding fields, theming, exposing filters, sorting or adding feeds. + +

Theme every vocabulary/term independently

+Using TVI: Taxonomy Views Integrator it's possible to theme every vocabulary or term with it's own view. All you have to do is clone the view in Step 1 and set it as the TVI active view in the vocab or term edit pages. diff --git a/help/theme-css.html b/help/theme-css.html new file mode 100644 index 00000000..a23d9e55 --- /dev/null +++ b/help/theme-css.html @@ -0,0 +1,76 @@ +Views uses a wide array of CSS classes on all of its content to ensure that you can easily and accurately select exactly the content you need in order to manipulate it with CSS. + +It is possible to enter a custom css class under Style settings. The CSS class names will be added to the view. This enables you to use specific CSS code for each view. You may define multiples classes separated by spaces. + +Typically, every view is wrapped in a div with the name of the view as part of its class (for all these examples, we will assume the name of the view is myview), as well as the generic class 'view': + +
+<div class="view view-myview">
+...
+</div>
+
+ +In your CSS, you can modify all views: + +
+div.view {
+  border: 1px solid black;
+}
+
+ +Or just your view: + +
+div.view-myview {
+  background: yellow;
+}
+
+ +By default, the general view template also provides the following classes to easily style other areas of the view: +
    +
  • .view-header
  • +
  • .view-filters
  • +
  • .view-content
  • +
  • .view-empty (if an "empty" text is used when the view has no results)
  • +
  • .view-footer
  • +
  • .feed-icon
  • +
  • .attachment-before (if using an "attachment" display)
  • +
  • .attachment-after (if using an "attachment" display)
  • +
+ +So for example: +
+div.view-myview div.view-header {
+  /* make the header stand out */
+  font-size: 120%;
+  font-weight: bold;
+}
+
+div.view-myview div.view-footer {
+  /* Make the footer less important */
+  font-size: 80%;
+  font-style: italic;
+  color: #CCC;
+}
+
+ +In the above example, we whimsically made the header bold and in a bigger font, and the footer smaller, italicized, and greyish. + +

Views with fields

+If your view has fields, each field is uniquely tagged with its ID. A field's ID may be gleaned from the Theme: Information page. Note that due to CSS rules, any _ in the id will be converted to - automatically, so if you have a field whose id is 'edit_node' (this is the field used to provide an "edit" link to a node), it will be 'edit-node'. Additionally, to make sure that the view IDs don't conflict with other css classes in the system, they will be pretended with 'views-field-'; thus, the final CSS class for the field with the id 'edit_node' will be views-field-edit-node. + +Exactly how this appears is going to depend upon the style you're using. For example, the 'unformatted' style uses div.views-field-edit-node and div.views-label-edit-node to access that particular field, but a table would use td.views-field-edit-node and th.views-field-edit-node to access the table header; or just .views-field-edit-node to affect both. + +
+.view-myview th {
+  color: red; /* make all headers red */
+}
+
+.view-myview .views-field-title {
+  font-weight: bold; /* Make the 'title' field bold */
+}
+
+.view-myview td.views-field-body {
+  font-size: 60%; /* Make the text in the body field small */
+}
+
diff --git a/help/top-pager.html b/help/top-pager.html new file mode 100644 index 00000000..68247af1 --- /dev/null +++ b/help/top-pager.html @@ -0,0 +1,18 @@ +Copy the views-view.tpl.php from the /views/theme directory to themes/yourtheme/theme directory. Find the following code... + + +
+  <?php if ($attachment_before): ?>
+    <div class="attachment-before">
+      <?php print $attachment_before; ?>
+    </div>
+  <?php endif; ?>
+
+ +Insert the following code after it (this is copied directly from the same code at the bottom of the tpl): + +
+  <?php if ($pager): ?>
+    <?php print $pager; ?>
+  <?php endif; ?>
+  
diff --git a/help/ui-crashes.html b/help/ui-crashes.html new file mode 100644 index 00000000..4e8a3893 --- /dev/null +++ b/help/ui-crashes.html @@ -0,0 +1,25 @@ +

Troubleshooting UI crashes

+ +There are a number of reasons why the Views UI may crash; the most common state or result of a crash is either a white screen (everyone's favorite WSOD), or a screen of what looks like garbage text. This is generally a javascript crash of some fashion. + +To get the most timely and accurate help in the issue queue, please try to gather this information: + +Check your javascript console. In Firefox, you can hit ctrl-shift-j or use firebug. Copy this information into the issue, or attach it as a text file. Really - this is the single biggest thing you can do to help figure out where the crash is coming from. + + +

JSON prepends data with jQuery, causing editing and preview problems.

+This section originally stems from this issue. + +Some modules may add PHP improperly, disrupting normal jQuery operation. Errors may look like + + +{ "default": "default" } + + +This can also be a server configuration issue. In one case, this was solved by commenting out + +auto_prepend_file = c:\wamp\www\php.ini.prepend + +in the php.ini. + +Another page to look at is the drupal.org page on White screens diff --git a/help/updating-view3.html b/help/updating-view3.html new file mode 100644 index 00000000..aa2d9770 --- /dev/null +++ b/help/updating-view3.html @@ -0,0 +1 @@ +Most views should automatically convert without issue. It's always a good idea and highly recommended that all views be exported for backup purposes before upgrading to Views 3.x. diff --git a/help/updating.html b/help/updating.html new file mode 100644 index 00000000..34a1d253 --- /dev/null +++ b/help/updating.html @@ -0,0 +1,7 @@ +Views 1 views stored in the database may not convert properly to Views 2. In order to aid with the conversion, a tool is available. In the main Views page, choose Tools -> Convert. Any views you choose will attempt to convert. + +Note that converted Views may not match perfectly! Adjustments may be necessary to retain the integrity of your previous views. + +Views 1 views may also be exported before upgrade, and then imported through the standard Import tab. Those views will attempt to convert during import. Exporting your views before upgrade has the added benefit of ensuring a backup. + +There are no known issues converting views from Views 2 to Views 3 at this time. diff --git a/help/upgrading.html b/help/upgrading.html new file mode 100644 index 00000000..b31b8f93 --- /dev/null +++ b/help/upgrading.html @@ -0,0 +1,8 @@ + +

Updating templates

+If you have theme files for node-view-$viewname of comment-view-$viewname you have +to convert them to node--view--$viewname or comment--view--$viewname. + +

Updating table templates

+The class variable was renamed to $classes. Additional if you want to add +classes, you should add it to $classes_array and drupal will automatically convert it to the $classes string. diff --git a/help/using-theme.html b/help/using-theme.html new file mode 100644 index 00000000..c4164eb4 --- /dev/null +++ b/help/using-theme.html @@ -0,0 +1,50 @@ +Views theme templates are straightforward to use with the Drupal theming system. If you are unfamiliar with the theming system at all, you should probably at least read drupal.org theming documentation. That said, these are the important things you need to know: + +
    +
  1. Copy a base Views template to one of the names provided from the Theme: Information section of the View. Copy this template right into your theme directory.
  2. +
  3. Clear the theme registry. See the instructions for how to do this.
  4. +
  5. Your new template should now operate; assuming you picked a nicely named template that includes the view name, that template should now be active for your view. A good example is views-view-list--foobar.tpl.php which would work for a view named 'foobar'.
  6. +
  7. You can now modify this template all you like.
  8. +
+ +For any template that uses fields, the fields will be in array. In order to use this effectively, you will need to understand enough PHP to fetch data from an array. This is a place where the devel module can really help you, because you can use its dsm() function right in your template to see what variables it uses. There is an alternative to dsm() that works without devel module, but it's a bit longer to use. + +For example, I placed the following code inside a loop in views-view-table.php.php: + <?php drupal_set_message('<pre>' . var_export($row, true) . '</pre>'); ?> + + +And it produced this output: + array ( + 'nid' => '97', + 'title' => 'Scisco Ideo Vicis Feugiat Qui', + 'name' => 'luwrepuslan', + ) + + +My view had three fields: +Node: Nid +Node: Title +User: Name + + +The contents of the $row variable included these fields, in precisely the order that I had arranged them to using the Views rearrange link. Also worth noting, though, is that each field also has an identifier so it can easily be pulled out of the row should I want to display it differently. Using +<?php print $row['title']; ?> + + +Would print just the title for that row. Please remember that I'm doing this inside the loop, so this will get repeated for every row of the view. + +The IDs used to fetch items from the array, id $row['title'] can be quickly and easily looked up on the Theme: Information page. Once a field has been added to the view, its ID will not change, but note that if there are two "title" fields in a view, one will be 'title' and the other will be 'title_1', in order to prevent namespace collisions. + +The important thing here is that Views does provide IDs. Views doesn't tell you what these IDs are, but it's easy to get them by dumping the row data and doing a simple visual inspection. Views does guarantee that these IDs will not change, unless you actually add a new field and remove the existing one (in which case 'title', above, would become 'title_1'). + +

The basic fields template

+ +The most common template people will need to rewrite is the "simple" views-view-fields.tpl.php, which is the template used by the Fields row style and all it does is display a simple list of fields. However, it is not that simple to the user. Because the template can't inherently know what the fields are, it has to go through an array in a loop. + +This loop isn't very handy when you really want to have fine control over the template by placing your fields precisely where and how you want. Relax, though; if you know what your fields are, you can rewrite this. If you end up writing your own HTML, the only part that is really important is the content for each field. We know from above that you can get the ID for each field on the Theme: Information page from the view. In the header for the template, we can see that the fields are all in the $fields array, and each field is an object. That leads us to this: + +<?php print $fields['some_id']->content; ?> + +Assuming you replace some_id with an id found on the theme: information page, this code will print the content for that field. You can also get the label and some other data about the field, as well as the raw information. Complete details for what is available are documented directly in views-view-fields.tpl.php. + +Keep in mind that if you rewrite your templates using this, you'll need to maintain this template whenever changes are made to the fields of the view; while this isn't generally recommend, sometimes it's necessary to get the kind of control you might ultimately need. diff --git a/help/view-add.html b/help/view-add.html new file mode 100644 index 00000000..619d1b66 --- /dev/null +++ b/help/view-add.html @@ -0,0 +1,25 @@ +

When you want to create a new view navigate to Structure > Views . There you can see all the views specified. Fresh install has a few examples ready and you can choose "edit" to investigate how the is made. To create a new view press "Add new view".

+ +
+ +All the created Views +
+ +

You can enter the following information about it.

+

First, you must enter a view name. This is the unique name of the view. It must contain only alphanumeric characters and underscores; it is used to identify the view internally and to generate unique theming template names for this view. If you are overriding a module provided view, the name must not be changed or a new view will be created instead of overriding.

+

You can enter a description of the view. This description will appear on the Views administrative UI to tell you what the view is about. You can enter a view tag. It will be used help sort views on the administrative page.

+You must choose a view type. +
+
Node
+
Nodes are a Drupal site's primary content.
+
Comment
+
When you want to handle comments and information related to the and information related to them.
+
File
+
When you want to handle files and file information.
+
Node revision
+
When you want to handle node revision information, choose this.
+
Term
+
When you want to handle taxonomy, choose this one.
+
User
+
When you want to handle users and user information, choose this one.
+
diff --git a/help/view-settings.html b/help/view-settings.html new file mode 100644 index 00000000..906656e1 --- /dev/null +++ b/help/view-settings.html @@ -0,0 +1,5 @@ +With the Description and Tag you can set the "View description" and the "View tag". The tag help you to organize your views. Only one tag may be entered for a given view. Note that this tag can also be used in themeing, so do not use words already reserved by Drupal for use, such as "Page". + +You can clone a view. When you click on the clone link on the upper right, all the options in the current view will copy to a new view, which can be given a new name and edited further. + +You can export a view. You can the copy and paste the code from a view into a module or something else. It is a array with all options for that view. diff --git a/help/view-type.html b/help/view-type.html new file mode 100644 index 00000000..bdb80fc0 --- /dev/null +++ b/help/view-type.html @@ -0,0 +1,21 @@ +

The view type describes how this view is stored; Views is capable of having Views entirely in code that are not in the database. This allows modules to easily ship with Views built in, and it allows you to create a module to store your views for easy deployment between development and production servers.

+ +
+
Normal
+
Normal views are stored in your database and are completely local to your system.
+ +
Default
+
Default views are stored only in code and are not anywhere in your database. They may be enabled or disabled but you may not completely remove them from your system. You can override the view which will create a local copy of your view. If you do this, future updates to the version in code will not affect your view.
+ +
Overridden
+
Overridden views are stored both in code and in the database; while overridden, the version that is in code is completely dormant. If you revert the view, the version in the database will be deleted, and the version that is in code will once again be used.
+
+ +You may store your views in code with the following procedure: +
    +
  1. Create a module to store the views.
  2. +
  3. Add the function MODULENAME_views_default_views() to this module.
  4. +
  5. Export the view you wish to store in your module in code. Cut and paste that into the abovenamed function. Make sure the last line of the view is: $views[$view->name] = $view;
  6. +
  7. Make sure the last line of the function is return $views;
  8. +
  9. After you make any changes, be sure to clear the Views' cache. You may do this from the Tools menu.
  10. +
diff --git a/help/views.help.ini b/help/views.help.ini new file mode 100644 index 00000000..bd2d4577 --- /dev/null +++ b/help/views.help.ini @@ -0,0 +1,359 @@ +[advanced help settings] +line break = TRUE + +[about] +title = "What is Views?" +weight = -50 + +[getting-started] +title = "Getting started" +weight = -40 + +[new] +title = "What's new in Views 3" +weight = -30 + +[example-users-by-role] +title = "Create a page to list users by role" +parent = getting-started +weight = -20 + +[example-recent-stories] +title = "Create a block of recent stories" +parent = getting-started +weight = 0 + +[example-user-feed] +title = "Create an RSS feed of user posts" +parent = getting-started +weight = 30 + +[example-author-block] +title = "Create a block of author's recent blog posts" +parent = getting-started +weight = 40 + + +[view-add] +title = "Add a View" +weight = -10 + +[view-settings] +title = "View settings" +weight = 0 + +[basic-settings] +title = "Basic settings" +weight = 10 + +[advanced-settings] +title= "Advanced settings" +weight = 20 + +[display] +title = "Displays" +weight = -10 + +[display-default] +title = "Default display" +parent = display +weight = -20 + +[display-page] +title = "Page display" +parent = display +weight = -15 + +[display-block] +title = "Block display" +parent = display +weight = -10 + +[display-attachment] +title = "Attachment display" +parent = display + +[display-feed] +title = "Feed display" +parent = display + +[style-settings] +title = "Style settings" +weight = 30 + +[style] +title = "Output styles (View styles)" +parent = style-settings +weight = -20 + +[style-grid] +title = "Grid (output style)" +parent = style +weight = 0 + +[style-list] +title = "HTML List (output style)" +parent = style +weight = 10 + +[style-table] +title = "Table (output style)" +parent = style +weight = 20 + +[style-unformatted] +title = "Unformatted (output style)" +parent = style +weight = 30 + +[style-rss] +title = "RSS output style" +parent = style +weight = 50 + +[style-grouping] +title = "Grouping in styles" +parent = style +weight = -7 + +[style-row] +title = "Row styles" +weight = -10 +parent = style-settings + +[style-jump] +title = "Jump menu output style" +parent = style +weight = -4 + +[style-fields] +title = "Fields" +parent = style-row +weight = -10 + +[style-node] +title = "Node" +parent = style-row +weight = 0 + +[style-node-rss] +title = "Node RSS item row style" +parent = style-row +weight = 10 + +[style-comment-rss] +title = "Comment RSS item row style" +parent = style-row +weight = 20 + +[performance] +title = "Performance" + +[performance-views-vs-displays] +title = "Multiple Views vs Multiple Displays" +parent = performance + +[analyze-theme] +title = "Theme information" +parent = style-settings +weight = 30 + +[using-theme] +title = "Using Views templates" +parent = analyze-theme +weight = 40 + +[theme-css] +title = "Using CSS with Views" +parent = style-settings +weight = 20 + +[advanced-style-settings] +title = "Advanced Style Settings" +parent = style-settings + +[group-by] +title = "Group by" +parent = field + +[menu] +title = "Menu options (page display)" +parent = display-page + +[path] +title = "Path options (page display)" +parent = display-page + +[exposed-form] +title = "Exposed Form" +weight = 45 + +[header] +title = "Header" +weight = 50 + +[footer] +title = "Footer" +weight = 60 + +[empty-text] +title = "Empty Text" +weight = 70 + +[field] +title = "Fields" +weight = 80 + +[relationship] +title = "Relationships" +weight = 90 + +[aggregation] +title = "Aggregation" +weight = 90 + +[argument] +title = "Arguments/Contextual Filters" +weight = 100 + +[style-summary-unformatted] +title = "Summary Style: Unformatted (output style)" +parent = argument + +[style-summary] +title = "Summary Style: List (output style)" +parent = argument + +[sort] +title = "Sort criteria" +weight = 110 + +[filter] +title = "Filters" +weight = 120 + +[overrides] +title = "What are overrides?" +parent = display + +[embed] +title = "Embedding a view into other parts of your site" +weight = 140 + +[upgrading] +title = "Upgrading your Views from Drupal 6 to Drupal 7" + +[updating-view3] +title = "Updating your views from Views 2 to Views 3" +weight = 160 + +[misc-notes] +title = "Known Issues and Workarounds" + +[reports] +title = "Reports" + + +; API related +[api] +title = "Views' API" +weight = 170 + +[api-tables] +title = "Describing tables to Views" +weight = -100 +parent = api + +[api-default-views] +title = "Using default views in your module" +weight = -90 +parent = api + +[api-handlers] +title = "How Views handlers work" +weight = -50 +parent = api + +[api-handler-area] +title = "How to write an area handler" +weight = -40 +parent = api + +[api-plugins] +title = "How Views plugins work" +weight = -40 +parent = api + +[api-forms] +title = "Outputting form elements from handlers" +weight = -30 +parent = api + +[api-upgrading] +title = "Upgrading to Drupal 7 (API)" +parent = api + +[api-example] +title = "Integrating the Node Example module" +parent = api +weight = 100 + +[alter-exposed-filter] +title = "Altering the default value of an exposed filter" +parent = api +weight = 101 + +[get-total-rows] +title = "How to get a total number of rows for a View with a filter and no pager"] +parent = "api" +weight = 102 + +[drush] +title = "Drush commands for Views" +parent = api +weight = 103 + +;Troubleshooting +[troubleshooting] +title = "Troubleshooting tips and gotchas" +weight = 110 + +[ui-crashes] +parent = troubleshooting +title = "UI crashes and whitescreens" +weight = 115 + +;Other places to get help +[other-help] +title = "Other places to get help" +weight = 200 + +[demo-video] +title = "Video demos for Views" +weight = 210 +parent = other-help + +;More examples +[top-pager] +title = "Adding a pager to the top and bottom of a view" +weight = 215 + +[select-multple-nids-contextual-filters] +title = "Selecting multiple nids with contextual filters (arguments)" +weight = 216 + +[taxonomy-page-override] +title = "Overriding the default taxonomy pages with the Taxonomy term view" +weight = 217 + +[only-link-title-for-published-nodes] +title = "A Views field template that creates title links only for published nodes" +weight = 218 + +[example-filter-by-current-user] +title = "Example to filter content by the current logged-in user" +weight = 219 + +[example-slideshow-thumb-pager] +title = "Example to create a slideshow with thumbnails as a pager underneath" +weight = 220 diff --git a/images/arrow-active.png b/images/arrow-active.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbd3c27f29f9a1781276a2ae8faa7afd5d2d36e GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^96-#^!3-qdU8!RPQY`6?zK#qG*KS<#k1zuAB}-f* zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%xd!-zxO#bc1qB6pdwbu!dGq1J zhmRgT^6~LGa^%RaUAy+}+xPF^zmq3VUc7kG&(CkurcE0*Y)DB-@%Q&XfByW98#g9T zp6u!A`Tzg_bT_}9Km)5hT^vIyZY2pcGc_~q33e1ZAi&eJ@5JOk4e3v=cNxxdo5pOtSyp&= zq=*u;;juS+&!^sqIKI`*y5!=d4HJFtY|OS_GUb?xvGd|Nujesp{W22WQ%mvv4FO#o^BR@ndm literal 0 HcmV?d00001 diff --git a/images/expanded-options.png b/images/expanded-options.png new file mode 100644 index 0000000000000000000000000000000000000000..b7b755c0a44f7ebae4972e9489f7d004c0ab82c7 GIT binary patch literal 228 zcmVww{j7*Vo28PTTa|E`mIR+~;bCfMvxdsyM z(5ekkRsEa&-}g5%GyJU<0Jd$L=lM6Mlp>-Q0E7@k6hipggNVi$A_4$>6U(xA?~miC z(WYsvwNlDgvc9*j>oiUKzCXvV>zs3a-+$74R*X?=Jq&}^I;C`RuQW5~oX2r2rQEI4 eCv_nr-uDldTYa;G_K`FI0000FPSxL!-G(ZEN0l@R;&!0VeHaj~zGc$Ad?%k=Wsfme+k&%(}=g*%$eY&@| zx2LD4y}iA;x!Ldcd%a$d$5U2T=61V_i;MH~^YilZa&mIg)6;D>o5f-=8jVp=Q5uZ~ z!!Z72005$%Bg->au2`Iwn`xaFF9cs~eJ2u20Tzhk*Zd`D01*H8UQFYI`ODUyXk3ED zxvyN++QFpM(*8|9Q7p5Egbv#wpF!Ux5mbup#Y36|oiD>$7)tUFrKS#Tvj!a-b{=%Z z8772J&V66bNmu_e`wf){qRI^#lGs)nsiW6e7{)}AjSKZOtyhRdNHEM07)EI+NGui+ zUb9-!rKEp-T=+q_ufOfkdxsZxcJ**g2g2EH`k!tb*&XcV7zK=|u5mObCoU|)p$MpB z$0UZz(BUx!B#D6C3`MPBBu$R5agCnmrcH5o&i(HLQaHl}knE%xex4MY>rlFzvPfK(S z@GGvUvjuF%+)B&HZgm=nIoN;2re=#DX=V| zZP6;q8ttuRv51J=088P7M3Vgqy=L1@}L!tBxRo?z2~Kr8Oye8=_lexufMj=f+| zqH^_+0aqR|5HKQ@>xZn<6n@vq@f+=+DlZ_OI7&d1^8+g55BK3#T~aQ!-*Xm&tdJie%4_B zXF=}#6E(=pS`3vWS@k&g+La39Wi(|%p&%S8d`x3f6Unp!o24DQ|7t0x zXP=xjP_rH1b7$Z7z7%x&1?a}LJSbM3>K+VE70%g|lCLzZg1o)MPcsMYVbF*3>>WF_Eg)56Gk0FXPQVQ?ma__1u$b&@uA`) z21W!-$_Mr2C>GGtv{pxhlYsOlM~^9rDICw%!S{$iKOkeZ8k`_5WXh2zX3xK$e1~7^ zEa*kPop*~1-e2v1sn~ybfxY839{a|Co-_Ev+B@0;!!>KYsrg_@o34U` zKtY~SHB2nkX_T0T6A6SYYSdiDGZ@|*XzRt_4c=8HN8%43;*oji;{`hMDL{^%(IW!U zxBuEVapFflgM6{6`D*(U&l~+Pi9G({q2JHn)^+A&oikiMa@DXr_@z|%Zc1KU)8VXzT^_4ucOtb;jpNj> z-j+o~U=>6Vr>i*<3P@C%AklPyZD~v1v$r7;4eaBpSltoMYtrdxrf+xE*=Pm^MdYU@ z7_$CG4vdMIL|7yimgsfTF#v4lN;Hso^0zmMDY#FMfHS)(j>y@NS?0*oMOveo zqU|h+GrAi6B0a(f;t43E(h33$0a&$_k|Nz|vORRT=4dzK;OncVrA=S&3vm1NRi8xH zafk?tH>nUC86*uRsr2GX-VWEDdOh? z>Bkkz8Q18?eXV`UC!>)|otN(Yzq8>VO|c(3yJbC(eLG<-KgEh-KEWB^m#31LMp?_9 z@;K7JMX)I*siPqyPw8Y8dqbZ&6F?B%SaXR^R7xJC8B2AqSI6`OOXhsEGClW5stZ?= zSynpD;x!QqqO;V|G7yxM`B|k*_9n@HX;4{VQK4;e?!CjE{_gj4cx->`!DwGN&TKP# z>kTppe%GK?%TT3BDHID~vs{Y6Z(bZ1LwfQSiz0}0%s^cg%D5ruFD>v5)dh}{VXTgd gkVHhdSYkS?`sR^#23&_qB-rHTo?$`S3&$7y4~a^U-v9sr literal 0 HcmV?d00001 diff --git a/images/loading.gif b/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..2dbcd624aa8b28a0e6cbe5a54d4cb03534009e38 GIT binary patch literal 6733 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 Rq)8m-k46__`8E)6{SW##k+}c> literal 0 HcmV?d00001 diff --git a/images/overridden.gif b/images/overridden.gif new file mode 100644 index 0000000000000000000000000000000000000000..b7811910ef8bc56ffd61757641c3a062353ef8ed GIT binary patch literal 175 zcmZ?wbhEHb({UEKYH@w#fz=mwmp3E z{LPy;4<0`~a`f1qJ$vpwc>MS8-=oKlzk2oR|NsA=zyD?+1QdU=FfuTRFzA33g6w2q z)lpFGQ&}p&xv;0b*`ece$3(-s-pXu;Z5=sh@h)F1!!W6n#X#7QtBGl`3oj#sH2}{> BNSXiu literal 0 HcmV?d00001 diff --git a/images/sprites.png b/images/sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..9fcb940227cf489b93a7339e71b5455cea6a2571 GIT binary patch literal 1777 zcmVG<3u1O2iEVLJFc31V;Pr&V7CAqrSdZnbWzPn|yig z{qcF{oO@sIxh))}lmr)!<@tb*ghbdAc@oB?=n zdjk(+{ZhoZJCO^TAiY^5A0DB+k0+u4z*5`bXN~v_gt9n`5~;k9Q?)wRJC zo;;v}AOs-@LH~ac+d*ziv1>D}=T!Eyf8V0PZU&)tk#K0-V>tbsT!PI1L{Z0uHT{DL z`U7(f_kIK+2tf#XWV;CARynC)KUQ@+58ziit5{vA{a^hk3In7UqI4K`NGEH;*$E%0 zrW@vP;aTM8ivl@_bpEu%E@mg}hD;(?6uw2w{Nn1R9QA)m*=t^utOEy^(X#ZNYGXnN zTj)1R00#02Qd&Vd(RhEeW_Omr;BDZdbnfTifE?JQ={8b)q$GThK-UsD=TooQ=~*k^E+;(e9Yp*fsWw&V707wUw4=g6s|LK?ahs*MQ+Vp^&K zSsJRpR*Z~c)@iy{qTyPJ+L&;HT`6B*8%(ehnM6p(bVm*QiFiAx8q@+h z02&GE04)H;f?fo@ON&zcp+SK=E>diataO4biua3%J$1iwY^Y#+DoTcQ{2+3@$69Fs!OuhN$Xglq!EfzrX{M3gJnQL{^aM0N$QeEmnno~ry;iW5Os zLW-wR{B8t62tp8o9y(~iPN4Ri$BGpNx$+MD_LeObUj=U*nidrlysw!b0bD;nf=*%Q zN1Hd7Yv)G*)6I{-&4-X56&8MQPksb|G z)6b8TD;Hkx0#SAiUirHD5%{l}A5~Or#rz0C2tp8o9$|i@%#B_yZ4ibI6L#L}@@=Fd zX?e~%?9Yu0W20>A+|yDgnoqmxZ|yz2di*=8pV`4l)=QsV&YKV)`Ei~5)_g%AIek6i z7pcj^YcKxMbNIWJ@may2^&vs5*w(vWw~jM?ddb~2(QGm9HAL{E4ir2+D>z%b2b7kU zW=cv*dM_?6ZdGh-?8t_OhQ?-(*SlxO)}-ajmyeE$iaI?W%C&3PI+~iAHq_PCZ994L zWcPp=O_?%fh{a-AmzbDn&(6*!-QC?JK0f}b>gwt%tbN3Y5&Lp-a^egI18Ht<9(nHE zxw6K_#!*wJPF-{U{Q18OiXArtAbB>Mts_4_|E$;Roi}F87$G@1xkAcuy1LRF4hMty z&1UltnVFdrS%0wXM`-?@SIpW>xNC&d>0AXJXTreYU|A%%yMwi7Wo1QXWMo7_{RC*A zFq7OYYn`x;dj@~1qn%( zGHd49ys#hI8F{YVPmYV6UvuhuTHXGXi}A^(T+#2pe6hA;j%lcI#xt*+5imbO5P}ed zAk2?2Kf?S7^CQfU5QHEEAqYVTLJ)!wgdhYV2tf!!5P}edAOs-@L5~ddw*UhGYI9L& Ts#Icj00000NkvXXu0mjfjNMYx literal 0 HcmV?d00001 diff --git a/images/status-active.gif b/images/status-active.gif new file mode 100644 index 0000000000000000000000000000000000000000..207e95c3fa8cc31a89af150ad74059b1667922b6 GIT binary patch literal 2196 zcmeH{Urbw79LLW&_xASmFQt@HSs}NhO2-Jh1BM+kb4v^B62vmdY!-sIxUi{Y-eKaZ zd%2gEe>0H6h_VE=!^E(JL`)>X2jVS^Zcfvn5<-k2WvFCNG`ghG7rZ6$WnPVsKJ5GO zJ8$RvJD>CY{Z5~0cApFxfB|GdPWa6w{k0|AwWK|oJQ2I!Je@i}^Zo0eZod2F_QJwK zJhi*9v9}~^YcH8hrc$Ymjg76Xt@{st+t|%+?PYgm$u+ybCo8+3&1pZopVPthPs$Sl z|6c-*eO+r)N4wV(XsWJa2q<^z_?=Wy@>!YdIyWW&Uj-18tONF|3Mu0pM+@E|b#0}^ z!~zi%9q%)3&a(p~G`B1c3*4%%lzk?{ByJ6=pegKss;|dO;n%+FeCW7{V%4V@w$Z{GnR7Ggdfd96mCSRsm(IU3X`eH-@mY!nv5{mPH7adf{@tFd+{D#Z*P7E4Cu( zNI<&yxF8`-(K~6DmV_bgDJSM6QPSy-`a53zHsK*ro@??84uYGYiss#G335cQ(q^ z%wp~p0g$crQ*_&{2!a61R3ETvWG(*SZ8O_GR^u5Yc|OD{*}kN}6zL0fe!xt};GOz%hKaYMskC&qRw=<`U&U>^cAo zLBAqcYczaZ@J3LHlNVx%1c};=4Y#YG7RXW%xDepEe!!I1`hwd%{Td?N=QPn}&`2C( ze3|@EBBE1Mq|)`TYFdd2uA0le6o z6d!7|&nD=vC$NZI_5Tu^++t8Y=|vzw z1Q9I-fe?vcx1AJNZgm0}!B}H#d-&TI?-^oT* zRA10W&q#7unMxBUvZJU0H>48~K*CV@D7`H;MzIa8(QDnFN!<(7r69X=cc1~zTKw(l Pm4SkyKc+`yv*-Q array( + 'IE' => 'lte IE 7', + '!IE' => FALSE + ), + 'preprocess' => FALSE, + ); + + $list[$module_path . '/css/views-admin.theme.css'] = array(); + + // Add in any theme specific CSS files we have + $themes = list_themes(); + $theme_key = $GLOBALS['theme']; + while ($theme_key) { + // Try to find the admin css file for non-core themes. + if (!in_array($theme_key, array('garland', 'seven', 'bartik'))) { + $theme_path = drupal_get_path('theme', $theme_key); + // First search in the css directory, then in the root folder of the theme. + if (file_exists($theme_path . "/css/views-admin.$theme_key.css")) { + $list[$theme_path . "/css/views-admin.$theme_key.css"] = array( + 'group' => CSS_THEME, + ); + } + else if (file_exists($theme_path . "/views-admin.$theme_key.css")) { + $list[$theme_path . "/views-admin.$theme_key.css"] = array( + 'group' => CSS_THEME, + ); + } + } + else { + $list[$module_path . "/css/views-admin.$theme_key.css"] = array( + 'group' => CSS_THEME, + ); + } + $theme_key = isset($themes[$theme_key]->base_theme) ? $themes[$theme_key]->base_theme : ''; + } + // Views contains style overrides for the following modules + $module_list = array('contextual', 'advanced_help', 'ctools'); + foreach ($module_list as $module) { + if (module_exists($module)) { + $list[$module_path . '/css/views-admin.' . $module . '.css'] = array(); + } + } + + + return $list; +} + +/** + * Adds standard Views administration CSS to the current page. + */ +function views_ui_add_admin_css() { + foreach (views_ui_get_admin_css() as $file => $options) { + drupal_add_css($file, $options); + } +} + +/** + * Check to see if the advanced help module is installed, and if not put up + * a message. + * + * Only call this function if the user is already in a position for this to + * be useful. + */ +function views_ui_check_advanced_help() { + if (!variable_get('views_ui_show_advanced_help_warning', TRUE)) { + return; + } + + if (!module_exists('advanced_help')) { + $filename = db_query_range("SELECT filename FROM {system} WHERE type = 'module' AND name = 'advanced_help'", 0, 1) + ->fetchField(); + if ($filename && file_exists($filename)) { + drupal_set_message(t('If you enable the advanced help module, Views will provide more and better help. Hide this message.', array('@modules' => url('admin/modules'),'@hide' => url('admin/structure/views/settings')))); + } + else { + drupal_set_message(t('If you install the advanced help module from !href, Views will provide more and better help. Hide this message.', array('!href' => l('http://drupal.org/project/advanced_help', 'http://drupal.org/project/advanced_help'), '@hide' => url('admin/structure/views/settings')))); + } + } +} + +/** + * Returns the results of the live preview. + */ +function views_ui_preview($view, $display_id, $args = array()) { + // When this function is invoked as a page callback, each Views argument is + // passed separately. + if (!is_array($args)) { + $args = array_slice(func_get_args(), 2); + } + + // Save $_GET['q'] so it can be restored before returning from this function. + $q = $_GET['q']; + + // Determine where the query and performance statistics should be output. + $show_query = variable_get('views_ui_show_sql_query', FALSE); + $show_info = variable_get('views_ui_show_preview_information', FALSE); + $show_location = variable_get('views_ui_show_sql_query_where', 'above'); + + $show_stats = variable_get('views_ui_show_performance_statistics', FALSE); + if ($show_stats) { + $show_stats = variable_get('views_ui_show_sql_query_where', 'above'); + } + + $combined = $show_query && $show_stats; + + $rows = array('query' => array(), 'statistics' => array()); + $output = ''; + + $errors = $view->validate(); + if ($errors === TRUE) { + $view->ajax = TRUE; + $view->live_preview = TRUE; + $view->views_ui_context = TRUE; + + // AJAX happens via $_POST but everything expects exposed data to + // be in GET. Copy stuff but remove ajax-framework specific keys. + // If we're clicking on links in a preview, though, we could actually + // still have some in $_GET, so we use $_REQUEST to ensure we get it all. + $exposed_input = $_REQUEST; + foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state', 'form_id', 'form_build_id', 'form_token') as $key) { + if (isset($exposed_input[$key])) { + unset($exposed_input[$key]); + } + } + + $view->set_exposed_input($exposed_input); + + + if (!$view->set_display($display_id)) { + return t('Invalid display id @display', array('@display' => $display_id)); + } + + $view->set_arguments($args); + + // Store the current view URL for later use: + if ($view->display_handler->get_option('path')) { + $path = $view->get_url(); + } + + // Make view links come back to preview. + $view->override_path = 'admin/structure/views/nojs/preview/' . $view->name . '/' . $display_id; + + // Also override $_GET['q'] so we get the pager. + $original_path = current_path(); + $_GET['q'] = $view->override_path; + if ($args) { + $_GET['q'] .= '/' . implode('/', $args); + } + + // Suppress contextual links of entities within the result set during a + // Preview. + // @todo We'll want to add contextual links specific to editing the View, so + // the suppression may need to be moved deeper into the Preview pipeline. + views_ui_contextual_links_suppress_push(); + $preview = $view->preview($display_id, $args); + views_ui_contextual_links_suppress_pop(); + + // Reset variables. + unset($view->override_path); + $_GET['q'] = $original_path; + + // Prepare the query information and statistics to show either above or + // below the view preview. + if ($show_info || $show_query || $show_stats) { + // Get information from the preview for display. + if (!empty($view->build_info['query'])) { + if ($show_query) { + $query = $view->build_info['query']; + // Only the sql default class has a method getArguments. + $quoted = array(); + + if (get_class($view->query) == 'views_plugin_query_default') { + $quoted = $query->getArguments(); + $connection = Database::getConnection(); + foreach ($quoted as $key => $val) { + if (is_array($val)) { + $quoted[$key] = implode(', ', array_map(array($connection, 'quote'), $val)); + } + else { + $quoted[$key] = $connection->quote($val); + } + } + } + $rows['query'][] = array('' . t('Query') . '', '
' . check_plain(strtr($query, $quoted)) . '
'); + if (!empty($view->additional_queries)) { + $queries = '' . t('These queries were run during view rendering:') . ''; + foreach ($view->additional_queries as $query) { + if ($queries) { + $queries .= "\n"; + } + $queries .= t('[@time ms]', array('@time' => intval($query[1] * 100000) / 100)) . ' ' . $query[0]; + } + + $rows['query'][] = array('' . t('Other queries') . '', '
' . $queries . '
'); + } + } + if ($show_info) { + $rows['query'][] = array('' . t('Title') . '', filter_xss_admin($view->get_title())); + if (isset($path)) { + $path = l($path, $path); + } + else { + $path = t('This display has no path.'); + } + $rows['query'][] = array('' . t('Path') . '', $path); + } + + if ($show_stats) { + $rows['statistics'][] = array('' . t('Query build time') . '', t('@time ms', array('@time' => intval($view->build_time * 100000) / 100))); + $rows['statistics'][] = array('' . t('Query execute time') . '', t('@time ms', array('@time' => intval($view->execute_time * 100000) / 100))); + $rows['statistics'][] = array('' . t('View render time') . '', t('@time ms', array('@time' => intval($view->render_time * 100000) / 100))); + + } + drupal_alter('views_preview_info', $rows, $view); + } + else { + // No query was run. Display that information in place of either the + // query or the performance statistics, whichever comes first. + if ($combined || ($show_location === 'above')) { + $rows['query'] = array(array('' . t('Query') . '', t('No query was run'))); + } + else { + $rows['statistics'] = array(array('' . t('Query') . '', t('No query was run'))); + } + } + } + } + else { + foreach ($errors as $error) { + drupal_set_message($error, 'error'); + } + $preview = t('Unable to preview due to validation errors.'); + } + + // Assemble the preview, the query info, and the query statistics in the + // requested order. + if ($show_location === 'above') { + if ($combined) { + $output .= '
' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '
'; + } + else { + $output .= '
' . theme('table', array('rows' => $rows['query'])) . '
'; + } + } + elseif ($show_stats === 'above') { + $output .= '
' . theme('table', array('rows' => $rows['statistics'])) . '
'; + } + + $output .= $preview; + + if ($show_location === 'below') { + if ($combined) { + $output .= '
' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '
'; + } + else { + $output .= '
' . theme('table', array('rows' => $rows['query'])) . '
'; + } + } + elseif ($show_stats === 'below') { + $output .= '
' . theme('table', array('rows' => $rows['statistics'])) . '
'; + } + + $_GET['q'] = $q; + return $output; +} + +/** + * Page callback to add a new view. + */ +function views_ui_add_page() { + views_ui_add_admin_css(); + drupal_set_title(t('Add new view')); + return drupal_get_form('views_ui_add_form'); +} + +/** + * Form builder for the "add new view" page. + */ +function views_ui_add_form($form, &$form_state) { + ctools_include('dependent'); + $form['#attached']['js'][] = drupal_get_path('module', 'views_ui') . '/js/views-admin.js'; + $form['#attributes']['class'] = array('views-admin'); + + $form['human_name'] = array( + '#type' => 'textfield', + '#title' => t('View name'), + '#required' => TRUE, + '#size' => 32, + '#default_value' => !empty($form_state['view']) ? $form_state['view']->human_name : '', + '#maxlength' => 255, + ); + $form['name'] = array( + '#type' => 'machine_name', + '#maxlength' => 128, + '#machine_name' => array( + 'exists' => 'views_get_view', + 'source' => array('human_name'), + ), + '#description' => t('A unique machine-readable name for this View. It must only contain lowercase letters, numbers, and underscores.'), + ); + + $form['description_enable'] = array( + '#type' => 'checkbox', + '#title' => t('Description'), + ); + $form['description'] = array( + '#type' => 'textfield', + '#title' => t('Provide description'), + '#title_display' => 'invisible', + '#size' => 64, + '#default_value' => !empty($form_state['view']) ? $form_state['view']->description : '', + '#dependency' => array( + 'edit-description-enable' => array(1), + ), + ); + + // Create a wrapper for the entire dynamic portion of the form. Everything + // that can be updated by AJAX goes somewhere inside here. For example, this + // is needed by "Show" dropdown (below); it changes the base table of the + // view and therefore potentially requires all options on the form to be + // dynamically updated. + $form['displays'] = array(); + + // Create the part of the form that allows the user to select the basic + // properties of what the view will display. + $form['displays']['show'] = array( + '#type' => 'fieldset', + '#tree' => TRUE, + '#attributes' => array('class' => array('container-inline')), + ); + + // Create the "Show" dropdown, which allows the base table of the view to be + // selected. + $wizard_plugins = views_ui_get_wizards(); + $options = array(); + foreach ($wizard_plugins as $key => $wizard) { + $options[$key] = $wizard['title']; + } + $form['displays']['show']['wizard_key'] = array( + '#type' => 'select', + '#title' => t('Show'), + '#options' => $options, + ); + $show_form = &$form['displays']['show']; + $show_form['wizard_key']['#default_value'] = views_ui_get_selected($form_state, array('show', 'wizard_key'), 'node', $show_form['wizard_key']); + // Changing this dropdown updates the entire content of $form['displays'] via + // AJAX. + views_ui_add_ajax_trigger($show_form, 'wizard_key', array('displays')); + + // Build the rest of the form based on the currently selected wizard plugin. + $wizard_key = $show_form['wizard_key']['#default_value']; + $get_instance = $wizard_plugins[$wizard_key]['get_instance']; + $wizard_instance = $get_instance($wizard_plugins[$wizard_key]); + $form = $wizard_instance->build_form($form, $form_state); + + $form['save'] = array( + '#type' => 'submit', + '#value' => t('Save & exit'), + '#validate' => array('views_ui_wizard_form_validate'), + '#submit' => array('views_ui_add_form_save_submit'), + ); + $form['continue'] = array( + '#type' => 'submit', + '#value' => t('Continue & edit'), + '#validate' => array('views_ui_wizard_form_validate'), + '#submit' => array('views_ui_add_form_store_edit_submit'), + '#process' => array_merge(array('views_ui_default_button'), element_info_property('submit', '#process', array())), + ); + $form['cancel'] = array( + '#type' => 'submit', + '#value' => t('Cancel'), + '#submit' => array('views_ui_add_form_cancel_submit'), + '#limit_validation_errors' => array(), + ); + + return $form; +} + +/** + * Helper form element validator: integer. + * + * The problem with this is that the function is private so it's not guaranteed + * that it might not be renamed/changed. In the future field.module or something else + * should provide a public validate function. + * + * @see _element_validate_integer_positive() + */ +function views_element_validate_integer($element, &$form_state) { + $value = $element['#value']; + if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) { + form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title']))); + } +} + +/** + * Gets the current value of a #select element, from within a form constructor function. + * + * This function is intended for use in highly dynamic forms (in particular the + * add view wizard) which are rebuilt in different ways depending on which + * triggering element (AJAX or otherwise) was most recently fired. For example, + * sometimes it is necessary to decide how to build one dynamic form element + * based on the value of a different dynamic form element that may not have + * even been present on the form the last time it was submitted. This function + * takes care of resolving those conflicts and gives you the proper current + * value of the requested #select element. + * + * By necessity, this function sometimes uses non-validated user input from + * $form_state['input'] in making its determination. Although it performs some + * minor validation of its own, it is not complete. The intention is that the + * return value of this function should only be used to help decide how to + * build the current form the next time it is reloaded, not to be saved as if + * it had gone through the normal, final form validation process. Do NOT use + * the results of this function for any other purpose besides deciding how to + * build the next version of the form. + * + * @param $form_state + * The standard associative array containing the current state of the form. + * @param $parents + * An array of parent keys that point to the part of the submitted form + * values that are expected to contain the element's value (in the case where + * this form element was actually submitted). In a simple case (assuming + * #tree is TRUE throughout the form), if the select element is located in + * $form['wrapper']['select'], so that the submitted form values would + * normally be found in $form_state['values']['wrapper']['select'], you would + * pass array('wrapper', 'select') for this parameter. + * @param $default_value + * The default value to return if the #select element does not currently have + * a proper value set based on the submitted input. + * @param $element + * An array representing the current version of the #select element within + * the form. + * + * @return + * The current value of the #select element. A common use for this is to feed + * it back into $element['#default_value'] so that the form will be rendered + * with the correct value selected. + */ +function views_ui_get_selected($form_state, $parents, $default_value, $element) { + // For now, don't trust this to work on anything but a #select element. + if (!isset($element['#type']) || $element['#type'] != 'select' || !isset($element['#options'])) { + return $default_value; + } + + // If there is a user-submitted value for this element that matches one of + // the currently available options attached to it, use that. We need to check + // $form_state['input'] rather than $form_state['values'] here because the + // triggering element often has the #limit_validation_errors property set to + // prevent unwanted errors elsewhere on the form. This means that the + // $form_state['values'] array won't be complete. We could make it complete + // by adding each required part of the form to the #limit_validation_errors + // property individually as the form is being built, but this is difficult to + // do for a highly dynamic and extensible form. This method is much simpler. + if (!empty($form_state['input'])) { + $key_exists = NULL; + $submitted = drupal_array_get_nested_value($form_state['input'], $parents, $key_exists); + // Check that the user-submitted value is one of the allowed options before + // returning it. This is not a substitute for actual form validation; + // rather it is necessary because, for example, the same select element + // might have #options A, B, and C under one set of conditions but #options + // D, E, F under a different set of conditions. So the form submission + // might have occurred with option A selected, but when the form is rebuilt + // option A is no longer one of the choices. In that case, we don't want to + // use the value that was submitted anymore but rather fall back to the + // default value. + if ($key_exists && in_array($submitted, array_keys($element['#options']))) { + return $submitted; + } + } + + // Fall back on returning the default value if nothing was returned above. + return $default_value; +} + +/** + * Converts a form element in the add view wizard to be AJAX-enabled. + * + * This function takes a form element and adds AJAX behaviors to it such that + * changing it triggers another part of the form to update automatically. It + * also adds a submit button to the form that appears next to the triggering + * element and that duplicates its functionality for users who do not have + * JavaScript enabled (the button is automatically hidden for users who do have + * JavaScript). + * + * To use this function, call it directly from your form builder function + * immediately after you have defined the form element that will serve as the + * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may + * mean that the non-JavaScript fallback button does not appear in the correct + * place in the form. + * + * @param $wrapping_element + * The element whose child will server as the AJAX trigger. For example, if + * $form['some_wrapper']['triggering_element'] represents the element which + * will trigger the AJAX behavior, you would pass $form['some_wrapper'] for + * this parameter. + * @param $trigger_key + * The key within the wrapping element that identifies which of its children + * serves as the AJAX trigger. In the above example, you would pass + * 'triggering_element' for this parameter. + * @param $refresh_parents + * An array of parent keys that point to the part of the form that will be + * refreshed by AJAX. For example, if triggering the AJAX behavior should + * cause $form['dynamic_content']['section'] to be refreshed, you would pass + * array('dynamic_content', 'section') for this parameter. + */ +function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_parents) { + $seen_ids = &drupal_static(__FUNCTION__ . ':seen_ids', array()); + $seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', array()); + + // Add the AJAX behavior to the triggering element. + $triggering_element = &$wrapping_element[$trigger_key]; + $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form'; + // We do not use drupal_html_id() to get an ID for the AJAX wrapper, because + // it remembers IDs across AJAX requests (and won't reuse them), but in our + // case we need to use the same ID from request to request so that the + // wrapper can be recognized by the AJAX system and its content can be + // dynamically updated. So instead, we will keep track of duplicate IDs + // (within a single request) on our own, later in this function. + $triggering_element['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refresh_parents) . '-wrapper'; + + // Add a submit button for users who do not have JavaScript enabled. It + // should be displayed next to the triggering element on the form. + $button_key = $trigger_key . '_trigger_update'; + $wrapping_element[$button_key] = array( + '#type' => 'submit', + // Hide this button when JavaScript is enabled. + '#attributes' => array('class' => array('js-hide')), + '#submit' => array('views_ui_nojs_submit'), + // Add a process function to limit this button's validation errors to the + // triggering element only. We have to do this in #process since until the + // form API has added the #parents property to the triggering element for + // us, we don't have any (easy) way to find out where its submitted values + // will eventually appear in $form_state['values']. + '#process' => array_merge(array('views_ui_add_limited_validation'), element_info_property('submit', '#process', array())), + // Add an after-build function that inserts a wrapper around the region of + // the form that needs to be refreshed by AJAX (so that the AJAX system can + // detect and dynamically update it). This is done in #after_build because + // it's a convenient place where we have automatic access to the complete + // form array, but also to minimize the chance that the HTML we add will + // get clobbered by code that runs after we have added it. + '#after_build' => array_merge(element_info_property('submit', '#after_build', array()), array('views_ui_add_ajax_wrapper')), + ); + // Copy #weight and #access from the triggering element to the button, so + // that the two elements will be displayed together. + foreach (array('#weight', '#access') as $property) { + if (isset($triggering_element[$property])) { + $wrapping_element[$button_key][$property] = $triggering_element[$property]; + } + } + // For easiest integration with the form API and the testing framework, we + // always give the button a unique #value, rather than playing around with + // #name. + $button_title = !empty($triggering_element['#title']) ? $triggering_element['#title'] : $trigger_key; + if (empty($seen_buttons[$button_title])) { + $wrapping_element[$button_key]['#value'] = t('Update "@title" choice', array( + '@title' => $button_title, + )); + $seen_buttons[$button_title] = 1; + } + else { + $wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', array( + '@title' => $button_title, + '@number' => ++$seen_buttons[$button_title], + )); + } + + // Attach custom data to the triggering element and submit button, so we can + // use it in both the process function and AJAX callback. + $ajax_data = array( + 'wrapper' => $triggering_element['#ajax']['wrapper'], + 'trigger_key' => $trigger_key, + 'refresh_parents' => $refresh_parents, + // Keep track of duplicate wrappers so we don't add the same wrapper to the + // page more than once. + 'duplicate_wrapper' => !empty($seen_ids[$triggering_element['#ajax']['wrapper']]), + ); + $seen_ids[$triggering_element['#ajax']['wrapper']] = TRUE; + $triggering_element['#views_ui_ajax_data'] = $ajax_data; + $wrapping_element[$button_key]['#views_ui_ajax_data'] = $ajax_data; +} + +/** + * Processes a non-JavaScript fallback submit button to limit its validation errors. + */ +function views_ui_add_limited_validation($element, &$form_state) { + // Retrieve the AJAX triggering element so we can determine its parents. (We + // know it's at the same level of the complete form array as the submit + // button, so all we have to do to find it is swap out the submit button's + // last array parent.) + $array_parents = $element['#array_parents']; + array_pop($array_parents); + $array_parents[] = $element['#views_ui_ajax_data']['trigger_key']; + $ajax_triggering_element = drupal_array_get_nested_value($form_state['complete form'], $array_parents); + + // Limit this button's validation to the AJAX triggering element, so it can + // update the form for that change without requiring that the rest of the + // form be filled out properly yet. + $element['#limit_validation_errors'] = array($ajax_triggering_element['#parents']); + + // If we are in the process of a form submission and this is the button that + // was clicked, the form API workflow in form_builder() will have already + // copied it to $form_state['triggering_element'] before our #process + // function is run. So we need to make the same modifications in $form_state + // as we did to the element itself, to ensure that #limit_validation_errors + // will actually be set in the correct place. + if (!empty($form_state['triggering_element'])) { + $clicked_button = &$form_state['triggering_element']; + if ($clicked_button['#name'] == $element['#name'] && $clicked_button['#value'] == $element['#value']) { + $clicked_button['#limit_validation_errors'] = $element['#limit_validation_errors']; + } + } + + return $element; +} + +/** + * After-build function that adds a wrapper to a form region (for AJAX refreshes). + * + * This function inserts a wrapper around the region of the form that needs to + * be refreshed by AJAX, based on information stored in the corresponding + * submit button form element. + */ +function views_ui_add_ajax_wrapper($element, &$form_state) { + // Don't add the wrapper
if the same one was already inserted on this + // form. + if (empty($element['#views_ui_ajax_data']['duplicate_wrapper'])) { + // Find the region of the complete form that needs to be refreshed by AJAX. + // This was earlier stored in a property on the element. + $complete_form = &$form_state['complete form']; + $refresh_parents = $element['#views_ui_ajax_data']['refresh_parents']; + $refresh_element = drupal_array_get_nested_value($complete_form, $refresh_parents); + + // The HTML ID that AJAX expects was also stored in a property on the + // element, so use that information to insert the wrapper
here. + $id = $element['#views_ui_ajax_data']['wrapper']; + $refresh_element += array( + '#prefix' => '', + '#suffix' => '', + ); + $refresh_element['#prefix'] = '
' . $refresh_element['#prefix']; + $refresh_element['#suffix'] .= '
'; + + // Copy the element that needs to be refreshed back into the form, with our + // modifications to it. + drupal_array_set_nested_value($complete_form, $refresh_parents, $refresh_element); + } + + return $element; +} + +/** + * Updates a part of the add view form via AJAX. + * + * @return + * The part of the form that has changed. + */ +function views_ui_ajax_update_form($form, $form_state) { + // The region that needs to be updated was stored in a property of the + // triggering element by views_ui_add_ajax_trigger(), so all we have to do is + // retrieve that here. + return drupal_array_get_nested_value($form, $form_state['triggering_element']['#views_ui_ajax_data']['refresh_parents']); +} + +/** + * Non-Javascript fallback for updating the add view form. + */ +function views_ui_nojs_submit($form, &$form_state) { + $form_state['rebuild'] = TRUE; +} + +/** + * Validate the add view form. + */ +function views_ui_wizard_form_validate($form, &$form_state) { + $wizard = views_ui_get_wizard($form_state['values']['show']['wizard_key']); + $form_state['wizard'] = $wizard; + $get_instance = $wizard['get_instance']; + $form_state['wizard_instance'] = $get_instance($wizard); + $errors = $form_state['wizard_instance']->validate($form, $form_state); + foreach ($errors as $name => $message) { + form_set_error($name, $message); + } +} + +/** + * Process the add view form, 'save'. + */ +function views_ui_add_form_save_submit($form, &$form_state) { + try { + $view = $form_state['wizard_instance']->create_view($form, $form_state); + } + catch (ViewsWizardException $e) { + drupal_set_message($e->getMessage(), 'error'); + $form_state['redirect'] = 'admin/structure/views'; + } + $view->save(); + + $form_state['redirect'] = 'admin/structure/views'; + if (!empty($view->display['page'])) { + $display = $view->display['page']; + if ($display->handler->has_path()) { + $one_path = $display->handler->get_option('path'); + if (strpos($one_path, '%') === FALSE) { + $form_state['redirect'] = $one_path; // PATH TO THE VIEW IF IT HAS ONE + return; + } + } + } + drupal_set_message(t('Your view was saved. You may edit it from the list below.')); +} + +/** + * Process the add view form, 'continue'. + */ +function views_ui_add_form_store_edit_submit($form, &$form_state) { + try { + $view = $form_state['wizard_instance']->create_view($form, $form_state); + } + catch (ViewsWizardException $e) { + drupal_set_message($e->getMessage(), 'error'); + $form_state['redirect'] = 'admin/structure/views'; + } + // Just cache it temporarily to edit it. + views_ui_cache_set($view); + + // If there is a destination query, ensure we still redirect the user to the + // edit view page, and then redirect the user to the destination. + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + unset($_GET['destination']); + } + $form_state['redirect'] = array('admin/structure/views/view/' . $view->name, array('query' => $destination)); +} + +/** + * Cancel the add view form. + */ +function views_ui_add_form_cancel_submit($form, &$form_state) { + $form_state['redirect'] = 'admin/structure/views'; +} + +/** + * Form element validation handler for a taxonomy autocomplete field. + * + * This allows a taxonomy autocomplete field to be validated outside the + * standard Field API workflow, without passing in a complete field widget. + * Instead, all that is required is that $element['#field_name'] contain the + * name of the taxonomy autocomplete field that is being validated. + * + * This function is currently not used for validation directly, although it + * could be. Instead, it is only used to store the term IDs and vocabulary name + * in the element value, based on the tags that the user typed in. + * + * @see taxonomy_autocomplete_validate() + */ +function views_ui_taxonomy_autocomplete_validate($element, &$form_state) { + $value = array(); + if ($tags = $element['#value']) { + // Get the machine names of the vocabularies we will search, keyed by the + // vocabulary IDs. + $field = field_info_field($element['#field_name']); + $vocabularies = array(); + if (!empty($field['settings']['allowed_values'])) { + foreach ($field['settings']['allowed_values'] as $tree) { + if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) { + $vocabularies[$vocabulary->vid] = $tree['vocabulary']; + } + } + } + // Store the term ID of each (valid) tag that the user typed. + $typed_terms = drupal_explode_tags($tags); + foreach ($typed_terms as $typed_term) { + if ($terms = taxonomy_term_load_multiple(array(), array('name' => trim($typed_term), 'vid' => array_keys($vocabularies)))) { + $term = array_pop($terms); + $value['tids'][] = $term->tid; + } + } + // Store the term IDs along with the name of the vocabulary. Currently + // Views (as well as the Field UI) assumes that there will only be one + // vocabulary, although technically the API allows there to be more than + // one. + if (!empty($value['tids'])) { + $value['tids'] = array_unique($value['tids']); + $value['vocabulary'] = array_pop($vocabularies); + } + } + form_set_value($element, $value, $form_state); +} + +/** + * Theme function; returns basic administrative information about a view. + * + * TODO: template + preprocess + */ +function theme_views_ui_view_info($variables) { + $view = $variables['view']; + $title = $view->get_human_name(); + + $displays = _views_ui_get_displays_list($view); + $displays = empty($displays) ? t('None') : format_plural(count($displays), 'Display', 'Displays') . ': ' . '' . implode(', ', $displays) . ''; + + switch ($view->type) { + case t('Default'): + default: + $type = t('In code'); + break; + + case t('Normal'): + $type = t('In database'); + break; + + case t('Overridden'): + $type = t('Database overriding code'); + } + + $output = ''; + $output .= '
' . $title . "
\n"; + $output .= '
' . $displays . "
\n"; + $output .= '
' . $type . "
\n"; + $output .= '
' . t('Type') . ': ' . $variables['base']. "
\n"; + return $output; +} + +/** + * Page to delete a view. + */ +function views_ui_break_lock_confirm($form, &$form_state, $view) { + $form_state['view'] = &$view; + $form = array(); + + if (empty($view->locked)) { + $form['message']['#markup'] = t('There is no lock on view %name to break.', array('%name' => $view->name)); + return $form; + } + + $cancel = 'admin/structure/views/view/' . $view->name . '/edit'; + if (!empty($_REQUEST['cancel'])) { + $cancel = $_REQUEST['cancel']; + } + + $account = user_load($view->locked->uid); + return confirm_form($form, + t('Are you sure you want to break the lock on view %name?', + array('%name' => $view->name)), + $cancel, + t('By breaking this lock, any unsaved changes made by !user will be lost!', array('!user' => theme('username', array('account' => $account)))), + t('Break lock'), + t('Cancel')); +} + +/** + * Submit handler to break_lock a view. + */ +function views_ui_break_lock_confirm_submit(&$form, &$form_state) { + ctools_object_cache_clear_all('view', $form_state['view']->name); + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit'; + drupal_set_message(t('The lock has been broken and you may now edit this view.')); +} + +/** + * Helper function to return the used display_id for the edit page + * + * This function handles access to the display. + */ +function views_ui_edit_page_display($view, $display_id) { + // Determine the displays available for editing. + if ($tabs = views_ui_edit_page_display_tabs($view, $display_id)) { + // If a display isn't specified, use the first one. + if (empty($display_id)) { + foreach ($tabs as $id => $tab) { + if (!isset($tab['#access']) || $tab['#access']) { + $display_id = $id; + break; + } + } + } + // If a display is specified, but we don't have access to it, return + // an access denied page. + if ($display_id && (!isset($tabs[$display_id]) || (isset($tabs[$display_id]['#access']) && !$tabs[$display_id]['#access']))) { + return MENU_ACCESS_DENIED; + } + + return $display_id; + } + elseif ($display_id) { + return MENU_ACCESS_DENIED; + } + else { + $display_id = NULL; + } + + return $display_id; +} + +/** + * Page callback for the Edit View page. + */ +function views_ui_edit_page($view, $display_id = NULL) { + $display_id = views_ui_edit_page_display($view, $display_id); + if (!in_array($display_id, array(MENU_ACCESS_DENIED, MENU_NOT_FOUND))) { + $build = array(); + $build['edit_form'] = drupal_get_form('views_ui_edit_form', $view, $display_id); + $build['preview'] = views_ui_build_preview($view, $display_id, FALSE); + } + else { + $build = $display_id; + } + + return $build; +} + +function views_ui_build_preview($view, $display_id, $render = TRUE) { + if (isset($_POST['ajax_html_ids'])) { + unset($_POST['ajax_html_ids']); + } + + $build = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('id' => 'views-preview-wrapper', 'class' => 'views-admin clearfix'), + ); + + $form_state = array('build_info' => array('args' => array($view, $display_id))); + $build['controls'] = drupal_build_form('views_ui_preview_form', $form_state); + + $args = array(); + if (!empty($form_state['values']['view_args'])) { + $args = explode('/', $form_state['values']['view_args']); + } + + $build['preview'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('id' => 'views-live-preview'), + '#markup' => $render ? views_ui_preview($view->clone_view(), $display_id, $args) : '', + ); + + return $build; +} + +/** + * Form builder callback for editing a View. + * + * @todo Remove as many #prefix/#suffix lines as possible. Use #theme_wrappers + * instead. + * + * @todo Rename to views_ui_edit_view_form(). See that function for the "old" + * version. + * + * @see views_ui_ajax_get_form() + */ +function views_ui_edit_form($form, &$form_state, $view, $display_id = NULL) { + // Do not allow the form to be cached, because $form_state['view'] can become + // stale between page requests. + // See views_ui_ajax_get_form() for how this affects #ajax. + // @todo To remove this and allow the form to be cacheable: + // - Change $form_state['view'] to $form_state['temporary']['view']. + // - Add a #process function to initialize $form_state['temporary']['view'] + // on cached form submissions. + // - Update ctools_include() to support cached forms, or else use + // form_load_include(). + $form_state['no_cache'] = TRUE; + + if ($display_id) { + if (!$view->set_display($display_id)) { + $form['#markup'] = t('Invalid display id @display', array('@display' => $display_id)); + return $form; + } + + $view->fix_missing_relationships(); + } + + ctools_include('dependent'); + $form['#attached']['js'][] = ctools_attach_js('dependent'); + $form['#attached']['js'][] = ctools_attach_js('collapsible-div'); + + $form['#tree'] = TRUE; + // @todo When more functionality is added to this form, cloning here may be + // too soon. But some of what we do with $view later in this function + // results in making it unserializable due to PDO limitations. + $form_state['view'] = clone($view); + + $form['#attached']['library'][] = array('system', 'ui.tabs'); + $form['#attached']['library'][] = array('system', 'ui.dialog'); + $form['#attached']['library'][] = array('system', 'drupal.ajax'); + $form['#attached']['library'][] = array('system', 'jquery.form'); + // TODO: This should be getting added to the page when an ajax popup calls + // for it, instead of having to add it manually here. + $form['#attached']['js'][] = 'misc/tabledrag.js'; + + $form['#attached']['css'] = views_ui_get_admin_css(); + $module_path = drupal_get_path('module', 'views_ui'); + + $form['#attached']['js'][] = $module_path . '/js/views-admin.js'; + $form['#attached']['js'][] = array( + 'data' => array('views' => array('ajax' => array( + 'id' => '#views-ajax-body', + 'title' => '#views-ajax-title', + 'popup' => '#views-ajax-popup', + 'defaultForm' => views_ui_get_default_ajax_message(), + ))), + 'type' => 'setting', + ); + + $form += array( + '#prefix' => '', + '#suffix' => '', + ); + $form['#prefix'] = $form['#prefix'] . '
'; + $form['#suffix'] = '
' . $form['#suffix']; + + $form['#attributes']['class'] = array('form-edit'); + + if (isset($view->locked) && is_object($view->locked)) { + $form['locked'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('view-locked', 'messages', 'warning')), + '#markup' => t('This view is being edited by user !user, and is therefore locked from editing by others. This lock is !age old. Click here to break this lock.', array('!user' => theme('username', array('account' => user_load($view->locked->uid))), '!age' => format_interval(REQUEST_TIME - $view->locked->updated), '!break' => url('admin/structure/views/view/' . $view->name . '/break-lock'))), + ); + } + if (isset($view->vid) && $view->vid == 'new') { + $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard the view.'); + } + else { + $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard your changes.'); + } + + $form['changed'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('view-changed', 'messages', 'warning')), + '#markup' => $message, + ); + if (empty($view->changed)) { + $form['changed']['#attributes']['class'][] = 'js-hide'; + } + + $form['help_text'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#markup' => t('Modify the display(s) of your view below or add new displays.'), + ); + + $form['actions'] = array( + '#type' => 'actions', + '#weight' => 0, + ); + + if (empty($view->changed)) { + $form['actions']['#attributes'] = array( + 'class' => array( + 'js-hide', + ), + ); + } + + $form['actions']['save'] = array( + '#type' => 'submit', + '#value' => t('Save'), + // Taken from the "old" UI. @TODO: Review and rename. + '#validate' => array('views_ui_edit_view_form_validate'), + '#submit' => array('views_ui_edit_view_form_submit'), + ); + $form['actions']['cancel'] = array( + '#type' => 'submit', + '#value' => t('Cancel'), + '#submit' => array('views_ui_edit_view_form_cancel'), + ); + + $form['displays'] = array( + '#prefix' => '

' . t('Displays') . '

' . "\n" . '
', + '#suffix' => '
', + ); + + $form['displays']['top'] = views_ui_render_display_top($view, $display_id); + + // The rest requires a display to be selected. + if ($display_id) { + $form_state['display_id'] = $display_id; + + // The part of the page where editing will take place. + // This element is the ctools collapsible-div container for the display edit elements. + $form['displays']['settings'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array( + 'class' => array( + 'views-display-settings', + 'box-margin', + 'ctools-collapsible-container', + ), + ), + '#id' => 'edit-display-settings', + ); + $display_title = views_ui_get_display_label($view, $display_id, FALSE); + // Add a handle for the ctools collapsible-div. The handle is the title of the display + $form['displays']['settings']['tab_title']['#markup'] = '

' . t('@display_title details', array('@display_title' => ucwords($display_title))) . '

'; + // Add a text that the display is disabled. + if (!empty($view->display[$display_id]->handler)) { + $enabled = $view->display[$display_id]->handler->get_option('enabled'); + if (empty($enabled)) { + $form['displays']['settings']['disabled']['#markup'] = t('This display is disabled.'); + } + } + // The ctools collapsible-div content + $form['displays']['settings']['settings_content']= array( + '#theme_wrappers' => array('container'), + '#id' => 'edit-display-settings-content', + '#attributes' => array( + 'class' => array( + 'ctools-collapsible-content', + ), + ), + ); + // Add the edit display content + $form['displays']['settings']['settings_content']['tab_content'] = views_ui_get_display_tab($view, $display_id); + $form['displays']['settings']['settings_content']['tab_content']['#theme_wrappers'] = array('container'); + $form['displays']['settings']['settings_content']['tab_content']['#attributes'] = array('class' => array('views-display-tab')); + $form['displays']['settings']['settings_content']['tab_content']['#id'] = 'views-tab-' . $display_id; + // Mark deleted displays as such. + if (!empty($view->display[$display_id]->deleted)) { + $form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-deleted'; + } + // Mark disabled displays as such. + if (empty($enabled)) { + $form['displays']['settings']['settings_content']['tab_content']['#attributes']['class'][] = 'views-display-disabled'; + } + + // The content of the popup dialog. + $form['ajax-area'] = array( + '#theme_wrappers' => array('container'), + '#id' => 'views-ajax-popup', + ); + $form['ajax-area']['ajax-title'] = array( + '#markup' => '

', + ); + $form['ajax-area']['ajax-body'] = array( + '#theme_wrappers' => array('container'), + '#id' => 'views-ajax-body', + '#markup' => views_ui_get_default_ajax_message(), + ); + } + + // If relationships had to be fixed, we want to get that into the cache + // so that edits work properly, and to try to get the user to save it + // so that it's not using weird fixed up relationships. + if (!empty($view->relationships_changed) && empty($_POST)) { + drupal_set_message(t('This view has been automatically updated to fix missing relationships. While this View should continue to work, you should verify that the automatic updates are correct and save this view.')); + views_ui_cache_set($view); + } + return $form; +} + +/** + * Provide the preview formulas and the preview output, too. + */ +function views_ui_preview_form($form, &$form_state, $view, $display_id = 'default') { + $form_state['no_cache'] = TRUE; + $form_state['view'] = $view; + + $form['#attributes'] = array('class' => array('clearfix',)); + + // Add a checkbox controlling whether or not this display auto-previews. + $form['live_preview'] = array( + '#type' => 'checkbox', + '#id' => 'edit-displays-live-preview', + '#title' => t('Auto preview'), + '#default_value' => variable_get('views_ui_always_live_preview', TRUE), + ); + + // Add the arguments textfield + $form['view_args'] = array( + '#type' => 'textfield', + '#title' => t('Preview with contextual filters:'), + '#description' => t('Separate contextual filter values with a "/". For example, %example.', array('%example' => '40/12/10')), + '#id' => 'preview-args', +// '#attributes' => array('class' => array('ctools-auto-submit')), + ); + + // Add the preview button + $form['button'] = array( + '#type' => 'submit', + '#value' => t('Update preview'), + '#attributes' => array('class' => array('arguments-preview', 'ctools-auto-submit-click')), + '#pre_render' => array('ctools_dependent_pre_render'), + '#prefix' => '
', + '#suffix' => '
', + '#id' => 'preview-submit', + '#submit' => array('views_ui_edit_form_submit_preview'), + '#ajax' => array( + 'path' => 'admin/structure/views/view/' . $view->name . '/preview/' . $display_id . '/ajax', + 'wrapper' => 'views-preview-wrapper', + 'event' => 'click', + 'progress' => array('type' => 'throbber'), + 'method' => 'replace', + ), + // Make ENTER in arguments textfield (and other controls) submit the form + // as this button, not the Save button. + // @todo This only works for JS users. To make this work for nojs users, + // we may need to split Preview into a separate form. + '#process' => array_merge(array('views_ui_default_button'), element_info_property('submit', '#process', array())), + ); + $form['#action'] = url('admin/structure/views/view/' . $view->name .'/preview/' . $display_id); + + return $form; +} + +/** + * Render the top of the display so it can be updated during ajax operations. + */ +function views_ui_render_display_top($view, $display_id) { + $element['#theme_wrappers'] = array('views_container'); + $element['#attributes']['class'] = array('views-display-top', 'clearfix'); + $element['#attributes']['id'] = array('views-display-top'); + + // Extra actions for the display + $element['extra_actions'] = array( + '#theme' => 'links__ctools_dropbutton', + '#attributes' => array( + 'id' => 'views-display-extra-actions', + 'class' => array( + 'horizontal', 'right', 'links', 'actions', + ), + ), + '#links' => array( + 'edit-details' => array( + 'title' => t('edit view name/description'), + 'href' => "admin/structure/views/nojs/edit-details/$view->name", + 'attributes' => array('class' => array('views-ajax-link')), + ), + 'analyze' => array( + 'title' => t('analyze view'), + 'href' => "admin/structure/views/nojs/analyze/$view->name/$display_id", + 'attributes' => array('class' => array('views-ajax-link')), + ), + 'clone' => array( + 'title' => t('clone view'), + 'href' => "admin/structure/views/view/$view->name/clone", + ), + 'export' => array( + 'title' => t('export view'), + 'href' => "admin/structure/views/view/$view->name/export", + ), + 'reorder' => array( + 'title' => t('reorder displays'), + 'href' => "admin/structure/views/nojs/reorder-displays/$view->name/$display_id", + 'attributes' => array('class' => array('views-ajax-link')), + ), + ), + ); + + // Let other modules add additional links here. + drupal_alter('views_ui_display_top_links', $element['extra_actions']['#links'], $view, $display_id); + + if (isset($view->type) && $view->type != t('Default')) { + if ($view->type == t('Overridden')) { + $element['extra_actions']['#links']['revert'] = array( + 'title' => t('revert view'), + 'href' => "admin/structure/views/view/$view->name/revert", + 'query' => array('destination' => "admin/structure/views/view/$view->name"), + ); + } + else { + $element['extra_actions']['#links']['delete'] = array( + 'title' => t('delete view'), + 'href' => "admin/structure/views/view/$view->name/delete", + ); + } + } + + // Determine the displays available for editing. + if ($tabs = views_ui_edit_page_display_tabs($view, $display_id)) { + if ($display_id) { + $tabs[$display_id]['#active'] = TRUE; + } + $tabs['#prefix'] = '

' . t('Secondary tabs') . '

    '; + $tabs['#suffix'] = '
'; + $element['tabs'] = $tabs; + } + + // Buttons for adding a new display. + foreach (views_fetch_plugin_names('display', NULL, array($view->base_table)) as $type => $label) { + $element['add_display'][$type] = array( + '#type' => 'submit', + '#value' => t('Add !display', array('!display' => $label)), + '#limit_validation_errors' => array(), + '#submit' => array('views_ui_edit_form_submit_add_display', 'views_ui_edit_form_submit_delay_destination'), + '#attributes' => array('class' => array('add-display')), + // Allow JavaScript to remove the 'Add ' prefix from the button label when + // placing the button in a "Add" dropdown menu. + '#process' => array_merge(array('views_ui_form_button_was_clicked'), element_info_property('submit', '#process', array())), + '#values' => array(t('Add !display', array('!display' => $label)), $label), + ); + } + + return $element; +} + +function views_ui_get_default_ajax_message() { + return '
' . t("Click on an item to edit that item's details.") . '
'; +} + +/** + * Submit handler to add a display to a view. + */ +function views_ui_edit_form_submit_add_display($form, &$form_state) { + $view = $form_state['view']; + + // Create the new display. + $parents = $form_state['triggering_element']['#parents']; + $display_type = array_pop($parents); + $display_id = $view->add_display($display_type); + views_ui_cache_set($view); + + // Redirect to the new display's edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $view->name . '/edit/' . $display_id; +} + +/** + * Submit handler to duplicate a display for a view. + */ +function views_ui_edit_form_submit_duplicate_display($form, &$form_state) { + $view = $form_state['view']; + $display_id = $form_state['display_id']; + + // Create the new display. + $display = $view->display[$display_id]; + $new_display_id = $view->add_display($display->display_plugin); + $view->display[$new_display_id] = clone $display; + $view->display[$new_display_id]->id = $new_display_id; + + // By setting the current display the changed marker will appear on the new + // display. + $view->current_display = $new_display_id; + views_ui_cache_set($view); + + // Redirect to the new display's edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $view->name . '/edit/' . $new_display_id; +} + +/** + * Submit handler to delete a display from a view. + */ +function views_ui_edit_form_submit_delete_display($form, &$form_state) { + $view = $form_state['view']; + $display_id = $form_state['display_id']; + + // Mark the display for deletion. + $view->display[$display_id]->deleted = TRUE; + views_ui_cache_set($view); + + // Redirect to the top-level edit page. The first remaining display will + // become the active display. + $form_state['redirect'] = 'admin/structure/views/view/' . $view->name; +} + +/** + * Submit handler to add a restore a removed display to a view. + */ +function views_ui_edit_form_submit_undo_delete_display($form, &$form_state) { + // Create the new display + $id = $form_state['display_id']; + $form_state['view']->display[$id]->deleted = FALSE; + + // Store in cache + views_ui_cache_set($form_state['view']); + + // Redirect to the top-level edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id; +} + +/** + * Submit handler to enable a disabled display. + */ +function views_ui_edit_form_submit_enable_display($form, &$form_state) { + $id = $form_state['display_id']; + // set_option doesn't work because this would might affect upper displays + $form_state['view']->display[$id]->handler->set_option('enabled', TRUE); + + // Store in cache + views_ui_cache_set($form_state['view']); + + // Redirect to the top-level edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id; +} + +/** + * Submit handler to disable display. + */ +function views_ui_edit_form_submit_disable_display($form, &$form_state) { + $id = $form_state['display_id']; + $form_state['view']->display[$id]->handler->set_option('enabled', FALSE); + + // Store in cache + views_ui_cache_set($form_state['view']); + + // Redirect to the top-level edit page. + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $id; +} + +/** + * Submit handler when Preview button is clicked. + */ +function views_ui_edit_form_submit_preview($form, &$form_state) { + // Rebuild the form with a pristine $view object. + $form_state['build_info']['args'][0] = views_ui_cache_load($form_state['view']->name); + $form_state['show_preview'] = TRUE; + $form_state['rebuild'] = TRUE; +} + +/** + * Submit handler for form buttons that do not complete a form workflow. + * + * The Edit View form is a multistep form workflow, but with state managed by + * the CTools object cache rather than $form_state['rebuild']. Without this + * submit handler, buttons that add or remove displays would redirect to the + * destination parameter (e.g., when the Edit View form is linked to from a + * contextual link). This handler can be added to buttons whose form submission + * should not yet redirect to the destination. + */ +function views_ui_edit_form_submit_delay_destination($form, &$form_state) { + if (isset($_GET['destination']) && $form_state['redirect'] !== FALSE) { + if (!isset($form_state['redirect'])) { + $form_state['redirect'] = $_GET['q']; + } + if (is_string($form_state['redirect'])) { + $form_state['redirect'] = array($form_state['redirect']); + } + $options = isset($form_state['redirect'][1]) ? $form_state['redirect'][1] : array(); + if (!isset($options['query']['destination'])) { + $options['query']['destination'] = $_GET['destination']; + } + $form_state['redirect'][1] = $options; + unset($_GET['destination']); + } +} + +/** + * Adds tabs for navigating across Displays when editing a View. + * + * This function can be called from hook_menu_local_tasks_alter() to implement + * these tabs as secondary local tasks, or it can be called from elsewhere if + * having them as secondary local tasks isn't desired. The caller is responsible + * for setting the active tab's #active property to TRUE. + * + * @param view $view + * The view which will be edited. + * @param $display_id + * The display_id which is edited on the current request. + */ +function views_ui_edit_page_display_tabs($view, $display_id = NULL) { + $tabs = array(); + + // Create a tab for each display. + foreach ($view->display as $id => $display) { + $tabs[$id] = array( + '#theme' => 'menu_local_task', + '#link' => array( + 'title' => views_ui_get_display_label($view, $id), + 'href' => 'admin/structure/views/view/' . $view->name . '/edit/' . $id, + 'localized_options' => array(), + ), + ); + if (!empty($display->deleted)) { + $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-deleted-link'; + } + if (isset($display->display_options['enabled']) && !$display->display_options['enabled']) { + $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'views-display-disabled-link'; + } + } + + // If the default display isn't supposed to be shown, don't display its tab, unless it's the only display. + if ((!views_ui_show_default_display($view) && $display_id != 'default') && count($tabs) > 1) { + $tabs['default']['#access'] = FALSE; + } + + // Mark the display tab as red to show validation errors. + $view->validate(); + foreach ($view->display as $id => $display) { + if (!empty($view->display_errors[$id])) { + // Always show the tab. + $tabs[$id]['#access'] = TRUE; + // Add a class to mark the error and a title to make a hover tip. + $tabs[$id]['#link']['localized_options']['attributes']['class'][] = 'error'; + $tabs[$id]['#link']['localized_options']['attributes']['title'] = t('This display has one or more validation errors; please review it.'); + } + } + + return $tabs; +} + +/** + * Controls whether or not the default display should have its own tab on edit. + */ +function views_ui_show_default_display($view) { + // Always show the default display for advanced users who prefer that mode. + $advanced_mode = variable_get('views_ui_show_master_display', FALSE); + // For other users, show the default display only if there are no others, and + // hide it if there's at least one "real" display. + $additional_displays = (count($view->display) == 1); + + return $advanced_mode || $additional_displays; +} + +/** + * Returns a renderable array representing the edit page for one display. + */ +function views_ui_get_display_tab($view, $display_id) { + $build = array(); + $display = $view->display[$display_id]; + // If the plugin doesn't exist, display an error message instead of an edit + // page. + if (empty($display->handler)) { + $title = isset($display->display_title) ? $display->display_title : t('Invalid'); + // @TODO: Improved UX for the case where a plugin is missing. + $build['#markup'] = t("Error: Display @display refers to a plugin named '@plugin', but that plugin is not available.", array('@display' => $display->id, '@plugin' => $display->display_plugin)); + } + // Build the content of the edit page. + else { + $build['details'] = views_ui_get_display_tab_details($view, $display); + } + // In AJAX context, views_ui_regenerate_tab() returns this outside of form + // context, so hook_form_views_ui_edit_form_alter() is insufficient. + drupal_alter('views_ui_display_tab', $build, $view, $display_id); + return $build; +} + +/** + * Helper function to get the display details section of the edit UI. + * + * @param $view + * @param $display + * + * @return array + * A renderable page build array. + */ +function views_ui_get_display_tab_details($view, $display) { + $display_title = views_ui_get_display_label($view, $display->id, FALSE); + $build = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('id' => 'edit-display-settings-details',), + ); + + $plugin = views_fetch_plugin_data('display', $view->display[$display->id]->display_plugin); + // The following is for display purposes only. We need to determine if there is more than one button and wrap + // the buttons in a .ctools-dropbutton class if more than one is present. Otherwise, we'll just wrap the + // actions in the .ctools-button class. + $isDisplayDeleted = !empty($display->deleted); + $isDeletable = empty($plugin['no remove']); + // The master display cannot be cloned. + $isDefault = $display->id == 'default'; + // @todo: Figure out why get_option doesn't work here. + $isEnabled = $display->handler->get_option('enabled'); + + if (!$isDisplayDeleted && $isDeletable && !$isDefault) { + $prefix = '
    '; + $suffix = '
'; + $itemElement = 'li'; + } + else { + $prefix = '
    '; + $suffix = '
'; + $itemElement = 'li'; + } + + if ($display->id != 'default') { + $build['top']['#theme_wrappers'] = array('container'); + $build['top']['#attributes']['id'] = 'edit-display-settings-top'; + $build['top']['#attributes']['class'] = array('views-ui-display-tab-actions', 'views-ui-display-tab-bucket', 'clearfix'); + + // The Delete, Duplicate and Undo Delete buttons. + $build['top']['actions'] = array( + '#prefix' => $prefix, + '#suffix' => $suffix, + ); + + if (!$isDisplayDeleted) { + if (!$isEnabled) { + $build['top']['actions']['enable'] = array( + '#type' => 'submit', + '#value' => t('enable @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array('views_ui_edit_form_submit_enable_display', 'views_ui_edit_form_submit_delay_destination'), + '#prefix' => '<' . $itemElement . ' class="enable">', + "#suffix" => '', + ); + } + // Add a link to view the page. + elseif ($display->handler->has_path()) { + $path = $display->handler->get_path(); + if (strpos($path, '%') === FALSE) { + $build['top']['actions']['path'] = array( + '#type' => 'link', + '#title' => t('view @display', array('@display' => $display->display_title)), + '#options' => array('alt' => array(t("Go to the real page for this display"))), + '#href' => $path, + '#prefix' => '<' . $itemElement . ' class="view">', + "#suffix" => '', + ); + } + } + if (!$isDefault) { + $build['top']['actions']['duplicate'] = array( + '#type' => 'submit', + '#value' => t('clone @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array('views_ui_edit_form_submit_duplicate_display', 'views_ui_edit_form_submit_delay_destination'), + '#prefix' => '<' . $itemElement . ' class="duplicate">', + "#suffix" => '', + ); + } + if ($isDeletable) { + $build['top']['actions']['delete'] = array( + '#type' => 'submit', + '#value' => t('delete @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array('views_ui_edit_form_submit_delete_display', 'views_ui_edit_form_submit_delay_destination'), + '#prefix' => '<' . $itemElement . ' class="delete">', + "#suffix" => '', + ); + } + if ($isEnabled) { + $build['top']['actions']['disable'] = array( + '#type' => 'submit', + '#value' => t('disable @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array('views_ui_edit_form_submit_disable_display', 'views_ui_edit_form_submit_delay_destination'), + '#prefix' => '<' . $itemElement . ' class="disable">', + "#suffix" => '', + ); + } + } + else { + $build['top']['actions']['undo_delete'] = array( + '#type' => 'submit', + '#value' => t('undo delete of @display_title', array('@display_title' => $display_title)), + '#limit_validation_errors' => array(), + '#submit' => array('views_ui_edit_form_submit_undo_delete_display', 'views_ui_edit_form_submit_delay_destination'), + '#prefix' => '<' . $itemElement . ' class="undo-delete">', + "#suffix" => '', + ); + } + + // The area above the three columns. + $build['top']['display_title'] = array( + '#theme' => 'views_ui_display_tab_setting', + '#description' => t('Display name'), + '#link' => $display->handler->option_link(check_plain($display_title), 'display_title'), + ); + } + + $build['columns'] = array(); + $build['columns']['#theme_wrappers'] = array('container'); + $build['columns']['#attributes'] = array('id' => 'edit-display-settings-main', 'class' => array('clearfix', 'views-display-columns'),); + + $build['columns']['first']['#theme_wrappers'] = array('container'); + $build['columns']['first']['#attributes'] = array('class' => array('views-display-column', 'first')); + + $build['columns']['second']['#theme_wrappers'] = array('container'); + $build['columns']['second']['#attributes'] = array('class' => array('views-display-column', 'second')); + + $build['columns']['second']['settings'] = array(); + $build['columns']['second']['header'] = array(); + $build['columns']['second']['footer'] = array(); + $build['columns']['second']['pager'] = array(); + + // The third column buckets are wrapped in a CTools collapsible div + $build['columns']['third']['#theme_wrappers'] = array('container'); + $build['columns']['third']['#attributes'] = array('class' => array('views-display-column', 'third', 'ctools-collapsible-container', 'ctools-collapsible-remember')); + // Specify an id that won't change after AJAX requests, so ctools can keep + // track of the user's preferred collapsible state. Use the same id across + // different displays of the same view, so changing displays doesn't + // recollapse the column. + $build['columns']['third']['#attributes']['id'] = 'views-ui-advanced-column-' . $view->name; + // Collapse the div by default. + if (!variable_get('views_ui_show_advanced_column', FALSE)) { + $build['columns']['third']['#attributes']['class'][] = 'ctools-collapsed'; + } + $build['columns']['third']['advanced'] = array('#markup' => '

' . t('Advanced') . '

',); + $build['columns']['third']['collapse']['#theme_wrappers'] = array('container'); + $build['columns']['third']['collapse']['#attributes'] = array('class' => array('ctools-collapsible-content',),); + + // Each option (e.g. title, access, display as grid/table/list) fits into one + // of several "buckets," or boxes (Format, Fields, Sort, and so on). + $buckets = array(); + + // Fetch options from the display plugin, with a list of buckets they go into. + $options = array(); + $display->handler->options_summary($buckets, $options); + + // Place each option into its bucket. + foreach ($options as $id => $option) { + // Each option self-identifies as belonging in a particular bucket. + $buckets[$option['category']]['build'][$id] = views_ui_edit_form_get_build_from_option($id, $option, $view, $display); + } + + // Place each bucket into the proper column. + foreach ($buckets as $id => $bucket) { + // Let buckets identify themselves as belonging in a column. + if (isset($bucket['column']) && isset($build['columns'][$bucket['column']])) { + $column = $bucket['column']; + } + // If a bucket doesn't pick one of our predefined columns to belong to, put + // it in the last one. + else { + $column = 'third'; + } + if (isset($bucket['build']) && is_array($bucket['build'])) { + // The third column is a CTools collapsible div, so + // the structure of the form is a little different for this column + if ($column === 'third') { + $build['columns'][$column]['collapse'][$id] = $bucket['build']; + $build['columns'][$column]['collapse'][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket'; + $build['columns'][$column]['collapse'][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : ''; + $build['columns'][$column]['collapse'][$id]['#name'] = !empty($bucket['title']) ? $bucket['title'] : $id; + } + else { + $build['columns'][$column][$id] = $bucket['build']; + $build['columns'][$column][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket'; + $build['columns'][$column][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : ''; + $build['columns'][$column][$id]['#name'] = !empty($bucket['title']) ? $bucket['title'] : $id; + } + } + } + + $build['columns']['first']['fields'] = views_ui_edit_form_get_bucket('field', $view, $display); + $build['columns']['first']['filters'] = views_ui_edit_form_get_bucket('filter', $view, $display); + $build['columns']['first']['sorts'] = views_ui_edit_form_get_bucket('sort', $view, $display); + $build['columns']['second']['header'] = views_ui_edit_form_get_bucket('header', $view, $display); + $build['columns']['second']['footer'] = views_ui_edit_form_get_bucket('footer', $view, $display); + $build['columns']['third']['collapse']['arguments'] = views_ui_edit_form_get_bucket('argument', $view, $display); + $build['columns']['third']['collapse']['relationships'] = views_ui_edit_form_get_bucket('relationship', $view, $display); + $build['columns']['third']['collapse']['empty'] = views_ui_edit_form_get_bucket('empty', $view, $display); + + return $build; +} + +/** + * Build a renderable array representing one option on the edit form. + * + * This function might be more logical as a method on an object, if a suitable + * object emerges out of refactoring. + */ +function views_ui_edit_form_get_build_from_option($id, $option, $view, $display) { + $option_build = array(); + $option_build['#theme'] = 'views_ui_display_tab_setting'; + + $option_build['#description'] = $option['title']; + + $option_build['#link'] = $display->handler->option_link($option['value'], $id, '', empty($option['desc']) ? '' : $option['desc']); + + $option_build['#links'] = array(); + if (!empty($option['links']) && is_array($option['links'])) { + foreach ($option['links'] as $link_id => $link_value) { + $option_build['#settings_links'][] = $display->handler->option_link($option['setting'], $link_id, 'views-button-configure', $link_value); + } + } + + if (!empty($display->handler->options['defaults'][$id])) { + $display_id = 'default'; + $option_build['#defaulted'] = TRUE; + } + else { + $display_id = $display->id; + if (!$display->handler->is_default_display()) { + if ($display->handler->defaultable_sections($id)) { + $option_build['#overridden'] = TRUE; + } + } + } + $option_build['#attributes']['class'][] = drupal_clean_css_identifier($display_id . '-' . $id); + if (!empty($view->changed_sections[$display_id . '-' . $id])) { + $option_build['#changed'] = TRUE; + } + return $option_build; +} + +function template_preprocess_views_ui_display_tab_setting(&$variables) { + static $zebra = 0; + $variables['zebra'] = ($zebra % 2 === 0 ? 'odd' : 'even'); + $zebra++; + + // Put the main link to the left side + array_unshift($variables['settings_links'], $variables['link']); + $variables['settings_links'] = implode(' | ', $variables['settings_links']); + + // Add classes associated with this display tab to the overall list. + $variables['classes_array'] = array_merge($variables['classes_array'], $variables['class']); + + if (!empty($variables['defaulted'])) { + $variables['classes_array'][] = 'defaulted'; + } + if (!empty($variables['overridden'])) { + $variables['classes_array'][] = 'overridden'; + $variables['attributes_array']['title'][] = t('Overridden'); + } + + // Append a colon to the description, if requested. + if ($variables['description'] && $variables['description_separator']) { + $variables['description'] .= t(':'); + } +} + +function template_preprocess_views_ui_display_tab_bucket(&$variables) { + $element = $variables['element']; + + $variables['item_help_icon'] = ''; + if (!empty($element['#item_help_icon'])) { + $variables['item_help_icon'] = render($element['#item_help_icon']); + } + if (!empty($element['#name'])) { + $variables['classes_array'][] = drupal_clean_css_identifier(strtolower($element['#name'])); + } + if (!empty($element['#overridden'])) { + $variables['classes_array'][] = 'overridden'; + $variables['attributes_array']['title'][] = t('Overridden'); + } + + $variables['content'] = $element['#children']; + $variables['title'] = $element['#title']; + $variables['actions'] = !empty($element['#actions']) ? $element['#actions'] : ''; +} + +function template_preprocess_views_ui_display_tab_column(&$variables) { + $element = $variables['element']; + + $variables['content'] = $element['#children']; + $variables['column'] = $element['#column']; +} + +/** + * Move form elements into fieldsets for presentation purposes. + * + * Many views forms use #tree = TRUE to keep their values in a hierarchy for + * easier storage. Moving the form elements into fieldsets during form building + * would break up that hierarchy. Therefore, we wait until the pre_render stage, + * where any changes we make affect presentation only and aren't reflected in + * $form_state['values']. + */ +function views_ui_pre_render_add_fieldset_markup($form) { + foreach (element_children($form) as $key) { + $element = $form[$key]; + // In our form builder functions, we added an arbitrary #fieldset property + // to any element that belongs in a fieldset. If this form element has that + // property, move it into its fieldset. + if (isset($element['#fieldset']) && isset($form[$element['#fieldset']])) { + $form[$element['#fieldset']][$key] = $element; + // Remove the original element this duplicates. + unset($form[$key]); + } + } + + return $form; +} + +/** + * Flattens the structure of an element containing the #flatten property. + * + * If a form element has #flatten = TRUE, then all of it's children + * get moved to the same level as the element itself. + * So $form['to_be_flattened'][$key] becomes $form[$key], and + * $form['to_be_flattened'] gets unset. + */ +function views_ui_pre_render_flatten_data($form) { + foreach (element_children($form) as $key) { + $element = $form[$key]; + if (!empty($element['#flatten'])) { + foreach (element_children($element) as $child_key) { + $form[$child_key] = $form[$key][$child_key]; + } + // All done, remove the now-empty parent. + unset($form[$key]); + } + } + + return $form; +} + +/** + * Moves argument options into their place. + * + * When configuring the default argument behavior, almost each of the radio + * buttons has its own fieldset shown bellow it when the radio button is + * clicked. That fieldset is created through a custom form process callback. + * Each element that has #argument_option defined and pointing to a default + * behavior gets moved to the appropriate fieldset. + * So if #argument_option is specified as 'default', the element is moved + * to the 'default_options' fieldset. + */ +function views_ui_pre_render_move_argument_options($form) { + foreach (element_children($form) as $key) { + $element = $form[$key]; + if (!empty($element['#argument_option'])) { + $container_name = $element['#argument_option'] . '_options'; + if (isset($form['no_argument']['default_action'][$container_name])) { + $form['no_argument']['default_action'][$container_name][$key] = $element; + } + // Remove the original element this duplicates. + unset($form[$key]); + } + } + return $form; +} + +/** + * Custom form radios process function. + * + * Roll out a single radios element to a list of radios, + * using the options array as index. + * While doing that, create a container element underneath each option, which + * contains the settings related to that option. + * + * @see form_process_radios() + */ +function views_ui_process_container_radios($element) { + if (count($element['#options']) > 0) { + foreach ($element['#options'] as $key => $choice) { + $element += array($key => array()); + // Generate the parents as the autogenerator does, so we will have a + // unique id for each radio button. + $parents_for_id = array_merge($element['#parents'], array($key)); + + $element[$key] += array( + '#type' => 'radio', + '#title' => $choice, + // The key is sanitized in drupal_attributes() during output from the + // theme function. + '#return_value' => $key, + '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : NULL, + '#attributes' => $element['#attributes'], + '#parents' => $element['#parents'], + '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + ); + $element[$key . '_options'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('views-admin-dependent')), + ); + } + } + return $element; +} + +/** + * Import a view from cut & paste. + */ +function views_ui_import_page($form, &$form_state) { + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('View name'), + '#description' => t('Enter the name to use for this view if it is different from the source view. Leave blank to use the name of the view.'), + ); + + $form['name_override'] = array( + '#type' => 'checkbox', + '#title' => t('Replace an existing view if one exists with the same name'), + ); + + $form['bypass_validation'] = array( + '#type' => 'checkbox', + '#title' => t('Bypass view validation'), + '#description' => t('Bypass the validation of plugins and handlers when importing this view.'), + ); + + $form['view'] = array( + '#type' => 'textarea', + '#title' => t('Paste view code here'), + '#required' => TRUE, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Import'), + '#submit' => array('views_ui_import_submit'), + '#validate' => array('views_ui_import_validate'), + ); + return $form; +} + +/** + * Validate handler to import a view. + */ +function views_ui_import_validate($form, &$form_state) { + $view = ''; + views_include('view'); + // Be forgiving if someone pastes views code that starts with 'api_version) || $view->api_version < 2) { + form_error($form['view'], t('That view is not compatible with this version of Views. + If you have a view from views1 you have to go to a drupal6 installation and import it there.')); + } + elseif (version_compare($view->api_version, views_api_version(), '>')) { + form_error($form['view'], t('That view is created for the version @import_version of views, but you only have @api_version', array( + '@import_version' => $view->api_version, + '@api_version' => views_api_version()))); + } + + // View name must be alphanumeric or underscores, no other punctuation. + if (!empty($form_state['values']['name']) && preg_match('/[^a-zA-Z0-9_]/', $form_state['values']['name'])) { + form_error($form['name'], t('View name must be alphanumeric or underscores only.')); + } + + if ($form_state['values']['name']) { + $view->name = $form_state['values']['name']; + } + + $test = views_get_view($view->name); + if (!$form_state['values']['name_override']) { + if ($test && $test->type != t('Default')) { + form_set_error('', t('A view by that name already exists; please choose a different name')); + } + } + else { + if ($test->vid) { + $view->vid = $test->vid; + } + } + + // Make sure base table gets set properly if it got moved. + $view->update(); + + $view->init_display(); + + $broken = FALSE; + + // Bypass the validation of view pluigns/handlers if option is checked. + if (!$form_state['values']['bypass_validation']) { + // Make sure that all plugins and handlers needed by this view actually exist. + foreach ($view->display as $id => $display) { + if (empty($display->handler) || !empty($display->handler->broken)) { + drupal_set_message(t('Display plugin @plugin is not available.', array('@plugin' => $display->display_plugin)), 'error'); + $broken = TRUE; + continue; + } + + $plugin = views_get_plugin('style', $display->handler->get_option('style_plugin')); + if (!$plugin) { + drupal_set_message(t('Style plugin @plugin is not available.', array('@plugin' => $display->handler->get_option('style_plugin'))), 'error'); + $broken = TRUE; + } + elseif ($plugin->uses_row_plugin()) { + $plugin = views_get_plugin('row', $display->handler->get_option('row_plugin')); + if (!$plugin) { + drupal_set_message(t('Row plugin @plugin is not available.', array('@plugin' => $display->handler->get_option('row_plugin'))), 'error'); + $broken = TRUE; + } + } + + foreach (views_object_types() as $type => $info) { + $handlers = $display->handler->get_handlers($type); + if ($handlers) { + foreach ($handlers as $id => $handler) { + if ($handler->broken()) { + drupal_set_message(t('@type handler @table.@field is not available.', array( + '@type' => $info['stitle'], + '@table' => $handler->table, + '@field' => $handler->field, + )), 'error'); + $broken = TRUE; + } + } + } + } + } + } + + if ($broken) { + form_set_error('', t('Unable to import view.')); + } + + $form_state['view'] = &$view; +} + +/** + * Submit handler for view import. + */ +function views_ui_import_submit($form, &$form_state) { + // Store in cache and then go to edit. + views_ui_cache_set($form_state['view']); + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit'; +} + +/** + * Validate that a view is complete and whole. + */ +function views_ui_edit_view_form_validate($form, &$form_state) { + // Do not validate cancel or delete or revert. + if (empty($form_state['clicked_button']['#value']) || $form_state['clicked_button']['#value'] != t('Save')) { + return; + } + + $errors = $form_state['view']->validate(); + if ($errors !== TRUE) { + foreach ($errors as $error) { + form_set_error('', $error); + } + } +} + +/** + * Submit handler for the edit view form. + */ +function views_ui_edit_view_form_submit($form, &$form_state) { + // Go through and remove displayed scheduled for removal. + foreach ($form_state['view']->display as $id => $display) { + if (!empty($display->deleted)) { + unset($form_state['view']->display[$id]); + } + } + // Rename display ids if needed. + foreach ($form_state['view']->display as $id => $display) { + if (!empty($display->new_id)) { + $form_state['view']->display[$id]->id = $display->new_id; + // Redirect the user to the renamed display to be sure that the page itself exists and doesn't throw errors. + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit/' . $display->new_id; + } + } + + // Direct the user to the right url, if the path of the display has changed. + if (!empty($_GET['destination'])) { + $destination = $_GET['destination']; + // Find out the first display which has a changed path and redirect to this url. + $old_view = views_get_view($form_state['view']->name); + foreach ($old_view->display as $id => $display) { + // Only check for displays with a path. + if (!isset($display->display_options['path'])) { + continue; + } + $old_path = $display->display_options['path']; + if (($display->display_plugin == 'page') && ($old_path == $destination) && ($old_path != $form_state['view']->display[$id]->display_options['path'])) { + $destination = $form_state['view']->display[$id]->display_options['path']; + unset($_GET['destination']); + } + } + $form_state['redirect'] = $destination; + } + + $form_state['view']->save(); + drupal_set_message(t('The view %name has been saved.', array('%name' => $form_state['view']->get_human_name()))); + + // Remove this view from cache so we can edit it properly. + ctools_object_cache_clear('view', $form_state['view']->name); +} + +/** + * Submit handler for the edit view form. + */ +function views_ui_edit_view_form_cancel($form, &$form_state) { + // Remove this view from cache so edits will be lost. + ctools_object_cache_clear('view', $form_state['view']->name); + if (empty($form['view']->vid)) { + // I seem to have to drupal_goto here because I can't get fapi to + // honor the redirect target. Not sure what I screwed up here. + drupal_goto('admin/structure/views'); + } +} + +function views_ui_edit_view_form_delete($form, &$form_state) { + unset($_REQUEST['destination']); + // Redirect to the delete confirm page + $form_state['redirect'] = array('admin/structure/views/view/' . $form_state['view']->name . '/delete', array('query' => drupal_get_destination() + array('cancel' => 'admin/structure/views/view/' . $form_state['view']->name . '/edit'))); +} + +/** + * Add information about a section to a display. + */ +function views_ui_edit_form_get_bucket($type, $view, $display) { + $build = array( + '#theme_wrappers' => array('views_ui_display_tab_bucket'), + ); + $types = views_object_types(); + + $build['#overridden'] = FALSE; + $build['#defaulted'] = FALSE; + if (module_exists('advanced_help')) { + $build['#item_help_icon'] = array( + '#theme' => 'advanced_help_topic', + '#module' => 'views', + '#topic' => $type, + ); + } + + $build['#name'] = $build['#title'] = $types[$type]['title']; + + // Different types now have different rearrange forms, so we use this switch + // to get the right one. + switch ($type) { + case 'filter': + $rearrange_url = "admin/structure/views/nojs/rearrange-$type/$view->name/$display->id/$type"; + $rearrange_text = t('and/or, rearrange'); + // TODO: Add another class to have another symbol for filter rearrange. + $class = 'icon compact rearrange'; + break; + case 'field': + // Fetch the style plugin info so we know whether to list fields or not. + $style_plugin = $display->handler->get_plugin(); + $uses_fields = $style_plugin && $style_plugin->uses_fields(); + if (!$uses_fields) { + $build['fields'][] = array( + '#markup' => t('The selected style or row format does not utilize fields.'), + '#theme_wrappers' => array('views_container'), + '#attributes' => array('class' => array('views-display-setting')), + ); + return $build; + } + + default: + $rearrange_url = "admin/structure/views/nojs/rearrange/$view->name/$display->id/$type"; + $rearrange_text = t('rearrange'); + $class = 'icon compact rearrange'; + } + + // Create an array of actions to pass to theme_links + $actions = array(); + $count_handlers = count($display->handler->get_handlers($type)); + $actions['add'] = array( + 'title' => t('add'), + 'href' => "admin/structure/views/nojs/add-item/$view->name/$display->id/$type", + 'attributes'=> array('class' => array('icon compact add', 'views-ajax-link'), 'title' => t('add'), 'id' => 'views-add-' . $type), + 'html' => TRUE, + ); + if ($count_handlers > 0) { + $actions['rearrange'] = array( + 'title' => $rearrange_text, + 'href' => $rearrange_url, + 'attributes' => array('class' => array($class, 'views-ajax-link'), 'title' => $rearrange_text, 'id' => 'views-rearrange-' . $type), + 'html' => TRUE, + ); + } + + // Render the array of links + $build['#actions'] = theme('links__ctools_dropbutton', + array( + 'links' => $actions, + 'attributes' => array( + 'class' => array('inline', 'links', 'actions', 'horizontal', 'right') + ), + 'class' => array('views-ui-settings-bucket-operations'), + ) + ); + + if (!$display->handler->is_default_display()) { + if (!$display->handler->is_defaulted($types[$type]['plural'])) { + $build['#overridden'] = TRUE; + } + else { + $build['#defaulted'] = TRUE; + } + } + + // If there's an options form for the bucket, link to it. + if (!empty($types[$type]['options'])) { + $build['#title'] = l($build['#title'], "admin/structure/views/nojs/config-type/$view->name/$display->id/$type", array('attributes' => array('class' => array('views-ajax-link'), 'id' => 'views-title-' . $type))); + } + + static $relationships = NULL; + if (!isset($relationships)) { + // Get relationship labels + $relationships = array(); + // @todo: get_handlers() + $handlers = $display->handler->get_option('relationships'); + if ($handlers) { + foreach ($handlers as $id => $info) { + $handler = $display->handler->get_handler('relationship', $id); + $relationships[$id] = $handler->label(); + } + } + } + + // Filters can now be grouped so we do a little bit extra: + $groups = array(); + $grouping = FALSE; + if ($type == 'filter') { + $group_info = $view->display_handler->get_option('filter_groups'); + // If there is only one group but it is using the "OR" filter, we still + // treat it as a group for display purposes, since we want to display the + // "OR" label next to items within the group. + if (!empty($group_info['groups']) && (count($group_info['groups']) > 1 || current($group_info['groups']) == 'OR')) { + $grouping = TRUE; + $groups = array(0 => array()); + } + } + + $build['fields'] = array(); + + foreach ($display->handler->get_option($types[$type]['plural']) as $id => $field) { + // Build the option link for this handler ("Node: ID = article"). + $build['fields'][$id] = array(); + $build['fields'][$id]['#theme'] = 'views_ui_display_tab_setting'; + + $handler = $display->handler->get_handler($type, $id); + if (empty($handler)) { + $build['fields'][$id]['#class'][] = 'broken'; + $field_name = t('Broken/missing handler: @table > @field', array('@table' => $field['table'], '@field' => $field['field'])); + $build['fields'][$id]['#link'] = l($field_name, "admin/structure/views/nojs/config-item/$view->name/$display->id/$type/$id", array('attributes' => array('class' => array('views-ajax-link')), 'html' => TRUE)); + continue; + } + + $field_name = check_plain($handler->ui_name(TRUE)); + if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) { + $field_name = '(' . $relationships[$field['relationship']] . ') ' . $field_name; + } + + $description = filter_xss_admin($handler->admin_summary()); + $link_text = $field_name . (empty($description) ? '' : " ($description)"); + $link_attributes = array('class' => array('views-ajax-link')); + if (!empty($field['exclude'])) { + $link_attributes['class'][] = 'views-field-excluded'; + } + $build['fields'][$id]['#link'] = l($link_text, "admin/structure/views/nojs/config-item/$view->name/$display->id/$type/$id", array('attributes' => $link_attributes, 'html' => TRUE)); + $build['fields'][$id]['#class'][] = drupal_clean_css_identifier($display->id . '-' . $type . '-' . $id); + if (!empty($view->changed_sections[$display->id . '-' . $type . '-' . $id])) { + // @TODO: #changed is no longer being used? + $build['fields'][$id]['#changed'] = TRUE; + } + + if ($display->handler->use_group_by() && $handler->use_group_by()) { + $build['fields'][$id]['#settings_links'][] = l('' . t('Aggregation settings') . '', "admin/structure/views/nojs/config-item-group/$view->name/$display->id/$type/$id", array('attributes' => array('class' => 'views-button-configure views-ajax-link', 'title' => t('Aggregation settings')), 'html' => true)); + } + + if ($handler->has_extra_options()) { + $build['fields'][$id]['#settings_links'][] = l('' . t('Settings') . '', "admin/structure/views/nojs/config-item-extra/$view->name/$display->id/$type/$id", array('attributes' => array('class' => array('views-button-configure', 'views-ajax-link'), 'title' => t('Settings')), 'html' => true)); + } + + if ($grouping) { + $gid = $handler->options['group']; + + // Show in default group if the group does not exist. + if (empty($group_info['groups'][$gid])) { + $gid = 0; + } + $groups[$gid][] = $id; + } + } + + // If using grouping, re-order fields so that they show up properly in the list. + if ($type == 'filter' && $grouping) { + $store = $build['fields']; + $build['fields'] = array(); + foreach ($groups as $gid => $contents) { + // Display an operator between each group. + if (!empty($build['fields'])) { + $build['fields'][] = array( + '#theme' => 'views_ui_display_tab_setting', + '#class' => array('views-group-text'), + '#link' => ($group_info['operator'] == 'OR' ? t('OR') : t('AND')), + ); + } + // Display an operator between each pair of filters within the group. + $keys = array_keys($contents); + $last = end($keys); + foreach ($contents as $key => $pid) { + if ($key != $last) { + $store[$pid]['#link'] .= '  ' . ($group_info['groups'][$gid] == 'OR' ? t('OR') : t('AND')); + } + $build['fields'][$pid] = $store[$pid]; + } + } + } + + return $build; +} + +/** + * Regenerate the current tab for AJAX updates. + */ +function views_ui_regenerate_tab(&$view, &$output, $display_id) { + if (!$view->set_display('default')) { + return; + } + + // Regenerate the main display area. + $build = views_ui_get_display_tab($view, $display_id); + views_ui_add_microweights($build); + $output[] = ajax_command_html('#views-tab-' . $display_id, drupal_render($build)); + + // Regenerate the top area so changes to display names and order will appear. + $build = views_ui_render_display_top($view, $display_id); + views_ui_add_microweights($build); + $output[] = ajax_command_replace('#views-display-top', drupal_render($build)); +} + +/** + * Recursively adds microweights to a render array, similar to what form_builder() does for forms. + * + * @todo Submit a core patch to fix drupal_render() to do this, so that all + * render arrays automatically preserve array insertion order, as forms do. + */ +function views_ui_add_microweights(&$build) { + $count = 0; + foreach (element_children($build) as $key) { + if (!isset($build[$key]['#weight'])) { + $build[$key]['#weight'] = $count/1000; + } + views_ui_add_microweights($build[$key]); + $count++; + } +} + +/** + * Provide a standard set of Apply/Cancel/OK buttons for the forms. Also provide + * a hidden op operator because the forms plugin doesn't seem to properly + * provide which button was clicked. + * + * TODO: Is the hidden op operator still here somewhere, or is that part of the + * docblock outdated? + */ +function views_ui_standard_form_buttons(&$form, &$form_state, $form_id, $name = NULL, $third = NULL, $submit = NULL) { + $form['buttons'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + + if (empty($name)) { + $name = t('Apply'); + $view = $form_state['view']; + if (!empty($view->stack) && count($view->stack) > 1) { + $name = t('Apply and continue'); + } + $names = array(t('Apply'), t('Apply and continue')); + } + + // Forms that are purely informational set an ok_button flag, so we know not + // to create an "Apply" button for them. + if (empty($form_state['ok_button'])) { + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => $name, + // The regular submit handler ($form_id . '_submit') does not apply if + // we're updating the default display. It does apply if we're updating + // the current display. Since we have no way of knowing at this point + // which display the user wants to update, views_ui_standard_submit will + // take care of running the regular submit handler as appropriate. + '#submit' => array('views_ui_standard_submit'), + ); + // Form API button click detection requires the button's #value to be the + // same between the form build of the initial page request, and the initial + // form build of the request processing the form submission. Ideally, the + // button's #value shouldn't change until the form rebuild step. However, + // views_ui_ajax_form() implements a different multistep form workflow than + // the Form API does, and adjusts $view->stack prior to form processing, so + // we compensate by extending button click detection code to support any of + // the possible button labels. + if (isset($names)) { + $form['buttons']['submit']['#values'] = $names; + $form['buttons']['submit']['#process'] = array_merge(array('views_ui_form_button_was_clicked'), element_info_property($form['buttons']['submit']['#type'], '#process', array())); + } + // If a validation handler exists for the form, assign it to this button. + if (function_exists($form_id . '_validate')) { + $form['buttons']['submit']['#validate'][] = $form_id . '_validate'; + } + } + + // Create a "Cancel" button. For purely informational forms, label it "OK". + $cancel_submit = function_exists($form_id . '_cancel') ? $form_id . '_cancel' : 'views_ui_standard_cancel'; + $form['buttons']['cancel'] = array( + '#type' => 'submit', + '#value' => empty($form_state['ok_button']) ? t('Cancel') : t('Ok'), + '#submit' => array($cancel_submit), + '#validate' => array(), + ); + + // Some forms specify a third button, with a name and submit handler. + if ($third) { + if (empty($submit)) { + $submit = 'third'; + } + $third_submit = function_exists($form_id . '_' . $submit) ? $form_id . '_' . $submit : 'views_ui_standard_cancel'; + + $form['buttons'][$submit] = array( + '#type' => 'submit', + '#value' => $third, + '#validate' => array(), + '#submit' => array($third_submit), + ); + } + + // Compatibility, to be removed later: // TODO: When is "later"? + // We used to set these items on the form, but now we want them on the $form_state: + if (isset($form['#title'])) { + $form_state['title'] = $form['#title']; + } + if (isset($form['#help_topic'])) { + $form_state['help_topic'] = $form['#help_topic']; + } + if (isset($form['#help_module'])) { + $form_state['help_module'] = $form['#help_module']; + } + if (isset($form['#url'])) { + $form_state['url'] = $form['#url']; + } + if (isset($form['#section'])) { + $form_state['#section'] = $form['#section']; + } + // Finally, we never want these cached -- our object cache does that for us. + $form['#no_cache'] = TRUE; + + // If this isn't an ajaxy form, then we want to set the title. + if (!empty($form['#title'])) { + drupal_set_title($form['#title']); + } +} + +/** + * Basic submit handler applicable to all 'standard' forms. + * + * This submit handler determines whether the user wants the submitted changes + * to apply to the default display or to the current display, and dispatches + * control appropriately. + */ +function views_ui_standard_submit($form, &$form_state) { + // Determine whether the values the user entered are intended to apply to + // the current display or the default display. + + list($was_defaulted, $is_defaulted, $revert) = views_ui_standard_override_values($form, $form_state); + + // Mark the changed section of the view as changed. + // TODO: Document why we are doing this, and see if we still need it. + if (!empty($form['#section'])) { + $form_state['view']->changed_sections[$form['#section']] = TRUE; + } + + // Based on the user's choice in the display dropdown, determine which display + // these changes apply to. + if ($revert) { + // If it's revert just change the override and return. + $display = &$form_state['view']->display[$form_state['display_id']]; + $display->handler->options_override($form, $form_state); + + // Don't execute the normal submit handling but still store the changed view into cache. + views_ui_cache_set($form_state['view']); + return; + } + elseif ($was_defaulted === $is_defaulted) { + // We're not changing which display these form values apply to. + // Run the regular submit handler for this form. + } + elseif ($was_defaulted && !$is_defaulted) { + // We were using the default display's values, but we're now overriding + // the default display and saving values specific to this display. + $display = &$form_state['view']->display[$form_state['display_id']]; + // options_override toggles the override of this section. + $display->handler->options_override($form, $form_state); + $display->handler->options_submit($form, $form_state); + } + elseif (!$was_defaulted && $is_defaulted) { + // We used to have an override for this display, but the user now wants + // to go back to the default display. + // Overwrite the default display with the current form values, and make + // the current display use the new default values. + $display = &$form_state['view']->display[$form_state['display_id']]; + // options_override toggles the override of this section. + $display->handler->options_override($form, $form_state); + $display->handler->options_submit($form, $form_state); + } + + $submit_handler = $form['#form_id'] . '_submit'; + if (function_exists($submit_handler)) { + $submit_handler($form, $form_state); + } +} + +/** + * Return the was_defaulted, is_defaulted and revert state of a form. + */ +function views_ui_standard_override_values($form, $form_state) { + // Make sure the dropdown exists in the first place. + if (isset($form_state['values']['override']['dropdown'])) { + // #default_value is used to determine whether it was the default value or not. + // So the available options are: $display, 'default' and 'default_revert', not 'defaults'. + $was_defaulted = ($form['override']['dropdown']['#default_value'] === 'defaults'); + $is_defaulted = ($form_state['values']['override']['dropdown'] === 'default'); + $revert = ($form_state['values']['override']['dropdown'] === 'default_revert'); + + if ($was_defaulted !== $is_defaulted && isset($form['#section'])) { + // We're changing which display these values apply to. + // Update the #section so it knows what to mark changed. + $form['#section'] = str_replace('default-', $form_state['display_id'] . '-', $form['#section']); + } + } + else { + // The user didn't get the dropdown for overriding the default display. + $was_defaulted = FALSE; + $is_defaulted = FALSE; + $revert = FALSE; + } + + return array($was_defaulted, $is_defaulted, $revert); +} + +/** + * Submit handler for cancel button + */ +function views_ui_standard_cancel($form, &$form_state) { + if (!empty($form_state['view']->changed) && isset($form_state['view']->form_cache)) { + unset($form_state['view']->form_cache); + views_ui_cache_set($form_state['view']); + } + + $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit'; +} + +/** + * Add a elements. + * The following behavior detaches the elements from the DOM, wraps them + * in an unordered list, then appends them to the list of tabs. + */ +Drupal.behaviors.viewsUiRenderAddViewButton = {}; + +Drupal.behaviors.viewsUiRenderAddViewButton.attach = function (context, settings) { + var $ = jQuery; + // Build the add display menu and pull the display input buttons into it. + var $menu = $('#views-display-menu-tabs', context).once('views-ui-render-add-view-button-processed'); + + if (!$menu.length) { + return; + } + var $addDisplayDropdown = $('
  • ' + Drupal.t('Add') + '
  • '); + var $displayButtons = $menu.nextAll('input.add-display').detach(); + $displayButtons.appendTo($addDisplayDropdown.find('.action-list')).wrap('
  • ') + .parent().first().addClass('first').end().last().addClass('last'); + // Remove the 'Add ' prefix from the button labels since they're being palced + // in an 'Add' dropdown. + // @todo This assumes English, but so does $addDisplayDropdown above. Add + // support for translation. + $displayButtons.each(function () { + var label = $(this).val(); + if (label.substr(0, 4) == 'Add ') { + $(this).val(label.substr(4)); + } + }); + $addDisplayDropdown.appendTo($menu); + + // Add the click handler for the add display button + $('li.add > a', $menu).bind('click', function (event) { + event.preventDefault(); + var $trigger = $(this); + Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger); + }); + // Add a mouseleave handler to close the dropdown when the user mouses + // away from the item. We use mouseleave instead of mouseout because + // the user is going to trigger mouseout when she moves from the trigger + // link to the sub menu items. + // We use the live binder because the open class on this item will be + // toggled on and off and we want the handler to take effect in the cases + // that the class is present, but not when it isn't. + $('li.add', $menu).live('mouseleave', function (event) { + var $this = $(this); + var $trigger = $this.children('a[href="#"]'); + if ($this.children('.action-list').is(':visible')) { + Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger); + } + }); +}; + +/** + * @note [@jessebeach] I feel like the following should be a more generic function and + * not written specifically for this UI, but I'm not sure where to put it. + */ +Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu = function ($trigger) { + $trigger.parent().toggleClass('open'); + $trigger.next().slideToggle('fast'); +} + + +Drupal.behaviors.viewsUiSearchOptions = {}; + +Drupal.behaviors.viewsUiSearchOptions.attach = function (context) { + var $ = jQuery; + // The add item form may have an id of views-ui-add-item-form--n. + var $form = $(context).find('form[id^="views-ui-add-item-form"]').first(); + // Make sure we don't add more than one event handler to the same form. + $form = $form.once('views-ui-filter-options'); + if ($form.length) { + new Drupal.viewsUi.OptionsSearch($form); + } +}; + +/** + * Constructor for the viewsUi.OptionsSearch object. + * + * The OptionsSearch object filters the available options on a form according + * to the user's search term. Typing in "taxonomy" will show only those options + * containing "taxonomy" in their label. + */ +Drupal.viewsUi.OptionsSearch = function ($form) { + this.$form = $form; + // Add a keyup handler to the search box. + this.$searchBox = this.$form.find('#edit-options-search'); + this.$searchBox.keyup(jQuery.proxy(this.handleKeyup, this)); + // Get a list of option labels and their corresponding divs and maintain it + // in memory, so we have as little overhead as possible at keyup time. + this.options = this.getOptions(this.$form.find('.filterable-option')); + // Restripe on initial loading. + this.handleKeyup(); + // Trap the ENTER key in the search box so that it doesn't submit the form. + this.$searchBox.keypress(function(event) { + if (event.which == 13) { + event.preventDefault(); + } + }); +}; + +/** + * Assemble a list of all the filterable options on the form. + * + * @param $allOptions + * A jQuery object representing the rows of filterable options to be + * shown and hidden depending on the user's search terms. + */ +Drupal.viewsUi.OptionsSearch.prototype.getOptions = function ($allOptions) { + var $ = jQuery; + var i, $label, $description, $option; + var options = []; + var length = $allOptions.length; + for (i = 0; i < length; i++) { + $option = $($allOptions[i]); + $label = $option.find('label'); + $description = $option.find('div.description'); + options[i] = { + // Search on the lowercase version of the label text + description. + 'searchText': $label.text().toLowerCase() + " " + $description.text().toLowerCase(), + // Maintain a reference to the jQuery object for each row, so we don't + // have to create a new object inside the performance-sensitive keyup + // handler. + '$div': $option + } + } + return options; +}; + +/** + * Keyup handler for the search box that hides or shows the relevant options. + */ +Drupal.viewsUi.OptionsSearch.prototype.handleKeyup = function (event) { + var found, i, j, option, search, words, wordsLength, zebraClass, zebraCounter; + + // Determine the user's search query. The search text has been converted to + // lowercase. + search = this.$searchBox.val().toLowerCase(); + words = search.split(' '); + wordsLength = words.length; + + // Start the counter for restriping rows. + zebraCounter = 0; + + // Search through the search texts in the form for matching text. + var length = this.options.length; + for (i = 0; i < length; i++) { + // Use a local variable for the option being searched, for performance. + option = this.options[i]; + found = true; + // Each word in the search string has to match the item in order for the + // item to be shown. + for (j = 0; j < wordsLength; j++) { + if (option.searchText.indexOf(words[j]) === -1) { + found = false; + } + } + if (found) { + // Show the checkbox row, and restripe it. + zebraClass = (zebraCounter % 2) ? 'odd' : 'even'; + option.$div.show(); + option.$div.removeClass('even odd'); + option.$div.addClass(zebraClass); + zebraCounter++; + } + else { + // The search string wasn't found; hide this item. + option.$div.hide(); + } + } +}; + + +Drupal.behaviors.viewsUiPreview = {}; +Drupal.behaviors.viewsUiPreview.attach = function (context, settings) { + var $ = jQuery; + + // Only act on the edit view form. + var contextualFiltersBucket = $('.views-display-column .views-ui-display-tab-bucket.contextual-filters', context); + if (contextualFiltersBucket.length == 0) { + return; + } + + // If the display has no contextual filters, hide the form where you enter + // the contextual filters for the live preview. If it has contextual filters, + // show the form. + var contextualFilters = $('.views-display-setting a', contextualFiltersBucket); + if (contextualFilters.length) { + $('#preview-args').parent().show(); + } + else { + $('#preview-args').parent().hide(); + } + + // Executes an initial preview. + if ($('#edit-displays-live-preview').once('edit-displays-live-preview').is(':checked')) { + $('#preview-submit').once('edit-displays-live-preview').click(); + } +}; + + +Drupal.behaviors.viewsUiRearrangeFilter = {}; +Drupal.behaviors.viewsUiRearrangeFilter.attach = function (context, settings) { + var $ = jQuery; + // Only act on the rearrange filter form. + if (typeof Drupal.tableDrag == 'undefined' || typeof Drupal.tableDrag['views-rearrange-filters'] == 'undefined') { + return; + } + + var table = $('#views-rearrange-filters', context).once('views-rearrange-filters'); + var operator = $('.form-item-filter-groups-operator', context).once('views-rearrange-filters'); + if (table.length) { + new Drupal.viewsUi.rearrangeFilterHandler(table, operator); + } +}; + +/** + * Improve the UI of the rearrange filters dialog box. + */ +Drupal.viewsUi.rearrangeFilterHandler = function (table, operator) { + var $ = jQuery; + // Keep a reference to the being altered and to the div containing + // the filter groups operator dropdown (if it exists). + this.table = table; + this.operator = operator; + this.hasGroupOperator = this.operator.length > 0; + + // Keep a reference to all draggable rows within the table. + this.draggableRows = $('.draggable', table); + + // Keep a reference to the buttons for adding and removing filter groups. + this.addGroupButton = $('input#views-add-group'); + this.removeGroupButtons = $('input.views-remove-group', table); + + // Add links that duplicate the functionality of the (hidden) add and remove + // buttons. + this.insertAddRemoveFilterGroupLinks(); + + // When there is a filter groups operator dropdown on the page, create + // duplicates of the dropdown between each pair of filter groups. + if (this.hasGroupOperator) { + this.dropdowns = this.duplicateGroupsOperator(); + this.syncGroupsOperators(); + } + + // Add methods to the tableDrag instance to account for operator cells (which + // span multiple rows), the operator labels next to each filter (e.g., "And" + // or "Or"), the filter groups, and other special aspects of this tableDrag + // instance. + this.modifyTableDrag(); + + // Initialize the operator labels (e.g., "And" or "Or") that are displayed + // next to the filters in each group, and bind a handler so that they change + // based on the values of the operator dropdown within that group. + this.redrawOperatorLabels(); + $('.views-group-title select', table) + .once('views-rearrange-filter-handler') + .bind('change.views-rearrange-filter-handler', $.proxy(this, 'redrawOperatorLabels')); + + // Bind handlers so that when a "Remove" link is clicked, we: + // - Update the rowspans of cells containing an operator dropdown (since they + // need to change to reflect the number of rows in each group). + // - Redraw the operator labels next to the filters in the group (since the + // filter that is currently displayed last in each group is not supposed to + // have a label display next to it). + $('a.views-groups-remove-link', this.table) + .once('views-rearrange-filter-handler') + .bind('click.views-rearrange-filter-handler', $.proxy(this, 'updateRowspans')) + .bind('click.views-rearrange-filter-handler', $.proxy(this, 'redrawOperatorLabels')); +}; + +/** + * Insert links that allow filter groups to be added and removed. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.insertAddRemoveFilterGroupLinks = function () { + var $ = jQuery; + + // Insert a link for adding a new group at the top of the page, and make it + // match the action links styling used in a typical page.tpl.php. Note that + // Drupal does not provide a theme function for this markup, so this is the + // best we can do. + $('') + .prependTo(this.table.parent()) + // When the link is clicked, dynamically click the hidden form button for + // adding a new filter group. + .once('views-rearrange-filter-handler') + .bind('click.views-rearrange-filter-handler', $.proxy(this, 'clickAddGroupButton')); + + // Find each (visually hidden) button for removing a filter group and insert + // a link next to it. + var length = this.removeGroupButtons.length; + for (i = 0; i < length; i++) { + var $removeGroupButton = $(this.removeGroupButtons[i]); + var buttonId = $removeGroupButton.attr('id'); + $('' + Drupal.t('Remove group') + '') + .insertBefore($removeGroupButton) + // When the link is clicked, dynamically click the corresponding form + // button. + .once('views-rearrange-filter-handler') + .bind('click.views-rearrange-filter-handler', {buttonId: buttonId}, $.proxy(this, 'clickRemoveGroupButton')); + } +}; + +/** + * Dynamically click the button that adds a new filter group. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.clickAddGroupButton = function () { + // Due to conflicts between Drupal core's AJAX system and the Views AJAX + // system, the only way to get this to work seems to be to trigger both the + // .mousedown() and .submit() events. + this.addGroupButton.mousedown(); + this.addGroupButton.submit(); + return false; +}; + +/** + * Dynamically click a button for removing a filter group. + * + * @param event + * Event being triggered, with event.data.buttonId set to the ID of the + * form button that should be clicked. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.clickRemoveGroupButton = function (event) { + // For some reason, here we only need to trigger .submit(), unlike for + // Drupal.viewsUi.rearrangeFilterHandler.prototype.clickAddGroupButton() + // where we had to trigger .mousedown() also. + jQuery('input#' + event.data.buttonId, this.table).submit(); + return false; +}; + +/** + * Move the groups operator so that it's between the first two groups, and + * duplicate it between any subsequent groups. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.duplicateGroupsOperator = function () { + var $ = jQuery; + var dropdowns, newRow; + + var titleRows = $('tr.views-group-title'), titleRow; + + // Get rid of the explanatory text around the operator; its placement is + // explanatory enough. + this.operator.find('label').add('div.description').addClass('element-invisible'); + this.operator.find('select').addClass('form-select'); + + // Keep a list of the operator dropdowns, so we can sync their behavior later. + dropdowns = this.operator; + + // Move the operator to a new row just above the second group. + titleRow = $('tr#views-group-title-2'); + newRow = $(''); + newRow.find('td').append(this.operator); + newRow.insertBefore(titleRow); + var i, length = titleRows.length; + // Starting with the third group, copy the operator to a new row above the + // group title. + for (i = 2; i < length; i++) { + titleRow = $(titleRows[i]); + // Make a copy of the operator dropdown and put it in a new table row. + var fakeOperator = this.operator.clone(); + fakeOperator.attr('id', ''); + newRow = $(''); + newRow.find('td').append(fakeOperator); + newRow.insertBefore(titleRow); + dropdowns = dropdowns.add(fakeOperator); + } + + return dropdowns; +}; + +/** + * Make the duplicated groups operators change in sync with each other. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.syncGroupsOperators = function () { + if (this.dropdowns.length < 2) { + // We only have one dropdown (or none at all), so there's nothing to sync. + return; + } + + this.dropdowns.change(jQuery.proxy(this, 'operatorChangeHandler')); +}; + +/** + * Click handler for the operators that appear between filter groups. + * + * Forces all operator dropdowns to have the same value. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.operatorChangeHandler = function (event) { + var $ = jQuery; + var $target = $(event.target); + var operators = this.dropdowns.find('select').not($target); + + // Change the other operators to match this new value. + operators.val($target.val()); +}; + +Drupal.viewsUi.rearrangeFilterHandler.prototype.modifyTableDrag = function () { + var tableDrag = Drupal.tableDrag['views-rearrange-filters']; + var filterHandler = this; + + /** + * Override the row.onSwap method from tabledrag.js. + * + * When a row is dragged to another place in the table, several things need + * to occur. + * - The row needs to be moved so that it's within one of the filter groups. + * - The operator cells that span multiple rows need their rowspan attributes + * updated to reflect the number of rows in each group. + * - The operator labels that are displayed next to each filter need to be + * redrawn, to account for the row's new location. + */ + tableDrag.row.prototype.onSwap = function () { + if (filterHandler.hasGroupOperator) { + // Make sure the row that just got moved (this.group) is inside one of + // the filter groups (i.e. below an empty marker row or a draggable). If + // it isn't, move it down one. + var thisRow = jQuery(this.group); + var previousRow = thisRow.prev('tr'); + if (previousRow.length && !previousRow.hasClass('group-message') && !previousRow.hasClass('draggable')) { + // Move the dragged row down one. + var next = thisRow.next(); + if (next.is('tr')) { + this.swap('after', next); + } + } + filterHandler.updateRowspans(); + } + // Redraw the operator labels that are displayed next to each filter, to + // account for the row's new location. + filterHandler.redrawOperatorLabels(); + }; + + /** + * Override the onDrop method from tabledrag.js. + */ + tableDrag.onDrop = function () { + var $ = jQuery; + + // If the tabledrag change marker (i.e., the "*") has been inserted inside + // a row after the operator label (i.e., "And" or "Or") rearrange the items + // so the operator label continues to appear last. + var changeMarker = $(this.oldRowElement).find('.tabledrag-changed'); + if (changeMarker.length) { + // Search for occurrences of the operator label before the change marker, + // and reverse them. + var operatorLabel = changeMarker.prevAll('.views-operator-label'); + if (operatorLabel.length) { + operatorLabel.insertAfter(changeMarker); + } + } + + // Make sure the "group" dropdown is properly updated when rows are dragged + // into an empty filter group. This is borrowed heavily from the block.js + // implementation of tableDrag.onDrop(). + var groupRow = $(this.rowObject.element).prevAll('tr.group-message').get(0); + var groupName = groupRow.className.replace(/([^ ]+[ ]+)*group-([^ ]+)-message([ ]+[^ ]+)*/, '$2'); + var groupField = $('select.views-group-select', this.rowObject.element); + if ($(this.rowObject.element).prev('tr').is('.group-message') && !groupField.is('.views-group-select-' + groupName)) { + var oldGroupName = groupField.attr('class').replace(/([^ ]+[ ]+)*views-group-select-([^ ]+)([ ]+[^ ]+)*/, '$2'); + groupField.removeClass('views-group-select-' + oldGroupName).addClass('views-group-select-' + groupName); + groupField.val(groupName); + } + }; +}; + + +/** + * Redraw the operator labels that are displayed next to each filter. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.redrawOperatorLabels = function () { + var $ = jQuery; + for (i = 0; i < this.draggableRows.length; i++) { + // Within the row, the operator labels are displayed inside the first table + // cell (next to the filter name). + var $draggableRow = $(this.draggableRows[i]); + var $firstCell = $('td:first', $draggableRow); + if ($firstCell.length) { + // The value of the operator label ("And" or "Or") is taken from the + // first operator dropdown we encounter, going backwards from the current + // row. This dropdown is the one associated with the current row's filter + // group. + var operatorValue = $draggableRow.prevAll('.views-group-title').find('option:selected').html(); + var operatorLabel = '' + operatorValue + ''; + // If the next visible row after this one is a draggable filter row, + // display the operator label next to the current row. (Checking for + // visibility is necessary here since the "Remove" links hide the removed + // row but don't actually remove it from the document). + var $nextRow = $draggableRow.nextAll(':visible').eq(0); + var $existingOperatorLabel = $firstCell.find('.views-operator-label'); + if ($nextRow.hasClass('draggable')) { + // If an operator label was already there, replace it with the new one. + if ($existingOperatorLabel.length) { + $existingOperatorLabel.replaceWith(operatorLabel); + } + // Otherwise, append the operator label to the end of the table cell. + else { + $firstCell.append(operatorLabel); + } + } + // If the next row doesn't contain a filter, then this is the last row + // in the group. We don't want to display the operator there (since + // operators should only display between two related filters, e.g. + // "filter1 AND filter2 AND filter3"). So we remove any existing label + // that this row has. + else { + $existingOperatorLabel.remove(); + } + } + } +}; + +/** + * Update the rowspan attribute of each cell containing an operator dropdown. + */ +Drupal.viewsUi.rearrangeFilterHandler.prototype.updateRowspans = function () { + var $ = jQuery; + var i, $row, $currentEmptyRow, draggableCount, $operatorCell; + var rows = $(this.table).find('tr'); + var length = rows.length; + for (i = 0; i < length; i++) { + $row = $(rows[i]); + if ($row.hasClass('views-group-title')) { + // This row is a title row. + // Keep a reference to the cell containing the dropdown operator. + $operatorCell = $($row.find('td.group-operator')); + // Assume this filter group is empty, until we find otherwise. + draggableCount = 0; + $currentEmptyRow = $row.next('tr'); + $currentEmptyRow.removeClass('group-populated').addClass('group-empty'); + // The cell with the dropdown operator should span the title row and + // the "this group is empty" row. + $operatorCell.attr('rowspan', 2); + } + else if (($row).hasClass('draggable') && $row.is(':visible')) { + // We've found a visible filter row, so we now know the group isn't empty. + draggableCount++; + $currentEmptyRow.removeClass('group-empty').addClass('group-populated'); + // The operator cell should span all draggable rows, plus the title. + $operatorCell.attr('rowspan', draggableCount + 1); + } + } +}; + +Drupal.behaviors.viewsFilterConfigSelectAll = {}; + +/** + * Add a select all checkbox, which checks each checkbox at once. + */ +Drupal.behaviors.viewsFilterConfigSelectAll.attach = function(context) { + var $ = jQuery; + // Show the select all checkbox. + $('#views-ui-config-item-form div.form-item-options-value-all', context).once(function() { + $(this).show(); + }) + .find('input[type=checkbox]') + .click(function() { + var checked = $(this).is(':checked'); + // Update all checkbox beside the select all checkbox. + $(this).parents('.form-checkboxes').find('input[type=checkbox]').each(function() { + $(this).attr('checked', checked); + }); + }); + // Uncheck the select all checkbox if any of the others are unchecked. + $('#views-ui-config-item-form div.form-type-checkbox').not($('.form-item-options-value-all')).find('input[type=checkbox]').each(function() { + $(this).click(function() { + if ($(this).is('checked') == 0) { + $('#edit-options-value-all').removeAttr('checked'); + } + }); + }); +}; + +/** + * Ensure the desired default button is used when a form is implicitly submitted via an ENTER press on textfields, radios, and checkboxes. + * + * @see http://www.w3.org/TR/html5/association-of-controls-and-forms.html#implicit-submission + */ +Drupal.behaviors.viewsImplicitFormSubmission = {}; +Drupal.behaviors.viewsImplicitFormSubmission.attach = function (context, settings) { + var $ = jQuery; + $(':text, :password, :radio, :checkbox', context).once('viewsImplicitFormSubmission', function() { + $(this).keypress(function(event) { + if (event.which == 13) { + var formId = this.form.id; + if (formId && settings.viewsImplicitFormSubmission && settings.viewsImplicitFormSubmission[formId] && settings.viewsImplicitFormSubmission[formId].defaultButton) { + event.preventDefault(); + var buttonId = settings.viewsImplicitFormSubmission[formId].defaultButton; + var $button = $('#' + buttonId, this.form); + if ($button.length == 1 && $button.is(':enabled')) { + if (Drupal.ajax && Drupal.ajax[buttonId]) { + $button.trigger(Drupal.ajax[buttonId].element_settings.event); + } + else { + $button.click(); + } + } + } + } + }); + }); +}; + +/** + * Remove icon class from elements that are themed as buttons or dropbuttons. + */ +Drupal.behaviors.viewsRemoveIconClass = {}; +Drupal.behaviors.viewsRemoveIconClass.attach = function (context, settings) { + jQuery('.ctools-button', context).once('RemoveIconClass', function () { + var $ = jQuery; + var $this = $(this); + $('.icon', $this).removeClass('icon'); + $('.horizontal', $this).removeClass('horizontal'); + }); +}; + +/** + * Change "Expose filter" buttons into checkboxes. + */ +Drupal.behaviors.viewsUiCheckboxify = {}; +Drupal.behaviors.viewsUiCheckboxify.attach = function (context, settings) { + var $ = jQuery; + var $buttons = $('#edit-options-expose-button-button, #edit-options-group-button-button').once('views-ui-checkboxify'); + var length = $buttons.length; + var i; + for (i = 0; i < length; i++) { + new Drupal.viewsUi.Checkboxifier($buttons[i]); + } +}; + +/** + * Change the default widget to select the default group according to the + * selected widget for the exposed group. + */ +Drupal.behaviors.viewsUiChangeDefaultWidget = {}; +Drupal.behaviors.viewsUiChangeDefaultWidget.attach = function (context, settings) { + var $ = jQuery; + function change_default_widget(multiple) { + if (multiple) { + $('input.default-radios').hide(); + $('td.any-default-radios-row').parent().hide(); + $('input.default-checkboxes').show(); + } + else { + $('input.default-checkboxes').hide(); + $('td.any-default-radios-row').parent().show(); + $('input.default-radios').show(); + } + } + // Update on widget change. + $('input[name="options[group_info][multiple]"]').change(function() { + change_default_widget($(this).attr("checked")); + }); + // Update the first time the form is rendered. + $('input[name="options[group_info][multiple]"]').trigger('change'); +}; + +/** + * Attaches an expose filter button to a checkbox that triggers its click event. + * + * @param button + * The DOM object representing the button to be checkboxified. + */ +Drupal.viewsUi.Checkboxifier = function (button) { + var $ = jQuery; + this.$button = $(button); + this.$parent = this.$button.parent('div.views-expose, div.views-grouped'); + this.$input = this.$parent.find('input:checkbox, input:radio'); + // Hide the button and its description. + this.$button.hide(); + this.$parent.find('.exposed-description, .grouped-description').hide(); + + this.$input.click($.proxy(this, 'clickHandler')); + +}; + +/** + * When the checkbox is checked or unchecked, simulate a button press. + */ +Drupal.viewsUi.Checkboxifier.prototype.clickHandler = function (e) { + this.$button.mousedown(); + this.$button.submit(); +}; + +/** + * Change the Apply button text based upon the override select state. + */ +Drupal.behaviors.viewsUiOverrideSelect = {}; +Drupal.behaviors.viewsUiOverrideSelect.attach = function (context, settings) { + var $ = jQuery; + $('#edit-override-dropdown', context).once('views-ui-override-button-text', function() { + // Closures! :( + var $submit = $('#edit-submit', context); + var old_value = $submit.val(); + + $submit.once('views-ui-override-button-text') + .bind('mouseup', function() { + $(this).val(old_value); + return true; + }); + + $(this).bind('change', function() { + if ($(this).val() == 'default') { + $submit.val(Drupal.t('Apply (all displays)')); + } + else if ($(this).val() == 'default_revert') { + $submit.val(Drupal.t('Revert to default')); + } + else { + $submit.val(Drupal.t('Apply (this display)')); + } + }) + .trigger('change'); + }); + +}; + +Drupal.viewsUi.resizeModal = function (e, no_shrink) { + var $ = jQuery; + var $modal = $('.views-ui-dialog'); + var $scroll = $('.scroll', $modal); + if ($modal.size() == 0 || $modal.css('display') == 'none') { + return; + } + + var maxWidth = parseInt($(window).width() * .85); // 70% of window + var minWidth = parseInt($(window).width() * .6); // 70% of window + + // Set the modal to the minwidth so that our width calculation of + // children works. + $modal.css('width', minWidth); + var width = minWidth; + + // Don't let the window get more than 80% of the display high. + var maxHeight = parseInt($(window).height() * .8); + var minHeight = 200; + if (no_shrink) { + minHeight = $modal.height(); + } + + if (minHeight > maxHeight) { + minHeight = maxHeight; + } + + var height = 0; + + // Calculate the height of the 'scroll' region. + var scrollHeight = 0; + + scrollHeight += parseInt($scroll.css('padding-top')); + scrollHeight += parseInt($scroll.css('padding-bottom')); + + $scroll.children().each(function() { + var w = $(this).innerWidth(); + if (w > width) { + width = w; + } + scrollHeight += $(this).outerHeight(true); + }); + + // Now, calculate what the difference between the scroll and the modal + // will be. + + var difference = 0; + difference += parseInt($scroll.css('padding-top')); + difference += parseInt($scroll.css('padding-bottom')); + difference += $('.views-override').outerHeight(true); + difference += $('.views-messages').outerHeight(true); + difference += $('#views-ajax-title').outerHeight(true); + difference += $('.views-add-form-selected').outerHeight(true); + difference += $('.form-buttons', $modal).outerHeight(true); + + height = scrollHeight + difference; + + if (height > maxHeight) { + height = maxHeight; + scrollHeight = maxHeight - difference; + } + else if (height < minHeight) { + height = minHeight; + scrollHeight = minHeight - difference; + } + + if (width > maxWidth) { + width = maxWidth; + } + + // Get where we should move content to + var top = ($(window).height() / 2) - (height / 2); + var left = ($(window).width() / 2) - (width / 2); + + $modal.css({ + 'top': top + 'px', + 'left': left + 'px', + 'width': width + 'px', + 'height': height + 'px' + }); + + // Ensure inner popup height matches. + $(Drupal.settings.views.ajax.popup).css('height', height + 'px'); + + $scroll.css({ + 'height': scrollHeight + 'px', + 'max-height': scrollHeight + 'px' + }); + +}; + +jQuery(function() { + jQuery(window).bind('resize', Drupal.viewsUi.resizeModal); + jQuery(window).bind('scroll', Drupal.viewsUi.resizeModal); +}); diff --git a/js/views-contextual.js b/js/views-contextual.js new file mode 100644 index 00000000..2e9dcf7a --- /dev/null +++ b/js/views-contextual.js @@ -0,0 +1,16 @@ +/** + * @file + * Javascript related to contextual links. + */ +(function ($) { + +Drupal.behaviors.viewsContextualLinks = { + attach: function (context) { + // If there are views-related contextual links attached to the main page + // content, find the smallest region that encloses both the links and the + // view, and display it as a contextual links region. + $('.views-contextual-links-page', context).closest(':has(.view)').addClass('contextual-links-region'); + } +}; + +})(jQuery); diff --git a/js/views-list.js b/js/views-list.js new file mode 100644 index 00000000..9c6c5f06 --- /dev/null +++ b/js/views-list.js @@ -0,0 +1,21 @@ +/** + * @file + * Javascript related to the main view list. + */ +(function ($) { + +Drupal.behaviors.viewsUIList = { + attach: function (context) { + $('#ctools-export-ui-list-items thead a').once('views-ajax-processed').each(function() { + $(this).click(function() { + var query = $.deparam.querystring(this.href); + $('#ctools-export-ui-list-form select[name=order]').val(query['order']); + $('#ctools-export-ui-list-form select[name=sort]').val(query['sort']); + $('#ctools-export-ui-list-form input.ctools-auto-submit-click').trigger('click'); + return false; + }); + }); + } +}; + +})(jQuery); diff --git a/modules/aggregator.views.inc b/modules/aggregator.views.inc new file mode 100644 index 00000000..0fcae2c2 --- /dev/null +++ b/modules/aggregator.views.inc @@ -0,0 +1,406 @@ + 'iid', + 'title' => t('Aggregator item'), + 'help' => t("Aggregator items are imported from external RSS and Atom news feeds."), + ); + + // ---------------------------------------------------------------- + // Fields + + // item id. + $data['aggregator_item']['iid'] = array( + 'title' => t('Feed Item ID'), + 'help' => t('The unique ID of the aggregator item.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + 'numeric' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // iid + $data['aggregator_item']['iid'] = array( + 'title' => t('Item ID'), + 'help' => t('The unique ID of the aggregator item.'), // The help that appears on the UI, + // Information for displaying the iid + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + // Information for accepting a iid as an argument + 'argument' => array( + 'handler' => 'views_handler_argument_aggregator_iid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + ), + // Information for accepting a nid as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + // Information for sorting on a nid. + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // title + $data['aggregator_item']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The title of the aggregator item.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_aggregator_title_link', + 'extra' => array('link'), + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // link + $data['aggregator_item']['link'] = array( + 'title' => t('Link'), // The item it appears as on the UI, + 'help' => t('The link to the original source URL of the item.'), + 'field' => array( + 'handler' => 'views_handler_field_url', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // author + $data['aggregator_item']['author'] = array( + 'title' => t('Author'), // The item it appears as on the UI, + 'help' => t('The author of the original imported item.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_aggregator_xss', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // guid + $data['aggregator_item']['guid'] = array( + 'title' => t('GUID'), // The item it appears as on the UI, + 'help' => t('The guid of the original imported item.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_xss', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // feed body + $data['aggregator_item']['description'] = array( + 'title' => t('Body'), // The item it appears as on the UI, + 'help' => t('The actual content of the imported item.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_aggregator_xss', + 'click sortable' => FALSE, + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // item timestamp + $data['aggregator_item']['timestamp'] = array( + 'title' => t('Timestamp'), // The item it appears as on the UI, + 'help' => t('The date the original feed item was posted. (With some feeds, this will be the date it was imported.)'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_date', + ), + ); + + + // ---------------------------------------------------------------------- + // Aggregator feed table + + $data['aggregator_feed']['table']['group'] = t('Aggregator feed'); + + // Explain how this table joins to others. + $data['aggregator_feed']['table']['join'] = array( + 'aggregator_item' => array( + 'left_field' => 'fid', + 'field' => 'fid', + ), + ); + + // fid + $data['aggregator_feed']['fid'] = array( + 'title' => t('Feed ID'), + 'help' => t('The unique ID of the aggregator feed.'), // The help that appears on the UI, + // Information for displaying the fid + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + // Information for accepting a fid as an argument + 'argument' => array( + 'handler' => 'views_handler_argument_aggregator_fid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + ), + // Information for accepting a nid as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + // Information for sorting on a fid. + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // title + $data['aggregator_feed']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The title of the aggregator feed.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_aggregator_title_link', + 'extra' => array('link'), + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // link + $data['aggregator_feed']['link'] = array( + 'title' => t('Link'), // The item it appears as on the UI, + 'help' => t('The link to the source URL of the feed.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_url', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // feed last updated + $data['aggregator_feed']['checked'] = array( + 'title' => t('Last checked'), // The item it appears as on the UI, + 'help' => t('The date the feed was last checked for new content.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_date', + ), + ); + + // feed description + $data['aggregator_feed']['description'] = array( + 'title' => t('Description'), // The item it appears as on the UI, + 'help' => t('The description of the aggregator feed.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_xss', + 'click sortable' => FALSE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // feed last updated + $data['aggregator_feed']['modified'] = array( + 'title' => t('Last modified'), // The item it appears as on the UI, + 'help' => t('The date of the most recent new content on the feed.'), + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_date', + ), + ); + + // ---------------------------------------------------------------------- + // Aggregator category feed table + + $data['aggregator_category_feed']['table']['join'] = array( + 'aggregator_item' => array( + 'left_field' => 'fid', + 'field' => 'fid', + ), + ); + + // ---------------------------------------------------------------------- + // Aggregator category table + + $data['aggregator_category']['table']['group'] = t('Aggregator category'); + + $data['aggregator_category']['table']['join'] = array( + 'aggregator_item' => array( + 'left_table' => 'aggregator_category_feed', + 'left_field' => 'cid', + 'field' => 'cid', + ), + ); + + // cid + $data['aggregator_category']['cid'] = array( + 'title' => t('Category ID'), + 'help' => t('The unique ID of the aggregator category.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_aggregator_category_cid', + 'name field' => 'title', + 'numeric' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_aggregator_category_cid', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // title + $data['aggregator_category']['title'] = array( + 'title' => t('Category'), + 'help' => t('The title of the aggregator category.'), + 'field' => array( + 'handler' => 'views_handler_field_aggregator_category', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + return $data; +} + +/** + * Implements hook_views_plugins(). + */ +function aggregator_views_plugins() { + return array( + 'module' => 'views', // This just tells our themes are elsewhere. + 'row' => array( + 'aggregator_rss' => array( + 'title' => t('Aggregator item'), + 'help' => t('Display the aggregator item using the data from the original source.'), + 'handler' => 'views_plugin_row_aggregator_rss', + 'path' => drupal_get_path('module', 'views') . '/modules/node', // not necessary for most modules + 'theme' => 'views_view_row_rss', + 'base' => array('aggregator_item'), // only works with 'node' as base. + 'uses options' => TRUE, + 'type' => 'feed', + 'help topic' => 'style-aggregator-rss', + ), + ), + ); +} diff --git a/modules/aggregator/views_handler_argument_aggregator_category_cid.inc b/modules/aggregator/views_handler_argument_aggregator_category_cid.inc new file mode 100644 index 00000000..92ae8b7f --- /dev/null +++ b/modules/aggregator/views_handler_argument_aggregator_category_cid.inc @@ -0,0 +1,26 @@ + $this->value)); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } +} diff --git a/modules/aggregator/views_handler_argument_aggregator_fid.inc b/modules/aggregator/views_handler_argument_aggregator_fid.inc new file mode 100644 index 00000000..41476268 --- /dev/null +++ b/modules/aggregator/views_handler_argument_aggregator_fid.inc @@ -0,0 +1,26 @@ + $this->value)); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } +} diff --git a/modules/aggregator/views_handler_argument_aggregator_iid.inc b/modules/aggregator/views_handler_argument_aggregator_iid.inc new file mode 100644 index 00000000..d959b042 --- /dev/null +++ b/modules/aggregator/views_handler_argument_aggregator_iid.inc @@ -0,0 +1,30 @@ +value), '%d')); + + $result = db_select('aggregator_item') + ->condition('iid', $this->value, 'IN') + ->fields(array('title')) + ->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } +} diff --git a/modules/aggregator/views_handler_field_aggregator_category.inc b/modules/aggregator/views_handler_field_aggregator_category.inc new file mode 100644 index 00000000..99fffa1e --- /dev/null +++ b/modules/aggregator/views_handler_field_aggregator_category.inc @@ -0,0 +1,60 @@ +additional_fields['cid'] = 'cid'; + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_to_category'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to category option + */ + function options_form(&$form, &$form_state) { + $form['link_to_category'] = array( + '#title' => t('Link this field to its aggregator category page'), + '#description' => t('This will override any other link you have set.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_category']), + ); + parent::options_form($form, $form_state); + } + + /** + * Render whatever the data is as a link to the category. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + $cid = $this->get_value($values, 'cid'); + if (!empty($this->options['link_to_category']) && !empty($cid) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "aggregator/category/$cid"; + } + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/aggregator/views_handler_field_aggregator_title_link.inc b/modules/aggregator/views_handler_field_aggregator_title_link.inc new file mode 100644 index 00000000..d8bf5789 --- /dev/null +++ b/modules/aggregator/views_handler_field_aggregator_title_link.inc @@ -0,0 +1,55 @@ +additional_fields['link'] = 'link'; + } + + function option_definition() { + $options = parent::option_definition(); + + $options['display_as_link'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link to the page being visited. + */ + function options_form(&$form, &$form_state) { + $form['display_as_link'] = array( + '#title' => t('Display as link'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['display_as_link']), + ); + parent::options_form($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } + + function render_link($data, $values) { + $link = $this->get_value($values, 'link'); + if (!empty($this->options['display_as_link'])) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = $link; + $this->options['alter']['html'] = TRUE; + } + + return $data; + } +} diff --git a/modules/aggregator/views_handler_field_aggregator_xss.inc b/modules/aggregator/views_handler_field_aggregator_xss.inc new file mode 100644 index 00000000..d39b1013 --- /dev/null +++ b/modules/aggregator/views_handler_field_aggregator_xss.inc @@ -0,0 +1,18 @@ +get_value($values); + return aggregator_filter_xss($value); + } +} diff --git a/modules/aggregator/views_handler_filter_aggregator_category_cid.inc b/modules/aggregator/views_handler_filter_aggregator_category_cid.inc new file mode 100644 index 00000000..f9931c8f --- /dev/null +++ b/modules/aggregator/views_handler_filter_aggregator_category_cid.inc @@ -0,0 +1,26 @@ +value_options)) { + return; + } + + $this->value_options = array(); + + $result = db_query('SELECT * FROM {aggregator_category} ORDER BY title'); + foreach ($result as $category) { + $this->value_options[$category->cid] = $category->title; + } + } +} diff --git a/modules/aggregator/views_plugin_row_aggregator_rss.inc b/modules/aggregator/views_plugin_row_aggregator_rss.inc new file mode 100644 index 00000000..672952e0 --- /dev/null +++ b/modules/aggregator/views_plugin_row_aggregator_rss.inc @@ -0,0 +1,74 @@ + 'default'); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['item_length'] = array( + '#type' => 'select', + '#title' => t('Display type'), + '#options' => array( + 'fulltext' => t('Full text'), + 'teaser' => t('Title plus teaser'), + 'title' => t('Title only'), + 'default' => t('Use default RSS settings'), + ), + '#default_value' => $this->options['item_length'], + ); + } + + function render($row) { + $iid = $row->{$this->field_alias}; + $sql = "SELECT ai.iid, ai.fid, ai.title, ai.link, ai.author, ai.description, "; + $sql .= "ai.timestamp, ai.guid, af.title AS feed_title, ai.link AS feed_LINK "; + $sql .= "FROM {aggregator_item} ai LEFT JOIN {aggregator_feed} af ON ai.fid = af.fid "; + $sql .= "WHERE ai.iid = :iid"; + + $item = db_query($sql, array(':iid' => $iid))->fetchObject(); + + $item->elements = array( + array( + 'key' => 'pubDate', + 'value' => gmdate('r', $item->timestamp), + ), + array( + 'key' => 'dc:creator', + 'value' => $item->author, + ), + array( + 'key' => 'guid', + 'value' => $item->guid, + 'attributes' => array('isPermaLink' => 'false') + ), + ); + + foreach ($item->elements as $element) { + if (isset($element['namespace'])) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $element['namespace']); + } + } + + return theme($this->theme_functions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item + )); + } +} diff --git a/modules/book.views.inc b/modules/book.views.inc new file mode 100644 index 00000000..15a21830 --- /dev/null +++ b/modules/book.views.inc @@ -0,0 +1,131 @@ + array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + $data['book']['bid'] = array( + 'title' => t('Top level book'), + 'help' => t('The book the node is in.'), + 'relationship' => array( + 'base' => 'node', + 'handler' => 'views_handler_relationship', + 'label' => t('Book'), + ), + // There is no argument here; if you need an argument, add the relationship + // and use the node: nid argument. + ); + + // ---------------------------------------------------------------------- + // menu_links table -- this is aliased so we can get just book relations + + // Book hierarchy and weight data are now in {menu_links}. + $data['book_menu_links']['table']['group'] = t('Book'); + $data['book_menu_links']['table']['join'] = array( + 'node' => array( + 'table' => 'menu_links', + 'left_table' => 'book', + 'left_field' => 'mlid', + 'field' => 'mlid', + ), + ); + + $data['book_menu_links']['weight'] = array( + 'title' => t('Weight'), + 'help' => t('The weight of the book page.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + $data['book_menu_links']['depth'] = array( + 'title' => t('Depth'), + 'help' => t('The depth of the book page in the hierarchy; top level books have a depth of 1.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'argument' => array( + 'handler' => 'views_handler_argument', + ), + ); + + $data['book_menu_links']['p'] = array( + 'title' => t('Hierarchy'), + 'help' => t('The order of pages in the book hierarchy.'), + 'sort' => array( + 'handler' => 'views_handler_sort_menu_hierarchy', + ), + ); + + // ---------------------------------------------------------------------- + // book_parent table -- this is an alias of the book table which + // represents the parent book. + + // The {book} record for the parent node. + $data['book_parent']['table']['group'] = t('Book'); + $data['book_parent']['table']['join'] = array( + 'node' => array( + 'table' => 'book', + 'left_table' => 'book_menu_links', + 'left_field' => 'plid', + 'field' => 'mlid', + ), + ); + + $data['book_parent']['nid'] = array( + 'title' => t('Parent'), + 'help' => t('The parent book node.'), + 'relationship' => array( + 'base' => 'node', + 'base field' => 'nid', + 'handler' => 'views_handler_relationship', + 'label' => t('Book parent'), + ), + ); + + return $data; +} + +/** + * Implements hook_views_plugins(). + */ +function book_views_plugins() { + return array( + 'module' => 'views', + 'argument default' => array( + 'book_root' => array( + 'title' => t('Book root from current node'), + 'handler' => 'views_plugin_argument_default_book_root' + ), + ), + ); +} diff --git a/modules/book/views_plugin_argument_default_book_root.inc b/modules/book/views_plugin_argument_default_book_root.inc new file mode 100644 index 00000000..1ce30467 --- /dev/null +++ b/modules/book/views_plugin_argument_default_book_root.inc @@ -0,0 +1,21 @@ +book['bid'])) { + return $node->book['bid']; + } + } + } +} diff --git a/modules/comment.views.inc b/modules/comment.views.inc new file mode 100644 index 00000000..65ef18ca --- /dev/null +++ b/modules/comment.views.inc @@ -0,0 +1,662 @@ + 'cid', + 'title' => t('Comment'), + 'help' => t("Comments are responses to node content."), + 'access query tag' => 'comment_access', + ); + $data['comment']['table']['entity type'] = 'comment'; + + // Provide a "default relationship" to keep older views from choking. + $data['comment']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'cid', + ), + ); + + // ---------------------------------------------------------------- + // Fields + + // subject + $data['comment']['subject'] = array( + 'title' => t('Title'), + 'help' => t('The title of the comment.'), + 'field' => array( + 'handler' => 'views_handler_field_comment', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // cid + $data['comment']['cid'] = array( + 'title' => t('ID'), + 'help' => t('The comment ID of the field'), + 'field' => array( + 'handler' => 'views_handler_field_comment', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + ); + + // name (of comment author) + $data['comment']['name'] = array( + 'title' => t('Author'), + 'help' => t("The name of the comment's author. Can be rendered as a link to the author's homepage."), + 'field' => array( + 'handler' => 'views_handler_field_comment_username', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // homepage + $data['comment']['homepage'] = array( + 'title' => t("Author's website"), + 'help' => t("The website address of the comment's author. Can be rendered as a link. Will be empty if the author is a registered user."), + 'field' => array( + 'handler' => 'views_handler_field_url', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // hostname + $data['comment']['hostname'] = array( + 'title' => t('Hostname'), + 'help' => t('Hostname of user that posted the comment.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // mail + $data['comment']['mail'] = array( + 'title' => t('Mail'), + 'help' => t('Email of user that posted the comment. Will be empty if the author is a registered user.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // created (when comment was posted) + $data['comment']['created'] = array( + 'title' => t('Post date'), + 'help' => t('Date and time of when the comment was created.'), + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + + // Language field + if (module_exists('locale')) { + $data['comment']['language'] = array( + 'title' => t('Language'), + 'help' => t('The language the comment is in.'), + 'field' => array( + 'handler' => 'views_handler_field_locale_language', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_locale_language', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_locale_language', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + } + + $data['comments']['timestamp']['moved to'] = array('comment', 'changed'); + // changed (when comment was last updated) + $data['comment']['changed'] = array( + 'title' => t('Updated date'), + 'help' => t('Date and time of when the comment was last updated.'), + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + $data['comments']['timestamp_fulldate']['moved to'] = array('comment', 'changed_fulldata'); + $data['comment']['changed_fulldata'] = array( + 'title' => t('Created date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_fulldate', + ), + ); + + $data['comments']['timestamp_year_month']['moved to'] = array('comment', 'changed_year_month'); + $data['comment']['changed_year_month'] = array( + 'title' => t('Created year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_year_month', + ), + ); + + $data['comments']['timestamp_year']['moved to'] = array('comment', 'changed_year'); + $data['comment']['changed_year'] = array( + 'title' => t('Created year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_year', + ), + ); + + $data['comments']['timestamp_month']['moved to'] = array('comment', 'changed_month'); + $data['comment']['changed_month'] = array( + 'title' => t('Created month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_month', + ), + ); + + $data['comments']['timestamp_day']['moved to'] = array('comment', 'changed_day'); + $data['comment']['changed_day'] = array( + 'title' => t('Created day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_day', + ), + ); + + $data['comments']['timestamp_week']['moved to'] = array('comment', 'changed_week'); + $data['comment']['changed_week'] = array( + 'title' => t('Created week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_week', + ), + ); + + // status (approved or not) + $data['comment']['status'] = array( + 'title' => t('Approved'), + 'help' => t('Whether the comment is approved (or still in the moderation queue).'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'approved-not-approved' => array(t('Approved'), t('Not Approved')), + ), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Approved comment'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // link to view comment + $data['comment']['view_comment'] = array( + 'field' => array( + 'title' => t('View link'), + 'help' => t('Provide a simple link to view the comment.'), + 'handler' => 'views_handler_field_comment_link', + ), + ); + + // link to edit comment + $data['comment']['edit_comment'] = array( + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the comment.'), + 'handler' => 'views_handler_field_comment_link_edit', + ), + ); + + // link to delete comment + $data['comment']['delete_comment'] = array( + 'field' => array( + 'title' => t('Delete link'), + 'help' => t('Provide a simple link to delete the comment.'), + 'handler' => 'views_handler_field_comment_link_delete', + ), + ); + + + // link to approve comment + $data['comment']['approve_comment'] = array( + 'field' => array( + 'title' => t('Approve link'), + 'help' => t('Provide a simple link to approve the comment.'), + 'handler' => 'views_handler_field_comment_link_approve', + ), + ); + + // link to reply to comment + $data['comment']['replyto_comment'] = array( + 'field' => array( + 'title' => t('Reply-to link'), + 'help' => t('Provide a simple link to reply to the comment.'), + 'handler' => 'views_handler_field_comment_link_reply', + ), + ); + + $data['comment']['thread'] = array( + 'field' => array( + 'title' => t('Depth'), + 'help' => t('Display the depth of the comment if it is threaded.'), + 'handler' => 'views_handler_field_comment_depth', + ), + 'sort' => array( + 'title' => t('Thread'), + 'help' => t('Sort by the threaded order. This will keep child comments together with their parents.'), + 'handler' => 'views_handler_sort_comment_thread', + ), + ); + + $data['comment']['nid'] = array( + 'title' => t('Nid'), + 'help' => t('The node ID to which the comment is a reply to.'), + 'relationship' => array( + 'title' => t('Content'), + 'help' => t('The content to which the comment is a reply to.'), + 'base' => 'node', + 'base field' => 'nid', + 'handler' => 'views_handler_relationship', + 'label' => t('Content'), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + ), + ); + + $data['comment']['uid'] = array( + 'title' => t('Author uid'), + 'help' => t('If you need more fields than the uid add the comment: author relationship'), + 'relationship' => array( + 'title' => t('Author'), + 'help' => t("The User ID of the comment's author."), + 'base' => 'users', + 'base field' => 'uid', + 'handler' => 'views_handler_relationship', + 'label' => t('author'), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + 'field' => array( + 'handler' => 'views_handler_field_user', + ), + ); + + $data['comment']['pid'] = array( + 'title' => t('Parent CID'), + 'help' => t('The Comment ID of the parent comment.'), + 'field' => array( + 'handler' => 'views_handler_field', + ), + 'relationship' => array( + 'title' => t('Parent comment'), + 'help' => t('The parent comment.'), + 'base' => 'comment', + 'base field' => 'cid', + 'handler' => 'views_handler_relationship', + 'label' => t('Parent comment'), + ), + ); + + // ---------------------------------------------------------------------- + // node_comment_statistics table + + // define the group + $data['node_comment_statistics']['table']['group'] = t('Content'); + + // joins + $data['node_comment_statistics']['table']['join'] = array( + //...to the node table + 'node' => array( + 'type' => 'INNER', + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + // last_comment_timestamp + $data['node_comment_statistics']['last_comment_timestamp'] = array( + 'title' => t('Last comment time'), + 'help' => t('Date and time of when the last comment was posted.'), + 'field' => array( + 'handler' => 'views_handler_field_last_comment_timestamp', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + // last_comment_name (author's name) + $data['node_comment_statistics']['last_comment_name'] = array( + 'title' => t("Last comment author"), + 'help' => t('The name of the author of the last posted comment.'), + 'field' => array( + 'handler' => 'views_handler_field_ncs_last_comment_name', + 'click sortable' => TRUE, + 'no group by' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_ncs_last_comment_name', + 'no group by' => TRUE, + ), + ); + + // comment_count + $data['node_comment_statistics']['comment_count'] = array( + 'title' => t('Comment count'), + 'help' => t('The number of comments a node has.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument', + ), + ); + + // last_comment_timestamp + $data['node_comment_statistics']['last_updated'] = array( + 'title' => t('Updated/commented date'), + 'help' => t('The most recent of last comment posted or node updated time.'), + 'field' => array( + 'handler' => 'views_handler_field_ncs_last_updated', + 'click sortable' => TRUE, + 'no group by' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_ncs_last_updated', + 'no group by' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_ncs_last_updated', + ), + ); + + $data['node_comment_statistics']['cid'] = array( + 'title' => t('Last comment CID'), + 'help' => t('Display the last comment of a node'), + 'relationship' => array( + 'title' => t('Last Comment'), + 'help' => t('The last comment of a node.'), + 'group' => t('Comment'), + 'base' => 'comment', + 'base field' => 'cid', + 'handler' => 'views_handler_relationship', + 'label' => t('Last Comment'), + ), + ); + + // last_comment_uid + $data['node_comment_statistics']['last_comment_uid'] = array( + 'title' => t('Last comment uid'), + 'help' => t('The User ID of the author of the last comment of a node.'), + 'relationship' => array( + 'title' => t('Last comment author'), + 'base' => 'users', + 'base field' => 'uid', + 'handler' => 'views_handler_relationship', + 'label' => t('Last comment author'), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + ), + ); + + return $data; +} + +/** + * Use views_data_alter to add items to the node table that are + * relevant to comments. + */ +function comment_views_data_alter(&$data) { + // new comments + $data['node']['new_comments'] = array( + 'title' => t('New comments'), + 'help' => t('The number of new comments on the node.'), + 'field' => array( + 'handler' => 'views_handler_field_node_new_comments', + 'no group by' => TRUE, + ), + ); + + $data['node']['comments_link'] = array( + 'field' => array( + 'title' => t('Add comment link'), + 'help' => t('Display the standard add comment link used on regular nodes, which will only display if the viewing user has access to add a comment.'), + 'handler' => 'views_handler_field_comment_node_link', + ), + ); + + // Comment status of the node + $data['node']['comment'] = array( + 'title' => t('Comment status'), + 'help' => t('Whether comments are enabled or disabled on the node.'), + 'field' => array( + 'handler' => 'views_handler_field_node_comment', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_node_comment', + ), + ); + + $data['node']['uid_touch'] = array( + 'title' => t('User posted or commented'), + 'help' => t('Display nodes only if a user posted the node or commented on the node.'), + 'argument' => array( + 'field' => 'uid', + 'name table' => 'users', + 'name field' => 'name', + 'handler' => 'views_handler_argument_comment_user_uid', + 'no group by' => TRUE, + ), + 'filter' => array( + 'field' => 'uid', + 'name table' => 'users', + 'name field' => 'name', + 'handler' => 'views_handler_filter_comment_user_uid' + ), + ); + + $data['node']['cid'] = array( + 'title' => t('Comments of the node'), + 'help' => t('Relate all comments on the node. This will create 1 duplicate record for every comment. Usually if you need this it is better to create a comment view.'), + 'relationship' => array( + 'group' => t('Comment'), + 'label' => t('Comments'), + 'base' => 'comment', + 'base field' => 'nid', + 'relationship field' => 'nid', + 'handler' => 'views_handler_relationship', + ), + ); + +} + +/** + * Implements hook_views_plugins(). + */ +function comment_views_plugins() { + return array( + 'module' => 'views', + 'row' => array( + 'comment' => array( + 'title' => t('Comment'), + 'help' => t('Display the comment with standard comment view.'), + 'handler' => 'views_plugin_row_comment_view', + 'theme' => 'views_view_row_comment', + 'path' => drupal_get_path('module', 'views') . '/modules/comment', // not necessary for most modules + 'base' => array('comment'), // only works with 'comment' as base. + 'uses options' => TRUE, + 'type' => 'normal', + 'help topic' => 'style-comment', + ), + 'comment_rss' => array( + 'title' => t('Comment'), + 'help' => t('Display the comment as RSS.'), + 'handler' => 'views_plugin_row_comment_rss', + 'theme' => 'views_view_row_rss', + 'path' => drupal_get_path('module', 'views') . '/modules/comment', // not necessary for most modules + 'base' => array('comment'), // only works with 'comment' as base. + 'uses options' => TRUE, + 'type' => 'feed', + 'help topic' => 'style-comment-rss', + ), + ), + ); +} + +/** + * Template helper for theme_views_view_row_comment + */ +function template_preprocess_views_view_row_comment(&$vars) { + $options = $vars['options']; + $view = &$vars['view']; + $plugin = &$view->style_plugin->row_plugin; + $comment = $plugin->comments[$vars['row']->{$vars['field_alias']}]; + $node = $plugin->nodes[$comment->nid]; + // Put the view on the node so we can retrieve it in the preprocess. + $node->view = &$view; + + $build = comment_view_multiple(array($comment->cid => $comment), $node, $plugin->options['view_mode']); + // If we're displaying the comments without links, remove them from the + // renderable array. There is no way to avoid building them in the first + // place (see comment_build_content()). + if (empty($options['links'])) { + foreach ($build as $cid => &$comment_build) { + if (isset($comment_build['links'])) { + unset($comment_build['links']); + } + } + } + $vars['comment'] = drupal_render($build); +} diff --git a/modules/comment.views_default.inc b/modules/comment.views_default.inc new file mode 100644 index 00000000..44ebac90 --- /dev/null +++ b/modules/comment.views_default.inc @@ -0,0 +1,283 @@ +name = 'comments_recent'; + $view->description = 'Contains a block and a page to list recent comments; the block will automatically link to the page, which displays the comment body as well as a link to the node.'; + $view->tag = 'default'; + $view->base_table = 'comment'; + $view->human_name = 'Recent comments'; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Recent comments'; + $handler->display->display_options['use_more'] = TRUE; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['pager']['options']['items_per_page'] = 5; + $handler->display->display_options['style_plugin'] = 'list'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Relationship: Comment: Content */ + $handler->display->display_options['relationships']['nid']['id'] = 'nid'; + $handler->display->display_options['relationships']['nid']['table'] = 'comment'; + $handler->display->display_options['relationships']['nid']['field'] = 'nid'; + /* Field: Comment: Title */ + $handler->display->display_options['fields']['subject']['id'] = 'subject'; + $handler->display->display_options['fields']['subject']['table'] = 'comment'; + $handler->display->display_options['fields']['subject']['field'] = 'subject'; + $handler->display->display_options['fields']['subject']['label'] = ''; + $handler->display->display_options['fields']['subject']['link_to_comment'] = 1; + /* Field: Comment: Updated date */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'comment'; + $handler->display->display_options['fields']['timestamp']['field'] = 'changed'; + $handler->display->display_options['fields']['timestamp']['label'] = ''; + $handler->display->display_options['fields']['timestamp']['date_format'] = 'time ago'; + /* Sort criterion: Comment: Updated date */ + $handler->display->display_options['sorts']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['sorts']['timestamp']['table'] = 'comment'; + $handler->display->display_options['sorts']['timestamp']['field'] = 'changed'; + $handler->display->display_options['sorts']['timestamp']['order'] = 'DESC'; + /* Filter criterion: Content: Published or admin */ + $handler->display->display_options['filters']['status_extra']['id'] = 'status_extra'; + $handler->display->display_options['filters']['status_extra']['table'] = 'node'; + $handler->display->display_options['filters']['status_extra']['field'] = 'status_extra'; + $handler->display->display_options['filters']['status_extra']['relationship'] = 'nid'; + $handler->display->display_options['filters']['status_extra']['group'] = 0; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['defaults']['style_plugin'] = FALSE; + $handler->display->display_options['style_plugin'] = 'list'; + $handler->display->display_options['defaults']['style_options'] = FALSE; + $handler->display->display_options['defaults']['row_plugin'] = FALSE; + $handler->display->display_options['row_plugin'] = 'fields'; + $handler->display->display_options['row_options']['inline'] = array( + 'title' => 'title', + 'timestamp' => 'timestamp', + ); + $handler->display->display_options['row_options']['separator'] = ' '; + $handler->display->display_options['defaults']['row_options'] = FALSE; + $handler->display->display_options['defaults']['fields'] = FALSE; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['relationship'] = 'nid'; + $handler->display->display_options['fields']['title']['label'] = 'Reply to'; + $handler->display->display_options['fields']['title']['link_to_node'] = 1; + /* Field: Comment: Updated date */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'comment'; + $handler->display->display_options['fields']['timestamp']['field'] = 'changed'; + $handler->display->display_options['fields']['timestamp']['label'] = ''; + $handler->display->display_options['fields']['timestamp']['date_format'] = 'time ago'; + /* Field: Comment: Title */ + $handler->display->display_options['fields']['subject']['id'] = 'subject'; + $handler->display->display_options['fields']['subject']['table'] = 'comment'; + $handler->display->display_options['fields']['subject']['field'] = 'subject'; + $handler->display->display_options['fields']['subject']['label'] = ''; + $handler->display->display_options['fields']['subject']['link_to_comment'] = 1; + /* Field: Comment: Comment */ + $handler->display->display_options['fields']['comment']['id'] = 'comment'; + $handler->display->display_options['fields']['comment']['table'] = 'field_data_comment_body'; + $handler->display->display_options['fields']['comment']['field'] = 'comment_body'; + $handler->display->display_options['fields']['comment']['label'] = ''; + $handler->display->display_options['path'] = 'comments/recent'; + + /* Display: Block */ + $handler = $view->new_display('block', 'Block', 'block'); + $translatables['comments_recent'] = array( + t('Master'), + t('Recent comments'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Content'), + t('Page'), + t('Reply to'), + t('Block'), + ); + + $views['comments_recent'] = $view; + + $view = new view; + $view->name = 'tracker'; + $view->description = 'Shows all new activity on system.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Tracker'; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Recent posts'; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '25'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'type' => 'type', + 'title' => 'title', + 'name' => 'name', + 'comment_count' => 'comment_count', + 'last_comment_timestamp' => 'last_comment_timestamp', + 'timestamp' => 'title', + 'new_comments' => 'comment_count', + ); + $handler->display->display_options['style_options']['default'] = 'last_comment_timestamp'; + $handler->display->display_options['style_options']['info'] = array( + 'type' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'title' => array( + 'sortable' => 1, + 'separator' => ' ', + ), + 'name' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'comment_count' => array( + 'sortable' => 1, + 'separator' => '
    ', + ), + 'last_comment_timestamp' => array( + 'sortable' => 1, + 'separator' => ' ', + ), + 'timestamp' => array( + 'separator' => '', + ), + 'new_comments' => array( + 'separator' => '', + ), + ); + $handler->display->display_options['style_options']['override'] = 1; + $handler->display->display_options['style_options']['order'] = 'desc'; + /* Relationship: Content: Author */ + $handler->display->display_options['relationships']['uid']['id'] = 'uid'; + $handler->display->display_options['relationships']['uid']['table'] = 'node'; + $handler->display->display_options['relationships']['uid']['field'] = 'uid'; + /* Field: Content: Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['relationship'] = 'uid'; + $handler->display->display_options['fields']['name']['label'] = 'Author'; + /* Field: Content: Comment count */ + $handler->display->display_options['fields']['comment_count']['id'] = 'comment_count'; + $handler->display->display_options['fields']['comment_count']['table'] = 'node_comment_statistics'; + $handler->display->display_options['fields']['comment_count']['field'] = 'comment_count'; + $handler->display->display_options['fields']['comment_count']['label'] = 'Replies'; + /* Field: Content: Last comment time */ + $handler->display->display_options['fields']['last_comment_timestamp']['id'] = 'last_comment_timestamp'; + $handler->display->display_options['fields']['last_comment_timestamp']['table'] = 'node_comment_statistics'; + $handler->display->display_options['fields']['last_comment_timestamp']['field'] = 'last_comment_timestamp'; + $handler->display->display_options['fields']['last_comment_timestamp']['label'] = 'Last Post'; + /* Field: Content: Has new content */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'history'; + $handler->display->display_options['fields']['timestamp']['field'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['label'] = ''; + $handler->display->display_options['fields']['timestamp']['link_to_node'] = 0; + $handler->display->display_options['fields']['timestamp']['comments'] = 1; + /* Field: Content: New comments */ + $handler->display->display_options['fields']['new_comments']['id'] = 'new_comments'; + $handler->display->display_options['fields']['new_comments']['table'] = 'node'; + $handler->display->display_options['fields']['new_comments']['field'] = 'new_comments'; + $handler->display->display_options['fields']['new_comments']['label'] = ''; + $handler->display->display_options['fields']['new_comments']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['new_comments']['suffix'] = ' new'; + $handler->display->display_options['fields']['new_comments']['link_to_comment'] = 1; + /* Sort criterion: Content: Last comment time */ + $handler->display->display_options['sorts']['last_comment_timestamp']['id'] = 'last_comment_timestamp'; + $handler->display->display_options['sorts']['last_comment_timestamp']['table'] = 'node_comment_statistics'; + $handler->display->display_options['sorts']['last_comment_timestamp']['field'] = 'last_comment_timestamp'; + /* Contextual filter: Content: User posted or commented */ + $handler->display->display_options['arguments']['uid_touch']['id'] = 'uid_touch'; + $handler->display->display_options['arguments']['uid_touch']['table'] = 'node'; + $handler->display->display_options['arguments']['uid_touch']['field'] = 'uid_touch'; + $handler->display->display_options['arguments']['uid_touch']['exception']['title_enable'] = 1; + $handler->display->display_options['arguments']['uid_touch']['title_enable'] = 1; + $handler->display->display_options['arguments']['uid_touch']['title'] = 'Recent posts for %1'; + $handler->display->display_options['arguments']['uid_touch']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['uid_touch']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['uid_touch']['specify_validation'] = 1; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = '1'; + $handler->display->display_options['filters']['status']['group'] = 0; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'tracker'; + $handler->display->display_options['menu']['type'] = 'normal'; + $handler->display->display_options['menu']['title'] = 'Recent posts'; + $translatables['tracker'] = array( + t('Master'), + t('Recent posts'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('Type'), + t('Title'), + t('Author'), + t('Replies'), + t('.'), + t(','), + t('Last Post'), + t(' new'), + t('All'), + t('Recent posts for %1'), + t('Page'), + ); + + $views['tracker'] = $view; + + return $views; +} diff --git a/modules/comment/views_handler_argument_comment_user_uid.inc b/modules/comment/views_handler_argument_comment_user_uid.inc new file mode 100644 index 00000000..d821f32c --- /dev/null +++ b/modules/comment/views_handler_argument_comment_user_uid.inc @@ -0,0 +1,61 @@ +argument) { + $title = variable_get('anonymous', t('Anonymous')); + } + else { + $title = db_query('SELECT u.name FROM {users} u WHERE u.uid = :uid', array(':uid' => $this->argument))->fetchField(); + } + if (empty($title)) { + return t('No user'); + } + + return check_plain($title); + } + + function default_actions($which = NULL) { + // Disallow summary views on this argument. + if (!$which) { + $actions = parent::default_actions(); + unset($actions['summary asc']); + unset($actions['summary desc']); + return $actions; + } + + if ($which != 'summary asc' && $which != 'summary desc') { + return parent::default_actions($which); + } + } + + function query($group_by = FALSE) { + $this->ensure_my_table(); + + $subselect = db_select('comment', 'c'); + $subselect->addField('c', 'cid'); + $subselect->condition('c.uid', $this->argument); + $subselect->where("c.nid = $this->table_alias.nid"); + + $condition = db_or() + ->condition("$this->table_alias.uid", $this->argument, '=') + ->exists($subselect); + + $this->query->add_where(0, $condition); + } + + function get_sort_name() { + return t('Numerical', array(), array('context' => 'Sort order')); + } +} diff --git a/modules/comment/views_handler_field_comment.inc b/modules/comment/views_handler_field_comment.inc new file mode 100644 index 00000000..7ca3256f --- /dev/null +++ b/modules/comment/views_handler_field_comment.inc @@ -0,0 +1,73 @@ +options['link_to_comment'])) { + $this->additional_fields['cid'] = 'cid'; + $this->additional_fields['nid'] = 'nid'; + } + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_to_comment'] = array('default' => TRUE, 'bool' => TRUE); + $options['link_to_node'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link-to-comment option + */ + function options_form(&$form, &$form_state) { + $form['link_to_comment'] = array( + '#title' => t('Link this field to its comment'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_comment'], + ); + $form['link_to_node'] = array( + '#title' => t('Link field to the node if there is no comment.'), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_node'], + ); + parent::options_form($form, $form_state); + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_comment'])) { + $this->options['alter']['make_link'] = TRUE; + $nid = $this->get_value($values, 'nid'); + $cid = $this->get_value($values, 'cid'); + if (!empty($cid)) { + $this->options['alter']['path'] = "comment/" . $cid; + $this->options['alter']['fragment'] = "comment-" . $cid; + } + // If there is no comment link to the node. + else if ($this->options['link_to_node']) { + $this->options['alter']['path'] = "node/" . $nid; + } + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/comment/views_handler_field_comment_depth.inc b/modules/comment/views_handler_field_comment_depth.inc new file mode 100644 index 00000000..4840a1e5 --- /dev/null +++ b/modules/comment/views_handler_field_comment_depth.inc @@ -0,0 +1,21 @@ +get_value($values); + return count(explode('.', $comment_thread)) - 1; + } +} diff --git a/modules/comment/views_handler_field_comment_link.inc b/modules/comment/views_handler_field_comment_link.inc new file mode 100644 index 00000000..162924e1 --- /dev/null +++ b/modules/comment/views_handler_field_comment_link.inc @@ -0,0 +1,69 @@ + '', 'translatable' => TRUE); + $options['link_to_node'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + $form['link_to_node'] = array( + '#title' => t('Link field to the node if there is no comment.'), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_node'], + ); + parent::options_form($form, $form_state); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + $value = $this->get_value($values, 'cid'); + return $this->render_link($this->sanitize_value($value), $values); + } + + function render_link($data, $values) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('view'); + $comment = $this->get_value($values); + $nid = $comment->nid; + $cid = $comment->cid; + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['html'] = TRUE; + + if (!empty($cid)) { + $this->options['alter']['path'] = "comment/" . $cid; + $this->options['alter']['fragment'] = "comment-" . $cid; + } + // If there is no comment link to the node. + else if ($this->options['link_to_node']) { + $this->options['alter']['path'] = "node/" . $nid; + } + + return $text; + } +} diff --git a/modules/comment/views_handler_field_comment_link_approve.inc b/modules/comment/views_handler_field_comment_link_approve.inc new file mode 100644 index 00000000..0953d0c8 --- /dev/null +++ b/modules/comment/views_handler_field_comment_link_approve.inc @@ -0,0 +1,36 @@ +get_value($values, 'status'); + + // Don't show an approve link on published nodes. + if ($status == COMMENT_PUBLISHED) { + return; + } + + $text = !empty($this->options['text']) ? $this->options['text'] : t('approve'); + $cid = $this->get_value($values, 'cid'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "comment/" . $cid . "/approve"; + $this->options['alter']['query'] = drupal_get_destination() + array('token' => drupal_get_token("comment/$cid/approve")); + + return $text; + } +} diff --git a/modules/comment/views_handler_field_comment_link_delete.inc b/modules/comment/views_handler_field_comment_link_delete.inc new file mode 100644 index 00000000..c55ac1cf --- /dev/null +++ b/modules/comment/views_handler_field_comment_link_delete.inc @@ -0,0 +1,29 @@ +options['text']) ? $this->options['text'] : t('delete'); + $cid = $this->get_value($values, 'cid'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "comment/" . $cid . "/delete"; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } +} diff --git a/modules/comment/views_handler_field_comment_link_edit.inc b/modules/comment/views_handler_field_comment_link_edit.inc new file mode 100644 index 00000000..0b06c0e9 --- /dev/null +++ b/modules/comment/views_handler_field_comment_link_edit.inc @@ -0,0 +1,52 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['destination'] = array( + '#type' => 'checkbox', + '#title' => t('Use destination'), + '#description' => t('Add destination to the link'), + '#default_value' => $this->options['destination'], + '#fieldset' => 'more', + ); + } + + function render_link($data, $values) { + parent::render_link($data, $values); + // ensure user has access to edit this comment. + $comment = $this->get_value($values); + if (!comment_access('edit', $comment)) { + return; + } + + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + unset($this->options['alter']['fragment']); + + if (!empty($this->options['destination'])) { + $this->options['alter']['query'] = drupal_get_destination(); + } + + $this->options['alter']['path'] = "comment/" . $comment->cid . "/edit"; + + return $text; + } +} diff --git a/modules/comment/views_handler_field_comment_link_reply.inc b/modules/comment/views_handler_field_comment_link_reply.inc new file mode 100644 index 00000000..47d0f170 --- /dev/null +++ b/modules/comment/views_handler_field_comment_link_reply.inc @@ -0,0 +1,29 @@ +options['text']) ? $this->options['text'] : t('reply'); + $nid = $this->get_value($values, 'nid'); + $cid = $this->get_value($values, 'cid'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "comment/reply/" . $nid . '/' . $cid; + + return $text; + } +} diff --git a/modules/comment/views_handler_field_comment_node_link.inc b/modules/comment/views_handler_field_comment_node_link.inc new file mode 100644 index 00000000..7feecfba --- /dev/null +++ b/modules/comment/views_handler_field_comment_node_link.inc @@ -0,0 +1,64 @@ +additional_fields['nid'] = array( + 'field' => 'nid', + ); + $this->additional_fields['type'] = array( + 'field' => 'type', + ); + $this->additional_fields['comment'] = array( + 'field' => 'comment', + ); + } + + function option_definition() { + $options = parent::option_definition(); + $options['teaser'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['teaser'] = array( + '#type' => 'checkbox', + '#title' => t('Show teaser-style link'), + '#default_value' => $this->options['teaser'], + '#description' => t('Show the comment link in the form used on standard node teasers, rather than the full node form.'), + ); + + parent::options_form($form, $form_state); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + // Build fake $node. + $node = $this->get_value($values); + + // Call comment.module's hook_link: comment_link($type, $node = NULL, $teaser = FALSE) + // Call node by reference so that something is changed here + comment_node_view($node, $this->options['teaser'] ? 'teaser' : 'full'); + // question: should we run these through: drupal_alter('link', $links, $node); + // might this have unexpected consequences if these hooks expect items in $node that we don't have? + + // Only render the links, if they are defined. + return !empty($node->content['links']['comment']) ? drupal_render($node->content['links']['comment']) : ''; + } +} diff --git a/modules/comment/views_handler_field_comment_username.inc b/modules/comment/views_handler_field_comment_username.inc new file mode 100644 index 00000000..887a74e3 --- /dev/null +++ b/modules/comment/views_handler_field_comment_username.inc @@ -0,0 +1,58 @@ +additional_fields['uid'] = 'uid'; + $this->additional_fields['homepage'] = 'homepage'; + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_to_user'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['link_to_user'] = array( + '#title' => t("Link this field to its user or an author's homepage"), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_user'], + ); + parent::options_form($form, $form_state); + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_user'])) { + $account = new stdClass(); + $account->uid = $this->get_value($values, 'uid'); + $account->name = $this->get_value($values); + $account->homepage = $this->get_value($values, 'homepage'); + + return theme('username', array( + 'account' => $account + )); + } + else { + return $data; + } + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/comment/views_handler_field_last_comment_timestamp.inc b/modules/comment/views_handler_field_last_comment_timestamp.inc new file mode 100644 index 00000000..e7cf8bde --- /dev/null +++ b/modules/comment/views_handler_field_last_comment_timestamp.inc @@ -0,0 +1,28 @@ +additional_fields['comment_count'] = 'comment_count'; + } + + function render($values) { + $comment_count = $this->get_value($values, 'comment_count'); + if (empty($this->options['empty_zero']) || $comment_count) { + return parent::render($values); + } + else { + return NULL; + } + } +} diff --git a/modules/comment/views_handler_field_ncs_last_comment_name.inc b/modules/comment/views_handler_field_ncs_last_comment_name.inc new file mode 100644 index 00000000..c9c6885e --- /dev/null +++ b/modules/comment/views_handler_field_ncs_last_comment_name.inc @@ -0,0 +1,54 @@ +ensure_my_table(); + // join 'users' to this table via vid + $join = new views_join(); + $join->construct('users', $this->table_alias, 'last_comment_uid', 'uid'); + $join->extra = array(array('field' => 'uid', 'operator' => '!=', 'value' => '0')); + + // ncs_user alias so this can work with the sort handler, below. +// $this->user_table = $this->query->add_relationship(NULL, $join, 'users', $this->relationship); + $this->user_table = $this->query->ensure_table('ncs_users', $this->relationship, $join); + + $this->field_alias = $this->query->add_field(NULL, "COALESCE($this->user_table.name, $this->table_alias.$this->field)", $this->table_alias . '_' . $this->field); + + $this->user_field = $this->query->add_field($this->user_table, 'name'); + $this->uid = $this->query->add_field($this->table_alias, 'last_comment_uid'); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['link_to_user'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + function render($values) { + if (!empty($this->options['link_to_user'])) { + $account = new stdClass(); + $account->name = $this->get_value($values); + $account->uid = $values->{$this->uid}; + return theme('username', array( + 'account' => $account + )); + } + else { + return $this->sanitize_value($this->get_value($values)); + } + } +} diff --git a/modules/comment/views_handler_field_ncs_last_updated.inc b/modules/comment/views_handler_field_ncs_last_updated.inc new file mode 100644 index 00000000..d1d73060 --- /dev/null +++ b/modules/comment/views_handler_field_ncs_last_updated.inc @@ -0,0 +1,18 @@ +ensure_my_table(); + $this->node_table = $this->query->ensure_table('node', $this->relationship); + $this->field_alias = $this->query->add_field(NULL, "GREATEST(" . $this->node_table . ".changed, " . $this->table_alias . ".last_comment_timestamp)", $this->table_alias . '_' . $this->field); + } +} diff --git a/modules/comment/views_handler_field_node_comment.inc b/modules/comment/views_handler_field_node_comment.inc new file mode 100644 index 00000000..d863c447 --- /dev/null +++ b/modules/comment/views_handler_field_node_comment.inc @@ -0,0 +1,26 @@ +get_value($values); + switch ($value) { + case COMMENT_NODE_HIDDEN: + default: + return t('Hidden'); + case COMMENT_NODE_CLOSED: + return t('Closed'); + case COMMENT_NODE_OPEN: + return t('Open'); + } + } +} diff --git a/modules/comment/views_handler_field_node_new_comments.inc b/modules/comment/views_handler_field_node_new_comments.inc new file mode 100644 index 00000000..70b0581f --- /dev/null +++ b/modules/comment/views_handler_field_node_new_comments.inc @@ -0,0 +1,115 @@ +options['hide_empty'] = TRUE; + unset($this->options['no_empty']); + } + } + + function construct() { + parent::construct(); + $this->additional_fields['nid'] = 'nid'; + $this->additional_fields['type'] = 'type'; + $this->additional_fields['comment_count'] = array('table' => 'node_comment_statistics', 'field' => 'comment_count'); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['link_to_comment'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['link_to_comment'] = array( + '#title' => t('Link this field to new comments'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_comment'], + ); + + parent::options_form($form, $form_state); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + $this->field_alias = $this->table . '_' . $this->field; + } + + function pre_render(&$values) { + global $user; + if (!$user->uid || empty($values)) { + return; + } + + $nids = array(); + $ids = array(); + foreach ($values as $id => $result) { + $nids[] = $result->{$this->aliases['nid']}; + $values[$id]->{$this->field_alias} = 0; + // Create a reference so we can find this record in the values again. + if (empty($ids[$result->{$this->aliases['nid']}])) { + $ids[$result->{$this->aliases['nid']}] = array(); + } + $ids[$result->{$this->aliases['nid']}][] = $id; + } + + if ($nids) { + $result = db_query("SELECT n.nid, COUNT(c.cid) as num_comments FROM {node} n INNER JOIN {comment} c ON n.nid = c.nid + LEFT JOIN {history} h ON h.nid = n.nid AND h.uid = :h_uid WHERE n.nid IN (:nids) + AND c.changed > GREATEST(COALESCE(h.timestamp, :timestamp), :timestamp) AND c.status = :status GROUP BY n.nid ", array( + ':status' => COMMENT_PUBLISHED, + ':h_uid' => $user->uid, + ':nids' => $nids, + ':timestamp' => NODE_NEW_LIMIT, + )); + + foreach ($result as $node) { + foreach ($ids[$node->nid] as $id) { + $values[$id]->{$this->field_alias} = $node->num_comments; + } + } + } + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_comment']) && $data !== NULL && $data !== '') { + $node = new stdClass(); + $node->nid = $this->get_value($values, 'nid'); + $node->type = $this->get_value($values, 'type'); + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'node/' . $node->nid; + $this->options['alter']['query'] = comment_new_page_count($this->get_value($values, 'comment_count'), $this->get_value($values), $node); + $this->options['alter']['fragment'] = 'new'; + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + if (!empty($value)) { + return $this->render_link(parent::render($values), $values); + } + else { + $this->options['alter']['make_link'] = FALSE; + } + } +} diff --git a/modules/comment/views_handler_filter_comment_user_uid.inc b/modules/comment/views_handler_filter_comment_user_uid.inc new file mode 100644 index 00000000..e76ebb79 --- /dev/null +++ b/modules/comment/views_handler_filter_comment_user_uid.inc @@ -0,0 +1,29 @@ +ensure_my_table(); + + $subselect = db_select('comment', 'c'); + $subselect->addField('c', 'cid'); + $subselect->condition('c.uid', $this->value, $this->operator); + $subselect->where("c.nid = $this->table_alias.nid"); + + $condition = db_or() + ->condition("$this->table_alias.uid", $this->value, $this->operator) + ->exists($subselect); + + $this->query->add_where($this->options['group'], $condition); + } +} diff --git a/modules/comment/views_handler_filter_ncs_last_updated.inc b/modules/comment/views_handler_filter_ncs_last_updated.inc new file mode 100644 index 00000000..2319edff --- /dev/null +++ b/modules/comment/views_handler_filter_ncs_last_updated.inc @@ -0,0 +1,25 @@ +ensure_my_table(); + $this->node_table = $this->query->ensure_table('node', $this->relationship); + + $field = "GREATEST(" . $this->node_table . ".changed, " . $this->table_alias . ".last_comment_timestamp)"; + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } +} diff --git a/modules/comment/views_handler_filter_node_comment.inc b/modules/comment/views_handler_filter_node_comment.inc new file mode 100644 index 00000000..befce107 --- /dev/null +++ b/modules/comment/views_handler_filter_node_comment.inc @@ -0,0 +1,21 @@ +value_options = array( + COMMENT_NODE_HIDDEN => t('Hidden'), + COMMENT_NODE_CLOSED => t('Closed'), + COMMENT_NODE_OPEN => t('Open'), + ); + } +} diff --git a/modules/comment/views_handler_sort_comment_thread.inc b/modules/comment/views_handler_sort_comment_thread.inc new file mode 100644 index 00000000..e513a93e --- /dev/null +++ b/modules/comment/views_handler_sort_comment_thread.inc @@ -0,0 +1,28 @@ +ensure_my_table(); + + //Read comment_render() in comment.module for an explanation of the + //thinking behind this sort. + if ($this->options['order'] == 'DESC') { + $this->query->add_orderby($this->table_alias, $this->real_field, $this->options['order']); + } + else { + $alias = $this->table_alias . '_' . $this->real_field . 'asc'; + //@todo is this secure? + $this->query->add_orderby(NULL, "SUBSTRING({$this->table_alias}.{$this->real_field}, 1, (LENGTH({$this->table_alias}.{$this->real_field}) - 1))", $this->options['order'], $alias); + } + } +} diff --git a/modules/comment/views_handler_sort_ncs_last_comment_name.inc b/modules/comment/views_handler_sort_ncs_last_comment_name.inc new file mode 100644 index 00000000..613045a1 --- /dev/null +++ b/modules/comment/views_handler_sort_ncs_last_comment_name.inc @@ -0,0 +1,30 @@ +ensure_my_table(); + $join = new views_join(); + $join->construct('users', $this->table_alias, 'last_comment_uid', 'uid'); + + // @todo this might be safer if we had an ensure_relationship rather than guessing + // the table alias. Though if we did that we'd be guessing the relationship name + // so that doesn't matter that much. +// $this->user_table = $this->query->add_relationship(NULL, $join, 'users', $this->relationship); + $this->user_table = $this->query->ensure_table('ncs_users', $this->relationship, $join); + $this->user_field = $this->query->add_field($this->user_table, 'name'); + + // Add the field. + $this->query->add_orderby(NULL, "LOWER(COALESCE($this->user_table.name, $this->table_alias.$this->field))", $this->options['order'], $this->table_alias . '_' . $this->field); + } +} diff --git a/modules/comment/views_handler_sort_ncs_last_updated.inc b/modules/comment/views_handler_sort_ncs_last_updated.inc new file mode 100644 index 00000000..83f0f547 --- /dev/null +++ b/modules/comment/views_handler_sort_ncs_last_updated.inc @@ -0,0 +1,19 @@ +ensure_my_table(); + $this->node_table = $this->query->ensure_table('node', $this->relationship); + $this->field_alias = $this->query->add_orderby(NULL, "GREATEST(" . $this->node_table . ".changed, " . $this->table_alias . ".last_comment_timestamp)", $this->options['order'], $this->table_alias . '_' . $this->field); + } +} diff --git a/modules/comment/views_plugin_row_comment_rss.inc b/modules/comment/views_plugin_row_comment_rss.inc new file mode 100644 index 00000000..d287b8e7 --- /dev/null +++ b/modules/comment/views_plugin_row_comment_rss.inc @@ -0,0 +1,152 @@ + 'default'); + $options['links'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['item_length'] = array( + '#type' => 'select', + '#title' => t('Display type'), + '#options' => $this->options_form_summary_options(), + '#default_value' => $this->options['item_length'], + ); + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + } + + + function pre_render($result) { + $cids = array(); + $nids = array(); + + foreach ($result as $row) { + $cids[] = $row->cid; + } + + $this->comments = comment_load_multiple($cids); + foreach ($this->comments as &$comment) { + $comment->depth = count(explode('.', $comment->thread)) - 1; + $nids[] = $comment->nid; + } + + $this->nodes = node_load_multiple($nids); + } + + /** + * Return the main options, which are shown in the summary title + * + * @see views_plugin_row_node_rss::options_form_summary_options() + * @todo: Maybe provide a views_plugin_row_rss_entity and reuse this method + * in views_plugin_row_comment|node_rss.inc + */ + function options_form_summary_options() { + $entity_info = entity_get_info('node'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + $options['title'] = t('Title only'); + $options['default'] = t('Use site default RSS settings'); + return $options; + } + + + function render($row) { + global $base_url; + + $cid = $row->{$this->field_alias}; + if (!is_numeric($cid)) { + return; + } + + $item_length = $this->options['item_length']; + if ($item_length == 'default') { + $item_length = variable_get('feed_item_length', 'teaser'); + } + + // Load the specified comment and its associated node: + $comment = $this->comments[$cid]; + if (empty($comment) || empty($this->nodes[$comment->nid])) { + return; + } + + $item_text = ''; + + $uri = entity_uri('comment', $comment); + $comment->link = url($uri['path'], $uri['options'] + array('absolute' => TRUE)); + $comment->rss_namespaces = array(); + $comment->rss_elements = array( + array( + 'key' => 'pubDate', + 'value' => gmdate('r', $comment->created), + ), + array( + 'key' => 'dc:creator', + 'value' => $comment->name, + ), + array( + 'key' => 'guid', + 'value' => 'comment ' . $comment->cid . ' at ' . $base_url, + 'attributes' => array('isPermaLink' => 'false'), + ), + ); + + // The comment gets built and modules add to or modify + // $comment->rss_elements and $comment->rss_namespaces. + $build = comment_view($comment, $this->nodes[$comment->nid], 'rss'); + unset($build['#theme']); + + if (!empty($comment->rss_namespaces)) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $comment->rss_namespaces); + } + + // Hide the links if desired. + if (!$this->options['links']) { + hide($build['links']); + } + + if ($item_length != 'title') { + // We render comment contents and force links to be last. + $build['links']['#weight'] = 1000; + $item_text .= drupal_render($build); + } + + $item = new stdClass(); + $item->description = $item_text; + $item->title = $comment->subject; + $item->link = $comment->link; + $item->elements = $comment->rss_elements; + $item->cid = $comment->cid; + + return theme($this->theme_functions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item + )); + } +} diff --git a/modules/comment/views_plugin_row_comment_view.inc b/modules/comment/views_plugin_row_comment_view.inc new file mode 100644 index 00000000..f78fa366 --- /dev/null +++ b/modules/comment/views_plugin_row_comment_view.inc @@ -0,0 +1,97 @@ + TRUE, 'bool' => TRUE); + $options['view_mode'] = array('default' => 'full'); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $options = $this->options_form_summary_options(); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t('View mode'), + '#default_value' => $this->options['view_mode'], + ); + + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + } + + + /** + * Return the main options, which are shown in the summary title. + */ + function options_form_summary_options() { + $entity_info = entity_get_info('comment'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + if (empty($options)) { + $options = array( + 'full' => t('Full content') + ); + } + + return $options; + } + + function pre_render($result) { + $cids = array(); + + foreach ($result as $row) { + $cids[] = $row->cid; + } + + // Load all comments. + $cresult = comment_load_multiple($cids); + $nids = array(); + foreach ($cresult as $comment) { + $comment->depth = count(explode('.', $comment->thread)) - 1; + $this->comments[$comment->cid] = $comment; + $nids[] = $comment->nid; + } + + // Load all nodes of the comments. + $nodes = node_load_multiple(array_unique($nids)); + foreach ($nodes as $node) { + $this->nodes[$node->nid] = $node; + } + } +} diff --git a/modules/contact.views.inc b/modules/contact.views.inc new file mode 100644 index 00000000..412d824d --- /dev/null +++ b/modules/contact.views.inc @@ -0,0 +1,21 @@ + array( + 'title' => t('Link to contact page'), + 'help' => t('Provide a simple link to the user contact page.'), + 'handler' => 'views_handler_field_contact_link', + ), + ); +} diff --git a/modules/contact/views_handler_field_contact_link.inc b/modules/contact/views_handler_field_contact_link.inc new file mode 100644 index 00000000..9d22f01d --- /dev/null +++ b/modules/contact/views_handler_field_contact_link.inc @@ -0,0 +1,57 @@ +options['text']) ? t('contact') : $this->options['text']; + parent::options_form($form, $form_state); + } + + // An example of field level access control. + // We must override the access method in the parent class, as that requires + // the 'access user profiles' permission, which the contact form does not. + function access() { + return user_access('access user contact forms'); + } + + function render_link($data, $values) { + global $user; + $uid = $this->get_value($values, 'uid'); + + if (empty($uid)) { + return; + } + + $account = user_load($uid); + if (empty($account)) { + return; + } + + // Check access when we pull up the user account so we know + // if the user has made the contact page available. + $menu_item = menu_get_item("user/$uid/contact"); + if (!$menu_item['access'] || empty($account->data['contact'])) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'user/' . $account->uid . '/contact'; + $this->options['alter']['attributes'] = array('title' => t('Contact %user', array('%user' => $account->name))); + + $text = $this->options['text']; + + return $text; + } +} diff --git a/modules/field.views.inc b/modules/field.views.inc new file mode 100644 index 00000000..873153ea --- /dev/null +++ b/modules/field.views.inc @@ -0,0 +1,504 @@ + $entity_type) { + foreach ($entity_type as $bundle) { + if (isset($bundle[$field_name])) { + $label_counter[$bundle[$field_name]['label']] = isset($label_counter[$bundle[$field_name]['label']]) ? ++$label_counter[$bundle[$field_name]['label']] : 1; + $all_labels[$entity_name][$bundle[$field_name]['label']] = TRUE; + } + } + } + if (empty($label_counter)) { + return array($field_name, $all_labels); + } + // Sort the field lables by it most used label and return the most used one. + arsort($label_counter); + $label_counter = array_keys($label_counter); + return array($label_counter[0], $all_labels); +} + +/** + * Default views data implementation for a field. + */ +function field_views_field_default_views_data($field) { + $field_types = field_info_field_types(); + + // Check the field module is available. + if (!isset($field_types[$field['type']])) { + return; + } + + $data = array(); + + $current_table = _field_sql_storage_tablename($field); + $revision_table = _field_sql_storage_revision_tablename($field); + + // The list of entity:bundle that this field is used in. + $bundles_names = array(); + $supports_revisions = FALSE; + $entity_tables = array(); + $current_tables = array(); + $revision_tables = array(); + $groups = array(); + + $group_name = count($field['bundles']) > 1 ? t('Field') : NULL; + + // Build the relationships between the field table and the entity tables. + foreach ($field['bundles'] as $entity => $bundles) { + $entity_info = entity_get_info($entity); + $groups[$entity] = $entity_info['label']; + + // Override Node to Content. + if ($groups[$entity] == t('Node')) { + $groups[$entity] = t('Content'); + } + + // If only one bundle use this as the default name. + if (empty($group_name)) { + $group_name = $groups[$entity]; + } + + $entity_tables[$entity_info['base table']] = $entity; + $current_tables[$entity] = $entity_info['base table']; + if (isset($entity_info['revision table'])) { + $entity_tables[$entity_info['revision table']] = $entity; + $revision_tables[$entity] = $entity_info['revision table']; + } + + $data[$current_table]['table']['join'][$entity_info['base table']] = array( + 'left_field' => $entity_info['entity keys']['id'], + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => $entity), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + + if (!empty($entity_info['entity keys']['revision']) && !empty($entity_info['revision table'])) { + $data[$revision_table]['table']['join'][$entity_info['revision table']] = array( + 'left_field' => $entity_info['entity keys']['revision'], + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => $entity), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + + $supports_revisions = TRUE; + } + + foreach ($bundles as $bundle) { + $bundles_names[] = t('@entity:@bundle', array('@entity' => $entity, '@bundle' => $bundle)); + } + } + + $tables = array(); + $tables[FIELD_LOAD_CURRENT] = $current_table; + if ($supports_revisions) { + $tables[FIELD_LOAD_REVISION] = $revision_table; + } + + $add_fields = array('delta', 'language', 'bundle'); + foreach ($field['columns'] as $column_name => $attributes) { + $add_fields[] = _field_sql_storage_columnname($field['field_name'], $column_name); + } + + // Note: we don't have a label available here, because we are at the field + // level, not at the instance level. So we just go through all instances + // and take the one which is used the most frequently. + $field_name = $field['field_name']; + list($label, $all_labels) = field_views_field_label($field_name); + foreach ($tables as $type => $table) { + if ($type == FIELD_LOAD_CURRENT) { + $group = $group_name; + $old_column = 'entity_id'; + $column = $field['field_name']; + } + else { + $group = t('@group (historical data)', array('@group' => $group_name)); + $old_column = 'revision_id'; + $column = $field['field_name'] . '-' . $old_column; + } + + $data[$table][$old_column]['field']['moved to'] = array($table, $column); + $data[$table][$column] = array( + 'group' => $group, + 'title' => $label, + 'title short' => $label, + 'help' => t('Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + + // Go through and create a list of aliases for all possible combinations of + // entity type + name. + $aliases = array(); + $also_known = array(); + foreach ($all_labels as $entity_name => $labels) { + foreach ($labels as $label_name => $true) { + if ($type == FIELD_LOAD_CURRENT) { + if ($group_name != $groups[$entity_name] || $label != $label_name) { + $aliases[] = array( + 'base' => $current_tables[$entity_name], + 'group' => $groups[$entity_name], + 'title' => $label_name, + 'help' => t('This is an alias of @group: @field.', array('@group' => $group_name, '@field' => $label)), + ); + } + $also_known[] = t('@group: @field', array('@group' => $groups[$entity_name], '@field' => $label_name)); + } + else { + if ($group_name != $groups[$entity_name] && $label != $label_name && isset($revision_tables[$entity_name])) { + $aliases[] = array( + 'base' => $revision_tables[$entity_name], + 'group' => t('@group (historical data)', array('@group' => $groups[$entity_name])), + 'title' => $label_name, + 'help' => t('This is an alias of @group: @field.', array('@group' => $group_name, '@field' => $label)), + ); + } + $also_known[] = t('@group (historical data): @field', array('@group' => $groups[$entity_name], '@field' => $label_name)); + } + } + } + if ($aliases) { + $data[$table][$column]['aliases'] = $aliases; + $data[$table][$column]['help'] .= ' ' . t('Also known as: !also.', array('!also' => implode(', ', $also_known))); + } + + $keys = array_keys($field['columns']); + $real_field = reset($keys); + $data[$table][$column]['field'] = array( + 'table' => $table, + 'handler' => 'views_handler_field_field', + 'click sortable' => TRUE, + 'field_name' => $field['field_name'], + // Provide a real field for group by. + 'real field' => $column . '_' . $real_field, + 'additional fields' => $add_fields, + 'entity_tables' => $entity_tables, + // Default the element type to div, let the UI change it if necessary. + 'element type' => 'div', + 'is revision' => $type == FIELD_LOAD_REVISION, + ); + } + + foreach ($field['columns'] as $column => $attributes) { + $allow_sort = TRUE; + + // 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': + case 'numeric': + case 'float': + $filter = 'views_handler_filter_numeric'; + $argument = 'views_handler_argument_numeric'; + $sort = 'views_handler_sort'; + break; + case 'text': + case 'blob': + // It does not make sense to sort by blob or text. + $allow_sort = FALSE; + default: + $filter = 'views_handler_filter_string'; + $argument = 'views_handler_argument_string'; + $sort = 'views_handler_sort'; + break; + } + + + if (count($field['columns']) == 1 || $column == 'value') { + $title = t('@label (!name)', array('@label' => $label, '!name' => $field['field_name'])); + // CCK used the first 10 characters of $label. Probably doesn't matter. + $title_short = $label; + } + else { + $title = t('@label (!name:!column)', array('@label' => $label, '!name' => $field['field_name'], '!column' => $column)); + $title_short = t('@label:!column', array('@label' => $label, '!column' => $column)); + } + + foreach ($tables as $type => $table) { + if ($type == FIELD_LOAD_CURRENT) { + $group = $group_name; + } + else { + $group = t('@group (historical data)', array('@group' => $group_name)); + } + $column_real_name = $field['storage']['details']['sql'][$type][$table][$column]; + + // Load all the fields from the table by default. + $additional_fields = array_values($field['storage']['details']['sql'][$type][$table]); + + $data[$table][$column_real_name] = array( + 'group' => $group, + 'title' => $title, + 'title short' => $title_short, + 'help' => t('Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + + // Go through and create a list of aliases for all possible combinations of + // entity type + name. + $aliases = array(); + $also_known = array(); + foreach ($all_labels as $entity_name => $labels) { + foreach ($labels as $label_name => $true) { + if ($group_name != $groups[$entity_name] || $label != $label_name) { + if (count($field['columns']) == 1 || $column == 'value') { + $alias_title = t('@label (!name)', array('@label' => $label_name, '!name' => $field['field_name'])); + // CCK used the first 10 characters of $label. Probably doesn't matter. + } + else { + $alias_title = t('@label (!name:!column)', array('@label' => $label_name, '!name' => $field['field_name'], '!column' => $column)); + } + $aliases[] = array( + 'group' => $groups[$entity_name], + 'title' => $alias_title, + 'help' => t('This is an alias of @group: @field.', array('@group' => $group_name, '@field' => $title)), + ); + } + $also_known[] = t('@group: @field', array('@group' => $groups[$entity_name], '@field' => $title)); + } + } + if ($aliases) { + $data[$table][$column_real_name]['aliases'] = $aliases; + $data[$table][$column_real_name]['help'] .= ' ' . t('Also known as: !also.', array('!also' => implode(', ', $also_known))); + } + + $data[$table][$column_real_name]['argument'] = array( + 'field' => $column_real_name, + 'table' => $table, + 'handler' => $argument, + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'empty field name' => t('- No value -'), + ); + $data[$table][$column_real_name]['filter'] = array( + 'field' => $column_real_name, + 'table' => $table, + 'handler' => $filter, + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + if (!empty($allow_sort)) { + $data[$table][$column_real_name]['sort'] = array( + 'field' => $column_real_name, + 'table' => $table, + 'handler' => $sort, + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + ); + } + + // Expose additional delta column for multiple value fields. + if ($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) { + $title_delta = t('@label (!name:delta)', array('@label' => $label, '!name' => $field['field_name'])); + $title_short_delta = t('@label:delta', array('@label' => $label)); + + $data[$table]['delta'] = array( + 'group' => $group, + 'title' => $title_delta, + 'title short' => $title_short_delta, + 'help' => t('Delta - Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + $data[$table]['delta']['field'] = array( + 'handler' => 'views_handler_field_numeric', + ); + $data[$table]['delta']['argument'] = array( + 'field' => 'delta', + 'table' => $table, + 'handler' => 'views_handler_argument_numeric', + 'additional fields' => $additional_fields, + 'empty field name' => t('- No value -'), + 'field_name' => $field['field_name'], + ); + $data[$table]['delta']['filter'] = array( + 'field' => 'delta', + 'table' => $table, + 'handler' => 'views_handler_filter_numeric', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + $data[$table]['delta']['sort'] = array( + 'field' => 'delta', + 'table' => $table, + 'handler' => 'views_handler_sort', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + ); + } + + // Expose additional language column for translatable fields. + if (!empty($field['translatable'])) { + $title_language = t('@label (!name:language)', array('@label' => $label, '!name' => $field['field_name'])); + $title_short_language = t('@label:language', array('@label' => $label)); + + $data[$table]['language'] = array( + 'group' => $group, + 'title' => $title_language, + 'title short' => $title_short_language, + 'help' => t('Language - Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + $data[$table]['language']['field'] = array( + 'handler' => 'views_handler_field_locale_language', + ); + $data[$table]['language']['argument'] = array( + 'field' => 'language', + 'table' => $table, + 'handler' => 'views_handler_argument_locale_language', + 'additional fields' => $additional_fields, + 'empty field name' => t(''), + 'field_name' => $field['field_name'], + ); + $data[$table]['language']['filter'] = array( + 'field' => 'language', + 'table' => $table, + 'handler' => 'views_handler_filter_locale_language', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + $data[$table]['language']['sort'] = array( + 'field' => 'language', + 'table' => $table, + 'handler' => 'views_handler_sort', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + ); + } + + // Expose additional language column for translatable fields. + if (!empty($field['translatable'])) { + $title_language = t('@label (!name:language)', array('@label' => $label, '!name' => $field['field_name'])); + $title_short_language = t('@label:language', array('@label' => $label)); + + $data[$table]['language'] = array( + 'group' => $group, + 'title' => $title_language, + 'title short' => $title_short_language, + 'help' => t('Language - Appears in: @bundles.', array('@bundles' => implode(', ', $bundles_names))), + ); + $data[$table]['language']['field'] = array( + 'handler' => 'views_handler_field_locale_language', + ); + $data[$table]['language']['argument'] = array( + 'field' => 'language', + 'table' => $table, + 'handler' => 'views_handler_argument_locale_language', + 'additional fields' => $additional_fields, + 'empty field name' => t(''), + 'field_name' => $field['field_name'], + ); + $data[$table]['language']['filter'] = array( + 'field' => 'language', + 'table' => $table, + 'handler' => 'views_handler_filter_locale_language', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + 'allow empty' => TRUE, + ); + $data[$table]['language']['sort'] = array( + 'field' => 'language', + 'table' => $table, + 'handler' => 'views_handler_sort', + 'additional fields' => $additional_fields, + 'field_name' => $field['field_name'], + ); + } + } + } + + return $data; +} + +/** + * Have a different filter handler for lists. This should allow to select values of the list. + */ +function list_field_views_data($field) { + $data = field_views_field_default_views_data($field); + foreach ($data as $table_name => $table_data) { + foreach ($table_data as $field_name => $field_data) { + if (isset($field_data['filter']) && $field_name != 'delta') { + $data[$table_name][$field_name]['filter']['handler'] = 'views_handler_filter_field_list'; + } + if (isset($field_data['argument']) && $field_name != 'delta') { + if ($field['type'] == 'list_text') { + $data[$table_name][$field_name]['argument']['handler'] = 'views_handler_argument_field_list_string'; + } + else { + $data[$table_name][$field_name]['argument']['handler'] = 'views_handler_argument_field_list'; + } + } + } + } + return $data; +} diff --git a/modules/field/views_handler_argument_field_list.inc b/modules/field/views_handler_argument_field_list.inc new file mode 100644 index 00000000..e0f7abe8 --- /dev/null +++ b/modules/field/views_handler_argument_field_list.inc @@ -0,0 +1,58 @@ +definition['field_name']); + $this->allowed_values = list_allowed_values($field); + } + + function option_definition() { + $options = parent::option_definition(); + $options['summary']['contains']['human'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['summary']['human'] = array( + '#title' => t('Display list value as human readable'), + '#type' => 'checkbox', + '#default_value' => $this->options['summary']['human'], + '#dependency' => array('radio:options[default_action]' => array('summary')), + ); + } + + + function summary_name($data) { + $value = $data->{$this->name_alias}; + // If the list element has a human readable name show it, + if (isset($this->allowed_values[$value]) && !empty($this->options['summary']['human'])) { + return field_filter_xss($this->allowed_values[$value]); + } + // else fallback to the key. + else { + return check_plain($value); + } + } +} diff --git a/modules/field/views_handler_argument_field_list_string.inc b/modules/field/views_handler_argument_field_list_string.inc new file mode 100644 index 00000000..67a9f2d9 --- /dev/null +++ b/modules/field/views_handler_argument_field_list_string.inc @@ -0,0 +1,59 @@ +definition['field_name']); + $this->allowed_values = list_allowed_values($field); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['summary']['contains']['human'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['summary']['human'] = array( + '#title' => t('Display list value as human readable'), + '#type' => 'checkbox', + '#default_value' => $this->options['summary']['human'], + '#dependency' => array('radio:options[default_action]' => array('summary')), + ); + } + + + function summary_name($data) { + $value = $data->{$this->name_alias}; + // If the list element has a human readable name show it, + if (isset($this->allowed_values[$value]) && !empty($this->options['summary']['human'])) { + return $this->case_transform(field_filter_xss($this->allowed_values[$value]), $this->options['case']); + } + // else fallback to the key. + else { + return $this->case_transform(check_plain($value), $this->options['case']); + } + } +} diff --git a/modules/field/views_handler_field_field.inc b/modules/field/views_handler_field_field.inc new file mode 100644 index 00000000..b50a0aea --- /dev/null +++ b/modules/field/views_handler_field_field.inc @@ -0,0 +1,931 @@ + $formatter) { + foreach ($formatter['field types'] as $formatter_field_type) { + // Check that the field type exists. + if (isset($field_types[$formatter_field_type])) { + $options[$formatter_field_type][$name] = $formatter['label']; + } + } + } + } + + if ($field_type) { + return !empty($options[$field_type]) ? $options[$field_type] : array(); + } + return $options; +} + +/** + * A field that displays fieldapi fields. + * + * @ingroup views_field_handlers + */ +class views_handler_field_field extends views_handler_field { + /** + * An array to store field renderable arrays for use by render_items. + * + * @var array + */ + public $items = array(); + + /** + * Store the field information. + * + * @var array + */ + public $field_info = array(); + + + /** + * Does the field supports multiple field values. + * + * @var bool + */ + public $multiple; + + /** + * Does the rendered fields get limited. + * + * @var bool + */ + public $limit_values; + + /** + * A shortcut for $view->base_table. + * + * @var string + */ + public $base_table; + + /** + * Store the field instance. + * + * @var array + */ + public $instance; + + function init(&$view, &$options) { + parent::init($view, $options); + + $this->field_info = $field = field_info_field($this->definition['field_name']); + $this->multiple = FALSE; + $this->limit_values = FALSE; + + if ($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) { + $this->multiple = TRUE; + + // If "Display all values in the same row" is FALSE, then we always limit + // in order to show a single unique value per row. + if (!$this->options['group_rows']) { + $this->limit_values = TRUE; + } + + // If "First and last only" is chosen, limit the values + if (!empty($this->options['delta_first_last'])) { + $this->limit_values = TRUE; + } + + // Otherwise, we only limit values if the user hasn't selected "all", 0, or + // the value matching field cardinality. + if ((intval($this->options['delta_limit']) && ($this->options['delta_limit'] != $field['cardinality'])) || intval($this->options['delta_offset'])) { + $this->limit_values = TRUE; + } + } + + // Convert old style entity id group column to new format. + // @todo Remove for next major version. + if ($this->options['group_column'] == 'entity id') { + $this->options['group_column'] = 'entity_id'; + } + } + + /** + * Check whether current user has access to this handler. + * + * @return bool + * Return TRUE if the user has access to view this field. + */ + function access() { + $base_table = $this->get_base_table(); + return field_access('view', $this->field_info, $this->definition['entity_tables'][$base_table]); + } + + /** + * Set the base_table and base_table_alias. + * + * @return string + * The base table which is used in the current view "context". + */ + function get_base_table() { + if (!isset($this->base_table)) { + // This base_table is coming from the entity not the field. + $this->base_table = $this->view->base_table; + + // If the current field is under a relationship you can't be sure that the + // base table of the view is the base table of the current field. + // For example a field from a node author on a node view does have users as base table. + if (!empty($this->options['relationship']) && $this->options['relationship'] != 'none') { + $relationships = $this->view->display_handler->get_option('relationships'); + if (!empty($relationships[$this->options['relationship']])) { + $options = $relationships[$this->options['relationship']]; + $data = views_fetch_data($options['table']); + $this->base_table = $data[$options['field']]['relationship']['base']; + } + } + } + + return $this->base_table; + } + + /** + * Called to add the field to a query. + * + * By default, the only columns added to the query are entity_id and + * entity_type. This is because other needed data is fetched by entity_load(). + * Other columns are added only if they are used in groupings, or if + * 'add fields to query' is specifically set to TRUE in the field definition. + * + * The 'add fields to query' switch is used by modules which need all data + * present in the query itself (such as "sphinx"). + */ + function query($use_groupby = FALSE) { + $this->get_base_table(); + + $params = array(); + if ($use_groupby) { + // When grouping on a "field API" field (whose "real_field" is set to + // entity_id), retrieve the minimum entity_id to have a valid entity_id to + // pass to field_view_field(). + $params = array( + 'function' => 'min', + ); + + $this->ensure_my_table(); + } + + // Get the entity type according to the base table of the field. + // Then add it to the query as a formula. That way we can avoid joining + // the field table if all we need is entity_id and entity_type. + $entity_type = $this->definition['entity_tables'][$this->base_table]; + $entity_info = entity_get_info($entity_type); + + if (isset($this->relationship)) { + $this->base_table_alias = $this->relationship; + } + else { + $this->base_table_alias = $this->base_table; + } + + // We always need the base field (entity_id / revision_id). + if (empty($this->definition['is revision'])) { + $this->field_alias = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['id'], '', $params); + } + else { + $this->field_alias = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['revision'], '', $params); + $this->aliases['entity_id'] = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['id'], '', $params); + } + + + // The alias needs to be unique, so we use both the field table and the entity type. + $entity_type_alias = $this->definition['table'] . '_' . $entity_type . '_entity_type'; + $this->aliases['entity_type'] = $this->query->add_field(NULL, "'$entity_type'", $entity_type_alias); + + $fields = $this->additional_fields; + // We've already added entity_type, so we can remove it from the list. + $entity_type_key = array_search('entity_type', $fields); + if ($entity_type_key !== FALSE) { + unset($fields[$entity_type_key]); + } + + if ($use_groupby) { + // Add the fields that we're actually grouping on. + $options = array(); + + if ($this->options['group_column'] != 'entity_id') { + $options = array($this->options['group_column'] => $this->options['group_column']); + } + + $options += is_array($this->options['group_columns']) ? $this->options['group_columns'] : array(); + + + $fields = array(); + $rkey = $this->definition['is revision'] ? 'FIELD_LOAD_REVISION' : 'FIELD_LOAD_CURRENT'; + // Go through the list and determine the actual column name from field api. + foreach ($options as $column) { + $name = $column; + if (isset($this->field_info['storage']['details']['sql'][$rkey][$this->table][$column])) { + $name = $this->field_info['storage']['details']['sql'][$rkey][$this->table][$column]; + } + + $fields[$column] = $name; + } + + $this->group_fields = $fields; + } + + // Add additional fields (and the table join itself) if needed. + if ($this->add_field_table($use_groupby)) { + $this->ensure_my_table(); + $this->add_additional_fields($fields); + + // Filter by language, if field translation is enabled. + $field = $this->field_info; + if (field_is_translatable($entity_type, $field) && !empty($this->view->display_handler->options['field_language_add_to_query'])) { + $column = $this->table_alias . '.language'; + // By the same reason as field_language the field might be LANGUAGE_NONE in reality so allow it as well. + // @see this::field_language() + global $language_content; + $default_language = language_default('language'); + $language = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'), + array($language_content->language, $default_language), + $this->view->display_handler->options['field_language']); + $placeholder = $this->placeholder(); + $language_fallback_candidates = array($language); + if (variable_get('locale_field_language_fallback', TRUE)) { + require_once DRUPAL_ROOT . '/includes/language.inc'; + $language_fallback_candidates = array_merge($language_fallback_candidates, language_fallback_get_candidates()); + } + else { + $language_fallback_candidates[] = LANGUAGE_NONE; + } + $this->query->add_where_expression(0, "$column IN($placeholder) OR $column IS NULL", array($placeholder => $language_fallback_candidates)); + } + } + + // The revision id inhibits grouping. + // So, stop here if we're using grouping, or if aren't adding all columns to + // the query. + if ($use_groupby || empty($this->definition['add fields to query'])) { + return; + } + + $this->add_additional_fields(array('revision_id')); + } + + /** + * Determine if the field table should be added to the query. + */ + function add_field_table($use_groupby) { + // Grouping is enabled, or we are explicitly required to do this. + if ($use_groupby || !empty($this->definition['add fields to query'])) { + return TRUE; + } + // This a multiple value field, but "group multiple values" is not checked. + if ($this->multiple && !$this->options['group_rows']) { + return TRUE; + } + return FALSE; + } + + /** + * Determine if this field is click sortable. + */ + function click_sortable() { + // Not click sortable in any case. + if (empty($this->definition['click sortable'])) { + return FALSE; + } + // A field is not click sortable if it's a multiple field with + // "group multiple values" checked, since a click sort in that case would + // add a join to the field table, which would produce unwanted duplicates. + if ($this->multiple && $this->options['group_rows']) { + return FALSE; + } + return TRUE; + } + + /** + * Called to determine what to tell the clicksorter. + */ + function click_sort($order) { + // No column selected, can't continue. + if (empty($this->options['click_sort_column'])) { + return; + } + + $this->ensure_my_table(); + $column = _field_sql_storage_columnname($this->definition['field_name'], $this->options['click_sort_column']); + if (!isset($this->aliases[$column])) { + // Column is not in query; add a sort on it (without adding the column). + $this->aliases[$column] = $this->table_alias . '.' . $column; + } + $this->query->add_orderby(NULL, NULL, $order, $this->aliases[$column]); + } + + function option_definition() { + $options = parent::option_definition(); + + // option_definition runs before init/construct, so no $this->field_info + $field = field_info_field($this->definition['field_name']); + $field_type = field_info_field_types($field['type']); + $column_names = array_keys($field['columns']); + $default_column = ''; + // Try to determine a sensible default. + if (count($column_names) == 1) { + $default_column = $column_names[0]; + } + elseif (in_array('value', $column_names)) { + $default_column = 'value'; + } + + // If the field has a "value" column, we probably need that one. + $options['click_sort_column'] = array( + 'default' => $default_column, + ); + $options['type'] = array( + 'default' => $field_type['default_formatter'], + ); + $options['settings'] = array( + 'default' => array(), + ); + $options['group_column'] = array( + 'default' => $default_column, + ); + $options['group_columns'] = array( + 'default' => array(), + ); + + // Options used for multiple value fields. + $options['group_rows'] = array( + 'default' => TRUE, + 'bool' => TRUE, + ); + // If we know the exact number of allowed values, then that can be + // the default. Otherwise, default to 'all'. + $options['delta_limit'] = array( + 'default' => ($field['cardinality'] > 1) ? $field['cardinality'] : 'all', + ); + $options['delta_offset'] = array( + 'default' => 0, + ); + $options['delta_reversed'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + $options['delta_first_last'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + + $options['multi_type'] = array( + 'default' => 'separator' + ); + $options['separator'] = array( + 'default' => ', ' + ); + + $options['field_api_classes'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $field = $this->field_info; + $formatters = _field_view_formatter_options($field['type']); + $column_names = array_keys($field['columns']); + + // If this is a multiple value field, add its options. + if ($this->multiple) { + $this->multiple_options_form($form, $form_state); + } + + // No need to ask the user anything if the field has only one column. + if (count($field['columns']) == 1) { + $form['click_sort_column'] = array( + '#type' => 'value', + '#value' => isset($column_names[0]) ? $column_names[0] : '', + ); + } + else { + $form['click_sort_column'] = array( + '#type' => 'select', + '#title' => t('Column used for click sorting'), + '#options' => drupal_map_assoc($column_names), + '#default_value' => $this->options['click_sort_column'], + '#description' => t('Used by Style: Table to determine the actual column to click sort the field on. The default is usually fine.'), + '#fieldset' => 'more', + ); + } + + $form['type'] = array( + '#type' => 'select', + '#title' => t('Formatter'), + '#options' => $formatters, + '#default_value' => $this->options['type'], + '#ajax' => array( + 'path' => views_ui_build_form_url($form_state), + ), + '#submit' => array('views_ui_config_item_form_submit_temporary'), + '#executes_submit_callback' => TRUE, + ); + + $form['field_api_classes'] = array( + '#title' => t('Use field template'), + '#type' => 'checkbox', + '#default_value' => $this->options['field_api_classes'], + '#description' => t('If checked, field api classes will be added using field.tpl.php (or equivalent). This is not recommended unless your CSS depends upon these classes. If not checked, template will not be used.'), + '#fieldset' => 'style_settings', + '#weight' => 20, + ); + + if ($this->multiple) { + $form['field_api_classes']['#description'] .= ' ' . t('Checking this option will cause the group Display Type and Separator values to be ignored.'); + } + + // Get the currently selected formatter. + $format = $this->options['type']; + + $formatter = field_info_formatter_types($format); + $settings = $this->options['settings'] + field_info_formatter_settings($format); + + // Provide an instance array for hook_field_formatter_settings_form(). + ctools_include('fields'); + $this->instance = ctools_fields_fake_field_instance($this->definition['field_name'], '_custom', $formatter, $settings); + + // Store the settings in a '_custom' view mode. + $this->instance['display']['_custom'] = array( + 'type' => $format, + 'settings' => $settings, + ); + + // Get the settings form. + $settings_form = array('#value' => array()); + $function = $formatter['module'] . '_field_formatter_settings_form'; + if (function_exists($function)) { + $settings_form = $function($field, $this->instance, '_custom', $form, $form_state); + } + $form['settings'] = $settings_form; + } + + /** + * Provide options for multiple value fields. + */ + function multiple_options_form(&$form, &$form_state) { + $field = $this->field_info; + + $form['multiple_field_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Multiple field settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 5, + ); + + $form['group_rows'] = array( + '#title' => t('Display all values in the same row'), + '#type' => 'checkbox', + '#default_value' => $this->options['group_rows'], + '#description' => t('If checked, multiple values for this field will be shown in the same row. If not checked, each value in this field will create a new row. If using group by, please make sure to group by "Entity ID" for this setting to have any effect.'), + '#fieldset' => 'multiple_field_settings', + ); + + // Make the string translatable by keeping it as a whole rather than + // translating prefix and suffix separately. + list($prefix, $suffix) = explode('@count', t('Display @count value(s)')); + + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) { + $type = 'textfield'; + $options = NULL; + $size = 5; + } + else { + $type = 'select'; + $options = drupal_map_assoc(range(1, $field['cardinality'])); + $size = 1; + } + $form['multi_type'] = array( + '#type' => 'radios', + '#title' => t('Display type'), + '#options' => array( + 'ul' => t('Unordered list'), + 'ol' => t('Ordered list'), + 'separator' => t('Simple separator'), + ), + '#dependency' => array('edit-options-group-rows' => array(TRUE)), + '#default_value' => $this->options['multi_type'], + '#fieldset' => 'multiple_field_settings', + ); + + $form['separator'] = array( + '#type' => 'textfield', + '#title' => t('Separator'), + '#default_value' => $this->options['separator'], + '#dependency' => array( + 'radio:options[multi_type]' => array('separator'), + 'edit-options-group-rows' => array(TRUE), + ), + '#dependency_count' => 2, + '#fieldset' => 'multiple_field_settings', + ); + + $form['delta_limit'] = array( + '#type' => $type, + '#size' => $size, + '#field_prefix' => $prefix, + '#field_suffix' => $suffix, + '#options' => $options, + '#default_value' => $this->options['delta_limit'], + '#prefix' => '
    ', + '#dependency' => array('edit-options-group-rows' => array(TRUE)), + '#fieldset' => 'multiple_field_settings', + ); + + list($prefix, $suffix) = explode('@count', t('starting from @count')); + $form['delta_offset'] = array( + '#type' => 'textfield', + '#size' => 5, + '#field_prefix' => $prefix, + '#field_suffix' => $suffix, + '#default_value' => $this->options['delta_offset'], + '#dependency' => array('edit-options-group-rows' => array(TRUE)), + '#description' => t('(first item is 0)'), + '#fieldset' => 'multiple_field_settings', + ); + $form['delta_reversed'] = array( + '#title' => t('Reversed'), + '#type' => 'checkbox', + '#default_value' => $this->options['delta_reversed'], + '#suffix' => $suffix, + '#dependency' => array('edit-options-group-rows' => array(TRUE)), + '#description' => t('(start from last values)'), + '#fieldset' => 'multiple_field_settings', + ); + $form['delta_first_last'] = array( + '#title' => t('First and last only'), + '#type' => 'checkbox', + '#default_value' => $this->options['delta_first_last'], + '#suffix' => '
    ', + '#dependency' => array('edit-options-group-rows' => array(TRUE)), + '#fieldset' => 'multiple_field_settings', + ); + } + + /** + * Extend the groupby form with group columns. + */ + function groupby_form(&$form, &$form_state) { + parent::groupby_form($form, $form_state); + // With "field API" fields, the column target of the grouping function + // and any additional grouping columns must be specified. + $group_columns = array( + 'entity_id' => t('Entity ID'), + ) + drupal_map_assoc(array_keys($this->field_info['columns']), 'ucfirst'); + + $form['group_column'] = array( + '#type' => 'select', + '#title' => t('Group column'), + '#default_value' => $this->options['group_column'], + '#description' => t('Select the column of this field to apply the grouping function selected above.'), + '#options' => $group_columns, + ); + + $options = drupal_map_assoc(array('bundle', 'language', 'entity_type'), 'ucfirst'); + + // Add on defined fields, noting that they're prefixed with the field name. + $form['group_columns'] = array( + '#type' => 'checkboxes', + '#title' => t('Group columns (additional)'), + '#default_value' => $this->options['group_columns'], + '#description' => t('Select any additional columns of this field to include in the query and to group on.'), + '#options' => $options + $group_columns, + ); + } + + function groupby_form_submit(&$form, &$form_state) { + parent::groupby_form_submit($form, $form_state); + $item =& $form_state['handler']->options; + + // Add settings for "field API" fields. + $item['group_column'] = $form_state['values']['options']['group_column']; + $item['group_columns'] = array_filter($form_state['values']['options']['group_columns']); + } + + /** + * Load the entities for all fields that are about to be displayed. + */ + function post_execute(&$values) { + if (!empty($values)) { + // Divide the entity ids by entity type, so they can be loaded in bulk. + $entities_by_type = array(); + $revisions_by_type = array(); + foreach ($values as $key => $object) { + if (isset($this->aliases['entity_type']) && isset($object->{$this->aliases['entity_type']}) && isset($object->{$this->field_alias}) && !isset($values[$key]->_field_data[$this->field_alias])) { + $entity_type = $object->{$this->aliases['entity_type']}; + if (empty($this->definition['is revision'])) { + $entity_id = $object->{$this->field_alias}; + $entities_by_type[$entity_type][$key] = $entity_id; + } + else { + $revision_id = $object->{$this->field_alias}; + $entity_id = $object->{$this->aliases['entity_id']}; + $entities_by_type[$entity_type][$key] = array($entity_id, $revision_id); + } + } + } + + // Load the entities. + foreach ($entities_by_type as $entity_type => $entity_ids) { + $entity_info = entity_get_info($entity_type); + if (empty($this->definition['is revision'])) { + $entities = entity_load($entity_type, $entity_ids); + $keys = $entity_ids; + } + else { + // Revisions can't be loaded multiple, so we have to load them + // one by one. + $entities = array(); + $keys = array(); + foreach ($entity_ids as $key => $combined) { + list($entity_id, $revision_id) = $combined; + $entity = entity_load($entity_type, array($entity_id), array($entity_info['entity keys']['revision'] => $revision_id)); + if ($entity) { + $entities[$revision_id] = array_shift($entity); + $keys[$key] = $revision_id; + } + } + } + + foreach ($keys as $key => $entity_id) { + // If this is a revision, load the revision instead. + if (isset($entities[$entity_id])) { + $values[$key]->_field_data[$this->field_alias] = array( + 'entity_type' => $entity_type, + 'entity' => $entities[$entity_id], + ); + } + } + } + + // Now, transfer the data back into the resultset so it can be easily used. + foreach ($values as $row_id => &$value) { + $value->{'field_' . $this->options['id']} = $this->set_items($value, $row_id); + } + } + } + + /** + * Render all items in this field together. + * + * When using advanced render, each possible item in the list is rendered + * individually. Then the items are all pasted together. + */ + function render_items($items) { + if (!empty($items)) { + if (!$this->options['group_rows']) { + return implode('', $items); + } + + if ($this->options['multi_type'] == 'separator') { + return implode(filter_xss_admin($this->options['separator']), $items); + } + else { + return theme('item_list', + array( + 'items' => $items, + 'title' => NULL, + 'type' => $this->options['multi_type'] + )); + } + } + } + + function get_items($values) { + return $values->{'field_' . $this->options['id']}; + } + + function get_value($values, $field = NULL) { + // Go ahead and render and store in $this->items. + $entity = clone $values->_field_data[$this->field_alias]['entity']; + + $entity_type = $values->_field_data[$this->field_alias]['entity_type']; + $langcode = $this->field_language($entity_type, $entity); + // If we are grouping, copy our group fields into the cloned entity. + // It's possible this will cause some weirdness, but there's only + // so much we can hope to do. + if (!empty($this->group_fields)) { + // first, test to see if we have a base value. + $base_value = array(); + // Note: We would copy original values here, but it can cause problems. + // For example, text fields store cached filtered values as + // 'safe_value' which doesn't appear anywhere in the field definition + // so we can't affect it. Other side effects could happen similarly. + $data = FALSE; + foreach ($this->group_fields as $field_name => $column) { + if (property_exists($values, $this->aliases[$column])) { + $base_value[$field_name] = $values->{$this->aliases[$column]}; + if (isset($base_value[$field_name])) { + $data = TRUE; + } + } + } + + // If any of our aggregated fields have data, fake it: + if ($data) { + // Now, overwrite the original value with our aggregated value. + // This overwrites it so there is always just one entry. + $entity->{$this->definition['field_name']}[$langcode] = array($base_value); + } + else { + $entity->{$this->definition['field_name']}[$langcode] = array(); + } + } + + // The field we are trying to display doesn't exist on this entity. + if (!isset($entity->{$this->definition['field_name']})) { + return array(); + } + + // We are supposed to show only certain deltas. + if ($this->limit_values && !empty($entity->{$this->definition['field_name']})) { + $all_values = !empty($entity->{$this->definition['field_name']}[$langcode]) ? $entity->{$this->definition['field_name']}[$langcode] : array(); + if ($this->options['delta_reversed']) { + $all_values = array_reverse($all_values); + } + + // Offset is calculated differently when row grouping for a field is + // not enabled. Since there are multiple rows, the delta needs to be + // taken into account, so that different values are shown per row. + if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($values->{$this->aliases['delta']})) { + $delta_limit = 1; + $offset = $values->{$this->aliases['delta']}; + } + // Single fields don't have a delta available so choose 0. + elseif (!$this->options['group_rows'] && !$this->multiple) { + $delta_limit = 1; + $offset = 0; + } + else { + $delta_limit = $this->options['delta_limit']; + $offset = intval($this->options['delta_offset']); + + // We should only get here in this case if there's an offset, and + // in that case we're limiting to all values after the offset. + if ($delta_limit == 'all') { + $delta_limit = count($all_values) - $offset; + } + } + + // Determine if only the first and last values should be shown + $delta_first_last = $this->options['delta_first_last']; + + $new_values = array(); + for ($i = 0; $i < $delta_limit; $i++) { + $new_delta = $offset + $i; + + if (isset($all_values[$new_delta])) { + // If first-last option was selected, only use the first and last values + if (!$delta_first_last + // Use the first value. + || $new_delta == $offset + // Use the last value. + || $new_delta == ($delta_limit + $offset - 1)) { + $new_values[] = $all_values[$new_delta]; + } + } + } + $entity->{$this->definition['field_name']}[$langcode] = $new_values; + } + + if ($field == 'entity') { + return $entity; + } + else { + return !empty($entity->{$this->definition['field_name']}[$langcode]) ? $entity->{$this->definition['field_name']}[$langcode] : array(); + } + } + + /** + * Return an array of items for the field. + */ + function set_items($values, $row_id) { + if (empty($values->_field_data[$this->field_alias]) || empty($values->_field_data[$this->field_alias]['entity'])) { + return array(); + } + + $display = array( + 'type' => $this->options['type'], + 'settings' => $this->options['settings'], + 'label' => 'hidden', + // Pass the View object in the display so that fields can act on it. + 'views_view' => $this->view, + 'views_field' => $this, + 'views_row_id' => $row_id, + ); + + + $entity_type = $values->_field_data[$this->field_alias]['entity_type']; + $entity = $this->get_value($values, 'entity'); + if (!$entity) { + return array(); + } + + $langcode = $this->field_language($entity_type, $entity); + $render_array = field_view_field($entity_type, $entity, $this->definition['field_name'], $display, $langcode); + + $items = array(); + if ($this->options['field_api_classes']) { + // Make a copy. + $array = $render_array; + return array(array('rendered' => drupal_render($render_array))); + } + + foreach (element_children($render_array) as $count) { + $items[$count]['rendered'] = $render_array[$count]; + // field_view_field() adds an #access property to the render array that + // determines whether or not the current user is allowed to view the + // field in the context of the current entity. We need to respect this + // parameter when we pull out the children of the field array for + // rendering. + if (isset($render_array['#access'])) { + $items[$count]['rendered']['#access'] = $render_array['#access']; + } + // Only add the raw field items (for use in tokens) if the current user + // has access to view the field content. + if ((!isset($items[$count]['rendered']['#access']) || $items[$count]['rendered']['#access']) && !empty($render_array['#items'][$count])) { + $items[$count]['raw'] = $render_array['#items'][$count]; + } + } + return $items; + } + + function render_item($count, $item) { + return render($item['rendered']); + } + + function document_self_tokens(&$tokens) { + $field = $this->field_info; + foreach ($field['columns'] as $id => $column) { + $tokens['[' . $this->options['id'] . '-' . $id . ']'] = t('Raw @column', array('@column' => $id)); + } + } + + function add_self_tokens(&$tokens, $item) { + $field = $this->field_info; + foreach ($field['columns'] as $id => $column) { + // Use filter_xss_admin because it's user data and we can't be sure it is safe. + // We know nothing about the data, though, so we can't really do much else. + + if (isset($item['raw'])) { + // If $item['raw'] is an array then we can use as is, if it's an object + // we cast it to an array, if it's neither, we can't use it. + $raw = is_array($item['raw']) ? $item['raw'] : + (is_object($item['raw']) ? (array)$item['raw'] : NULL); + } + if (isset($raw) && isset($raw[$id]) && is_scalar($raw[$id])) { + $tokens['[' . $this->options['id'] . '-' . $id . ']'] = filter_xss_admin($raw[$id]); + } + else { + // Take sure that empty values are replaced as well. + $tokens['[' . $this->options['id'] . '-' . $id . ']'] = ''; + } + } + } + + /** + * Return the language code of the language the field should be displayed in, + * according to the settings. + */ + function field_language($entity_type, $entity) { + global $language_content; + + if (field_is_translatable($entity_type, $this->field_info)) { + $default_language = language_default('language'); + $language = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'), + array($language_content->language, $default_language), + $this->view->display_handler->options['field_language']); + + // Give the Field Language API a chance to fallback to a different language + // (or LANGUAGE_NONE), in case the field has no data for the selected language. + // field_view_field() does this as well, but since the returned language code + // is used before calling it, the fallback needs to happen explicitly. + $language = field_language($entity_type, $entity, $this->field_info['field_name'], $language); + + return $language; + } + else { + return LANGUAGE_NONE; + } + } +} diff --git a/modules/field/views_handler_filter_field_list.inc b/modules/field/views_handler_filter_field_list.inc new file mode 100644 index 00000000..440d55bb --- /dev/null +++ b/modules/field/views_handler_filter_field_list.inc @@ -0,0 +1,32 @@ +options['operator'] == 'in') { + $this->options['operator'] = 'or'; + } + if ($this->options['operator'] == 'not in') { + $this->options['operator'] = 'not'; + } + $this->operator = $this->options['operator']; + } + + + function get_value_options() { + $field = field_info_field($this->definition['field_name']); + $this->value_options = list_allowed_values($field); + } +} diff --git a/modules/field/views_handler_relationship_entity_reverse.inc b/modules/field/views_handler_relationship_entity_reverse.inc new file mode 100644 index 00000000..89f24835 --- /dev/null +++ b/modules/field/views_handler_relationship_entity_reverse.inc @@ -0,0 +1,84 @@ +field_info = field_info_field($this->definition['field_name']); + } + + /** + * Called to implement a relationship in a query. + */ + function query() { + $this->ensure_my_table(); + // First, relate our base table to the current base table to the + // field, using the base table's id field to the field's column. + $views_data = views_fetch_data($this->table); + $left_field = $views_data['table']['base']['field']; + + $first = array( + 'left_table' => $this->table_alias, + 'left_field' => $left_field, + 'table' => $this->definition['field table'], + 'field' => $this->definition['field field'], + ); + if (!empty($this->options['required'])) { + $first['type'] = 'INNER'; + } + + if (!empty($this->definition['join_extra'])) { + $first['extra'] = $this->definition['join_extra']; + } + + if (!empty($this->definition['join_handler']) && class_exists($this->definition['join_handler'])) { + $first_join = new $this->definition['join_handler']; + } + else { + $first_join = new views_join(); + } + $first_join->definition = $first; + $first_join->construct(); + $first_join->adjusted = TRUE; + + $this->first_alias = $this->query->add_table($this->definition['field table'], $this->relationship, $first_join); + + // Second, relate the field table to the entity specified using + // the entity id on the field table and the entity's id field. + $second = array( + 'left_table' => $this->first_alias, + 'left_field' => 'entity_id', + 'table' => $this->definition['base'], + 'field' => $this->definition['base field'], + ); + + if (!empty($this->options['required'])) { + $second['type'] = 'INNER'; + } + + if (!empty($this->definition['join_handler']) && class_exists($this->definition['join_handler'])) { + $second_join = new $this->definition['join_handler']; + } + else { + $second_join = new views_join(); + } + $second_join->definition = $second; + $second_join->construct(); + $second_join->adjusted = TRUE; + + // use a short alias for this: + $alias = $this->definition['field_name'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $second_join, $this->definition['base'], $this->relationship); + } +} diff --git a/modules/file.views.inc b/modules/file.views.inc new file mode 100644 index 00000000..85569385 --- /dev/null +++ b/modules/file.views.inc @@ -0,0 +1,73 @@ + $table_data) { + // Add the relationship only on the fid field. + $data[$table_name][$field['field_name'] . '_fid']['relationship'] = array( + 'handler' => 'views_handler_relationship', + 'base' => 'file_managed', + 'entity type' => 'file', + 'base field' => 'fid', + 'label' => t('file from !field_name', array('!field_name' => $field['field_name'])), + ); + } + + return $data; +} + +/** + * Implements hook_field_views_data_views_data_alter(). + * + * Views integration to provide reverse relationships on file fields. + */ +function file_field_views_data_views_data_alter(&$data, $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + $entity_info = entity_get_info($entity_type); + $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; + + list($label, $all_labels) = field_views_field_label($field['field_name']); + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + + $data['file_managed'][$pseudo_field_name]['relationship'] = array( + 'title' => t('@entity using @field', array('@entity' => $entity, '@field' => $label)), + 'help' => t('Relate each @entity with a @field set to the file.', array('@entity' => $entity, '@field' => $label)), + 'handler' => 'views_handler_relationship_entity_reverse', + 'field_name' => $field['field_name'], + 'field table' => _field_sql_storage_tablename($field), + 'field field' => $field['field_name'] . '_fid', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('!field_name', array('!field_name' => $field['field_name'])), + 'join_extra' => array( + 0 => array( + 'field' => 'entity_type', + 'value' => $entity_type, + ), + 1 => array( + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ), + ), + ); + } +} diff --git a/modules/filter.views.inc b/modules/filter.views.inc new file mode 100644 index 00000000..0a3b4fce --- /dev/null +++ b/modules/filter.views.inc @@ -0,0 +1,33 @@ + array( + 'left_field' => 'format', + 'field' => 'format', + ), + 'node' => array( + 'left_table' => 'node_revision', + 'left_field' => 'format', + 'field' => 'format', + ), + ); + + return $data; +} diff --git a/modules/filter/views_handler_field_filter_format_name.inc b/modules/filter/views_handler_field_filter_format_name.inc new file mode 100644 index 00000000..0a7bf3b8 --- /dev/null +++ b/modules/filter/views_handler_field_filter_format_name.inc @@ -0,0 +1,36 @@ +additional_fields['name'] = array('table' => 'filter_formats', 'field' => 'name'); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + $format_name = $this->get_value($values, 'name'); + if (!$format_name) { + // Default or invalid input format. + // filter_formats() will reliably return the default format even if the + // current user is unprivileged. + $format = filter_formats(filter_default_format()); + return $this->sanitize_value($format->name); + } + return $this->sanitize_value($format_name); + } +} diff --git a/modules/image.views.inc b/modules/image.views.inc new file mode 100644 index 00000000..6fc6565b --- /dev/null +++ b/modules/image.views.inc @@ -0,0 +1,72 @@ + $table_data) { + // Add the relationship only on the fid field. + $data[$table_name][$field['field_name'] . '_fid']['relationship'] = array( + 'handler' => 'views_handler_relationship', + 'base' => 'file_managed', + 'base field' => 'fid', + 'label' => t('image from !field_name', array('!field_name' => $field['field_name'])), + ); + } + + return $data; +} + +/** + * Implements hook_field_views_data_views_data_alter(). + * + * Views integration to provide reverse relationships on image fields. + */ +function image_field_views_data_views_data_alter(&$data, $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + $entity_info = entity_get_info($entity_type); + $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; + + list($label, $all_labels) = field_views_field_label($field['field_name']); + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + + $data['file_managed'][$pseudo_field_name]['relationship'] = array( + 'title' => t('@entity using @field', array('@entity' => $entity, '@field' => $label)), + 'help' => t('Relate each @entity with a @field set to the image.', array('@entity' => $entity, '@field' => $label)), + 'handler' => 'views_handler_relationship_entity_reverse', + 'field_name' => $field['field_name'], + 'field table' => _field_sql_storage_tablename($field), + 'field field' => $field['field_name'] . '_fid', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('!field_name', array('!field_name' => $field['field_name'])), + 'join_extra' => array( + 0 => array( + 'field' => 'entity_type', + 'value' => $entity_type, + ), + 1 => array( + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ), + ), + ); + } +} diff --git a/modules/locale.views.inc b/modules/locale.views.inc new file mode 100644 index 00000000..3bff7dbd --- /dev/null +++ b/modules/locale.views.inc @@ -0,0 +1,221 @@ + 'lid', + 'title' => t('Locale source'), + 'help' => t('A source string for translation, in English or the default site language.'), + ); + + // lid + $data['locales_source']['lid'] = array( + 'title' => t('LID'), + 'help' => t('The ID of the source string.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + 'numeric' => TRUE, + 'validate type' => 'lid', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // location + $data['locales_source']['location'] = array( + 'group' => t('Locale source'), + 'title' => t('Location'), + 'help' => t('A description of the location or context of the string.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // Group field + $data['locales_source']['textgroup'] = array( + 'group' => t('Locale source'), + 'title' => t('Group'), + 'help' => t('The group the translation is in.'), + 'field' => array( + 'handler' => 'views_handler_field_locale_group', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_locale_group', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_locale_group', + ), + ); + + // Source field + $data['locales_source']['source'] = array( + 'group' => t('Locale source'), + 'title' => t('Source'), + 'help' => t('The full original string.'), + 'field' => array( + 'handler' => 'views_handler_field', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // Version field + $data['locales_source']['version'] = array( + 'group' => t('Locale source'), + 'title' => t('Version'), + 'help' => t('The version of Drupal core that this string is for.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_locale_version', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + $data['locales_source']['edit_lid'] = array( + 'group' => t('Locale source'), + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the translations.'), + 'handler' => 'views_handler_field_locale_link_edit', + ), + ); + + // ---------------------------------------------------------------------- + // Locales target table + + // Define the base group of this table. Fields that don't + // have a group defined will go into this field by default. + $data['locales_target']['table']['group'] = t('Locale target'); + + // Join information + $data['locales_target']['table']['join'] = array( + 'locales_source' => array( + 'left_field' => 'lid', + 'field' => 'lid', + ), + ); + + // Translation field + $data['locales_target']['translation'] = array( + 'group' => t('Locale target'), + 'title' => t('Translation'), + 'help' => t('The full translation string.'), + 'field' => array( + 'handler' => 'views_handler_field', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // Language field + $data['locales_target']['language'] = array( + 'group' => t('Locale target'), + 'title' => t('Language'), + 'help' => t('The language this translation is in.'), + 'field' => array( + 'handler' => 'views_handler_field_locale_language', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_locale_language', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_locale_language', + ), + ); + + $data['locales_target']['plid'] = array( + 'group' => t('Locale target'), + 'title' => t('Singular LID'), + 'help' => t('The ID of the parent translation.'), + 'field' => array( + 'handler' => 'views_handler_field', + ), + ); + + // Plural + $data['locales_target']['plural'] = array( + 'group' => t('Locale target'), + 'title' => t('Plural'), + 'help' => t('Whether or not the translation is plural.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Plural'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + return $data; +} + +/** + * Implements hook_views_data_alter(). + */ +function locale_views_data_alter(&$data) { + // Language field + $data['node']['language'] = array( + 'title' => t('Language'), + 'help' => t('The language the content is in.'), + 'field' => array( + 'handler' => 'views_handler_field_node_language', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_node_language', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_node_language', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); +} diff --git a/modules/locale/views_handler_argument_locale_group.inc b/modules/locale/views_handler_argument_locale_group.inc new file mode 100644 index 00000000..7ced8366 --- /dev/null +++ b/modules/locale/views_handler_argument_locale_group.inc @@ -0,0 +1,40 @@ +locale_group($data->{$this->name_alias}); + } + + /** + * Override the behavior of title(). Get the user friendly version + * of the language. + */ + function title() { + return $this->locale_group($this->argument); + } + + function locale_group($group) { + $groups = module_invoke_all('locale', 'groups'); + // Sort the list. + asort($groups); + return isset($groups[$group]) ? $groups[$group] : t('Unknown group'); + } +} diff --git a/modules/locale/views_handler_argument_locale_language.inc b/modules/locale/views_handler_argument_locale_language.inc new file mode 100644 index 00000000..316d4b15 --- /dev/null +++ b/modules/locale/views_handler_argument_locale_language.inc @@ -0,0 +1,38 @@ +locale_language($data->{$this->name_alias}); + } + + /** + * Override the behavior of title(). Get the user friendly version + * of the language. + */ + function title() { + return $this->locale_language($this->argument); + } + + function locale_language($langcode) { + $languages = views_language_list(); + return isset($languages[$langcode]) ? $languages[$langcode] : t('Unknown language'); + } +} diff --git a/modules/locale/views_handler_field_locale_group.inc b/modules/locale/views_handler_field_locale_group.inc new file mode 100644 index 00000000..393a9487 --- /dev/null +++ b/modules/locale/views_handler_field_locale_group.inc @@ -0,0 +1,21 @@ +get_value($values); + return isset($groups[$value]) ? $groups[$value] : ''; + } +} diff --git a/modules/locale/views_handler_field_locale_language.inc b/modules/locale/views_handler_field_locale_language.inc new file mode 100644 index 00000000..8038e2b3 --- /dev/null +++ b/modules/locale/views_handler_field_locale_language.inc @@ -0,0 +1,36 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['native_language'] = array( + '#title' => t('Native language'), + '#type' => 'checkbox', + '#default_value' => $this->options['native_language'], + '#description' => t('If enabled, the native name of the language will be displayed'), + ); + } + + function render($values) { + $languages = locale_language_list(empty($this->options['native_language']) ? 'name' : 'native'); + $value = $this->get_value($values); + return isset($languages[$value]) ? $languages[$value] : ''; + } +} diff --git a/modules/locale/views_handler_field_locale_link_edit.inc b/modules/locale/views_handler_field_locale_link_edit.inc new file mode 100644 index 00000000..37893551 --- /dev/null +++ b/modules/locale/views_handler_field_locale_link_edit.inc @@ -0,0 +1,60 @@ +additional_fields['lid'] = 'lid'; + } + + function option_definition() { + $options = parent::option_definition(); + + $options['text'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::options_form($form, $form_state); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function access() { + // Ensure user has access to edit translations. + return user_access('translate interface'); + } + + function render($values) { + $value = $this->get_value($values, 'lid'); + return $this->render_link($this->sanitize_value($value), $values); + } + + function render_link($data, $values) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'admin/build/translate/edit/' . $data; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } +} diff --git a/modules/locale/views_handler_field_node_language.inc b/modules/locale/views_handler_field_node_language.inc new file mode 100644 index 00000000..467605b0 --- /dev/null +++ b/modules/locale/views_handler_field_node_language.inc @@ -0,0 +1,37 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['native_language'] = array( + '#title' => t('Native language'), + '#type' => 'checkbox', + '#default_value' => $this->options['native_language'], + '#description' => t('If enabled, the native name of the language will be displayed'), + ); + } + + function render($values) { + $languages = views_language_list(empty($this->options['native_language']) ? 'name' : 'native'); + $value = $this->get_value($values); + $value = isset($languages[$value]) ? $languages[$value] : ''; + return $this->render_link($value, $values); + } +} diff --git a/modules/locale/views_handler_filter_locale_group.inc b/modules/locale/views_handler_filter_locale_group.inc new file mode 100644 index 00000000..5ec1e920 --- /dev/null +++ b/modules/locale/views_handler_filter_locale_group.inc @@ -0,0 +1,23 @@ +value_options)) { + $this->value_title = t('Group'); + $groups = module_invoke_all('locale', 'groups'); + // Sort the list. + asort($groups); + $this->value_options = $groups; + } + } +} diff --git a/modules/locale/views_handler_filter_locale_language.inc b/modules/locale/views_handler_filter_locale_language.inc new file mode 100644 index 00000000..eee12a6d --- /dev/null +++ b/modules/locale/views_handler_filter_locale_language.inc @@ -0,0 +1,26 @@ +value_options)) { + $this->value_title = t('Language'); + $languages = array( + '***CURRENT_LANGUAGE***' => t("Current user's language"), + '***DEFAULT_LANGUAGE***' => t("Default site language"), + LANGUAGE_NONE => t('No language') + ); + $languages = array_merge($languages, views_language_list()); + $this->value_options = $languages; + } + } +} diff --git a/modules/locale/views_handler_filter_locale_version.inc b/modules/locale/views_handler_filter_locale_version.inc new file mode 100644 index 00000000..71708608 --- /dev/null +++ b/modules/locale/views_handler_filter_locale_version.inc @@ -0,0 +1,28 @@ +value_options)) { + $this->value_title = t('Version'); + // Enable filtering by the current installed Drupal version. + $versions = array('***CURRENT_VERSION***' => t('Current installed version')); + $result = db_query('SELECT DISTINCT(version) FROM {locales_source} ORDER BY version'); + foreach ($result as $row) { + if (!empty($row->version)) { + $versions[$row->version] = $row->version; + } + } + $this->value_options = $versions; + } + } +} diff --git a/modules/locale/views_handler_filter_node_language.inc b/modules/locale/views_handler_filter_node_language.inc new file mode 100644 index 00000000..7592577b --- /dev/null +++ b/modules/locale/views_handler_filter_node_language.inc @@ -0,0 +1,26 @@ +value_options)) { + $this->value_title = t('Language'); + $languages = array( + '***CURRENT_LANGUAGE***' => t("Current user's language"), + '***DEFAULT_LANGUAGE***' => t("Default site language"), + LANGUAGE_NONE => t('No language') + ); + $languages = array_merge($languages, views_language_list()); + $this->value_options = $languages; + } + } +} diff --git a/modules/node.views.inc b/modules/node.views.inc new file mode 100644 index 00000000..71a10237 --- /dev/null +++ b/modules/node.views.inc @@ -0,0 +1,784 @@ + 'nid', + 'title' => t('Content'), + 'weight' => -10, + 'access query tag' => 'node_access', + 'defaults' => array( + 'field' => 'title', + ), + ); + $data['node']['table']['entity type'] = 'node'; + + $data['node']['table']['default_relationship'] = array( + 'node_revision' => array( + 'table' => 'node_revision', + 'field' => 'vid', + ), + ); + + // ---------------------------------------------------------------- + // node table -- fields + + // nid + $data['node']['nid'] = array( + 'title' => t('Nid'), + 'help' => t('The node ID.'), // The help that appears on the UI, + // Information for displaying the nid + 'field' => array( + 'handler' => 'views_handler_field_node', + 'click sortable' => TRUE, + ), + // Information for accepting a nid as an argument + 'argument' => array( + 'handler' => 'views_handler_argument_node_nid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + 'validate type' => 'nid', + ), + // Information for accepting a nid as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + // Information for sorting on a nid. + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // title + // This definition has more items in it than it needs to as an example. + $data['node']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The content title.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'field' => 'title', // the real field. This could be left out since it is the same. + 'group' => t('Content'), // The group it appears in on the UI. Could be left out. + 'handler' => 'views_handler_field_node', + 'click sortable' => TRUE, + 'link_to_node default' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + // Information for accepting a title as a filter + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // created field + $data['node']['created'] = array( + 'title' => t('Post date'), // The item it appears as on the UI, + 'help' => t('The date the content was posted.'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + // changed field + $data['node']['changed'] = array( + 'title' => t('Updated date'), // The item it appears as on the UI, + 'help' => t('The date the content was last updated.'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + // Content type + $data['node']['type'] = array( + 'title' => t('Type'), // The item it appears as on the UI, + 'help' => t('The content type (for example, "blog entry", "forum post", "story", etc).'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_node_type', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_node_type', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_node_type', + ), + ); + + // published status + $data['node']['status'] = array( + 'title' => t('Published'), + 'help' => t('Whether or not the content is published.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'published-notpublished' => array(t('Published'), t('Not published')), + ), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Published'), + 'type' => 'yes-no', + 'use equal' => TRUE, // Use status = 1 instead of status <> 0 in WHERE statment + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // published status + extra + $data['node']['status_extra'] = array( + 'title' => t('Published or admin'), + 'help' => t('Filters out unpublished content if the current user cannot view it.'), + 'filter' => array( + 'field' => 'status', + 'handler' => 'views_handler_filter_node_status', + 'label' => t('Published or admin'), + ), + ); + + // promote status + $data['node']['promote'] = array( + 'title' => t('Promoted to front page'), + 'help' => t('Whether or not the content is promoted to the front page.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'promoted-notpromoted' => array(t('Promoted'), t('Not promoted')), + ), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Promoted to front page'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // sticky + $data['node']['sticky'] = array( + 'title' => t('Sticky'), // The item it appears as on the UI, + 'help' => t('Whether or not the content is sticky.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'sticky' => array(t('Sticky'), t('Not sticky')), + ), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Sticky'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + 'help' => t('Whether or not the content is sticky. To list sticky content first, set this to descending.'), + ), + ); + + // Define some fields based upon views_handler_field_entity in the entity + // table so they can be re-used with other query backends. + // @see views_handler_field_entity + + $data['views_entity_node']['table']['group'] = t('Content'); + + $data['node']['view_node']['moved to'] = array('views_entity_node', 'view_node'); + $data['views_entity_node']['view_node'] = array( + 'field' => array( + 'title' => t('Link'), + 'help' => t('Provide a simple link to the content.'), + 'handler' => 'views_handler_field_node_link', + ), + ); + + $data['node']['edit_node']['moved to'] = array('views_entity_node', 'edit_node'); + $data['views_entity_node']['edit_node'] = array( + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the content.'), + 'handler' => 'views_handler_field_node_link_edit', + ), + ); + + $data['node']['delete_node']['moved to'] = array('views_entity_node', 'delete_node'); + $data['views_entity_node']['delete_node'] = array( + 'field' => array( + 'title' => t('Delete link'), + 'help' => t('Provide a simple link to delete the content.'), + 'handler' => 'views_handler_field_node_link_delete', + ), + ); + + $data['node']['path'] = array( + 'field' => array( + 'title' => t('Path'), + 'help' => t('The aliased path to this content.'), + 'handler' => 'views_handler_field_node_path', + ), + ); + + + // Bogus fields for aliasing purposes. + + $data['node']['created_fulldate'] = array( + 'title' => t('Created date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_fulldate', + ), + ); + + $data['node']['created_year_month'] = array( + 'title' => t('Created year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_year_month', + ), + ); + + $data['node']['created_year'] = array( + 'title' => t('Created year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_year', + ), + ); + + $data['node']['created_month'] = array( + 'title' => t('Created month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_month', + ), + ); + + $data['node']['created_day'] = array( + 'title' => t('Created day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_day', + ), + ); + + $data['node']['created_week'] = array( + 'title' => t('Created week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_week', + ), + ); + + $data['node']['changed_fulldate'] = array( + 'title' => t('Updated date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_fulldate', + ), + ); + + $data['node']['changed_year_month'] = array( + 'title' => t('Updated year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_year_month', + ), + ); + + $data['node']['changed_year'] = array( + 'title' => t('Updated year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_year', + ), + ); + + $data['node']['changed_month'] = array( + 'title' => t('Updated month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_month', + ), + ); + + $data['node']['changed_day'] = array( + 'title' => t('Updated day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_day', + ), + ); + + $data['node']['changed_week'] = array( + 'title' => t('Updated week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'changed', + 'handler' => 'views_handler_argument_node_created_week', + ), + ); + + // uid field + $data['node']['uid'] = array( + 'title' => t('Author uid'), + 'help' => t('The user authoring the content. If you need more fields than the uid add the content: author relationship'), + 'relationship' => array( + 'title' => t('Author'), + 'help' => t('Relate content to the user who created it.'), + 'handler' => 'views_handler_relationship', + 'base' => 'users', + 'field' => 'uid', + 'label' => t('author'), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_user_name', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + 'field' => array( + 'handler' => 'views_handler_field_user', + ), + ); + + $data['node']['uid_revision'] = array( + 'title' => t('User has a revision'), + 'help' => t('All nodes where a certain user has a revision'), + 'real field' => 'nid', + 'filter' => array( + 'handler' => 'views_handler_filter_node_uid_revision', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_node_uid_revision', + ), + ); + + // ---------------------------------------------------------------------- + // Content revision table + + // Define the base group of this table. Fields that don't + // have a group defined will go into this field by default. + $data['node_revisions']['moved to'] = 'node_revision'; + $data['node_revision']['table']['entity type'] = 'node'; + $data['node_revision']['table']['group'] = t('Content revision'); + // Support the conversion of the field body + $data['node_revisions']['body']['moved to'] = array('field_revision_data', 'body-revision_id'); + + // Advertise this table as a possible base table + $data['node_revision']['table']['base'] = array( + 'field' => 'vid', + 'title' => t('Content revision'), + 'help' => t('Content revision is a history of changes to content.'), + 'defaults' => array( + 'field' => 'title', + ), + ); + + // For other base tables, explain how we join + $data['node_revision']['table']['join'] = array( + // Directly links to node table. + 'node' => array( + 'left_field' => 'vid', + 'field' => 'vid', + ), + ); + + $data['node_revision']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'nid', + ), + ); + + // uid field for node revision + $data['node_revision']['uid'] = array( + 'title' => t('User'), + 'help' => t('Relate a content revision to the user who created the revision.'), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'users', + 'base field' => 'uid', + 'label' => t('revision user'), + ), + ); + + // nid + $data['node_revision']['nid'] = array( + 'title' => t('Nid'), + // The help that appears on the UI. + 'help' => t('The revision NID of the content revision.'), + // Information for displaying the nid. + 'field' => array( + 'click sortable' => TRUE, + ), + // Information for accepting a nid as an argument. + 'argument' => array( + 'handler' => 'views_handler_argument_node_nid', + 'click sortable' => TRUE, + 'numeric' => TRUE, + ), + // Information for accepting a nid as a filter. + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + // Information for sorting on a nid. + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'node', + 'base field' => 'nid', + 'title' => t('Content'), + 'label' => t('Get the actual content from a content revision.'), + ), + ); + + // vid + $data['node_revision']['vid'] = array( + 'title' => t('Vid'), + // The help that appears on the UI. + 'help' => t('The revision ID of the content revision.'), + // Information for displaying the vid. + 'field' => array( + 'click sortable' => TRUE, + ), + // Information for accepting a vid as an argument. + 'argument' => array( + 'handler' => 'views_handler_argument_node_vid', + 'click sortable' => TRUE, + 'numeric' => TRUE, + ), + // Information for accepting a vid as a filter. + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + // Information for sorting on a vid. + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'node', + 'base field' => 'vid', + 'title' => t('Content'), + 'label' => t('Get the actual content from a content revision.'), + ), + ); + + // title + $data['node_revision']['title'] = array( + 'title' => t('Title'), // The item it appears as on the UI, + 'help' => t('The content title.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'field' => 'title', // the real field + 'handler' => 'views_handler_field_node_revision', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // log field + $data['node_revision']['log'] = array( + 'title' => t('Log message'), // The item it appears as on the UI, + 'help' => t('The log message entered when the revision was created.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_xss', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // revision timestamp + // changed field + $data['node_revision']['timestamp'] = array( + 'title' => t('Updated date'), // The item it appears as on the UI, + 'help' => t('The date the node was last updated.'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + $data['node_revision']['link_to_revision'] = array( + 'field' => array( + 'title' => t('Link'), + 'help' => t('Provide a simple link to the revision.'), + 'handler' => 'views_handler_field_node_revision_link', + ), + ); + + $data['node_revision']['revert_revision'] = array( + 'field' => array( + 'title' => t('Revert link'), + 'help' => t('Provide a simple link to revert to the revision.'), + 'handler' => 'views_handler_field_node_revision_link_revert', + ), + ); + + $data['node_revision']['delete_revision'] = array( + 'field' => array( + 'title' => t('Delete link'), + 'help' => t('Provide a simple link to delete the content revision.'), + 'handler' => 'views_handler_field_node_revision_link_delete', + ), + ); + + // ---------------------------------------------------------------------- + // Node access table + + // Define the base group of this table. Fields that don't + // have a group defined will go into this field by default. + $data['node_access']['table']['group'] = t('Content access'); + + // For other base tables, explain how we join + $data['node_access']['table']['join'] = array( + // Directly links to node table. + 'node' => array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + // nid field + $data['node_access']['nid'] = array( + 'title' => t('Access'), + 'help' => t('Filter by access.'), + 'filter' => array( + 'handler' => 'views_handler_filter_node_access', + 'help' => t('Filter for content by view access. Not necessary if you are using node as your base table.'), + ), + ); + + // ---------------------------------------------------------------------- + // History table + + // We're actually defining a specific instance of the table, so let's + // alias it so that we can later add the real table for other purposes if we + // need it. + $data['history_user']['moved to'] = 'history'; + $data['history']['table']['group'] = t('Content'); + + // Explain how this table joins to others. + $data['history']['table']['join'] = array( + // Directly links to node table. + 'node' => array( + 'table' => 'history', + 'left_field' => 'nid', + 'field' => 'nid', + 'extra' => array( + array('field' => 'uid', 'value' => '***CURRENT_USER***', 'numeric' => TRUE), + ), + ), + ); + + $data['history']['timestamp'] = array( + 'title' => t('Has new content'), + 'field' => array( + 'handler' => 'views_handler_field_history_user_timestamp', + 'help' => t('Show a marker if the content is new or updated.'), + ), + 'filter' => array( + 'help' => t('Show only content that is new or updated.'), + 'handler' => 'views_handler_filter_history_user_timestamp', + ), + ); + return $data; +} + +/** + * Implements hook_views_plugins(). + */ +function node_views_plugins() { + return array( + 'module' => 'views', // This just tells our themes are elsewhere. + 'row' => array( + 'node' => array( + 'title' => t('Content'), + 'help' => t('Display the content with standard node view.'), + 'handler' => 'views_plugin_row_node_view', + 'path' => drupal_get_path('module', 'views') . '/modules/node', // not necessary for most modules + 'base' => array('node'), // only works with 'node' as base. + 'uses options' => TRUE, + 'type' => 'normal', + 'help topic' => 'style-node', + ), + 'node_rss' => array( + 'title' => t('Content'), + 'help' => t('Display the content with standard node view.'), + 'handler' => 'views_plugin_row_node_rss', + 'path' => drupal_get_path('module', 'views') . '/modules/node', // not necessary for most modules + 'theme' => 'views_view_row_rss', + 'base' => array('node'), // only works with 'node' as base. + 'uses options' => TRUE, + 'type' => 'feed', + 'help topic' => 'style-node-rss', + ), + ), + 'argument validator' => array( + 'node' => array( + 'title' => t('Content'), + 'handler' => 'views_plugin_argument_validate_node', + ), + ), + 'argument default' => array( + 'node' => array( + 'title' => t('Content ID from URL'), + 'handler' => 'views_plugin_argument_default_node' + ), + ), + ); +} + +/** + * Implements hook_preprocess_node(). + */ +function node_row_node_view_preprocess_node(&$vars) { + $node = $vars['node']; + $options = $vars['view']->style_plugin->row_plugin->options; + + // Prevent the comment form from showing up if this is not a page display. + if ($vars['view_mode'] == 'full' && !$vars['view']->display_handler->has_path()) { + $node->comment = FALSE; + } + + if (!$options['links']) { + unset($vars['content']['links']); + } + + if (!empty($options['comments']) && user_access('access comments') && $node->comment) { + $vars['content']['comments'] = comment_node_page_additions($node); + } +} + +/** + * Implements hook_views_query_substitutions(). + */ +function node_views_query_substitutions() { + return array( + '***ADMINISTER_NODES***' => intval(user_access('administer nodes')), + '***VIEW_OWN_UNPUBLISHED_NODES***' => intval(user_access('view own unpublished content')), + '***BYPASS_NODE_ACCESS***' => intval(user_access('bypass node access')), + ); +} + +/** + * Implements hook_views_analyze(). + */ +function node_views_analyze($view) { + $ret = array(); + // Check for something other than the default display: + if ($view->base_table == 'node') { + foreach ($view->display as $id => $display) { + if (empty($display->handler)) { + continue; + } + if (!$display->handler->is_defaulted('access') || !$display->handler->is_defaulted('filters')) { + // check for no access control + $access = $display->handler->get_option('access'); + if (empty($access['type']) || $access['type'] == 'none') { + $select = db_select('role', 'r'); + $select->innerJoin('role_permission', 'p', 'r.rid = p.rid'); + $result = $select->fields('r', array('name')) + ->fields('p', array('permission')) + ->condition('r.name', array('anonymous user', 'authenticated user'), 'IN') + ->condition('p.permission', 'access content') + ->execute(); + + foreach ($result as $role) { + $role->safe = TRUE; + $roles[$role->name] = $role; + } + if (!($roles['anonymous user']->safe && $roles['authenticated user']->safe)) { + $ret[] = views_ui_analysis(t('Some roles lack permission to access content, but display %display has no access control.', array('%display' => $display->display_title)), 'warning'); + } + $filters = $display->handler->get_option('filters'); + foreach ($filters as $filter) { + if ($filter['table'] == 'node' && ($filter['field'] == 'status' || $filter['field'] == 'status_extra')) { + continue 2; + } + } + $ret[] = views_ui_analysis(t('Display %display has no access control but does not contain a filter for published nodes.', array('%display' => $display->display_title)), 'warning'); + } + } + } + } + foreach ($view->display as $id => $display) { + if ($display->display_plugin == 'page') { + if ($display->handler->get_option('path') == 'node/%') { + $ret[] = views_ui_analysis(t('Display %display has set node/% as path. This will not produce what you want. If you want to have multiple versions of the node view, use panels.', array('%display' => $display->display_title)), 'warning'); + } + } + } + + return $ret; +} diff --git a/modules/node.views_default.inc b/modules/node.views_default.inc new file mode 100644 index 00000000..93886a7d --- /dev/null +++ b/modules/node.views_default.inc @@ -0,0 +1,315 @@ +name = 'archive'; + $view->description = 'Display a list of months that link to content for that month.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Archive'; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Monthly archive'; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + /* Sort criterion: Content: Post date */ + $handler->display->display_options['sorts']['created']['id'] = 'created'; + $handler->display->display_options['sorts']['created']['table'] = 'node'; + $handler->display->display_options['sorts']['created']['field'] = 'created'; + $handler->display->display_options['sorts']['created']['order'] = 'DESC'; + /* Contextual filter: Content: Created year + month */ + $handler->display->display_options['arguments']['created_year_month']['id'] = 'created_year_month'; + $handler->display->display_options['arguments']['created_year_month']['table'] = 'node'; + $handler->display->display_options['arguments']['created_year_month']['field'] = 'created_year_month'; + $handler->display->display_options['arguments']['created_year_month']['default_action'] = 'summary'; + $handler->display->display_options['arguments']['created_year_month']['exception']['title_enable'] = 1; + $handler->display->display_options['arguments']['created_year_month']['title_enable'] = 1; + $handler->display->display_options['arguments']['created_year_month']['title'] = '%1'; + $handler->display->display_options['arguments']['created_year_month']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['created_year_month']['summary']['sort_order'] = 'desc'; + $handler->display->display_options['arguments']['created_year_month']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['created_year_month']['summary_options']['override'] = TRUE; + $handler->display->display_options['arguments']['created_year_month']['summary_options']['items_per_page'] = '30'; + $handler->display->display_options['arguments']['created_year_month']['specify_validation'] = 1; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 1; + $handler->display->display_options['filters']['status']['group'] = 0; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'archive'; + + /* Display: Block */ + $handler = $view->new_display('block', 'Block', 'block'); + $handler->display->display_options['defaults']['arguments'] = FALSE; + /* Contextual filter: Content: Created year + month */ + $handler->display->display_options['arguments']['created_year_month']['id'] = 'created_year_month'; + $handler->display->display_options['arguments']['created_year_month']['table'] = 'node'; + $handler->display->display_options['arguments']['created_year_month']['field'] = 'created_year_month'; + $handler->display->display_options['arguments']['created_year_month']['default_action'] = 'summary'; + $handler->display->display_options['arguments']['created_year_month']['exception']['title_enable'] = 1; + $handler->display->display_options['arguments']['created_year_month']['title_enable'] = 1; + $handler->display->display_options['arguments']['created_year_month']['title'] = '%1'; + $handler->display->display_options['arguments']['created_year_month']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['created_year_month']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['created_year_month']['summary_options']['items_per_page'] = '30'; + $handler->display->display_options['arguments']['created_year_month']['specify_validation'] = 1; + $translatables['archive'] = array( + t('Master'), + t('Monthly archive'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('All'), + t('%1'), + t('Page'), + t('Block'), + ); + + $views['archive'] = $view; + + $view = new view; + $view->name = 'frontpage'; + $view->description = 'Emulates the default Drupal front page; you may set the default home page path to this view to make it your front page.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Front page'; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + $handler->display->display_options['row_options']['links'] = 1; + /* Sort criterion: Content: Sticky */ + $handler->display->display_options['sorts']['sticky']['id'] = 'sticky'; + $handler->display->display_options['sorts']['sticky']['table'] = 'node'; + $handler->display->display_options['sorts']['sticky']['field'] = 'sticky'; + $handler->display->display_options['sorts']['sticky']['order'] = 'DESC'; + /* Sort criterion: Content: Post date */ + $handler->display->display_options['sorts']['created']['id'] = 'created'; + $handler->display->display_options['sorts']['created']['table'] = 'node'; + $handler->display->display_options['sorts']['created']['field'] = 'created'; + $handler->display->display_options['sorts']['created']['order'] = 'DESC'; + /* Filter criterion: Content: Promoted to front page */ + $handler->display->display_options['filters']['promote']['id'] = 'promote'; + $handler->display->display_options['filters']['promote']['table'] = 'node'; + $handler->display->display_options['filters']['promote']['field'] = 'promote'; + $handler->display->display_options['filters']['promote']['value'] = '1'; + $handler->display->display_options['filters']['promote']['group'] = 0; + $handler->display->display_options['filters']['promote']['expose']['operator'] = FALSE; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = '1'; + $handler->display->display_options['filters']['status']['group'] = 0; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'frontpage'; + + /* Display: Feed */ + $handler = $view->new_display('feed', 'Feed', 'feed'); + $handler->display->display_options['defaults']['title'] = FALSE; + $handler->display->display_options['title'] = 'Front page feed'; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['style_plugin'] = 'rss'; + $handler->display->display_options['row_plugin'] = 'node_rss'; + $handler->display->display_options['path'] = 'rss.xml'; + $handler->display->display_options['displays'] = array( + 'default' => 'default', + 'page' => 'page', + ); + $handler->display->display_options['sitename_title'] = '1'; + $translatables['frontpage'] = array( + t('Master'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('Page'), + t('Feed'), + t('Front page feed'), + ); + + $views['frontpage'] = $view; + + $view = new view; + $view->name = 'glossary'; + $view->description = 'A list of all content, by letter.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Glossary'; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['use_ajax'] = TRUE; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = 36; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'title' => 'title', + 'name' => 'name', + 'changed' => 'changed', + ); + $handler->display->display_options['style_options']['default'] = 'title'; + $handler->display->display_options['style_options']['info'] = array( + 'title' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'name' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'changed' => array( + 'sortable' => 1, + 'separator' => '', + ), + ); + $handler->display->display_options['style_options']['override'] = 1; + $handler->display->display_options['style_options']['sticky'] = 0; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['link_to_node'] = 1; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['label'] = 'Author'; + $handler->display->display_options['fields']['name']['link_to_user'] = 1; + /* Field: Content: Updated date */ + $handler->display->display_options['fields']['changed']['id'] = 'changed'; + $handler->display->display_options['fields']['changed']['table'] = 'node'; + $handler->display->display_options['fields']['changed']['field'] = 'changed'; + $handler->display->display_options['fields']['changed']['label'] = 'Last update'; + $handler->display->display_options['fields']['changed']['date_format'] = 'large'; + /* Contextual filter: Content: Title */ + $handler->display->display_options['arguments']['title']['id'] = 'title'; + $handler->display->display_options['arguments']['title']['table'] = 'node'; + $handler->display->display_options['arguments']['title']['field'] = 'title'; + $handler->display->display_options['arguments']['title']['default_action'] = 'default'; + $handler->display->display_options['arguments']['title']['exception']['title_enable'] = 1; + $handler->display->display_options['arguments']['title']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['title']['default_argument_options']['argument'] = 'a'; + $handler->display->display_options['arguments']['title']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['title']['specify_validation'] = 1; + $handler->display->display_options['arguments']['title']['glossary'] = 1; + $handler->display->display_options['arguments']['title']['limit'] = '1'; + $handler->display->display_options['arguments']['title']['case'] = 'upper'; + $handler->display->display_options['arguments']['title']['path_case'] = 'lower'; + $handler->display->display_options['arguments']['title']['transform_dash'] = 0; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'glossary'; + $handler->display->display_options['menu']['type'] = 'normal'; + $handler->display->display_options['menu']['title'] = 'Glossary'; + $handler->display->display_options['menu']['weight'] = '0'; + + /* Display: Attachment */ + $handler = $view->new_display('attachment', 'Attachment', 'attachment'); + $handler->display->display_options['pager']['type'] = 'none'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['defaults']['arguments'] = FALSE; + /* Contextual filter: Content: Title */ + $handler->display->display_options['arguments']['title']['id'] = 'title'; + $handler->display->display_options['arguments']['title']['table'] = 'node'; + $handler->display->display_options['arguments']['title']['field'] = 'title'; + $handler->display->display_options['arguments']['title']['default_action'] = 'summary'; + $handler->display->display_options['arguments']['title']['exception']['title_enable'] = 1; + $handler->display->display_options['arguments']['title']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['title']['default_argument_options']['argument'] = 'a'; + $handler->display->display_options['arguments']['title']['summary']['format'] = 'unformatted_summary'; + $handler->display->display_options['arguments']['title']['summary_options']['items_per_page'] = '25'; + $handler->display->display_options['arguments']['title']['summary_options']['inline'] = 1; + $handler->display->display_options['arguments']['title']['summary_options']['separator'] = ' | '; + $handler->display->display_options['arguments']['title']['specify_validation'] = 1; + $handler->display->display_options['arguments']['title']['glossary'] = 1; + $handler->display->display_options['arguments']['title']['limit'] = '1'; + $handler->display->display_options['arguments']['title']['case'] = 'upper'; + $handler->display->display_options['arguments']['title']['path_case'] = 'lower'; + $handler->display->display_options['arguments']['title']['transform_dash'] = 0; + $handler->display->display_options['displays'] = array( + 'default' => 'default', + 'page' => 'page', + ); + $handler->display->display_options['inherit_arguments'] = 0; + $translatables['glossary'] = array( + t('Master'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('Title'), + t('Author'), + t('Last update'), + t('All'), + t('Page'), + t('Attachment'), + ); + + $views['glossary'] = $view; + + return $views; +} diff --git a/modules/node.views_template.inc b/modules/node.views_template.inc new file mode 100644 index 00000000..b2b184d9 --- /dev/null +++ b/modules/node.views_template.inc @@ -0,0 +1,134 @@ +name = 'image_gallery'; + $view->description = 'Shows all images which was uploaded on the "field_image" field'; + $view->tag = ''; + $view->base_table = 'node'; + $view->human_name = 'Image Gallery'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Defaults */ + $handler = $view->new_display('default', 'Defaults', 'default'); + $handler->display->display_options['title'] = 'Image gallery'; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '24'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['pager']['options']['expose']['items_per_page_options_all'] = 0; + $handler->display->display_options['style_plugin'] = 'grid'; + $handler->display->display_options['style_options']['fill_single_line'] = 1; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Image */ + $handler->display->display_options['fields']['field_image']['id'] = 'field_image'; + $handler->display->display_options['fields']['field_image']['table'] = 'field_data_field_image'; + $handler->display->display_options['fields']['field_image']['field'] = 'field_image'; + $handler->display->display_options['fields']['field_image']['label'] = ''; + $handler->display->display_options['fields']['field_image']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['field_image']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['field_image']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['field_image']['alter']['external'] = 0; + $handler->display->display_options['fields']['field_image']['alter']['trim'] = 0; + $handler->display->display_options['fields']['field_image']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['field_image']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['field_image']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['field_image']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['field_image']['alter']['html'] = 0; + $handler->display->display_options['fields']['field_image']['element_label_colon'] = 1; + $handler->display->display_options['fields']['field_image']['element_default_classes'] = 1; + $handler->display->display_options['fields']['field_image']['hide_empty'] = 0; + $handler->display->display_options['fields']['field_image']['empty_zero'] = 0; + $handler->display->display_options['fields']['field_image']['click_sort_column'] = 'fid'; + $handler->display->display_options['fields']['field_image']['settings'] = array( + 'image_style' => 'thumbnail', + 'image_link' => 'content', + ); + $handler->display->display_options['fields']['field_image']['field_api_classes'] = 0; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['label'] = 'Author'; + $handler->display->display_options['fields']['name']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['name']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['name']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['name']['alter']['external'] = 0; + $handler->display->display_options['fields']['name']['alter']['trim'] = 0; + $handler->display->display_options['fields']['name']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['name']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['name']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['name']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['name']['alter']['html'] = 0; + $handler->display->display_options['fields']['name']['element_label_colon'] = 1; + $handler->display->display_options['fields']['name']['element_default_classes'] = 1; + $handler->display->display_options['fields']['name']['hide_empty'] = 0; + $handler->display->display_options['fields']['name']['empty_zero'] = 0; + $handler->display->display_options['fields']['name']['link_to_user'] = 1; + $handler->display->display_options['fields']['name']['overwrite_anonymous'] = 0; + /* Contextual filter: Content: Has taxonomy term ID */ + $handler->display->display_options['arguments']['tid']['id'] = 'tid'; + $handler->display->display_options['arguments']['tid']['table'] = 'taxonomy_index'; + $handler->display->display_options['arguments']['tid']['field'] = 'tid'; + $handler->display->display_options['arguments']['tid']['default_action'] = 'summary'; + $handler->display->display_options['arguments']['tid']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['tid']['default_argument_skip_url'] = 0; + $handler->display->display_options['arguments']['tid']['summary']['number_of_records'] = '1'; + $handler->display->display_options['arguments']['tid']['summary']['format'] = 'unformatted_summary'; + $handler->display->display_options['arguments']['tid']['summary_options']['items_per_page'] = '25'; + $handler->display->display_options['arguments']['tid']['summary_options']['inline'] = 0; + $handler->display->display_options['arguments']['tid']['break_phrase'] = 0; + $handler->display->display_options['arguments']['tid']['add_table'] = 0; + $handler->display->display_options['arguments']['tid']['require_value'] = 0; + $handler->display->display_options['arguments']['tid']['reduce_duplicates'] = 0; + $handler->display->display_options['arguments']['tid']['set_breadcrumb'] = 0; + /* Filter criterion: Content: Image (field_image) - fid */ + $handler->display->display_options['filters']['field_image_fid']['id'] = 'field_image_fid'; + $handler->display->display_options['filters']['field_image_fid']['table'] = 'field_data_field_image'; + $handler->display->display_options['filters']['field_image_fid']['field'] = 'field_image_fid'; + $handler->display->display_options['filters']['field_image_fid']['operator'] = 'not empty'; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = '1'; + + /* Display: Gallery page */ + $handler = $view->new_display('page', 'Gallery page', 'page_1'); + $handler->display->display_options['path'] = 'gallery'; + $translatables['image_gallery'] = array( + t('Defaults'), + t('Image gallery'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('Author'), + t('All'), + t('Gallery page'), + ); + + $views[$view->name] = $view; + + return $views; +} diff --git a/modules/node/views_handler_argument_dates_various.inc b/modules/node/views_handler_argument_dates_various.inc new file mode 100644 index 00000000..5f4e4b2b --- /dev/null +++ b/modules/node/views_handler_argument_dates_various.inc @@ -0,0 +1,177 @@ +format = 'F j, Y'; + $this->arg_format = 'Ymd'; + $this->formula = views_date_sql_format($this->arg_format, "***table***.$this->real_field"); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $created = $data->{$this->name_alias}; + return format_date(strtotime($created . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + return format_date(strtotime($this->argument . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); + } +} + +/** + * Argument handler for a year (CCYY) + */ +class views_handler_argument_node_created_year extends views_handler_argument_date { + /** + * Constructor implementation + */ + function construct() { + parent::construct(); + $this->arg_format = 'Y'; + $this->formula = views_date_sql_extract('YEAR', "***table***.$this->real_field"); + } +} + +/** + * Argument handler for a year plus month (CCYYMM) + */ +class views_handler_argument_node_created_year_month extends views_handler_argument_date { + /** + * Constructor implementation + */ + function construct() { + parent::construct(); + $this->format = 'F Y'; + $this->arg_format = 'Ym'; + $this->formula = views_date_sql_format($this->arg_format, "***table***.$this->real_field"); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $created = $data->{$this->name_alias}; + return format_date(strtotime($created . "15" . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + return format_date(strtotime($this->argument . "15" . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); + } +} + +/** + * Argument handler for a month (MM) + */ +class views_handler_argument_node_created_month extends views_handler_argument_date { + /** + * Constructor implementation + */ + function construct() { + parent::construct(); + $this->formula = views_date_sql_extract('MONTH', "***table***.$this->real_field"); + $this->format = 'F'; + $this->arg_format = 'm'; + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $month = str_pad($data->{$this->name_alias}, 2, '0', STR_PAD_LEFT); + return format_date(strtotime("2005" . $month . "15" . " 00:00:00 UTC" ), 'custom', $this->format, 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + $month = str_pad($this->argument, 2, '0', STR_PAD_LEFT); + return format_date(strtotime("2005" . $month . "15" . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); + } + + function summary_argument($data) { + // Make sure the argument contains leading zeroes. + return str_pad($data->{$this->base_alias}, 2, '0', STR_PAD_LEFT); + } +} + +/** + * Argument handler for a day (DD) + */ +class views_handler_argument_node_created_day extends views_handler_argument_date { + /** + * Constructor implementation + */ + function construct() { + parent::construct(); + $this->formula = views_date_sql_extract('DAY', "***table***.$this->real_field"); + $this->format = 'j'; + $this->arg_format = 'd'; + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $day = str_pad($data->{$this->name_alias}, 2, '0', STR_PAD_LEFT); + // strtotime respects server timezone, so we need to set the time fixed as utc time + return format_date(strtotime("2005" . "05" . $day . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); + } + + /** + * Provide a link to the next level of the view + */ + function title() { + $day = str_pad($this->argument, 2, '0', STR_PAD_LEFT); + return format_date(strtotime("2005" . "05" . $day . " 00:00:00 UTC"), 'custom', $this->format, 'UTC'); + } + + function summary_argument($data) { + // Make sure the argument contains leading zeroes. + return str_pad($data->{$this->base_alias}, 2, '0', STR_PAD_LEFT); + } +} + +/** + * Argument handler for a week. + */ +class views_handler_argument_node_created_week extends views_handler_argument_date { + /** + * Constructor implementation + */ + function construct() { + parent::construct(); + $this->arg_format = 'w'; + $this->formula = views_date_sql_extract('WEEK', "***table***.$this->real_field"); + } + + /** + * Provide a link to the next level of the view + */ + function summary_name($data) { + $created = $data->{$this->name_alias}; + return t('Week @week', array('@week' => $created)); + } +} diff --git a/modules/node/views_handler_argument_node_language.inc b/modules/node/views_handler_argument_node_language.inc new file mode 100644 index 00000000..170388a3 --- /dev/null +++ b/modules/node/views_handler_argument_node_language.inc @@ -0,0 +1,36 @@ +node_language($data->{$this->name_alias}); + } + + /** + * Override the behavior of title(). Get the user friendly version of the + * node type. + */ + function title() { + return $this->node_language($this->argument); + } + + function node_language($langcode) { + $languages = views_language_list(); + return isset($languages[$langcode]) ? $languages[$langcode] : t('Unknown language'); + } +} diff --git a/modules/node/views_handler_argument_node_nid.inc b/modules/node/views_handler_argument_node_nid.inc new file mode 100644 index 00000000..b0dbee09 --- /dev/null +++ b/modules/node/views_handler_argument_node_nid.inc @@ -0,0 +1,24 @@ + $this->value)); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } +} diff --git a/modules/node/views_handler_argument_node_type.inc b/modules/node/views_handler_argument_node_type.inc new file mode 100644 index 00000000..ea99d7c6 --- /dev/null +++ b/modules/node/views_handler_argument_node_type.inc @@ -0,0 +1,39 @@ +node_type($data->{$this->name_alias}); + } + + /** + * Override the behavior of title(). Get the user friendly version of the + * node type. + */ + function title() { + return $this->node_type($this->argument); + } + + function node_type($type) { + $output = node_type_get_name($type); + if (empty($output)) { + $output = t('Unknown content type'); + } + return check_plain($output); + } +} diff --git a/modules/node/views_handler_argument_node_uid_revision.inc b/modules/node/views_handler_argument_node_uid_revision.inc new file mode 100644 index 00000000..142882a3 --- /dev/null +++ b/modules/node/views_handler_argument_node_uid_revision.inc @@ -0,0 +1,18 @@ +ensure_my_table(); + $placeholder = $this->placeholder(); + $this->query->add_where_expression(0, "$this->table_alias.uid = $placeholder OR ((SELECT COUNT(*) FROM {node_revision} nr WHERE nr.uid = $placeholder AND nr.nid = $this->table_alias.nid) > 0)", array($placeholder => $this->argument)); + } +} diff --git a/modules/node/views_handler_argument_node_vid.inc b/modules/node/views_handler_argument_node_vid.inc new file mode 100644 index 00000000..1f970ad1 --- /dev/null +++ b/modules/node/views_handler_argument_node_vid.inc @@ -0,0 +1,26 @@ + $this->value)); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } +} diff --git a/modules/node/views_handler_field_history_user_timestamp.inc b/modules/node/views_handler_field_history_user_timestamp.inc new file mode 100644 index 00000000..dfe4931e --- /dev/null +++ b/modules/node/views_handler_field_history_user_timestamp.inc @@ -0,0 +1,82 @@ +uid) { + $this->additional_fields['created'] = array('table' => 'node', 'field' => 'created'); + $this->additional_fields['changed'] = array('table' => 'node', 'field' => 'changed'); + if (module_exists('comment') && !empty($this->options['comments'])) { + $this->additional_fields['last_comment'] = array('table' => 'node_comment_statistics', 'field' => 'last_comment_timestamp'); + } + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['comments'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + if (module_exists('comment')) { + $form['comments'] = array( + '#type' => 'checkbox', + '#title' => t('Check for new comments as well'), + '#default_value' => !empty($this->options['comments']), + '#fieldset' => 'more', + ); + } + } + + function query() { + // Only add ourselves to the query if logged in. + global $user; + if (!$user->uid) { + return; + } + parent::query(); + } + + function render($values) { + // Let's default to 'read' state. + // This code shadows node_mark, but it reads from the db directly and + // we already have that info. + $mark = MARK_READ; + global $user; + if ($user->uid) { + $last_read = $this->get_value($values); + $changed = $this->get_value($values, 'changed'); + + $last_comment = module_exists('comment') && !empty($this->options['comments']) ? $this->get_value($values, 'last_comment') : 0; + + if (!$last_read && $changed > NODE_NEW_LIMIT) { + $mark = MARK_NEW; + } + elseif ($changed > $last_read && $changed > NODE_NEW_LIMIT) { + $mark = MARK_UPDATED; + } + elseif ($last_comment > $last_read && $last_comment > NODE_NEW_LIMIT) { + $mark = MARK_UPDATED; + } + return $this->render_link(theme('mark', array('type' => $mark)), $values); + } + } +} diff --git a/modules/node/views_handler_field_node.inc b/modules/node/views_handler_field_node.inc new file mode 100644 index 00000000..f712a538 --- /dev/null +++ b/modules/node/views_handler_field_node.inc @@ -0,0 +1,80 @@ +options['link_to_node'])) { + $this->additional_fields['nid'] = array('table' => 'node', 'field' => 'nid'); + if (module_exists('translation')) { + $this->additional_fields['language'] = array('table' => 'node', 'field' => 'language'); + } + } + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_to_node'] = array('default' => isset($this->definition['link_to_node default']) ? $this->definition['link_to_node default'] : FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to node option + */ + function options_form(&$form, &$form_state) { + $form['link_to_node'] = array( + '#title' => t('Link this field to the original piece of content'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_node']), + ); + + parent::options_form($form, $form_state); + } + + /** + * Render whatever the data is as a link to the node. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + if (!empty($this->options['link_to_node']) && !empty($this->additional_fields['nid'])) { + if ($data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/" . $this->get_value($values, 'nid'); + if (isset($this->aliases['language'])) { + $languages = language_list(); + $language = $this->get_value($values, 'language'); + if (isset($languages[$language])) { + $this->options['alter']['language'] = $languages[$language]; + } + else { + unset($this->options['alter']['language']); + } + } + } + else { + $this->options['alter']['make_link'] = FALSE; + } + } + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/node/views_handler_field_node_link.inc b/modules/node/views_handler_field_node_link.inc new file mode 100644 index 00000000..7e9bbd2a --- /dev/null +++ b/modules/node/views_handler_field_node_link.inc @@ -0,0 +1,48 @@ + '', 'translatable' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::options_form($form, $form_state); + + // The path is set by render_link function so don't allow to set it. + $form['alter']['path'] = array('#access' => FALSE); + $form['alter']['external'] = array('#access' => FALSE); + } + + function render($values) { + if ($entity = $this->get_value($values)) { + return $this->render_link($entity, $values); + } + } + + function render_link($node, $values) { + if (node_access('view', $node)) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid"; + $text = !empty($this->options['text']) ? $this->options['text'] : t('view'); + return $text; + } + } +} diff --git a/modules/node/views_handler_field_node_link_delete.inc b/modules/node/views_handler_field_node_link_delete.inc new file mode 100644 index 00000000..8271c0ba --- /dev/null +++ b/modules/node/views_handler_field_node_link_delete.inc @@ -0,0 +1,31 @@ +options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid/delete"; + $this->options['alter']['query'] = drupal_get_destination(); + + $text = !empty($this->options['text']) ? $this->options['text'] : t('delete'); + return $text; + } +} diff --git a/modules/node/views_handler_field_node_link_edit.inc b/modules/node/views_handler_field_node_link_edit.inc new file mode 100644 index 00000000..4e8aad00 --- /dev/null +++ b/modules/node/views_handler_field_node_link_edit.inc @@ -0,0 +1,31 @@ +options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid/edit"; + $this->options['alter']['query'] = drupal_get_destination(); + + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + return $text; + } +} diff --git a/modules/node/views_handler_field_node_path.inc b/modules/node/views_handler_field_node_path.inc new file mode 100644 index 00000000..f47f85fa --- /dev/null +++ b/modules/node/views_handler_field_node_path.inc @@ -0,0 +1,47 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + function construct() { + parent::construct(); + $this->additional_fields['nid'] = 'nid'; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['absolute'] = array( + '#type' => 'checkbox', + '#title' => t('Use absolute link (begins with "http://")'), + '#default_value' => $this->options['absolute'], + '#description' => t('Enable this option to output an absolute link. Required if you want to use the path as a link destination (as in "output this field as a link" above).'), + '#fieldset' => 'alter', + ); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + $nid = $this->get_value($values, 'nid'); + return url("node/$nid", array('absolute' => $this->options['absolute'])); + } +} diff --git a/modules/node/views_handler_field_node_revision.inc b/modules/node/views_handler_field_node_revision.inc new file mode 100644 index 00000000..d29b0708 --- /dev/null +++ b/modules/node/views_handler_field_node_revision.inc @@ -0,0 +1,74 @@ +options['link_to_node_revision'])) { + $this->additional_fields['vid'] = 'vid'; + $this->additional_fields['nid'] = 'nid'; + if (module_exists('translation')) { + $this->additional_fields['language'] = array('table' => 'node', 'field' => 'language'); + } + } + } + function option_definition() { + $options = parent::option_definition(); + $options['link_to_node_revision'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to revision option. + */ + function options_form(&$form, &$form_state) { + $form['link_to_node_revision'] = array( + '#title' => t('Link this field to its content revision'), + '#description' => t('This will override any other link you have set.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_node_revision']), + ); + parent::options_form($form, $form_state); + } + + /** + * Render whatever the data is as a link to the node. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + if (!empty($this->options['link_to_node_revision']) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $nid = $this->get_value($values, 'nid'); + $vid = $this->get_value($values, 'vid'); + $this->options['alter']['path'] = 'node/' . $nid; + if ($nid != $vid) { + $this->options['alter']['path'] .= "/revisions/$vid/view"; + } + if (module_exists('translation')) { + $language = $this->get_value($values, 'language'); + $languages = language_list(); + if (isset($languages[$language])) { + $this->options['alter']['language'] = $languages[$language]; + } + } + } + else { + return parent::render_link($data, $values); + } + return $data; + } +} diff --git a/modules/node/views_handler_field_node_revision_link.inc b/modules/node/views_handler_field_node_revision_link.inc new file mode 100644 index 00000000..69047bbb --- /dev/null +++ b/modules/node/views_handler_field_node_revision_link.inc @@ -0,0 +1,66 @@ +additional_fields['node_vid'] = array('table' => 'node_revision', 'field' => 'vid'); + } + + function access() { + return user_access('view revisions') || user_access('administer nodes'); + } + + function render_link($data, $values) { + list($node, $vid) = $this->get_revision_entity($values, 'view'); + if (!isset($vid)) { + return; + } + + // Current revision uses the node view path. + $path = 'node/' . $node->nid; + if ($node->vid != $vid) { + $path .= "/revisions/$vid/view"; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = $path; + $this->options['alter']['query'] = drupal_get_destination(); + + return !empty($this->options['text']) ? $this->options['text'] : t('view'); + } + + /** + * Returns the revision values of a node. + * + * @param object $values + * An object containing all retrieved values. + * @param string $op + * The operation being performed. + * + * @return array + * A numerically indexed array containing the current node object and the + * revision ID for this row. + */ + function get_revision_entity($values, $op) { + $vid = $this->get_value($values, 'node_vid'); + $node = $this->get_value($values); + // Unpublished nodes ignore access control. + $node->status = 1; + // Ensure user has access to perform the operation on this node. + if (!node_access($op, $node)) { + return array($node, NULL); + } + return array($node, $vid); + } +} diff --git a/modules/node/views_handler_field_node_revision_link_delete.inc b/modules/node/views_handler_field_node_revision_link_delete.inc new file mode 100644 index 00000000..e0d00a78 --- /dev/null +++ b/modules/node/views_handler_field_node_revision_link_delete.inc @@ -0,0 +1,36 @@ +get_revision_entity($values, 'delete'); + if (!isset($vid)) { + return; + } + + // Current revision cannot be deleted. + if ($node->vid == $vid) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'node/' . $node->nid . "/revisions/$vid/delete"; + $this->options['alter']['query'] = drupal_get_destination(); + + return !empty($this->options['text']) ? $this->options['text'] : t('delete'); + } +} diff --git a/modules/node/views_handler_field_node_revision_link_revert.inc b/modules/node/views_handler_field_node_revision_link_revert.inc new file mode 100644 index 00000000..af204427 --- /dev/null +++ b/modules/node/views_handler_field_node_revision_link_revert.inc @@ -0,0 +1,36 @@ +get_revision_entity($values, 'update'); + if (!isset($vid)) { + return; + } + + // Current revision cannot be reverted. + if ($node->vid == $vid) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'node/' . $node->nid . "/revisions/$vid/revert"; + $this->options['alter']['query'] = drupal_get_destination(); + + return !empty($this->options['text']) ? $this->options['text'] : t('revert'); + } +} diff --git a/modules/node/views_handler_field_node_type.inc b/modules/node/views_handler_field_node_type.inc new file mode 100644 index 00000000..ba8ee3eb --- /dev/null +++ b/modules/node/views_handler_field_node_type.inc @@ -0,0 +1,49 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide machine_name option for to node type display. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['machine_name'] = array( + '#title' => t('Output machine name'), + '#description' => t('Display field as the content type machine name.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['machine_name']), + ); + } + + /** + * Render node type as human readable name, unless using machine_name option. + */ + function render_name($data, $values) { + if ($this->options['machine_name'] != 1 && $data !== NULL && $data !== '') { + return t($this->sanitize_value(node_type_get_name($data))); + } + return $this->sanitize_value($data); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->render_name($value, $values), $values); + } +} diff --git a/modules/node/views_handler_filter_history_user_timestamp.inc b/modules/node/views_handler_filter_history_user_timestamp.inc new file mode 100644 index 00000000..acdb8313 --- /dev/null +++ b/modules/node/views_handler_filter_history_user_timestamp.inc @@ -0,0 +1,87 @@ +options['expose']['label'])) { + $label = $this->options['expose']['label']; + } + else { + $label = t('Has new content'); + } + $form['value'] = array( + '#type' => 'checkbox', + '#title' => $label, + '#default_value' => $this->value, + ); + } + } + + function query() { + global $user; + // This can only work if we're logged in. + if (!$user || !$user->uid) { + return; + } + + // Don't filter if we're exposed and the checkbox isn't selected. + if ((!empty($this->options['exposed'])) && empty($this->value)) { + return; + } + + // Hey, Drupal kills old history, so nodes that haven't been updated + // since NODE_NEW_LIMIT are bzzzzzzzt outta here! + + $limit = REQUEST_TIME - NODE_NEW_LIMIT; + + $this->ensure_my_table(); + $field = "$this->table_alias.$this->real_field"; + $node = $this->query->ensure_table('node', $this->relationship); + + $clause = ''; + $clause2 = ''; + if (module_exists('comment')) { + $ncs = $this->query->ensure_table('node_comment_statistics', $this->relationship); + $clause = ("OR $ncs.last_comment_timestamp > (***CURRENT_TIME*** - $limit)"); + $clause2 = "OR $field < $ncs.last_comment_timestamp"; + } + + // NULL means a history record doesn't exist. That's clearly new content. + // Unless it's very very old content. Everything in the query is already + // type safe cause none of it is coming from outside here. + $this->query->add_where_expression($this->options['group'], "($field IS NULL AND ($node.changed > (***CURRENT_TIME*** - $limit) $clause)) OR $field < $node.changed $clause2"); + } + + function admin_summary() { + if (!empty($this->options['exposed'])) { + return t('exposed'); + } + } +} diff --git a/modules/node/views_handler_filter_node_access.inc b/modules/node/views_handler_filter_node_access.inc new file mode 100644 index 00000000..a29b13f4 --- /dev/null +++ b/modules/node/views_handler_filter_node_access.inc @@ -0,0 +1,40 @@ +ensure_my_table(); + $grants = db_or(); + foreach (node_access_grants('view') as $realm => $gids) { + foreach ($gids as $gid) { + $grants->condition(db_and() + ->condition($table . '.gid', $gid) + ->condition($table . '.realm', $realm) + ); + } + } + + $this->query->add_where('AND', $grants); + $this->query->add_where('AND', $table . '.grant_view', 1, '>='); + } + } +} diff --git a/modules/node/views_handler_filter_node_status.inc b/modules/node/views_handler_filter_node_status.inc new file mode 100644 index 00000000..2afb2869 --- /dev/null +++ b/modules/node/views_handler_filter_node_status.inc @@ -0,0 +1,22 @@ +ensure_my_table(); + $this->query->add_where_expression($this->options['group'], "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_NODES*** = 1) OR ***BYPASS_NODE_ACCESS*** = 1"); + } +} diff --git a/modules/node/views_handler_filter_node_type.inc b/modules/node/views_handler_filter_node_type.inc new file mode 100644 index 00000000..7f8ab4b7 --- /dev/null +++ b/modules/node/views_handler_filter_node_type.inc @@ -0,0 +1,26 @@ +value_options)) { + $this->value_title = t('Content types'); + $types = node_type_get_types(); + $options = array(); + foreach ($types as $type => $info) { + $options[$type] = t($info->name); + } + asort($options); + $this->value_options = $options; + } + } +} diff --git a/modules/node/views_handler_filter_node_uid_revision.inc b/modules/node/views_handler_filter_node_uid_revision.inc new file mode 100644 index 00000000..4d3d9a70 --- /dev/null +++ b/modules/node/views_handler_filter_node_uid_revision.inc @@ -0,0 +1,25 @@ +ensure_my_table(); + + $placeholder = $this->placeholder(); + + $args = array_values($this->value); + + $this->query->add_where_expression($this->options['group'], "$this->table_alias.uid IN($placeholder) " . $condition . " OR + ((SELECT COUNT(*) FROM {node_revision} nr WHERE nr.uid IN($placeholder) AND nr.nid = $this->table_alias.nid) > 0)", array($placeholder => $args), + $args); + } +} diff --git a/modules/node/views_plugin_argument_default_node.inc b/modules/node/views_plugin_argument_default_node.inc new file mode 100644 index 00000000..65fc0eb8 --- /dev/null +++ b/modules/node/views_plugin_argument_default_node.inc @@ -0,0 +1,26 @@ +nid; + } + } + + if (arg(0) == 'node' && is_numeric(arg(1))) { + return arg(1); + } + } +} diff --git a/modules/node/views_plugin_argument_validate_node.inc b/modules/node/views_plugin_argument_validate_node.inc new file mode 100644 index 00000000..018965da --- /dev/null +++ b/modules/node/views_plugin_argument_validate_node.inc @@ -0,0 +1,135 @@ + array()); + $options['access'] = array('default' => FALSE, 'bool' => TRUE); + $options['access_op'] = array('default' => 'view'); + $options['nid_type'] = array('default' => 'nid'); + + return $options; + } + + function options_form(&$form, &$form_state) { + $types = node_type_get_types(); + $options = array(); + foreach ($types as $type => $info) { + $options[$type] = check_plain(t($info->name)); + } + + $form['types'] = array( + '#type' => 'checkboxes', + '#title' => t('Content types'), + '#options' => $options, + '#default_value' => $this->options['types'], + '#description' => t('Choose one or more content types to validate with.'), + ); + + $form['access'] = array( + '#type' => 'checkbox', + '#title' => t('Validate user has access to the content'), + '#default_value' => $this->options['access'], + ); + $form['access_op'] = array( + '#type' => 'radios', + '#title' => t('Access operation to check'), + '#options' => array('view' => t('View'), 'update' => t('Edit'), 'delete' => t('Delete')), + '#default_value' => $this->options['access_op'], + '#dependency' => array('edit-options-validate-options-node-access' => array(TRUE)), + ); + + $form['nid_type'] = array( + '#type' => 'select', + '#title' => t('Filter value format'), + '#options' => array( + 'nid' => t('Node ID'), + 'nids' => t('Node IDs separated by , or +'), + ), + '#default_value' => $this->options['nid_type'], + ); + } + + function options_submit(&$form, &$form_state, &$options = array()) { + // filter trash out of the options so we don't store giant unnecessary arrays + $options['types'] = array_filter($options['types']); + } + + function convert_options(&$options) { + if (!isset($options['types']) && !empty($this->argument->options['validate_argument_node_type'])) { + $options['types'] = isset($this->argument->options['validate_argument_node_type']) ? $this->argument->options['validate_argument_node_type'] : array(); + $options['access'] = !empty($this->argument->options['validate_argument_node_access']); + $options['access_op'] = isset($this->argument->options['validate_argument_node_access_op']) ? $this->argument->options['validate_argument_node_access_op'] : 'view'; + $options['nid_type'] = isset($this->argument->options['validate_argument_nid_type']) ? $this->argument->options['validate_argument_nid_type'] : array(); + } + } + + function validate_argument($argument) { + $types = $this->options['types']; + + switch ($this->options['nid_type']) { + case 'nid': + if (!is_numeric($argument)) { + return FALSE; + } + $node = node_load($argument); + if (!$node) { + return FALSE; + } + + if (!empty($this->options['access'])) { + if (!node_access($this->options['access_op'], $node)) { + return FALSE; + } + } + + // Save the title() handlers some work. + $this->argument->validated_title = check_plain($node->title); + + if (empty($types)) { + return TRUE; + } + + return isset($types[$node->type]); + break; + case 'nids': + $nids = new stdClass(); + $nids->value = array($argument); + $nids = views_break_phrase($argument, $nids); + if ($nids->value == array(-1)) { + return FALSE; + } + + $test = drupal_map_assoc($nids->value); + $titles = array(); + + $result = db_query("SELECT * FROM {node} WHERE nid IN (:nids)", array(':nids' => $nids->value)); + foreach ($result as $node) { + if ($types && empty($types[$node->type])) { + return FALSE; + } + + if (!empty($this->options['access'])) { + if (!node_access($this->options['access_op'], $node)) { + return FALSE; + } + } + + $titles[] = check_plain($node->title); + unset($test[$node->nid]); + } + + $this->argument->validated_title = implode($nids->operator == 'or' ? ' + ' : ', ', $titles); + // If this is not empty, we did not find a nid. + return empty($test); + } + } +} diff --git a/modules/node/views_plugin_row_node_rss.inc b/modules/node/views_plugin_row_node_rss.inc new file mode 100644 index 00000000..5da746b2 --- /dev/null +++ b/modules/node/views_plugin_row_node_rss.inc @@ -0,0 +1,174 @@ + 'default'); + $options['links'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Override init function to convert fulltext view-mode to full. + */ + function init(&$view, &$display, $options = NULL) { + parent::init($view, $display, $options); + + if ($this->options['item_length'] == 'fulltext') { + $this->options['item_length'] = 'full'; + } + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['item_length'] = array( + '#type' => 'select', + '#title' => t('Display type'), + '#options' => $this->options_form_summary_options(), + '#default_value' => $this->options['item_length'], + ); + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + } + + /** + * Return the main options, which are shown in the summary title. + */ + function options_form_summary_options() { + $entity_info = entity_get_info('node'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + $options['title'] = t('Title only'); + $options['default'] = t('Use site default RSS settings'); + return $options; + } + + function summary_title() { + $options = $this->options_form_summary_options(); + return check_plain($options[$this->options['item_length']]); + } + + + function pre_render($values) { + $nids = array(); + foreach ($values as $row) { + $nids[] = $row->{$this->field_alias}; + } + if (!empty($nids)) { + $this->nodes = node_load_multiple($nids); + } + } + + function render($row) { + // For the most part, this code is taken from node_feed() in node.module + global $base_url; + + $nid = $row->{$this->field_alias}; + if (!is_numeric($nid)) { + return; + } + + $display_mode = $this->options['item_length']; + if ($display_mode == 'default') { + $display_mode = variable_get('feed_item_length', 'teaser'); + } + + // Load the specified node: + $node = $this->nodes[$nid]; + if (empty($node)) { + return; + } + + $item_text = ''; + + $uri = entity_uri('node', $node); + $node->link = url($uri['path'], $uri['options'] + array('absolute' => TRUE)); + $node->rss_namespaces = array(); + $node->rss_elements = array( + array( + 'key' => 'pubDate', + 'value' => gmdate('r', $node->created), + ), + array( + 'key' => 'dc:creator', + 'value' => $node->name, + ), + array( + 'key' => 'guid', + 'value' => $node->nid . ' at ' . $base_url, + 'attributes' => array('isPermaLink' => 'false'), + ), + ); + + // The node gets built and modules add to or modify $node->rss_elements + // and $node->rss_namespaces. + + $build_mode = $display_mode; + + $build = node_view($node, $build_mode); + unset($build['#theme']); + + if (!empty($node->rss_namespaces)) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $node->rss_namespaces); + } + elseif (function_exists('rdf_get_namespaces')) { + // Merge RDF namespaces in the XML namespaces in case they are used + // further in the RSS content. + $xml_rdf_namespaces = array(); + foreach (rdf_get_namespaces() as $prefix => $uri) { + $xml_rdf_namespaces['xmlns:' . $prefix] = $uri; + } + $this->view->style_plugin->namespaces += $xml_rdf_namespaces; + } + + // Hide the links if desired. + if (!$this->options['links']) { + hide($build['links']); + } + + if ($display_mode != 'title') { + // We render node contents and force links to be last. + $build['links']['#weight'] = 1000; + $item_text .= drupal_render($build); + } + + $item = new stdClass(); + $item->description = $item_text; + $item->title = $node->title; + $item->link = $node->link; + $item->elements = $node->rss_elements; + $item->nid = $node->nid; + + return theme($this->theme_functions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item + )); + } +} diff --git a/modules/node/views_plugin_row_node_view.inc b/modules/node/views_plugin_row_node_view.inc new file mode 100644 index 00000000..4aefe461 --- /dev/null +++ b/modules/node/views_plugin_row_node_view.inc @@ -0,0 +1,110 @@ +options['teaser'])) { + $this->options['build_mode'] = $this->options['teaser'] ? 'teaser' : 'full'; + } + // Handle existing views which has used build_mode instead of view_mode. + if (isset($this->options['build_mode'])) { + $this->options['view_mode'] = $this->options['build_mode']; + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['view_mode'] = array('default' => 'teaser'); + $options['links'] = array('default' => TRUE, 'bool' => TRUE); + $options['comments'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $options = $this->options_form_summary_options(); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t('View mode'), + '#default_value' => $this->options['view_mode'], + ); + $form['links'] = array( + '#type' => 'checkbox', + '#title' => t('Display links'), + '#default_value' => $this->options['links'], + ); + $form['comments'] = array( + '#type' => 'checkbox', + '#title' => t('Display comments'), + '#default_value' => $this->options['comments'], + ); + } + + /** + * Return the main options, which are shown in the summary title. + */ + function options_form_summary_options() { + $entity_info = entity_get_info('node'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + if (empty($options)) { + $options = array( + 'teaser' => t('Teaser'), + 'full' => t('Full content') + ); + } + + return $options; + } + + function summary_title() { + $options = $this->options_form_summary_options(); + return check_plain($options[$this->options['view_mode']]); + } + + function pre_render($values) { + $nids = array(); + foreach ($values as $row) { + $nids[] = $row->{$this->field_alias}; + } + $this->nodes = node_load_multiple($nids); + } + + function render($row) { + if (isset($this->nodes[$row->{$this->field_alias}])) { + $node = $this->nodes[$row->{$this->field_alias}]; + $node->view = $this->view; + $build = node_view($node, $this->options['view_mode']); + + return drupal_render($build); + } + } +} \ No newline at end of file diff --git a/modules/poll.views.inc b/modules/poll.views.inc new file mode 100644 index 00000000..d3fd76ad --- /dev/null +++ b/modules/poll.views.inc @@ -0,0 +1,47 @@ + array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + // ---------------------------------------------------------------- + // Fields + + // poll active status + $data['poll']['active'] = array( + 'title' => t('Active'), + 'help' => t('Whether the poll is open for voting.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Active'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + return $data; +} diff --git a/modules/profile.views.inc b/modules/profile.views.inc new file mode 100644 index 00000000..89db913a --- /dev/null +++ b/modules/profile.views.inc @@ -0,0 +1,217 @@ + array( + 'left_table' => 'profile_value', + 'left_field' => 'uid', + 'field' => 'uid', + ), + 'users' => array( + 'left_table' => 'profile_value', + 'left_field' => 'uid', + 'field' => 'uid', + ), + ); + + $fields = profile_views_get_fields(); + foreach ($fields as $field) { + $table_name = 'profile_value_' . str_replace('-', '_', $field->name); + $data[$table_name] = array( + 'table' => array( + 'group' => t('Profile'), + 'join' => array( + 'node' => array( + 'table' => 'profile_value', + 'left_table' => 'users', + 'left_field' => 'uid', + 'field' => 'uid', + 'extra' => array(array('field' => 'fid', 'value' => $field->fid)), + ), + 'users' => array( + 'table' => 'profile_value', + 'left_field' => 'uid', + 'field' => 'uid', + 'extra' => array(array('field' => 'fid', 'value' => $field->fid)), + ), + ), + ), + ); + // All fields in the table are named 'value'. + $data[$table_name]['value'] = profile_views_fetch_field($field); + } + + return $data; +} + +/** + * Get all profile fields + */ +function profile_views_get_fields() { + static $fields = NULL; + + if (!isset($fields)) { + $fields = array(); + $results = db_query("SELECT * FROM {profile_field} ORDER BY category, weight"); + + foreach ($results as $row) { + if (!empty($row->options)) { + if (!in_array(substr($row->options, 0, 2), array('a:', 'b:', 'i:', 'f:', 'o:', 's:', ))) { + // unserialized fields default version + $options = $row->options; + unset($row->options); + $row->options = $options; + } + else { + // serialized fields or modified version + $row->options = unserialize($row->options); + } + } + $fields[$row->fid] = $row; + } + } + return $fields; +} + + +/** + * Add profile fields to view table + */ +function profile_views_fetch_field($field) { + $data = array( + 'title' => t('@category: @field-name', array('@category' => $field->category, '@field-name' => $field->title)), + ); + + // Add fields specific to the profile type. + switch ($field->type) { + case 'textfield': + $data += array( + 'help' => t('Profile textfield'), + 'field' => array( + 'handler' => 'views_handler_field_user', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + break; + case 'textarea': + $data += array( + 'help' => t('Profile textarea'), + 'field' => array( + 'handler' => 'views_handler_field_markup', + 'format' => filter_default_format(), + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + break; + case 'checkbox': + $data += array( + 'help' => t('Profile checkbox'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'accept null' => TRUE, + ), + // @todo there ought to be a boolean argument handler + ); + + break; + case 'url': + $data += array( + 'help' => t('Profile URL'), + 'field' => array( + 'handler' => 'views_handler_field_url', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + break; + case 'selection': + $data += array( + 'help' => t('Profile selection'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_profile_selection', + 'fid' => $field->fid, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + break; + case 'list': + $data += array( + 'help' => t('Profile freeform list %field-name.', array('%field-name' => $field->title)), + 'field' => array( + 'handler' => 'views_handler_field_profile_list', + 'no group by' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + break; + case 'date': + $data += array( + 'help' => t('Profile date %field-name.', array('%field-name' => $field->title)), + 'field' => array( + 'handler' => 'views_handler_field_profile_date', + ), + ); + + break; + } + + // @todo: add access control to hidden fields. + return $data; +} diff --git a/modules/profile/views_handler_field_profile_date.inc b/modules/profile/views_handler_field_profile_date.inc new file mode 100644 index 00000000..2d9fe966 --- /dev/null +++ b/modules/profile/views_handler_field_profile_date.inc @@ -0,0 +1,89 @@ +get_value($values); + if (!$value) { + return; + } + $value = unserialize($value); + $format = $this->options['date_format']; + switch ($format) { + case 'custom': + $format = $this->options['custom_date_format']; + break; + case 'small': + $format = variable_get('date_format_short', 'm/d/Y - H:i'); + break; + case 'medium': + $format = variable_get('date_format_medium', 'D, m/d/Y - H:i'); + break; + case 'large': + $format = variable_get('date_format_long', 'l, F j, Y - H:i'); + break; + } + + // Note: Avoid PHP's date() because it does not handle dates before + // 1970 on Windows. This would make the date field useless for e.g. + // birthdays. + + // But we *can* deal with non-year stuff: + $date = gmmktime(0, 0, 0, $value['month'], $value['day'], $value['year']); + $replace = array( + // day + 'd' => sprintf('%02d', $value['day']), + 'D' => NULL, + 'l' => NULL, + 'N' => NULL, + 'S' => date('S', $date), + 'w' => NULL, + 'j' => $value['day'], + // month + 'F' => date('F', $date), + 'm' => sprintf('%02d', $value['month']), + 'M' => date('M', $date), + 'n' => date('n', $date), + + 'Y' => $value['year'], + 'y' => substr($value['year'], 2, 2), + + // kill time stuff + 'a' => NULL, + 'A' => NULL, + 'g' => NULL, + 'G' => NULL, + 'h' => NULL, + 'H' => NULL, + 'i' => NULL, + 's' => NULL, + ':' => NULL, + 'T' => NULL, + ' - ' => NULL, + ':' => NULL, + ); + + return strtr($format, $replace); + } +} diff --git a/modules/profile/views_handler_field_profile_list.inc b/modules/profile/views_handler_field_profile_list.inc new file mode 100644 index 00000000..8917b93a --- /dev/null +++ b/modules/profile/views_handler_field_profile_list.inc @@ -0,0 +1,41 @@ +items = array(); + foreach ($values as $value) { + $field = $this->get_value($value); + $this->items[$field] = array(); + foreach (preg_split("/[,\n\r]/", $field) as $item) { + if ($item != '' && $item !== NULL) { + $this->items[$field][] = array('item' => $item); + } + } + } + } + + function render_item($count, $item) { + return $item['item']; + } + + function document_self_tokens(&$tokens) { + $tokens['[' . $this->options['id'] . '-item' . ']'] = t('The text of the profile item.'); + } + + function add_self_tokens(&$tokens, $item) { + $tokens['[' . $this->options['id'] . '-item' . ']'] = $item['item']; + } +} diff --git a/modules/profile/views_handler_filter_profile_selection.inc b/modules/profile/views_handler_filter_profile_selection.inc new file mode 100644 index 00000000..d3403c90 --- /dev/null +++ b/modules/profile/views_handler_filter_profile_selection.inc @@ -0,0 +1,30 @@ +value_options)) { + return; + } + + $this->value_options = array(); + $all_options = profile_views_get_fields(); + $field = $all_options[$this->definition['fid']]; + + $lines = preg_split("/[,\n\r]/", $field->options); + foreach ($lines as $line) { + if ($line = trim($line)) { + $this->value_options[$line] = $line; + } + } + } +} diff --git a/modules/search.views.inc b/modules/search.views.inc new file mode 100644 index 00000000..dad84bb7 --- /dev/null +++ b/modules/search.views.inc @@ -0,0 +1,202 @@ + array( + 'left_field' => 'nid', + 'field' => 'sid', + ), + ); + + $data['search_total']['table']['join'] = array( + 'node' => array( + 'left_table' => 'search_index', + 'left_field' => 'word', + 'field' => 'word', + ), + 'users' => array( + 'left_table' => 'search_index', + 'left_field' => 'word', + 'field' => 'word', + ) + ); + + $data['search_dataset']['table']['join'] = array( + 'node' => array( + 'left_table' => 'search_index', + 'left_field' => 'sid', + 'field' => 'sid', + 'extra' => 'search_index.type = search_dataset.type', + 'type' => 'INNER', + ), + 'users' => array( + 'left_table' => 'search_index', + 'left_field' => 'sid', + 'field' => 'sid', + 'extra' => 'search_index.type = search_dataset.type', + 'type' => 'INNER', + ), + ); + + // ---------------------------------------------------------------- + // Fields + + // score + $data['search_index']['score'] = array( + 'title' => t('Score'), + 'help' => t('The score of the search item. This will not be used if the search filter is not also present.'), + 'field' => array( + 'handler' => 'views_handler_field_search_score', + 'click sortable' => TRUE, + 'float' => TRUE, + 'no group by' => TRUE, + ), + // Information for sorting on a search score. + 'sort' => array( + 'handler' => 'views_handler_sort_search_score', + 'no group by' => TRUE, + ), + ); + + // Search node links: forward links. + $data['search_node_links_from']['table']['group'] = t('Search'); + $data['search_node_links_from']['table']['join'] = array( + 'node' => array( + 'arguments' => array('search_node_links', 'node', 'nid', 'nid', NULL, 'INNER'), + ), + ); + $data['search_node_links_from']['sid'] = array( + 'title' => t('Links from'), + 'help' => t('Other nodes that are linked from the node.'), + 'argument' => array( + 'handler' => 'views_handler_argument_node_nid', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_equality', + ), + ); + + // Search node links: backlinks. + $data['search_node_links_to']['table']['group'] = t('Search'); + $data['search_node_links_to']['table']['join'] = array( + 'node' => array( + 'arguments' => array('search_node_links', 'node', 'nid', 'sid', NULL, 'INNER'), + ), + ); + $data['search_node_links_to']['nid'] = array( + 'title' => t('Links to'), + 'help' => t('Other nodes that link to the node.'), + 'argument' => array( + 'handler' => 'views_handler_argument_node_nid', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_equality', + ), + ); + + // search filter + $data['search_index']['keys'] = array( + 'title' => t('Search Terms'), // The item it appears as on the UI, + 'help' => t('The terms to search for.'), // The help that appears on the UI, + // Information for searching terms using the full search syntax + 'filter' => array( + 'handler' => 'views_handler_filter_search', + 'no group by' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_search', + 'no group by' => TRUE, + ), + ); + + return $data; +} + +/** + * Implements hook_views_plugins(). + */ +function search_views_plugins() { + return; + // DISABLED. This currently doesn't work. + return array( + 'module' => 'views', // This just tells our themes are elsewhere. + 'row' => array( + 'search' => array( + 'title' => t('Search'), + 'help' => t('Display the results with standard search view.'), + 'handler' => 'views_plugin_row_search_view', + 'theme' => 'views_view_row_search', + 'path' => drupal_get_path('module', 'views') . '/modules/search', // not necessary for most modules + 'base' => array('node'), // only works with 'node' as base. + 'type' => 'normal', + ), + 'views_handler_argument_search' => array( + 'parent' => 'views_handler_argument', + ), + ), + ); +} + +/** + * Template helper for theme_views_view_row_search + */ +function template_preprocess_views_view_row_search(&$vars) { + $vars['node'] = ''; // make sure var is defined. + $nid = $vars['row']->nid; + if (!is_numeric($nid)) { + return; + } + + // @todo: Once the search row is fixed this node_load should be replace by a node_load_multiple + $node = node_load($nid); + + if (empty($node)) { + return; + } + + // Build the node body. + $node = node_build_content($node, FALSE, FALSE); + $node->body = drupal_render($node->content); + + // Fetch comments for snippet + $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index'); + + // Fetch terms for snippet + $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index'); + + $vars['url'] = url('node/' . $nid); + $vars['title'] = check_plain($node->title); + + $info = array(); + $info['type'] = node_type_get_name($node); + $info['user'] = theme('username', array('acccount' => $node)); + $info['date'] = format_date($node->changed, 'small'); + $extra = module_invoke_all('node_search_result', $node); + if (isset($extra) && is_array($extra)) { + $info = array_merge($info, $extra); + } + $vars['info_split'] = $info; + $vars['info'] = implode(' - ', $info); + + $vars['node'] = $node; + // @todo: get score from ??? +//$vars['score'] = $item->score; + $vars['snippet'] = search_excerpt($vars['view']->value, $node->body); +} diff --git a/modules/search.views_default.inc b/modules/search.views_default.inc new file mode 100644 index 00000000..449dfb3c --- /dev/null +++ b/modules/search.views_default.inc @@ -0,0 +1,118 @@ +name = 'backlinks'; + $view->description = 'Displays a list of nodes that link to the node, using the search backlinks table.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Backlinks'; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = 30; + $handler->display->display_options['style_plugin'] = 'list'; + $handler->display->display_options['style_options']['type'] = 'ol'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* No results behavior: Global: Text area */ + $handler->display->display_options['empty']['text']['id'] = 'area'; + $handler->display->display_options['empty']['text']['table'] = 'views'; + $handler->display->display_options['empty']['text']['field'] = 'area'; + $handler->display->display_options['empty']['text']['empty'] = FALSE; + $handler->display->display_options['empty']['text']['content'] = 'No backlinks found.'; + $handler->display->display_options['empty']['text']['format'] = '1'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['link_to_node'] = 1; + /* Contextual filter: Search: Links to */ + $handler->display->display_options['arguments']['nid']['id'] = 'nid'; + $handler->display->display_options['arguments']['nid']['table'] = 'search_node_links_to'; + $handler->display->display_options['arguments']['nid']['field'] = 'nid'; + $handler->display->display_options['arguments']['nid']['default_action'] = 'not found'; + $handler->display->display_options['arguments']['nid']['title_enable'] = 1; + $handler->display->display_options['arguments']['nid']['title'] = 'Pages that link to %1'; + $handler->display->display_options['arguments']['nid']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['nid']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['nid']['specify_validation'] = 1; + $handler->display->display_options['arguments']['nid']['validate']['type'] = 'node'; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 1; + $handler->display->display_options['filters']['status']['group'] = 0; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'node/%/backlinks'; + $handler->display->display_options['menu']['type'] = 'tab'; + $handler->display->display_options['menu']['title'] = 'What links here'; + $handler->display->display_options['menu']['weight'] = '0'; + + /* Display: What links here */ + $handler = $view->new_display('block', 'What links here', 'block'); + $handler->display->display_options['defaults']['use_more'] = FALSE; + $handler->display->display_options['use_more'] = TRUE; + $handler->display->display_options['defaults']['style_plugin'] = FALSE; + $handler->display->display_options['style_plugin'] = 'list'; + $handler->display->display_options['defaults']['style_options'] = FALSE; + $handler->display->display_options['defaults']['row_plugin'] = FALSE; + $handler->display->display_options['row_plugin'] = 'fields'; + $handler->display->display_options['defaults']['row_options'] = FALSE; + $handler->display->display_options['defaults']['arguments'] = FALSE; + /* Contextual filter: Search: Links to */ + $handler->display->display_options['arguments']['nid']['id'] = 'nid'; + $handler->display->display_options['arguments']['nid']['table'] = 'search_node_links_to'; + $handler->display->display_options['arguments']['nid']['field'] = 'nid'; + $handler->display->display_options['arguments']['nid']['default_action'] = 'default'; + $handler->display->display_options['arguments']['nid']['title_enable'] = 1; + $handler->display->display_options['arguments']['nid']['title'] = 'What links here'; + $handler->display->display_options['arguments']['nid']['default_argument_type'] = 'node'; + $handler->display->display_options['arguments']['nid']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['nid']['specify_validation'] = 1; + $handler->display->display_options['arguments']['nid']['validate']['type'] = 'node'; + $translatables['backlinks'] = array( + t('Master'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('No backlinks found.'), + t('All'), + t('Pages that link to %1'), + t('Page'), + t('What links here'), + ); + + $views['backlinks'] = $view; + + return $views; +} diff --git a/modules/search/views_handler_argument_search.inc b/modules/search/views_handler_argument_search.inc new file mode 100644 index 00000000..f0a4a448 --- /dev/null +++ b/modules/search/views_handler_argument_search.inc @@ -0,0 +1,100 @@ +search_query)) { + $this->search_query = db_select('search_index', 'i', array('target' => 'slave'))->extend('viewsSearchQuery'); + $this->search_query->searchExpression($input, $this->view->base_table); + $this->search_query->publicParseSearchExpression(); + } + } + + /** + * Add this argument to the query. + */ + function query($group_by = FALSE) { + $required = FALSE; + $this->query_parse_search_expression($this->argument); + if (!isset($this->search_query)) { + $required = TRUE; + } + else { + $words = $this->search_query->words(); + if (empty($words)) { + $required = TRUE; + } + } + if ($required) { + if ($this->operator == 'required') { + $this->query->add_where(0, 'FALSE'); + } + } + else { + $search_index = $this->ensure_my_table(); + + $search_condition = db_and(); + + // Create a new join to relate the 'search_total' table to our current 'search_index' table. + $join = new views_join; + $join->construct('search_total', $search_index, 'word', 'word'); + $search_total = $this->query->add_relationship('search_total', $join, $search_index); + + $this->search_score = $this->query->add_field('', "SUM($search_index.score * $search_total.count)", 'score', array('aggregate' => TRUE)); + + if (empty($this->query->relationships[$this->relationship])) { + $base_table = $this->query->base_table; + } + else { + $base_table = $this->query->relationships[$this->relationship]['base']; + } + $search_condition->condition("$search_index.type", $base_table); + + if (!$this->search_query->simple()) { + $search_dataset = $this->query->add_table('search_dataset'); + $conditions = $this->search_query->conditions(); + $condition_conditions =& $conditions->conditions(); + foreach ($condition_conditions as $key => &$condition) { + // Take sure we just look at real conditions. + if (is_numeric($key)) { + // Replace the conditions with the table alias of views. + $this->search_query->condition_replace_string('d.', "$search_dataset.", $condition); + } + } + $search_conditions =& $search_condition->conditions(); + $search_conditions = array_merge($search_conditions, $condition_conditions); + } + else { + // Stores each condition, so and/or on the filter level will still work. + $or = db_or(); + foreach ($words as $word) { + $or->condition("$search_index.word", $word); + } + + $search_condition->condition($or); + } + + $this->query->add_where(0, $search_condition); + $this->query->add_groupby("$search_index.sid"); + $matches = $this->search_query->matches(); + $placeholder = $this->placeholder(); + $this->query->add_having_expression(0, "COUNT(*) >= $placeholder", array($placeholder => $matches)); + } + } +} diff --git a/modules/search/views_handler_field_search_score.inc b/modules/search/views_handler_field_search_score.inc new file mode 100644 index 00000000..0feddac6 --- /dev/null +++ b/modules/search/views_handler_field_search_score.inc @@ -0,0 +1,81 @@ + ''); + $options['alternate_order'] = array('default' => 'asc'); + + return $options; + } + + function options_form(&$form, &$form_state) { + $style_options = $this->view->display_handler->get_option('style_options'); + if (isset($style_options['default']) && $style_options['default'] == $this->options['id']) { + $handlers = $this->view->display_handler->get_handlers('field'); + $options = array('' => t('No alternate')); + foreach ($handlers as $id => $handler) { + $options[$id] = $handler->ui_name(); + } + + $form['alternate_sort'] = array( + '#type' => 'select', + '#title' => t('Alternative sort'), + '#description' => t('Pick an alternative default table sort field to use when the search score field is unavailable.'), + '#options' => $options, + '#default_value' => $this->options['alternate_sort'], + ); + + $form['alternate_order'] = array( + '#type' => 'select', + '#title' => t('Alternate sort order'), + '#options' => array('asc' => t('Ascending'), 'desc' => t('Descending')), + '#default_value' => $this->options['alternate_order'], + ); + } + + parent::options_form($form, $form_state); + } + + function query() { + // Check to see if the search filter added 'score' to the table. + // Our filter stores it as $handler->search_score -- and we also + // need to check its relationship to make sure that we're using the same + // one or obviously this won't work. + foreach ($this->view->filter as $handler) { + if (isset($handler->search_score) && $handler->relationship == $this->relationship) { + $this->field_alias = $handler->search_score; + $this->table_alias = $handler->table_alias; + return; + } + } + + // Hide this field if no search filter is in place. + $this->options['exclude'] = TRUE; + if (!empty($this->options['alternate_sort'])) { + if (isset($this->view->style_plugin->options['default']) && $this->view->style_plugin->options['default'] == $this->options['id']) { + // Since the style handler initiates fields, we plug these values right into the active handler. + $this->view->style_plugin->options['default'] = $this->options['alternate_sort']; + $this->view->style_plugin->options['order'] = $this->options['alternate_order']; + } + } + } + + function render($values) { + // Only render if we exist. + if (isset($this->table_alias)) { + return parent::render($values); + } + } +} diff --git a/modules/search/views_handler_filter_search.inc b/modules/search/views_handler_filter_search.inc new file mode 100644 index 00000000..16515a76 --- /dev/null +++ b/modules/search/views_handler_filter_search.inc @@ -0,0 +1,234 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Overrides views_handler_filter::options_form(). + * + * Add an option to remove search scores from the query. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['remove_score'] = array( + '#type' => 'checkbox', + '#title' => t('Remove search score'), + '#description' => t('Check this box to remove the search score from the query. This can help reduce help reduce duplicate search results when using this filter.'), + '#default_value' => $this->options['remove_score'], + ); + } + + + /** + * Provide simple equality operator + */ + function operator_form(&$form, &$form_state) { + $form['operator'] = array( + '#type' => 'radios', + '#title' => t('On empty input'), + '#default_value' => $this->operator, + '#options' => array( + 'optional' => t('Show All'), + 'required' => t('Show None'), + ), + ); + } + + /** + * Provide a simple textfield for equality + */ + function value_form(&$form, &$form_state) { + $form['value'] = array( + '#type' => 'textfield', + '#size' => 15, + '#default_value' => $this->value, + '#attributes' => array('title' => t('Enter the terms you wish to search for.')), + '#title' => empty($form_state['exposed']) ? t('Value') : '', + ); + } + + /** + * Validate the options form. + */ + function exposed_validate(&$form, &$form_state) { + if (!isset($this->options['expose']['identifier'])) { + return; + } + + $key = $this->options['expose']['identifier']; + if (!empty($form_state['values'][$key])) { + $this->query_parse_search_expression($form_state['values'][$key]); + if (count($this->search_query->words()) == 0) { + form_set_error($key, format_plural(variable_get('minimum_word_size', 3), 'You must include at least one positive keyword with 1 character or more.', 'You must include at least one positive keyword with @count characters or more.')); + } + } + } + + /** + * Take sure that parseSearchExpression is runned and everything is set up for it. + * + * @param $input + * The search phrase which was input by the user. + */ + function query_parse_search_expression($input) { + if (!isset($this->search_query)) { + $this->parsed = TRUE; + $this->search_query = db_select('search_index', 'i', array('target' => 'slave'))->extend('viewsSearchQuery'); + $this->search_query->searchExpression($input, $this->view->base_table); + $this->search_query->publicParseSearchExpression(); + } + } + + /** + * Add this filter to the query. + * + * Due to the nature of fapi, the value and the operator have an unintended + * level of indirection. You will find them in $this->operator + * and $this->value respectively. + */ + function query() { + // Since attachment views don't validate the exposed input, parse the search + // expression if required. + if (!$this->parsed) { + $this->query_parse_search_expression($this->value); + } + $required = FALSE; + if (!isset($this->search_query)) { + $required = TRUE; + } + else { + $words = $this->search_query->words(); + if (empty($words)) { + $required = TRUE; + } + } + if ($required) { + if ($this->operator == 'required') { + $this->query->add_where($this->options['group'], 'FALSE'); + } + } + else { + $search_index = $this->ensure_my_table(); + + $search_condition = db_and(); + + if (!$this->options['remove_score']) { + // Create a new join to relate the 'serach_total' table to our current 'search_index' table. + $join = new views_join; + $join->construct('search_total', $search_index, 'word', 'word'); + $search_total = $this->query->add_relationship('search_total', $join, $search_index); + + $this->search_score = $this->query->add_field('', "SUM($search_index.score * $search_total.count)", 'score', array('aggregate' => TRUE)); + } + + if (empty($this->query->relationships[$this->relationship])) { + $base_table = $this->query->base_table; + } + else { + $base_table = $this->query->relationships[$this->relationship]['base']; + } + $search_condition->condition("$search_index.type", $base_table); + if (!$this->search_query->simple()) { + $search_dataset = $this->query->add_table('search_dataset'); + $conditions = $this->search_query->conditions(); + $condition_conditions =& $conditions->conditions(); + foreach ($condition_conditions as $key => &$condition) { + // Take sure we just look at real conditions. + if (is_numeric($key)) { + // Replace the conditions with the table alias of views. + $this->search_query->condition_replace_string('d.', "$search_dataset.", $condition); + } + } + $search_conditions =& $search_condition->conditions(); + $search_conditions = array_merge($search_conditions, $condition_conditions); + } + else { + // Stores each condition, so and/or on the filter level will still work. + $or = db_or(); + foreach ($words as $word) { + $or->condition("$search_index.word", $word); + } + + $search_condition->condition($or); + } + + $this->query->add_where($this->options['group'], $search_condition); + $this->query->add_groupby("$search_index.sid"); + $matches = $this->search_query->matches(); + $placeholder = $this->placeholder(); + $this->query->add_having_expression($this->options['group'], "COUNT(*) >= $placeholder", array($placeholder => $matches)); + } + // Set to NULL to prevent PDO exception when views object is cached. + $this->search_query = NULL; + } +} + +/** + * Extends the core SearchQuery. + */ +class viewsSearchQuery extends SearchQuery { + public function &conditions() { + return $this->conditions; + } + public function words() { + return $this->words; + } + + public function simple() { + return $this->simple; + } + + public function matches() { + return $this->matches; + } + + public function publicParseSearchExpression() { + return $this->parseSearchExpression(); + } + + function condition_replace_string($search, $replace, &$condition) { + if ($condition['field'] instanceof DatabaseCondition) { + $conditions =& $condition['field']->conditions(); + foreach ($conditions as $key => &$subcondition) { + if (is_numeric($key)) { + $this->condition_replace_string($search, $replace, $subcondition); + } + } + } + else { + $condition['field'] = str_replace($search, $replace, $condition['field']); + } + } +} diff --git a/modules/search/views_handler_sort_search_score.inc b/modules/search/views_handler_sort_search_score.inc new file mode 100644 index 00000000..d37fb651 --- /dev/null +++ b/modules/search/views_handler_sort_search_score.inc @@ -0,0 +1,32 @@ +search_score -- and we also + // need to check its relationship to make sure that we're using the same + // one or obviously this won't work. + foreach (array('filter', 'argument') as $type) { + foreach ($this->view->{$type} as $handler) { + if (isset($handler->search_score) && $handler->relationship == $this->relationship) { + $this->query->add_orderby(NULL, NULL, $this->options['order'], $handler->search_score); + $this->table_alias = $handler->table_alias; + return; + } + } + } + + // Do absolutely nothing if there is no filter/argument in place; there is no reason to + // sort on the raw scores with this handler. + } +} diff --git a/modules/search/views_plugin_row_search_view.inc b/modules/search/views_plugin_row_search_view.inc new file mode 100644 index 00000000..e4aacdc3 --- /dev/null +++ b/modules/search/views_plugin_row_search_view.inc @@ -0,0 +1,39 @@ + TRUE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['score'] = array( + '#type' => 'checkbox', + '#title' => t('Display score'), + '#default_value' => $this->options['score'], + ); + } + + /** + * Override the behavior of the render() function. + */ + function render($row) { + return theme($this->theme_functions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $row + )); + } +} diff --git a/modules/statistics.views.inc b/modules/statistics.views.inc new file mode 100644 index 00000000..d6637f3e --- /dev/null +++ b/modules/statistics.views.inc @@ -0,0 +1,263 @@ + array( + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + + // totalcount + $data['node_counter']['totalcount'] = array( + 'title' => t('Total views'), + 'help' => t('The total number of times the node has been viewed.'), + + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // daycount + $data['node_counter']['daycount'] = array( + 'title' => t('Views today'), + 'help' => t('The total number of times the node has been viewed today.'), + + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // timestamp + $data['node_counter']['timestamp'] = array( + 'title' => t('Most recent view'), + 'help' => t('The most recent time the node has been viewed.'), + + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + + // ---------------------------------------------------------------- + // accesslog table + + $data['accesslog']['table']['group'] = t('Access log'); + + // Advertise this table as a possible base table + $data['accesslog']['table']['base'] = array( + 'field' => 'aid', + 'title' => t('Access log'), + 'help' => t('Stores site access information.'), + 'weight' => 10, + ); + + // For other base tables, explain how we join + $data['accesslog']['table']['join'] = array( + 'users' => array( + 'field' => 'uid', + 'left_field' => 'uid', + ), + ); + + // accesslog.aid + $data['accesslog']['aid'] = array( + 'title' => t('Aid'), + 'help' => t('Unique access event ID.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + 'name field' => 'wid', + 'numeric' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // session id + $data['accesslog']['sid'] = array( + 'title' => t('Session ID'), + 'help' => t('Browser session ID of user that visited page.'), + + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // title + $data['accesslog']['title'] = array( + 'title' => t('Page title'), + 'help' => t('Title of page visited.'), + + 'field' => array( + 'handler' => 'views_handler_field_accesslog_path', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // path + $data['accesslog']['path'] = array( + 'title' => t('Path'), + 'help' => t('Internal path to page visited (relative to Drupal root.)'), + + 'field' => array( + 'handler' => 'views_handler_field_accesslog_path', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + //No argument here. Can't send forward slashes as arguments. + //Can be worked around by node ID. + //(but what about aliases?) + ); + + // referrer + $data['accesslog']['url'] = array( + 'title' => t('Referrer'), + 'help' => t('Referrer URI.'), + 'field' => array( + 'handler' => 'views_handler_field_url', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // hostname + $data['accesslog']['hostname'] = array( + 'title' => t('Hostname'), + 'help' => t('Hostname of user that visited the page.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // user + $data['accesslog']['uid'] = array( + 'title' => t('User'), + 'help' => t('The user who visited the site.'), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'users', + 'base field' => 'uid', + ), + ); + + // timer + $data['accesslog']['timer'] = array( + 'title' => t('Timer'), + 'help' => t('Time in milliseconds that the page took to load.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // timestamp + $data['accesslog']['timestamp'] = array( + 'title' => t('Timestamp'), + 'help' => t('Timestamp of when the page was visited.'), + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + + return $data; +} diff --git a/modules/statistics.views_default.inc b/modules/statistics.views_default.inc new file mode 100644 index 00000000..84a94270 --- /dev/null +++ b/modules/statistics.views_default.inc @@ -0,0 +1,252 @@ +name = 'popular'; + $view->description = 'Shows the most-viewed nodes on the site. This requires the statistics to be enabled at administer >> reports >> access log settings.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Popular content'; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Popular content'; + $handler->display->display_options['use_more'] = TRUE; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '25'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'type' => 'type', + 'title' => 'title', + 'name' => 'name', + 'timestamp' => 'title', + 'totalcount' => 'totalcount', + ); + $handler->display->display_options['style_options']['default'] = '-1'; + $handler->display->display_options['style_options']['info'] = array( + 'type' => array( + 'sortable' => 0, + 'separator' => '', + ), + 'title' => array( + 'sortable' => 0, + 'separator' => '', + ), + 'name' => array( + 'sortable' => 0, + 'separator' => '', + ), + 'timestamp' => array( + 'separator' => '', + ), + 'totalcount' => array( + 'sortable' => 0, + 'separator' => '', + ), + ); + $handler->display->display_options['style_options']['override'] = 0; + $handler->display->display_options['style_options']['order'] = 'desc'; + /* Field: Content: Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['label'] = 'Author'; + /* Field: Content: Has new content */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'history'; + $handler->display->display_options['fields']['timestamp']['field'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['label'] = ''; + $handler->display->display_options['fields']['timestamp']['link_to_node'] = 0; + $handler->display->display_options['fields']['timestamp']['comments'] = 1; + /* Sort criterion: Content statistics: Total views */ + $handler->display->display_options['sorts']['totalcount']['id'] = 'totalcount'; + $handler->display->display_options['sorts']['totalcount']['table'] = 'node_counter'; + $handler->display->display_options['sorts']['totalcount']['field'] = 'totalcount'; + $handler->display->display_options['sorts']['totalcount']['order'] = 'DESC'; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = '1'; + $handler->display->display_options['filters']['status']['group'] = 0; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + /* Filter criterion: Content statistics: Total views */ + $handler->display->display_options['filters']['totalcount']['id'] = 'totalcount'; + $handler->display->display_options['filters']['totalcount']['table'] = 'node_counter'; + $handler->display->display_options['filters']['totalcount']['field'] = 'totalcount'; + $handler->display->display_options['filters']['totalcount']['operator'] = '>'; + $handler->display->display_options['filters']['totalcount']['value']['value'] = '0'; + $handler->display->display_options['filters']['totalcount']['group'] = 0; + $handler->display->display_options['filters']['totalcount']['expose']['operator'] = FALSE; + + /* Display: Popular (page) */ + $handler = $view->new_display('page', 'Popular (page)', 'page'); + $handler->display->display_options['path'] = 'popular/all'; + $handler->display->display_options['menu']['type'] = 'default tab'; + $handler->display->display_options['menu']['title'] = 'Popular content'; + $handler->display->display_options['menu']['weight'] = '-1'; + $handler->display->display_options['tab_options']['type'] = 'normal'; + $handler->display->display_options['tab_options']['title'] = 'Popular content'; + $handler->display->display_options['tab_options']['weight'] = ''; + + /* Display: Today (page) */ + $handler = $view->new_display('page', 'Today (page)', 'page_1'); + $handler->display->display_options['defaults']['fields'] = FALSE; + /* Field: Content: Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['label'] = 'Author'; + /* Field: Content: Has new content */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'history'; + $handler->display->display_options['fields']['timestamp']['field'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['label'] = ''; + $handler->display->display_options['fields']['timestamp']['link_to_node'] = 0; + $handler->display->display_options['fields']['timestamp']['comments'] = 1; + /* Field: Content statistics: Views today */ + $handler->display->display_options['fields']['daycount']['id'] = 'daycount'; + $handler->display->display_options['fields']['daycount']['table'] = 'node_counter'; + $handler->display->display_options['fields']['daycount']['field'] = 'daycount'; + $handler->display->display_options['defaults']['sorts'] = FALSE; + /* Sort criterion: Content statistics: Views today */ + $handler->display->display_options['sorts']['daycount']['id'] = 'daycount'; + $handler->display->display_options['sorts']['daycount']['table'] = 'node_counter'; + $handler->display->display_options['sorts']['daycount']['field'] = 'daycount'; + $handler->display->display_options['sorts']['daycount']['order'] = 'DESC'; + $handler->display->display_options['path'] = 'popular/today'; + $handler->display->display_options['menu']['type'] = 'tab'; + $handler->display->display_options['menu']['title'] = 'Today\'s popular content'; + $handler->display->display_options['menu']['weight'] = '0'; + $handler->display->display_options['tab_options']['type'] = 'normal'; + $handler->display->display_options['tab_options']['title'] = 'Popular content'; + $handler->display->display_options['tab_options']['weight'] = '0'; + + /* Display: Popular (block) */ + $handler = $view->new_display('block', 'Popular (block)', 'block'); + $handler->display->display_options['defaults']['style_plugin'] = FALSE; + $handler->display->display_options['style_plugin'] = 'list'; + $handler->display->display_options['defaults']['style_options'] = FALSE; + $handler->display->display_options['defaults']['row_plugin'] = FALSE; + $handler->display->display_options['row_plugin'] = 'fields'; + $handler->display->display_options['row_options']['inline'] = array( + 'title' => 'title', + 'totalcount' => 'totalcount', + ); + $handler->display->display_options['defaults']['row_options'] = FALSE; + $handler->display->display_options['defaults']['fields'] = FALSE; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['link_to_node'] = 1; + /* Field: Content statistics: Total views */ + $handler->display->display_options['fields']['totalcount']['id'] = 'totalcount'; + $handler->display->display_options['fields']['totalcount']['table'] = 'node_counter'; + $handler->display->display_options['fields']['totalcount']['field'] = 'totalcount'; + $handler->display->display_options['fields']['totalcount']['label'] = ''; + $handler->display->display_options['fields']['totalcount']['prefix'] = ' ('; + $handler->display->display_options['fields']['totalcount']['suffix'] = ')'; + + /* Display: Today (block) */ + $handler = $view->new_display('block', 'Today (block)', 'block_1'); + $handler->display->display_options['defaults']['title'] = FALSE; + $handler->display->display_options['title'] = 'Today\'s popular content'; + $handler->display->display_options['defaults']['link_display'] = FALSE; + $handler->display->display_options['link_display'] = 'page_1'; + $handler->display->display_options['defaults']['style_plugin'] = FALSE; + $handler->display->display_options['style_plugin'] = 'list'; + $handler->display->display_options['defaults']['style_options'] = FALSE; + $handler->display->display_options['defaults']['row_plugin'] = FALSE; + $handler->display->display_options['row_plugin'] = 'fields'; + $handler->display->display_options['row_options']['inline'] = array( + 'title' => 'title', + 'daycount' => 'daycount', + ); + $handler->display->display_options['defaults']['row_options'] = FALSE; + $handler->display->display_options['defaults']['fields'] = FALSE; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['link_to_node'] = 1; + /* Field: Content statistics: Views today */ + $handler->display->display_options['fields']['daycount']['id'] = 'daycount'; + $handler->display->display_options['fields']['daycount']['table'] = 'node_counter'; + $handler->display->display_options['fields']['daycount']['field'] = 'daycount'; + $handler->display->display_options['fields']['daycount']['label'] = ''; + $handler->display->display_options['fields']['daycount']['prefix'] = ' ('; + $handler->display->display_options['fields']['daycount']['suffix'] = ')'; + $handler->display->display_options['defaults']['sorts'] = FALSE; + /* Sort criterion: Content statistics: Views today */ + $handler->display->display_options['sorts']['daycount']['id'] = 'daycount'; + $handler->display->display_options['sorts']['daycount']['table'] = 'node_counter'; + $handler->display->display_options['sorts']['daycount']['field'] = 'daycount'; + $handler->display->display_options['sorts']['daycount']['order'] = 'DESC'; + $translatables['popular'] = array( + t('Master'), + t('Popular content'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('Type'), + t('Title'), + t('Author'), + t('Popular (page)'), + t('Today (page)'), + t('Views today'), + t('.'), + t(','), + t('Popular (block)'), + t(' ('), + t(')'), + t('Today (block)'), + t('Today\'s popular content'), + ); + + $views['popular'] = $view; + + return $views; +} diff --git a/modules/statistics/views_handler_field_accesslog_path.inc b/modules/statistics/views_handler_field_accesslog_path.inc new file mode 100644 index 00000000..85b2352d --- /dev/null +++ b/modules/statistics/views_handler_field_accesslog_path.inc @@ -0,0 +1,58 @@ +options['display_as_link'])) { + $this->additional_fields['path'] = 'path'; + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['display_as_link'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + /** + * Provide link to the page being visited. + */ + function options_form(&$form, &$form_state) { + $form['display_as_link'] = array( + '#title' => t('Display as link'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['display_as_link']), + ); + parent::options_form($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } + + function render_link($data, $values) { + if (!empty($this->options['display_as_link'])) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = $this->get_value($values, 'path'); + $this->options['alter']['html'] = TRUE; + } + + return $data; + } +} diff --git a/modules/system.views.inc b/modules/system.views.inc new file mode 100644 index 00000000..243cbc72 --- /dev/null +++ b/modules/system.views.inc @@ -0,0 +1,578 @@ + 'fid', + 'title' => t('File'), + 'help' => t("Files maintained by Drupal and various modules."), + 'defaults' => array( + 'field' => 'filename' + ), + ); + $data['file_managed']['table']['entity type'] = 'file'; + + // fid + $data['file_managed']['fid'] = array( + 'title' => t('File ID'), + 'help' => t('The ID of the file.'), + 'field' => array( + 'handler' => 'views_handler_field_file', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_file_fid', + 'name field' => 'filename', // the field to display in the summary. + 'numeric' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // filename + $data['file_managed']['filename'] = array( + 'title' => t('Name'), + 'help' => t('The name of the file.'), + 'field' => array( + 'handler' => 'views_handler_field_file', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // uri + $data['file_managed']['uri'] = array( + 'title' => t('Path'), + 'help' => t('The path of the file.'), + 'field' => array( + 'handler' => 'views_handler_field_file_uri', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // filemime + $data['file_managed']['filemime'] = array( + 'title' => t('Mime type'), + 'help' => t('The mime type of the file.'), + 'field' => array( + 'handler' => 'views_handler_field_file_filemime', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // extension + $data['file_managed']['extension'] = array( + 'title' => t('Extension'), + 'help' => t('The extension of the file.'), + 'real field' => 'filename', + 'field' => array( + 'handler' => 'views_handler_field_file_extension', + 'click sortable' => FALSE, + ), + ); + + // filesize + $data['file_managed']['filesize'] = array( + 'title' => t('Size'), + 'help' => t('The size of the file.'), + 'field' => array( + 'handler' => 'views_handler_field_file_size', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // status + $data['file_managed']['status'] = array( + 'title' => t('Status'), + 'help' => t('The status of the file.'), + 'field' => array( + 'handler' => 'views_handler_field_file_status', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_file_status', + ), + ); + + // timestamp field + $data['file_managed']['timestamp'] = array( + 'title' => t('Upload date'), + 'help' => t('The date the file was uploaded.'), + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + // uid + $data['file_managed']['uid'] = array( + 'title' => t('User who uploaded'), + 'help' => t('The user that uploaded the file.'), + 'relationship' => array( + 'title' => t('User who uploaded'), + 'label' => t('User who uploaded'), + 'base' => 'users', + 'base field' => 'uid', + ), + ); + + // ---------------------------------------------------------------------- + // file_usage table + + $data['file_usage']['table']['group'] = t('File Usage'); + + // Provide field-type-things to several base tables; on the core files table ("file_managed") so + // that we can create relationships from files to entities, and then on each core entity type base + // table so that we can provide general relationships between entities and files. + $data['file_usage']['table']['join'] = array( + // Link ourself to the {file_managed} table so we can provide file->entity relationships. + 'file_managed' => array( + 'field' => 'fid', + 'left_field' => 'fid', + ), + // Link ourself to the {node} table so we can provide node->file relationships. + 'node' => array( + 'field' => 'id', + 'left_field' => 'nid', + 'extra' => array(array('field' => 'type', 'value' => 'node')), + ), + // Link ourself to the {users} table so we can provide user->file relationships. + 'users' => array( + 'field' => 'id', + 'left_field' => 'uid', + 'extra' => array(array('field' => 'type', 'value' => 'user')), + ), + // Link ourself to the {comment} table so we can provide comment->file relationships. + 'comment' => array( + 'field' => 'id', + 'left_field' => 'cid', + 'extra' => array(array('field' => 'type', 'value' => 'comment')), + ), + // Link ourself to the {taxonomy_term_data} table so we can provide taxonomy_term->file relationships. + 'taxonomy_term_data' => array( + 'field' => 'id', + 'left_field' => 'tid', + 'extra' => array(array('field' => 'type', 'value' => 'taxonomy_term')), + ), + // Link ourself to the {taxonomy_vocabulary} table so we can provide taxonomy_vocabulary->file relationships. + 'taxonomy_vocabulary' => array( + 'field' => 'id', + 'left_field' => 'vid', + 'extra' => array(array('field' => 'type', 'value' => 'taxonomy_vocabulary')), + ), + ); + + // Provide a relationship between the files table and each entity type, and between each entity + // type and the files table. Entity->file relationships are type-restricted in the joins + // declared above, and file->entity relationships are type-restricted in the relationship + // declarations below. + + // Relationships between files and nodes. + $data['file_usage']['file_to_node'] = array( + 'title' => t('Content'), + 'help' => t('Content that is associated with this file, usually because this file is in a field on the content.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Content'), + 'label' => t('Content'), + 'base' => 'node', + 'base field' => 'nid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'node')), + ), + ); + $data['file_usage']['node_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this node, usually because it is in a field on the node.'), + // Only provide this field/relationship/etc. when the 'node' base table is present. + 'skip base' => array('file_managed', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and users. + $data['file_usage']['file_to_user'] = array( + 'title' => t('User'), + 'help' => t('A user that is associated with this file, usually because this file is in a field on the user.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('User'), + 'label' => t('User'), + 'base' => 'users', + 'base field' => 'uid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'user')), + ), + ); + $data['file_usage']['user_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this user, usually because it is in a field on the user.'), + // Only provide this field/relationship/etc. when the 'users' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and comments. + $data['file_usage']['file_to_comment'] = array( + 'title' => t('Comment'), + 'help' => t('A comment that is associated with this file, usually because this file is in a field on the comment.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Comment'), + 'label' => t('Comment'), + 'base' => 'comment', + 'base field' => 'cid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'comment')), + ), + ); + $data['file_usage']['comment_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this comment, usually because it is in a field on the comment.'), + // Only provide this field/relationship/etc. when the 'comment' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'users', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and taxonomy_terms. + $data['file_usage']['file_to_taxonomy_term'] = array( + 'title' => t('Taxonomy Term'), + 'help' => t('A taxonomy term that is associated with this file, usually because this file is in a field on the taxonomy term.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Taxonomy Term'), + 'label' => t('Taxonomy Term'), + 'base' => 'taxonomy_term_data', + 'base field' => 'tid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'taxonomy_term')), + ), + ); + $data['file_usage']['taxonomy_term_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this taxonomy term, usually because it is in a field on the taxonomy term.'), + // Only provide this field/relationship/etc. when the 'taxonomy_term_data' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'users', 'comment', 'taxonomy_vocabulary'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Relationships between files and taxonomy_vocabulary items. + $data['file_usage']['file_to_taxonomy_vocabulary'] = array( + 'title' => t('Taxonomy Vocabulary'), + 'help' => t('A taxonomy vocabulary that is associated with this file, usually because this file is in a field on the taxonomy vocabulary.'), + // Only provide this field/relationship/etc. when the 'file_managed' base table is present. + 'skip base' => array('node', 'node_revision', 'users', 'comment', 'taxonomy_term_data', 'taxonomy_vocabulary'), + 'real field' => 'id', + 'relationship' => array( + 'title' => t('Taxonomy Vocabulary'), + 'label' => t('Taxonomy Vocabulary'), + 'base' => 'taxonomy_vocabulary', + 'base field' => 'vid', + 'relationship field' => 'id', + 'extra' => array(array('table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'taxonomy_vocabulary')), + ), + ); + $data['file_usage']['taxonomy_vocabulary_to_file'] = array( + 'title' => t('File'), + 'help' => t('A file that is associated with this taxonomy vocabulary, usually because it is in a field on the taxonomy vocabulary.'), + // Only provide this field/relationship/etc. when the 'taxonomy_vocabulary' base table is present. + 'skip base' => array('file_managed', 'node', 'node_revision', 'users', 'comment', 'taxonomy_term_data'), + 'real field' => 'fid', + 'relationship' => array( + 'title' => t('File'), + 'label' => t('File'), + 'base' => 'file_managed', + 'base field' => 'fid', + 'relationship field' => 'fid', + ), + ); + + // Provide basic fields from the {file_usage} table to all of the base tables we've declared + // joins to (because there is no 'skip base' property on these fields). + $data['file_usage']['module'] = array( + 'title' => t('Module'), + 'help' => t('The module managing this file relationship.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + $data['file_usage']['type'] = array( + 'title' => t('Entity type'), + 'help' => t('The type of entity that is related to the file.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + $data['file_usage']['id'] = array( + 'title' => t('Entity ID'), + 'help' => t('The ID of the entity that is related to the file.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + $data['file_usage']['count'] = array( + 'title' => t('Use count'), + 'help' => t('The number of times the file is used by this entity.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // ---------------------------------------------------------------------- + // system table + $data['system']['table']['group'] = t('System'); + + // Advertise this table as a possible base table + $data['system']['table']['base'] = array( + 'field' => 'filename', + 'title' => t('Module/Theme/Theme engine'), + 'help' => t('Modules/Themes/Theme engines in your codebase.'), + ); + + // fields + // - filename + $data['system']['filename'] = array( + 'title' => t('Module/Theme/Theme engine filename'), + 'help' => t('The path of the primary file for this item, relative to the Drupal root; e.g. modules/node/node.module.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + 'name field' => 'filename', // the field to display in the summary. + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + // - name + $data['system']['name'] = array( + 'title' => t('Module/Theme/Theme engine name'), + 'help' => t('The name of the item; e.g. node.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + 'name field' => 'name', // the field to display in the summary. + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + // - type + $data['system']['type'] = array( + 'title' => t('Type'), + 'help' => t('The type of the item, either module, theme, or theme_engine.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + 'name field' => 'type', // the field to display in the summary. + ), + 'filter' => array( + 'handler' => 'views_handler_filter_system_type', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + // - status + $data['system']['status'] = array( + 'title' => t('Status'), + 'help' => t('Boolean indicating whether or not this item is enabled.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + 'name field' => 'status', // the field to display in the summary. + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + // - schema version + $data['system']['schema_version'] = array( + 'title' => t('Schema version'), + 'help' => t("The module's database schema version number. -1 if the module is not installed (its tables do not exist); 0 or the largest N of the module's hook_update_N() function that has either been run or existed when the module was first installed."), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + 'name field' => 'schema_version', // the field to display in the summary. + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + return $data; +} + +function _views_file_status($choice = NULL) { + $status = array( + 0 => t('Temporary'), + FILE_STATUS_PERMANENT => t('Permanent'), + ); + + if (isset($choice)) { + return isset($status[$choice]) ? $status[$choice] : t('Unknown'); + } + + return $status; +} diff --git a/modules/system/views_handler_argument_file_fid.inc b/modules/system/views_handler_argument_file_fid.inc new file mode 100644 index 00000000..aa2d9471 --- /dev/null +++ b/modules/system/views_handler_argument_file_fid.inc @@ -0,0 +1,28 @@ +fields('f', array('filename')) + ->condition('fid', $this->value) + ->execute() + ->fetchCol(); + foreach ($titles as &$title) { + $title = check_plain($title); + } + return $titles; + } +} diff --git a/modules/system/views_handler_field_file.inc b/modules/system/views_handler_field_file.inc new file mode 100644 index 00000000..4168acf7 --- /dev/null +++ b/modules/system/views_handler_field_file.inc @@ -0,0 +1,61 @@ +additional_fields['uri'] = 'uri'; + } + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_to_file'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to file option + */ + function options_form(&$form, &$form_state) { + $form['link_to_file'] = array( + '#title' => t('Link this field to download the file'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_file']), + ); + parent::options_form($form, $form_state); + } + + /** + * Render whatever the data is as a link to the file. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + if (!empty($this->options['link_to_file']) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = file_create_url($this->get_value($values, 'uri')); + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/system/views_handler_field_file_extension.inc b/modules/system/views_handler_field_file_extension.inc new file mode 100644 index 00000000..6f9a03fe --- /dev/null +++ b/modules/system/views_handler_field_file_extension.inc @@ -0,0 +1,19 @@ +get_value($values); + if (preg_match('/\.([^\.]+)$/', $value, $match)) { + return $this->sanitize_value($match[1]); + } + } +} diff --git a/modules/system/views_handler_field_file_filemime.inc b/modules/system/views_handler_field_file_filemime.inc new file mode 100644 index 00000000..318fdcff --- /dev/null +++ b/modules/system/views_handler_field_file_filemime.inc @@ -0,0 +1,38 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['filemime_image'] = array( + '#title' => t('Display an icon representing the file type, instead of the MIME text (such as "image/jpeg")'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['filemime_image']), + ); + parent::options_form($form, $form_state); + } + + function render($values) { + $data = $values->{$this->field_alias}; + if (!empty($this->options['filemime_image']) && $data !== NULL && $data !== '') { + $fake_file = (object) array('filemime' => $data); + $data = theme('file_icon', array('file' => $fake_file)); + } + + return $this->render_link($data, $values); + } +} diff --git a/modules/system/views_handler_field_file_status.inc b/modules/system/views_handler_field_file_status.inc new file mode 100644 index 00000000..ac1022c8 --- /dev/null +++ b/modules/system/views_handler_field_file_status.inc @@ -0,0 +1,18 @@ +get_value($values); + return _views_file_status($value); + } +} diff --git a/modules/system/views_handler_field_file_uri.inc b/modules/system/views_handler_field_file_uri.inc new file mode 100644 index 00000000..334e5051 --- /dev/null +++ b/modules/system/views_handler_field_file_uri.inc @@ -0,0 +1,35 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['file_download_path'] = array( + '#title' => t('Display download path instead of file storage URI'), + '#description' => t('This will provide the full download URL rather than the internal filestream address.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['file_download_path']), + ); + parent::options_form($form, $form_state); + } + + function render($values) { + $data = $values->{$this->field_alias}; + if (!empty($this->options['file_download_path']) && $data !== NULL && $data !== '') { + $data = file_create_url($data); + } + return $this->render_link($data, $values); + } +} diff --git a/modules/system/views_handler_filter_file_status.inc b/modules/system/views_handler_filter_file_status.inc new file mode 100644 index 00000000..6194395b --- /dev/null +++ b/modules/system/views_handler_filter_file_status.inc @@ -0,0 +1,19 @@ +value_options)) { + $this->value_options = _views_file_status(); + } + } +} diff --git a/modules/system/views_handler_filter_system_type.inc b/modules/system/views_handler_filter_system_type.inc new file mode 100644 index 00000000..84d4bcd8 --- /dev/null +++ b/modules/system/views_handler_filter_system_type.inc @@ -0,0 +1,21 @@ +value_options)) { + $this->value_title = t('Type'); + // Enable filtering by type. + $types = array(); + $types = db_query('SELECT DISTINCT(type) FROM {system} ORDER BY type')->fetchAllKeyed(0, 0); + $this->value_options = $types; + } + } +} diff --git a/modules/taxonomy.views.inc b/modules/taxonomy.views.inc new file mode 100644 index 00000000..58d62d14 --- /dev/null +++ b/modules/taxonomy.views.inc @@ -0,0 +1,540 @@ + array( + 'left_field' => 'vid', + 'field' => 'vid', + ), + ); + + // Provide a "default relationship" to keep older views from choking. + $data['taxonomy_vocabulary']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'term_node_tid', + ), + ); + + // vocabulary name + $data['taxonomy_vocabulary']['name'] = array( + 'title' => t('Name'), // The item it appears as on the UI, + 'field' => array( + 'help' => t('Name of the vocabulary a term is a member of. This will be the vocabulary that whichever term the "Taxonomy: Term" field is; and can similarly cause duplicates.'), + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + 'help' => t('The taxonomy vocabulary name'), + ), + ); + $data['taxonomy_vocabulary']['machine_name'] = array( + 'title' => t('Machine name'), // The item it appears as on the UI, + 'field' => array( + 'help' => t('Machine-Name of the vocabulary a term is a member of. This will be the vocabulary that whichever term the "Taxonomy: Term" field is; and can similarly cause duplicates.'), + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'help' => t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'), + 'handler' => 'views_handler_filter_vocabulary_machine_name', + ), + 'argument' => array( + 'help' => t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'), + 'handler' => 'views_handler_argument_vocabulary_machine_name', + ), + ); + $data['taxonomy_vocabulary']['vid'] = array( + 'title' => t('Vocabulary ID'), // The item it appears as on the UI, + 'help' => t('The taxonomy vocabulary ID'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_vocabulary_vid', + 'name field' => 'name', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + $data['taxonomy_vocabulary']['description'] = array( + 'title' => t('Description'), // The item it appears as on the UI, + 'help' => t('The taxonomy vocabulary description'), + 'field' => array( + 'handler' => 'views_handler_field', + ), + ); + $data['taxonomy_vocabulary']['weight'] = array( + 'title' => t('Weight'), + 'help' => t('The taxonomy vocabulary weight'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + 'name field' => 'weight', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // ---------------------------------------------------------------------- + // taxonomy_term_data table + + $data['term_data']['moved to'] = 'taxonomy_term_data'; + $data['taxonomy_term_data']['table']['group'] = t('Taxonomy term'); + $data['taxonomy_term_data']['table']['base'] = array( + 'field' => 'tid', + 'title' => t('Term'), + 'help' => t('Taxonomy terms are attached to nodes.'), + 'access query tag' => 'term_access', + ); + $data['taxonomy_term_data']['table']['entity type'] = 'taxonomy_term'; + + + + // The term data table + $data['taxonomy_term_data']['table']['join'] = array( + 'taxonomy_vocabulary' => array( + 'field' => 'vid', + 'left_field' => 'vid', + ), + // This is provided for many_to_one argument + 'taxonomy_index' => array( + 'field' => 'tid', + 'left_field' => 'tid', + ), + ); + + // Provide a "default relationship" to keep older views from choking. + $data['taxonomy_term_data']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'term_node_tid', + ), + ); + + // tid field + $data['taxonomy_term_data']['tid'] = array( + 'title' => t('Term ID'), + 'help' => t('The tid of a taxonomy term.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_taxonomy', + 'name field' => 'name', + 'zero is null' => TRUE, + ), + 'filter' => array( + 'title' => t('Term'), + 'help' => t('Taxonomy term chosen from autocomplete or select widget.'), + 'handler' => 'views_handler_filter_term_node_tid', + 'hierarchy table' => 'taxonomy_term_hierarchy', + 'numeric' => TRUE, + ), + ); + + // raw tid field + $data['taxonomy_term_data']['tid_raw'] = array( + 'title' => t('Term ID'), + 'help' => t('The tid of a taxonomy term.'), + 'real field' => 'tid', + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + 'allow empty' => TRUE, + ), + ); + + $data['taxonomy_term_data']['tid_representative'] = array( + 'relationship' => array( + 'title' => t('Representative node'), + 'label' => t('Representative node'), + 'help' => t('Obtains a single representative node for each term, according to a chosen sort criterion.'), + 'handler' => 'views_handler_relationship_groupwise_max', + 'relationship field' => 'tid', + 'outer field' => 'taxonomy_term_data.tid', + 'argument table' => 'taxonomy_term_data', + 'argument field' => 'tid', + 'base' => 'node', + 'field' => 'nid', + ), + ); + + // Term name field + $data['taxonomy_term_data']['name'] = array( + 'title' => t('Name'), + 'help' => t('The taxonomy term name.'), + 'field' => array( + 'handler' => 'views_handler_field_taxonomy', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + 'help' => t('Taxonomy term name.'), + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + 'help' => t('Taxonomy term name.'), + 'many to one' => TRUE, + 'empty field name' => t('Uncategorized'), + ), + ); + + // taxonomy weight + $data['taxonomy_term_data']['weight'] = array( + 'title' => t('Weight'), + 'help' => t('The term weight field'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + ), + ); + + // Term description + $data['taxonomy_term_data']['description'] = array( + 'title' => t('Term description'), + 'help' => t('The description associated with a taxonomy term.'), + 'field' => array( + 'handler' => 'views_handler_field_markup', + 'format' => array('field' => 'format'), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // Term vocabulary + $data['taxonomy_term_data']['vid'] = array( + 'title' => t('Vocabulary'), + 'help' => t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'), + 'filter' => array( + 'handler' => 'views_handler_filter_vocabulary_vid', + ), + ); + + // Link to edit the term + $data['taxonomy_term_data']['edit_term'] = array( + 'field' => array( + 'title' => t('Term edit link'), + 'help' => t('Provide a simple link to edit the term.'), + 'handler' => 'views_handler_field_term_link_edit', + ), + ); + + // ---------------------------------------------------------------------- + // taxonomy_index table + + $data['term_node']['moved to'] = 'taxonomy_index'; + $data['taxonomy_index']['table']['group'] = t('Taxonomy term'); + + $data['taxonomy_index']['table']['join'] = array( + 'taxonomy_term_data' => array( + // links directly to taxonomy_term_data via tid + 'left_field' => 'tid', + 'field' => 'tid', + ), + 'node' => array( + // links directly to node via nid + 'left_field' => 'nid', + 'field' => 'nid', + ), + 'taxonomy_term_hierarchy' => array( + 'left_field' => 'tid', + 'field' => 'tid', + ), + ); + + $data['taxonomy_index']['nid'] = array( + 'title' => t('Content with term'), + 'help' => t('Relate all content tagged with a term.'), + 'relationship' => array( + 'handler' => 'views_handler_relationship', + 'base' => 'node', + 'base field' => 'nid', + 'label' => t('node'), + 'skip base' => 'node', + ), + ); + + // @todo This stuff needs to move to a node field since + // really it's all about nodes. + // tid field + $data['taxonomy_index']['tid'] = array( + 'group' => t('Content'), + 'title' => t('Has taxonomy term ID'), + 'help' => t('Display content if it has the selected taxonomy terms.'), + 'argument' => array( + 'handler' => 'views_handler_argument_term_node_tid', + 'name table' => 'taxonomy_term_data', + 'name field' => 'name', + 'empty field name' => t('Uncategorized'), + 'numeric' => TRUE, + 'skip base' => 'taxonomy_term_data', + ), + 'filter' => array( + 'title' => t('Has taxonomy term'), + 'handler' => 'views_handler_filter_term_node_tid', + 'hierarchy table' => 'taxonomy_term_hierarchy', + 'numeric' => TRUE, + 'skip base' => 'taxonomy_term_data', + 'allow empty' => TRUE, + ), + ); + + // ---------------------------------------------------------------------- + // term_hierarchy table + + $data['taxonomy_term_hierarchy']['table']['group'] = t('Taxonomy term'); + + $data['term_hierarchy']['moved to'] = 'taxonomy_term_hierarchy'; + $data['taxonomy_term_hierarchy']['table']['join'] = array( + 'taxonomy_term_hierarchy' => array( + // links to self through left.parent = right.tid (going down in depth) + 'left_field' => 'tid', + 'field' => 'parent', + ), + 'taxonomy_term_data' => array( + // links directly to taxonomy_term_data via tid + 'left_field' => 'tid', + 'field' => 'tid', + ), + ); + + // Provide a "default relationship" to keep older views from choking. + $data['taxonomy_term_hierarchy']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'term_node_tid', + ), + ); + + $data['taxonomy_term_hierarchy']['parent'] = array( + 'title' => t('Parent term'), + 'help' => t('The parent term of the term. This can produce duplicate entries if you are using a vocabulary that allows multiple parents.'), + 'relationship' => array( + 'base' => 'taxonomy_term_data', + 'field' => 'parent', + 'label' => t('Parent'), + ), + 'filter' => array( + 'help' => t('Filter the results of "Taxonomy: Term" by the parent pid.'), + 'handler' => 'views_handler_filter_numeric', + ), + 'argument' => array( + 'help' => t('The parent term of the term.'), + 'handler' => 'views_handler_argument_taxonomy', + ), + ); + + return $data; +} + +/** + * Implements hook_views_data_alter(). + */ +function taxonomy_views_data_alter(&$data) { + $data['node']['term_node_tid'] = array( + 'title' => t('Taxonomy terms on node'), + 'help' => t('Relate nodes to taxonomy terms, specifiying which vocabulary or vocabularies to use. This relationship will cause duplicated records if there are multiple terms.'), + 'relationship' => array( + 'handler' => 'views_handler_relationship_node_term_data', + 'label' => t('term'), + 'base' => 'taxonomy_term_data', + ), + 'field' => array( + 'title' => t('All taxonomy terms'), + 'help' => t('Display all taxonomy terms associated with a node from specified vocabularies.'), + 'handler' => 'views_handler_field_term_node_tid', + 'no group by' => TRUE, + ), + ); + + $data['node']['term_node_tid_depth'] = array( + 'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth.'), + 'real field' => 'nid', + 'argument' => array( + 'title' => t('Has taxonomy term ID (with depth)'), + 'handler' => 'views_handler_argument_term_node_tid_depth', + 'accept depth modifier' => TRUE, + ), + 'filter' => array( + 'title' => t('Has taxonomy terms (with depth)'), + 'handler' => 'views_handler_filter_term_node_tid_depth', + ), + ); + + $data['node']['term_node_tid_depth_modifier'] = array( + 'title' => t('Has taxonomy term ID depth modifier'), + 'help' => t('Allows the "depth" for Taxonomy: Term ID (with depth) to be modified via an additional contextual filter value.'), + 'argument' => array( + 'handler' => 'views_handler_argument_term_node_tid_depth_modifier', + ), + ); +} + +/** + * Implements hook_field_views_data(). + * + * Views integration for taxonomy_term_reference fields. Adds a term relationship to the default + * field data. + * + * @see field_views_field_default_views_data() + */ +function taxonomy_field_views_data($field) { + $data = field_views_field_default_views_data($field); + foreach ($data as $table_name => $table_data) { + foreach ($table_data as $field_name => $field_data) { + if (isset($field_data['filter']) && $field_name != 'delta') { + $data[$table_name][$field_name]['filter']['handler'] = 'views_handler_filter_term_node_tid'; + $data[$table_name][$field_name]['filter']['vocabulary'] = $field['settings']['allowed_values'][0]['vocabulary']; + } + } + + // Add the relationship only on the tid field. + $data[$table_name][$field['field_name'] . '_tid']['relationship'] = array( + 'handler' => 'views_handler_relationship', + 'base' => 'taxonomy_term_data', + 'base field' => 'tid', + 'label' => t('term from !field_name', array('!field_name' => $field['field_name'])), + ); + + } + + return $data; +} + +/** + * Implements hook_field_views_data_views_data_alter(). + * + * Views integration to provide reverse relationships on term references. + */ +function taxonomy_field_views_data_views_data_alter(&$data, $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + $entity_info = entity_get_info($entity_type); + $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type; + + list($label, $all_labels) = field_views_field_label($field['field_name']); + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + + $data['taxonomy_term_data'][$pseudo_field_name]['relationship'] = array( + 'title' => t('@entity using @field', array('@entity' => $entity, '@field' => $label)), + 'help' => t('Relate each @entity with a @field set to the term.', array('@entity' => $entity, '@field' => $label)), + 'handler' => 'views_handler_relationship_entity_reverse', + 'field_name' => $field['field_name'], + 'field table' => _field_sql_storage_tablename($field), + 'field field' => $field['field_name'] . '_tid', + 'base' => $entity_info['base table'], + 'base field' => $entity_info['entity keys']['id'], + 'label' => t('!field_name', array('!field_name' => $field['field_name'])), + 'join_extra' => array( + 0 => array( + 'field' => 'entity_type', + 'value' => $entity_type, + ), + 1 => array( + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ), + ), + ); + } +} + +/** + * Implements hook_views_plugins(). + */ +function taxonomy_views_plugins() { + return array( + 'module' => 'views', // This just tells our themes are elsewhere. + 'argument validator' => array( + 'taxonomy_term' => array( + 'title' => t('Taxonomy term'), + 'handler' => 'views_plugin_argument_validate_taxonomy_term', + 'path' => drupal_get_path('module', 'views') . '/modules/taxonomy', // not necessary for most modules + ), + ), + 'argument default' => array( + 'taxonomy_tid' => array( + 'title' => t('Taxonomy term ID from URL'), + 'handler' => 'views_plugin_argument_default_taxonomy_tid', + 'path' => drupal_get_path('module', 'views') . '/modules/taxonomy', + 'parent' => 'fixed', + ), + ), + ); +} + +/** + * Helper function to set a breadcrumb for taxonomy. + */ +function views_taxonomy_set_breadcrumb(&$breadcrumb, &$argument) { + if (empty($argument->options['set_breadcrumb'])) { + return; + } + + $args = $argument->view->args; + $parents = taxonomy_get_parents_all($argument->argument); + foreach (array_reverse($parents) as $parent) { + // Unfortunately parents includes the current argument. Skip. + if ($parent->tid == $argument->argument) { + continue; + } + if (!empty($argument->options['use_taxonomy_term_path'])) { + $path = taxonomy_term_uri($parent); + $path = $path['path']; + } + else { + $args[$argument->position] = $parent->tid; + $path = $argument->view->get_url($args); + } + $breadcrumb[$path] = check_plain($parent->name); + } +} diff --git a/modules/taxonomy.views_default.inc b/modules/taxonomy.views_default.inc new file mode 100644 index 00000000..db53fc24 --- /dev/null +++ b/modules/taxonomy.views_default.inc @@ -0,0 +1,108 @@ +name = 'taxonomy_term'; + $view->description = 'A view to emulate Drupal core\'s handling of taxonomy/term.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Taxonomy term'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = TRUE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + /* Sort criterion: Content: Sticky */ + $handler->display->display_options['sorts']['sticky']['id'] = 'sticky'; + $handler->display->display_options['sorts']['sticky']['table'] = 'node'; + $handler->display->display_options['sorts']['sticky']['field'] = 'sticky'; + $handler->display->display_options['sorts']['sticky']['order'] = 'DESC'; + /* Sort criterion: Content: Post date */ + $handler->display->display_options['sorts']['created']['id'] = 'created'; + $handler->display->display_options['sorts']['created']['table'] = 'node'; + $handler->display->display_options['sorts']['created']['field'] = 'created'; + $handler->display->display_options['sorts']['created']['order'] = 'DESC'; + /* Contextual filter: Content: Has taxonomy term ID (with depth) */ + $handler->display->display_options['arguments']['term_node_tid_depth']['id'] = 'term_node_tid_depth'; + $handler->display->display_options['arguments']['term_node_tid_depth']['table'] = 'node'; + $handler->display->display_options['arguments']['term_node_tid_depth']['field'] = 'term_node_tid_depth'; + $handler->display->display_options['arguments']['term_node_tid_depth']['default_action'] = 'not found'; + $handler->display->display_options['arguments']['term_node_tid_depth']['exception']['title_enable'] = 1; + $handler->display->display_options['arguments']['term_node_tid_depth']['title_enable'] = 1; + $handler->display->display_options['arguments']['term_node_tid_depth']['title'] = '%1'; + $handler->display->display_options['arguments']['term_node_tid_depth']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['term_node_tid_depth']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['term_node_tid_depth']['specify_validation'] = 1; + $handler->display->display_options['arguments']['term_node_tid_depth']['validate']['type'] = 'taxonomy_term'; + $handler->display->display_options['arguments']['term_node_tid_depth']['depth'] = '0'; + $handler->display->display_options['arguments']['term_node_tid_depth']['break_phrase'] = 1; + /* Contextual filter: Content: Has taxonomy term ID depth modifier */ + $handler->display->display_options['arguments']['term_node_tid_depth_modifier']['id'] = 'term_node_tid_depth_modifier'; + $handler->display->display_options['arguments']['term_node_tid_depth_modifier']['table'] = 'node'; + $handler->display->display_options['arguments']['term_node_tid_depth_modifier']['field'] = 'term_node_tid_depth_modifier'; + $handler->display->display_options['arguments']['term_node_tid_depth_modifier']['exception']['title_enable'] = 1; + $handler->display->display_options['arguments']['term_node_tid_depth_modifier']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['term_node_tid_depth_modifier']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['term_node_tid_depth_modifier']['specify_validation'] = 1; + /* Filter criterion: Content: Published or admin */ + $handler->display->display_options['filters']['status_extra']['id'] = 'status_extra'; + $handler->display->display_options['filters']['status_extra']['table'] = 'node'; + $handler->display->display_options['filters']['status_extra']['field'] = 'status_extra'; + $handler->display->display_options['filters']['status_extra']['group'] = 0; + $handler->display->display_options['filters']['status_extra']['expose']['operator'] = FALSE; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'taxonomy/term/%'; + + /* Display: Feed */ + $handler = $view->new_display('feed', 'Feed', 'feed'); + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = 15; + $handler->display->display_options['style_plugin'] = 'rss'; + $handler->display->display_options['row_plugin'] = 'node_rss'; + $handler->display->display_options['path'] = 'taxonomy/term/%/%/feed'; + $handler->display->display_options['displays'] = array( + 'page' => 'page', + 'default' => 0, + ); + $translatables['taxonomy_term'] = array( + t('Master'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('All'), + t('%1'), + t('Page'), + t('Feed'), + ); + + $views['taxonomy_term'] = $view; + + return $views; +} diff --git a/modules/taxonomy/views_handler_argument_taxonomy.inc b/modules/taxonomy/views_handler_argument_taxonomy.inc new file mode 100644 index 00000000..10fc500b --- /dev/null +++ b/modules/taxonomy/views_handler_argument_taxonomy.inc @@ -0,0 +1,29 @@ +argument) { + $term = taxonomy_term_load($this->argument); + if (!empty($term)) { + return check_plain($term->name); + } + } + // TODO review text + return t('No name'); + } +} diff --git a/modules/taxonomy/views_handler_argument_term_node_tid.inc b/modules/taxonomy/views_handler_argument_term_node_tid.inc new file mode 100644 index 00000000..f47f08a8 --- /dev/null +++ b/modules/taxonomy/views_handler_argument_term_node_tid.inc @@ -0,0 +1,49 @@ + FALSE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['set_breadcrumb'] = array( + '#type' => 'checkbox', + '#title' => t("Set the breadcrumb for the term parents"), + '#description' => t('If selected, the breadcrumb trail will include all parent terms, each one linking to this view. Note that this only works if just one term was received.'), + '#default_value' => !empty($this->options['set_breadcrumb']), + ); + } + + function set_breadcrumb(&$breadcrumb) { + if (empty($this->options['set_breadcrumb']) || !is_numeric($this->argument)) { + return; + } + + return views_taxonomy_set_breadcrumb($breadcrumb, $this); + } + + function title_query() { + $titles = array(); + $result = db_select('taxonomy_term_data', 'td') + ->fields('td', array('name')) + ->condition('td.tid', $this->value) + ->execute(); + foreach ($result as $term) { + $titles[] = check_plain($term->name); + } + return $titles; + } +} diff --git a/modules/taxonomy/views_handler_argument_term_node_tid_depth.inc b/modules/taxonomy/views_handler_argument_term_node_tid_depth.inc new file mode 100644 index 00000000..116a4ded --- /dev/null +++ b/modules/taxonomy/views_handler_argument_term_node_tid_depth.inc @@ -0,0 +1,145 @@ + 0); + $options['break_phrase'] = array('default' => FALSE, 'bool' => TRUE); + $options['set_breadcrumb'] = array('default' => FALSE, 'bool' => TRUE); + $options['use_taxonomy_term_path'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['depth'] = array( + '#type' => 'weight', + '#title' => t('Depth'), + '#default_value' => $this->options['depth'], + '#description' => t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'), + ); + + $form['break_phrase'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple values'), + '#description' => t('If selected, users can enter multiple values in the form of 1+2+3. Due to the number of JOINs it would require, AND will be treated as OR with this filter.'), + '#default_value' => !empty($this->options['break_phrase']), + ); + + $form['set_breadcrumb'] = array( + '#type' => 'checkbox', + '#title' => t("Set the breadcrumb for the term parents"), + '#description' => t('If selected, the breadcrumb trail will include all parent terms, each one linking to this view. Note that this only works if just one term was received.'), + '#default_value' => !empty($this->options['set_breadcrumb']), + ); + + $form['use_taxonomy_term_path'] = array( + '#type' => 'checkbox', + '#title' => t("Use Drupal's taxonomy term path to create breadcrumb links"), + '#description' => t('If selected, the links in the breadcrumb trail will be created using the standard drupal method instead of the custom views method. This is useful if you are using modules like taxonomy redirect to modify your taxonomy term links.'), + '#default_value' => !empty($this->options['use_taxonomy_term_path']), + '#dependency' => array('edit-options-set-breadcrumb' => array(TRUE)), + ); + parent::options_form($form, $form_state); + } + + function set_breadcrumb(&$breadcrumb) { + if (empty($this->options['set_breadcrumb']) || !is_numeric($this->argument)) { + return; + } + + return views_taxonomy_set_breadcrumb($breadcrumb, $this); + } + + /** + * Override default_actions() to remove summary actions. + */ + function default_actions($which = NULL) { + if ($which) { + if (in_array($which, array('ignore', 'not found', 'empty', 'default'))) { + return parent::default_actions($which); + } + return; + } + $actions = parent::default_actions(); + unset($actions['summary asc']); + unset($actions['summary desc']); + unset($actions['summary asc by count']); + unset($actions['summary desc by count']); + return $actions; + } + + function query($group_by = FALSE) { + $this->ensure_my_table(); + + if (!empty($this->options['break_phrase'])) { + $tids = new stdClass(); + $tids->value = $this->argument; + $tids = views_break_phrase($this->argument, $tids); + if ($tids->value == array(-1)) { + return FALSE; + } + + if (count($tids->value) > 1) { + $operator = 'IN'; + } + else { + $operator = '='; + } + + $tids = $tids->value; + } + else { + $operator = "="; + $tids = $this->argument; + } + // Now build the subqueries. + $subquery = db_select('taxonomy_index', 'tn'); + $subquery->addField('tn', 'nid'); + $where = db_or()->condition('tn.tid', $tids, $operator); + $last = "tn"; + + if ($this->options['depth'] > 0) { + $subquery->leftJoin('taxonomy_term_hierarchy', 'th', "th.tid = tn.tid"); + $last = "th"; + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.parent = th$count.tid"); + $where->condition("th$count.tid", $tids, $operator); + $last = "th$count"; + } + } + elseif ($this->options['depth'] < 0) { + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.tid = th$count.parent"); + $where->condition("th$count.tid", $tids, $operator); + $last = "th$count"; + } + } + + $subquery->condition($where); + $this->query->add_where(0, "$this->table_alias.$this->real_field", $subquery, 'IN'); + } + + function title() { + $term = taxonomy_term_load($this->argument); + if (!empty($term)) { + return check_plain($term->name); + } + // TODO review text + return t('No name'); + } +} diff --git a/modules/taxonomy/views_handler_argument_term_node_tid_depth_modifier.inc b/modules/taxonomy/views_handler_argument_term_node_tid_depth_modifier.inc new file mode 100644 index 00000000..2f9dd4e7 --- /dev/null +++ b/modules/taxonomy/views_handler_argument_term_node_tid_depth_modifier.inc @@ -0,0 +1,64 @@ +view->args[$this->position]) ? $this->view->args[$this->position] : NULL; + if (!is_numeric($argument)) { + return; + } + + if ($argument > 10) { + $argument = 10; + } + + if ($argument < -10) { + $argument = -10; + } + + // figure out which argument preceded us. + $keys = array_reverse(array_keys($this->view->argument)); + $skip = TRUE; + foreach ($keys as $key) { + if ($key == $this->options['id']) { + $skip = FALSE; + continue; + } + + if ($skip) { + continue; + } + + if (empty($this->view->argument[$key])) { + continue; + } + + if (isset($handler)) { + unset($handler); + } + + $handler = &$this->view->argument[$key]; + if (empty($handler->definition['accept depth modifier'])) { + continue; + } + + // Finally! + $handler->options['depth'] = $argument; + } + } +} diff --git a/modules/taxonomy/views_handler_argument_vocabulary_machine_name.inc b/modules/taxonomy/views_handler_argument_vocabulary_machine_name.inc new file mode 100644 index 00000000..427cf2b0 --- /dev/null +++ b/modules/taxonomy/views_handler_argument_vocabulary_machine_name.inc @@ -0,0 +1,26 @@ + $this->argument))->fetchField(); + + if (empty($title)) { + return t('No vocabulary'); + } + + return check_plain($title); + } +} diff --git a/modules/taxonomy/views_handler_argument_vocabulary_vid.inc b/modules/taxonomy/views_handler_argument_vocabulary_vid.inc new file mode 100644 index 00000000..c6966405 --- /dev/null +++ b/modules/taxonomy/views_handler_argument_vocabulary_vid.inc @@ -0,0 +1,26 @@ + $this->argument))->fetchField(); + + if (empty($title)) { + return t('No vocabulary'); + } + + return check_plain($title); + } +} diff --git a/modules/taxonomy/views_handler_field_taxonomy.inc b/modules/taxonomy/views_handler_field_taxonomy.inc new file mode 100644 index 00000000..48da283d --- /dev/null +++ b/modules/taxonomy/views_handler_field_taxonomy.inc @@ -0,0 +1,85 @@ +additional_fields['vid'] = 'vid'; + $this->additional_fields['tid'] = 'tid'; + $this->additional_fields['vocabulary_machine_name'] = array( + 'table' => 'taxonomy_vocabulary', + 'field' => 'machine_name', + ); + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_to_taxonomy'] = array('default' => FALSE, 'bool' => TRUE); + $options['convert_spaces'] = array('default' => FALSE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to taxonomy option + */ + function options_form(&$form, &$form_state) { + $form['link_to_taxonomy'] = array( + '#title' => t('Link this field to its taxonomy term page'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_taxonomy']), + ); + $form['convert_spaces'] = array( + '#title' => t('Convert spaces in term names to hyphens'), + '#description' => t('This allows links to work with Views taxonomy term arguments.'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['convert_spaces']), + ); + parent::options_form($form, $form_state); + } + + /** + * Render whatever the data is as a link to the taxonomy. + * + * Data should be made XSS safe prior to calling this function. + */ + function render_link($data, $values) { + $tid = $this->get_value($values, 'tid'); + if (!empty($this->options['link_to_taxonomy']) && !empty($tid) && $data !== NULL && $data !== '') { + $term = new stdClass(); + $term->tid = $tid; + $term->vid = $this->get_value($values, 'vid'); + $term->vocabulary_machine_name = $values->{$this->aliases['vocabulary_machine_name']}; + $this->options['alter']['make_link'] = TRUE; + $uri = entity_uri('taxonomy_term', $term); + $this->options['alter']['path'] = $uri['path']; + } + + if (!empty($this->options['convert_spaces'])) { + $data = str_replace(' ', '-', $data); + } + + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/taxonomy/views_handler_field_term_link_edit.inc b/modules/taxonomy/views_handler_field_term_link_edit.inc new file mode 100644 index 00000000..2efb4a65 --- /dev/null +++ b/modules/taxonomy/views_handler_field_term_link_edit.inc @@ -0,0 +1,62 @@ +additional_fields['tid'] = 'tid'; + $this->additional_fields['vid'] = 'vid'; + $this->additional_fields['vocabulary_machine_name'] = array( + 'table' => 'taxonomy_vocabulary', + 'field' => 'machine_name', + ); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['text'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::options_form($form, $form_state); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + // Check there is an actual value, as on a relationship there may not be. + if ($tid = $this->get_value($values, 'tid')) { + // Mock a term object for taxonomy_term_edit_access(). Use machine name and + // vid to ensure compatibility with vid based and machine name based + // access checks. See http://drupal.org/node/995156 + $term = new stdClass(); + $term->vid = $values->{$this->aliases['vid']}; + $term->vocabulary_machine_name = $values->{$this->aliases['vocabulary_machine_name']}; + if (taxonomy_term_edit_access($term)) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + $tid = $this->get_value($values, 'tid'); + return l($text, 'taxonomy/term/'. $tid . '/edit', array('query' => drupal_get_destination())); + } + } + } +} diff --git a/modules/taxonomy/views_handler_field_term_node_tid.inc b/modules/taxonomy/views_handler_field_term_node_tid.inc new file mode 100644 index 00000000..4c6362e5 --- /dev/null +++ b/modules/taxonomy/views_handler_field_term_node_tid.inc @@ -0,0 +1,145 @@ +base_table and no if here? + if ($view->base_table == 'node_revision') { + $this->additional_fields['nid'] = array('table' => 'node_revision', 'field' => 'nid'); + } + else { + $this->additional_fields['nid'] = array('table' => 'node', 'field' => 'nid'); + } + + // Convert legacy vids option to machine name vocabularies. + if (!empty($this->options['vids'])) { + $vocabularies = taxonomy_get_vocabularies(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['link_to_taxonomy'] = array('default' => TRUE, 'bool' => TRUE); + $options['limit'] = array('default' => FALSE, 'bool' => TRUE); + $options['vocabularies'] = array('default' => array()); + + return $options; + } + + /** + * Provide "link to term" option. + */ + function options_form(&$form, &$form_state) { + $form['link_to_taxonomy'] = array( + '#title' => t('Link this field to its term page'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['link_to_taxonomy']), + ); + + $form['limit'] = array( + '#type' => 'checkbox', + '#title' => t('Limit terms by vocabulary'), + '#default_value'=> $this->options['limit'], + ); + + $options = array(); + $vocabularies = taxonomy_get_vocabularies(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#type' => 'checkboxes', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#dependency' => array('edit-options-limit' => array(TRUE)), + ); + + parent::options_form($form, $form_state); + } + + /** + * Add this term to the query + */ + function query() { + $this->add_additional_fields(); + } + + function pre_render(&$values) { + $this->field_alias = $this->aliases['nid']; + $nids = array(); + foreach ($values as $result) { + if (!empty($result->{$this->aliases['nid']})) { + $nids[] = $result->{$this->aliases['nid']}; + } + } + + if ($nids) { + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_index', 'tn', 'td.tid = tn.tid'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->addField('tn', 'nid', 'node_nid'); + $query->addField('tv', 'name', 'vocabulary'); + $query->addField('tv', 'machine_name', 'vocabulary_machine_name'); + $query->orderby('td.weight'); + $query->orderby('td.name'); + $query->condition('tn.nid', $nids); + $query->addTag('term_access'); + $vocabs = array_filter($this->options['vocabularies']); + if (!empty($this->options['limit']) && !empty($vocabs)) { + $query->condition('tv.machine_name', $vocabs); + } + $result = $query->execute(); + + foreach ($result as $term) { + $this->items[$term->node_nid][$term->tid]['name'] = check_plain($term->name); + $this->items[$term->node_nid][$term->tid]['tid'] = $term->tid; + $this->items[$term->node_nid][$term->tid]['vocabulary_machine_name'] = check_plain($term->vocabulary_machine_name); + $this->items[$term->node_nid][$term->tid]['vocabulary'] = check_plain($term->vocabulary); + + if (!empty($this->options['link_to_taxonomy'])) { + $this->items[$term->node_nid][$term->tid]['make_link'] = TRUE; + $this->items[$term->node_nid][$term->tid]['path'] = 'taxonomy/term/' . $term->tid; + } + } + } + } + + function render_item($count, $item) { + return $item['name']; + } + + function document_self_tokens(&$tokens) { + $tokens['[' . $this->options['id'] . '-tid' . ']'] = t('The taxonomy term ID for the term.'); + $tokens['[' . $this->options['id'] . '-name' . ']'] = t('The taxonomy term name for the term.'); + $tokens['[' . $this->options['id'] . '-vocabulary-machine-name' . ']'] = t('The machine name for the vocabulary the term belongs to.'); + $tokens['[' . $this->options['id'] . '-vocabulary' . ']'] = t('The name for the vocabulary the term belongs to.'); + } + + function add_self_tokens(&$tokens, $item) { + foreach(array('tid', 'name', 'vocabulary_machine_name', 'vocabulary') as $token) { + // Replace _ with - for the vocabulary machine name. + $tokens['[' . $this->options['id'] . '-' . str_replace('_', '-', $token). ']'] = isset($item[$token]) ? $item[$token] : ''; + } + } +} diff --git a/modules/taxonomy/views_handler_filter_term_node_tid.inc b/modules/taxonomy/views_handler_filter_term_node_tid.inc new file mode 100644 index 00000000..7eb868f7 --- /dev/null +++ b/modules/taxonomy/views_handler_filter_term_node_tid.inc @@ -0,0 +1,360 @@ +definition['vocabulary'])) { + $this->options['vocabulary'] = $this->definition['vocabulary']; + } + + // Convert legacy vid option to machine name vocabulary. + if (isset($this->options['vid']) && !empty($this->options['vid']) & empty($this->options['vocabulary'])) { + $vocabularies = taxonomy_get_vocabularies(); + $vid = $this->options['vid']; + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabulary'] = $vocabularies[$vid]->machine_name; + } + } + } + + function has_extra_options() { return TRUE; } + + function get_value_options() { /* don't overwrite the value options */ } + + function option_definition() { + $options = parent::option_definition(); + + $options['type'] = array('default' => 'textfield'); + $options['limit'] = array('default' => TRUE, 'bool' => TRUE); + $options['vocabulary'] = array('default' => 0); + $options['hierarchy'] = array('default' => 0); + $options['error_message'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + function extra_options_form(&$form, &$form_state) { + $vocabularies = taxonomy_get_vocabularies(); + $options = array(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + if ($this->options['limit']) { + // We only do this when the form is displayed. + if (empty($this->options['vocabulary'])) { + $first_vocabulary = reset($vocabularies); + $this->options['vocabulary'] = $first_vocabulary->machine_name; + } + + if (empty($this->definition['vocabulary'])) { + $form['vocabulary'] = array( + '#type' => 'radios', + '#title' => t('Vocabulary'), + '#options' => $options, + '#description' => t('Select which vocabulary to show terms for in the regular options.'), + '#default_value' => $this->options['vocabulary'], + ); + } + } + + $form['type'] = array( + '#type' => 'radios', + '#title' => t('Selection type'), + '#options' => array('select' => t('Dropdown'), 'textfield' => t('Autocomplete')), + '#default_value' => $this->options['type'], + ); + + $form['hierarchy'] = array( + '#type' => 'checkbox', + '#title' => t('Show hierarchy in dropdown'), + '#default_value' => !empty($this->options['hierarchy']), + '#dependency' => array('radio:options[type]' => array('select')), + ); + } + + function value_form(&$form, &$form_state) { + $vocabulary = taxonomy_vocabulary_machine_name_load($this->options['vocabulary']); + if (empty($vocabulary) && $this->options['limit']) { + $form['markup'] = array( + '#markup' => '
    ' . t('An invalid vocabulary is selected. Please change it in the options.') . '
    ', + ); + return; + } + + if ($this->options['type'] == 'textfield') { + $default = ''; + if ($this->value) { + $result = db_select('taxonomy_term_data', 'td') + ->fields('td') + ->condition('td.tid', $this->value) + ->execute(); + foreach ($result as $term) { + if ($default) { + $default .= ', '; + } + $default .= $term->name; + } + } + + $form['value'] = array( + '#title' => $this->options['limit'] ? t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)) : t('Select terms'), + '#type' => 'textfield', + '#default_value' => $default, + ); + + if ($this->options['limit']) { + $form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid; + } + } + else { + if (!empty($this->options['hierarchy']) && $this->options['limit']) { + $tree = taxonomy_get_tree($vocabulary->vid); + $options = array(); + + if ($tree) { + foreach ($tree as $term) { + $choice = new stdClass(); + $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name); + $options[] = $choice; + } + } + } + else { + $options = array(); + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->orderby('tv.weight'); + $query->orderby('tv.name'); + $query->orderby('td.weight'); + $query->orderby('td.name'); + $query->addTag('term_access'); + if ($this->options['limit']) { + $query->condition('tv.machine_name', $vocabulary->machine_name); + } + $result = $query->execute(); + foreach ($result as $term) { + $options[$term->tid] = $term->name; + } + } + + $default_value = (array) $this->value; + + if (!empty($form_state['exposed'])) { + $identifier = $this->options['expose']['identifier']; + + if (!empty($this->options['expose']['reduce'])) { + $options = $this->reduce_value_options($options); + + if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) { + $default_value = array(); + } + } + + if (empty($this->options['expose']['multiple'])) { + if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) { + $default_value = 'All'; + } + elseif (empty($default_value)) { + $keys = array_keys($options); + $default_value = array_shift($keys); + } + // Due to #1464174 there is a chance that array('') was saved in the admin ui. + // Let's choose a safe default value. + elseif ($default_value == array('')) { + $default_value = 'All'; + } + else { + $copy = $default_value; + $default_value = array_shift($copy); + } + } + } + $form['value'] = array( + '#type' => 'select', + '#title' => $this->options['limit'] ? t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name)) : t('Select terms'), + '#multiple' => TRUE, + '#options' => $options, + '#size' => min(9, count($options)), + '#default_value' => $default_value, + ); + + if (!empty($form_state['exposed']) && isset($identifier) && !isset($form_state['input'][$identifier])) { + $form_state['input'][$identifier] = $default_value; + } + } + + + if (empty($form_state['exposed'])) { + // Retain the helper option + $this->helper->options_form($form, $form_state); + } + } + + function value_validate($form, &$form_state) { + // We only validate if they've chosen the text field style. + if ($this->options['type'] != 'textfield') { + return; + } + + $values = drupal_explode_tags($form_state['values']['options']['value']); + $tids = $this->validate_term_strings($form['value'], $values); + + if ($tids) { + $form_state['values']['options']['value'] = $tids; + } + } + + function accept_exposed_input($input) { + if (empty($this->options['exposed'])) { + return TRUE; + } + + // If view is an attachment and is inheriting exposed filters, then assume + // exposed input has already been validated + if (!empty($this->view->is_attachment) && $this->view->display_handler->uses_exposed()) { + $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']]; + } + + // If it's non-required and there's no value don't bother filtering. + if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) { + return FALSE; + } + + $rc = parent::accept_exposed_input($input); + if ($rc) { + // If we have previously validated input, override. + if (isset($this->validated_exposed_input)) { + $this->value = $this->validated_exposed_input; + } + } + + return $rc; + } + + function exposed_validate(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + $identifier = $this->options['expose']['identifier']; + + // We only validate if they've chosen the text field style. + if ($this->options['type'] != 'textfield') { + if ($form_state['values'][$identifier] != 'All') { + $this->validated_exposed_input = (array) $form_state['values'][$identifier]; + } + return; + } + + if (empty($this->options['expose']['identifier'])) { + return; + } + + $values = drupal_explode_tags($form_state['values'][$identifier]); + + $tids = $this->validate_term_strings($form[$identifier], $values); + if ($tids) { + $this->validated_exposed_input = $tids; + } + } + + /** + * Validate the user string. Since this can come from either the form + * or the exposed filter, this is abstracted out a bit so it can + * handle the multiple input sources. + * + * @param $form + * The form which is used, either the views ui or the exposed filters. + * @param $values + * The taxonomy names which will be converted to tids. + * + * @return array + * The taxonomy ids fo all validated terms. + */ + function validate_term_strings(&$form, $values) { + if (empty($values)) { + return array(); + } + + $tids = array(); + $names = array(); + $missing = array(); + foreach ($values as $value) { + $missing[strtolower($value)] = TRUE; + $names[] = $value; + } + + if (!$names) { + return FALSE; + } + + $query = db_select('taxonomy_term_data', 'td'); + $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->condition('td.name', $names); + $query->condition('tv.machine_name', $this->options['vocabulary']); + $query->addTag('term_access'); + $result = $query->execute(); + foreach ($result as $term) { + unset($missing[strtolower($term->name)]); + $tids[] = $term->tid; + } + + if ($missing && !empty($this->options['error_message'])) { + form_error($form, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing))))); + } + elseif ($missing && empty($this->options['error_message'])) { + $tids = array(0); + } + + return $tids; + } + + function value_submit($form, &$form_state) { + // prevent array_filter from messing up our arrays in parent submit. + } + + function expose_form(&$form, &$form_state) { + parent::expose_form($form, $form_state); + if ($this->options['type'] != 'select') { + unset($form['expose']['reduce']); + } + $form['error_message'] = array( + '#type' => 'checkbox', + '#title' => t('Display error message'), + '#default_value' => !empty($this->options['error_message']), + ); + } + + function admin_summary() { + // set up $this->value_options for the parent summary + $this->value_options = array(); + + if ($this->value) { + $this->value = array_filter($this->value); + $result = db_select('taxonomy_term_data', 'td') + ->fields('td') + ->condition('td.tid', $this->value) + ->execute(); + foreach ($result as $term) { + $this->value_options[$term->tid] = $term->name; + } + } + return parent::admin_summary(); + } +} diff --git a/modules/taxonomy/views_handler_filter_term_node_tid_depth.inc b/modules/taxonomy/views_handler_filter_term_node_tid_depth.inc new file mode 100644 index 00000000..fe12780f --- /dev/null +++ b/modules/taxonomy/views_handler_filter_term_node_tid_depth.inc @@ -0,0 +1,100 @@ + t('Is one of'), + ); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['depth'] = array('default' => 0); + + return $options; + } + + function extra_options_form(&$form, &$form_state) { + parent::extra_options_form($form, $form_state); + + $form['depth'] = array( + '#type' => 'weight', + '#title' => t('Depth'), + '#default_value' => $this->options['depth'], + '#description' => t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'), + ); + } + + function query() { + // If no filter values are present, then do nothing. + if (count($this->value) == 0) { + return; + } + elseif (count($this->value) == 1) { + // Somethis $this->value is an array with a single element so convert it. + if (is_array($this->value)) { + $this->value = current($this->value); + } + $operator = '='; + } + else { + $operator = 'IN';# " IN (" . implode(', ', array_fill(0, sizeof($this->value), '%d')) . ")"; + } + + // The normal use of ensure_my_table() here breaks Views. + // So instead we trick the filter into using the alias of the base table. + // See http://drupal.org/node/271833 + // If a relationship is set, we must use the alias it provides. + if (!empty($this->relationship)) { + $this->table_alias = $this->relationship; + } + // If no relationship, then use the alias of the base table. + elseif (isset($this->query->table_queue[$this->query->base_table]['alias'])) { + $this->table_alias = $this->query->table_queue[$this->query->base_table]['alias']; + } + // This should never happen, but if it does, we fail quietly. + else { + return; + } + + // Now build the subqueries. + $subquery = db_select('taxonomy_index', 'tn'); + $subquery->addField('tn', 'nid'); + $where = db_or()->condition('tn.tid', $this->value, $operator); + $last = "tn"; + + if ($this->options['depth'] > 0) { + $subquery->leftJoin('taxonomy_term_hierarchy', 'th', "th.tid = tn.tid"); + $last = "th"; + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.parent = th$count.tid"); + $where->condition("th$count.tid", $this->value, $operator); + $last = "th$count"; + } + } + elseif ($this->options['depth'] < 0) { + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.tid = th$count.parent"); + $where->condition("th$count.tid", $this->value, $operator); + $last = "th$count"; + } + } + + $subquery->condition($where); + $this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", $subquery, 'IN'); + } +} diff --git a/modules/taxonomy/views_handler_filter_vocabulary_machine_name.inc b/modules/taxonomy/views_handler_filter_vocabulary_machine_name.inc new file mode 100644 index 00000000..062450c7 --- /dev/null +++ b/modules/taxonomy/views_handler_filter_vocabulary_machine_name.inc @@ -0,0 +1,25 @@ +value_options)) { + return; + } + + $this->value_options = array(); + $vocabularies = taxonomy_get_vocabularies(); + foreach ($vocabularies as $voc) { + $this->value_options[$voc->machine_name] = $voc->name; + } + } +} diff --git a/modules/taxonomy/views_handler_filter_vocabulary_vid.inc b/modules/taxonomy/views_handler_filter_vocabulary_vid.inc new file mode 100644 index 00000000..2759ee1e --- /dev/null +++ b/modules/taxonomy/views_handler_filter_vocabulary_vid.inc @@ -0,0 +1,25 @@ +value_options)) { + return; + } + + $this->value_options = array(); + $vocabularies = taxonomy_get_vocabularies(); + foreach ($vocabularies as $voc) { + $this->value_options[$voc->vid] = $voc->name; + } + } +} diff --git a/modules/taxonomy/views_handler_relationship_node_term_data.inc b/modules/taxonomy/views_handler_relationship_node_term_data.inc new file mode 100644 index 00000000..d7fbb4c9 --- /dev/null +++ b/modules/taxonomy/views_handler_relationship_node_term_data.inc @@ -0,0 +1,95 @@ +options['vids'])) { + $vocabularies = taxonomy_get_vocabularies(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + function option_definition() { + $options = parent::option_definition(); + $options['vocabularies'] = array('default' => array()); + return $options; + } + + function options_form(&$form, &$form_state) { + $vocabularies = taxonomy_get_vocabularies(); + $options = array(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#type' => 'checkboxes', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#description' => t('Choose which vocabularies you wish to relate. Remember that every term found will create a new record, so this relationship is best used on just one vocabulary that has only one term per node.'), + ); + parent::options_form($form, $form_state); + } + + /** + * Called to implement a relationship in a query. + */ + function query() { + $this->ensure_my_table(); + + $def = $this->definition; + $def['table'] = 'taxonomy_term_data'; + + if (!array_filter($this->options['vocabularies'])) { + $taxonomy_index = $this->query->add_table('taxonomy_index', $this->relationship); + $def['left_table'] = $taxonomy_index; + $def['left_field'] = 'tid'; + $def['field'] = 'tid'; + $def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER'; + } + else { + // If vocabularies are supplied join a subselect instead + $def['left_table'] = $this->table_alias; + $def['left_field'] = 'nid'; + $def['field'] = 'nid'; + $def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER'; + + $query = db_select('taxonomy_term_data', 'td'); + $query->addJoin($def['type'], 'taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->addJoin($def['type'], 'taxonomy_index', 'tn', 'tn.tid = td.tid'); + $query->condition('tv.machine_name', array_filter($this->options['vocabularies'])); + $query->addTag('term_access'); + $query->fields('td'); + $query->fields('tn', array('nid')); + $def['table formula'] = $query; + } + + $join = new views_join(); + + $join->definition = $def; + $join->construct(); + $join->adjusted = TRUE; + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, 'taxonomy_term_data', $this->relationship); + } +} diff --git a/modules/taxonomy/views_plugin_argument_default_taxonomy_tid.inc b/modules/taxonomy/views_plugin_argument_default_taxonomy_tid.inc new file mode 100644 index 00000000..9c1d81f9 --- /dev/null +++ b/modules/taxonomy/views_plugin_argument_default_taxonomy_tid.inc @@ -0,0 +1,154 @@ +options['vids'])) { + $vocabularies = taxonomy_get_vocabularies(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['term_page'] = array('default' => TRUE, 'bool' => TRUE); + $options['node'] = array('default' => FALSE, 'bool' => TRUE); + $options['anyall'] = array('default' => ','); + $options['limit'] = array('default' => FALSE, 'bool' => TRUE); + $options['vocabularies'] = array('default' => array()); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['term_page'] = array( + '#type' => 'checkbox', + '#title' => t('Load default filter from term page'), + '#default_value' => $this->options['term_page'], + ); + $form['node'] = array( + '#type' => 'checkbox', + '#title' => t('Load default filter from node page, that\'s good for related taxonomy blocks'), + '#default_value' => $this->options['node'], + ); + + $form['limit'] = array( + '#type' => 'checkbox', + '#title' => t('Limit terms by vocabulary'), + '#default_value'=> $this->options['limit'], + '#process' => array('form_process_checkbox', 'ctools_dependent_process'), + '#dependency' => array( + 'edit-options-argument-default-taxonomy-tid-node' => array(1), + ), + ); + + $options = array(); + $vocabularies = taxonomy_get_vocabularies(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#type' => 'checkboxes', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#process' => array('form_process_checkboxes', 'ctools_dependent_process'), + '#dependency' => array( + 'edit-options-argument-default-taxonomy-tid-limit' => array(1), + 'edit-options-argument-default-taxonomy-tid-node' => array(1), + ), + ); + + $form['anyall'] = array( + '#type' => 'radios', + '#title' => t('Multiple-value handling'), + '#default_value'=> $this->options['anyall'], + '#process' => array('form_process_radios', 'ctools_dependent_process'), + '#options' => array( + ',' => t('Filter to items that share all terms'), + '+' => t('Filter to items that share any term'), + ), + '#dependency' => array( + 'edit-options-argument-default-taxonomy-tid-node' => array(1), + ), + ); + } + + function options_submit(&$form, &$form_state, &$options = array()) { + // Filter unselected items so we don't unnecessarily store giant arrays. + $options['vocabularies'] = array_filter($options['vocabularies']); + } + + function get_argument() { + // Load default argument from taxonomy page. + if (!empty($this->options['term_page'])) { + if (arg(0) == 'taxonomy' && arg(1) == 'term' && is_numeric(arg(2))) { + return arg(2); + } + } + // Load default argument from node. + if (!empty($this->options['node'])) { + foreach (range(1, 3) as $i) { + $node = menu_get_object('node', $i); + if (!empty($node)) { + break; + } + } + // Just check, if a node could be detected. + if ($node) { + $taxonomy = array(); + $fields = field_info_instances('node', $node->type); + foreach ($fields as $name => $info) { + $field_info = field_info_field($name); + if ($field_info['type'] == 'taxonomy_term_reference') { + $items = field_get_items('node', $node, $name); + if (is_array($items)) { + foreach ($items as $item) { + $taxonomy[$item['tid']] = $field_info['settings']['allowed_values'][0]['vocabulary']; + } + } + } + } + if (!empty($this->options['limit'])) { + $tids = array(); + // filter by vocabulary + foreach ($taxonomy as $tid => $vocab) { + if (!empty($this->options['vocabularies'][$vocab])) { + $tids[] = $tid; + } + } + return implode($this->options['anyall'], $tids); + } + // Return all tids. + else { + return implode($this->options['anyall'], array_keys($taxonomy)); + } + } + } + + // If the current page is a view that takes tid as an argument, + // find the tid argument and return it. + $views_page = views_get_page_view(); + if ($views_page && isset($views_page->argument['tid'])) { + return $views_page->argument['tid']->argument; + } + } +} diff --git a/modules/taxonomy/views_plugin_argument_validate_taxonomy_term.inc b/modules/taxonomy/views_plugin_argument_validate_taxonomy_term.inc new file mode 100644 index 00000000..3a881998 --- /dev/null +++ b/modules/taxonomy/views_plugin_argument_validate_taxonomy_term.inc @@ -0,0 +1,222 @@ +options['vids'])) { + $vocabularies = taxonomy_get_vocabularies(); + foreach ($this->options['vids'] as $vid) { + if (isset($vocabularies[$vid], $vocabularies[$vid]->machine_name)) { + $this->options['vocabularies'][$vocabularies[$vid]->machine_name] = $vocabularies[$vid]->machine_name; + } + } + } + } + + function option_definition() { + $options = parent::option_definition(); + $options['vocabularies'] = array('default' => array()); + $options['type'] = array('default' => 'tid'); + $options['transform'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $vocabularies = taxonomy_get_vocabularies(); + $options = array(); + foreach ($vocabularies as $voc) { + $options[$voc->machine_name] = check_plain($voc->name); + } + + $form['vocabularies'] = array( + '#type' => 'checkboxes', + '#prefix' => '
    ', + '#suffix' => '
    ', + '#title' => t('Vocabularies'), + '#options' => $options, + '#default_value' => $this->options['vocabularies'], + '#description' => t('If you wish to validate for specific vocabularies, check them; if none are checked, all terms will pass.'), + ); + + $form['type'] = array( + '#type' => 'select', + '#title' => t('Filter value type'), + '#options' => array( + 'tid' => t('Term ID'), + 'tids' => t('Term IDs separated by , or +'), + 'name' => t('Term name'), + 'convert' => t('Term name converted to Term ID'), + ), + '#default_value' => $this->options['type'], + '#description' => t('Select the form of this filter value; if using term name, it is generally more efficient to convert it to a term ID and use Taxonomy: Term ID rather than Taxonomy: Term Name" as the filter.'), + ); + + $form['transform'] = array( + '#type' => 'checkbox', + '#title' => t('Transform dashes in URL to spaces in term name filter values'), + '#default_value' => $this->options['transform'], + ); + } + + function options_submit(&$form, &$form_state, &$options = array()) { + // Filter unselected items so we don't unnecessarily store giant arrays. + $options['vocabularies'] = array_filter($options['vocabularies']); + } + + function convert_options(&$options) { + if (!isset($options['vocabularies']) && !empty($this->argument->options['validate_argument_vocabulary'])) { + $options['vocabularies'] = $this->argument->options['validate_argument_vocabulary']; + $options['type'] = $this->argument->options['validate_argument_type']; + $options['transform'] = isset($this->argument->options['validate_argument_transform']) ? $this->argument->options['validate_argument_transform'] : FALSE; + } + } + + function validate_argument($argument) { + $vocabularies = array_filter($this->options['vocabularies']); + $type = $this->options['type']; + $transform = $this->options['transform']; + + switch ($type) { + case 'tid': + if (!is_numeric($argument)) { + return FALSE; + } + + $query = db_select('taxonomy_term_data', 'td'); + $query->leftJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->fields('tv', array('machine_name')); + $query->condition('td.tid', $argument); + $query->addTag('term_access'); + $term = $query->execute()->fetchObject(); + if (!$term) { + return FALSE; + } + $this->argument->validated_title = check_plain($term->name); + return empty($vocabularies) || !empty($vocabularies[$term->machine_name]); + + case 'tids': + // An empty argument is not a term so doesn't pass. + if (empty($argument)) { + return FALSE; + } + + $tids = new stdClass(); + $tids->value = $argument; + $tids = views_break_phrase($argument, $tids); + if ($tids->value == array(-1)) { + return FALSE; + } + + $test = drupal_map_assoc($tids->value); + $titles = array(); + + // check, if some tids already verified + static $validated_cache = array(); + foreach ($test as $tid) { + if (isset($validated_cache[$tid])) { + if ($validated_cache[$tid] === FALSE) { + return FALSE; + } + else { + $titles[] = $validated_cache[$tid]; + unset($test[$tid]); + } + } + } + + // if unverified tids left - verify them and cache results + if (count($test)) { + $query = db_select('taxonomy_term_data', 'td'); + $query->leftJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->fields('tv', array('machine_name')); + $query->condition('td.tid', $test); + + $result = $query->execute(); + + foreach ($result as $term) { + if ($vocabularies && empty($vocabularies[$term->machine_name])) { + $validated_cache[$term->tid] = FALSE; + return FALSE; + } + + $titles[] = $validated_cache[$term->tid] = check_plain($term->name); + unset($test[$term->tid]); + } + } + + // Remove duplicate titles + $titles = array_unique($titles); + + $this->argument->validated_title = implode($tids->operator == 'or' ? ' + ' : ', ', $titles); + // If this is not empty, we did not find a tid. + return empty($test); + + case 'name': + case 'convert': + $query = db_select('taxonomy_term_data', 'td'); + $query->leftJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->fields('td'); + $query->fields('tv', array('machine_name')); + if (!empty($vocabularies)) { + $query->condition('tv.machine_name', $vocabularies); + } + if ($transform) { + $query->where("replace(td.name, ' ', '-') = :name", array(':name' => $argument)); + } + else { + $query->condition('td.name', $argument); + } + $term = $query->execute()->fetchObject(); + + if ($term && (empty($vocabularies) || !empty($vocabularies[$term->machine_name]))) { + if ($type == 'convert') { + $this->argument->argument = $term->tid; + } + $this->argument->validated_title = check_plain($term->name); + return TRUE; + } + return FALSE; + } + } + + function process_summary_arguments(&$args) { + $type = $this->options['type']; + $transform = $this->options['transform']; + $vocabularies = array_filter($this->options['vocabularies']); + + if ($type == 'convert') { + $arg_keys = array_flip($args); + + $query = db_select('taxonomy_term_data', 'td'); + $query->condition('tid', $args); + $query->addField('td', 'tid', 'tid'); + if (!empty($vocabularies)) { + $query->leftJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); + $query->condition('tv.machine_name', $vocabularies); + } + if ($transform) { + $query->addExpression("REPLACE(td.name, ' ', '-')", 'name'); + } + else { + $query->addField('td', 'name', 'name'); + } + + foreach ($query->execute()->fetchAllKeyed() as $tid => $term) { + $args[$arg_keys[$tid]] = $term; + } + } + } +} diff --git a/modules/tracker.views.inc b/modules/tracker.views.inc new file mode 100644 index 00000000..ee14589d --- /dev/null +++ b/modules/tracker.views.inc @@ -0,0 +1,183 @@ + array( + 'type' => 'INNER', + 'left_field' => 'nid', + 'field' => 'nid', + ), + ); + $data['tracker_node']['nid'] = array( + 'title' => t('Nid'), + 'help' => t('The node ID of the node.'), + 'field' => array( + 'handler' => 'views_handler_field_node', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_node_nid', + 'name field' => 'title', + 'numeric' => TRUE, + 'validate type' => 'nid', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + $data['tracker_node']['changed'] = array( + 'title' => t('Updated date'), + 'help' => t('The date the node was last updated.'), + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + $data['tracker_node']['published'] = array( + 'title' => t('Published'), + 'help' => t('Whether or not the node is published.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_tracker_boolean_operator', + 'label' => t('Published'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + $data['tracker_user']['table']['group'] = t('Tracker - User'); + $data['tracker_user']['table']['join'] = array( + 'node' => array( + 'type' => 'INNER', + 'left_field' => 'nid', + 'field' => 'nid', + ), + 'user' => array( + 'type' => 'INNER', + 'left_field' => 'uid', + 'field' => 'uid', + ), + ); + $data['tracker_user']['nid'] = array( + 'title' => t('Nid'), + 'help' => t('The node ID of the node a user created or commented on. You must use an argument or filter on UID or you will get misleading results using this field.'), + 'field' => array( + 'handler' => 'views_handler_field_node', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_node_nid', + 'name field' => 'title', + 'numeric' => TRUE, + 'validate type' => 'nid', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + $data['tracker_user']['uid'] = array( + 'title' => t('Uid'), + 'help' => t('The user ID of a user who touched the node (either created or commented on it).'), + 'field' => array( + 'handler' => 'views_handler_field_user', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_user_uid', + 'name field' => 'name', + ), + 'filter' => array( + 'title' => t('Name'), + 'handler' => 'views_handler_filter_user_name', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + $data['tracker_user']['changed'] = array( + 'title' => t('Updated date'), + 'help' => t('The date the node was last updated or commented on. You must use an argument or filter on UID or you will get misleading results using this field.'), + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + $data['tracker_user']['published'] = array( + 'title' => t('Published'), + 'help' => t('Whether or not the node is published. You must use an argument or filter on UID or you will get misleading results using this field.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_tracker_boolean_operator', + 'label' => t('Published'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + return $data; +} + +/** + * Implementation of hook_views_data_alter(). + */ +function tracker_views_data_alter(&$data) { + // Provide additional uid_touch handlers which are handled by tracker + $data['node']['uid_touch_tracker'] = array( + 'group' => t('Tracker - User'), + 'title' => t('User posted or commented'), + 'help' => t('Display nodes only if a user posted the node or commented on the node.'), + 'argument' => array( + 'field' => 'uid', + 'name table' => 'users', + 'name field' => 'name', + 'handler' => 'views_handler_argument_tracker_comment_user_uid', + 'no group by' => TRUE, + ), + 'filter' => array( + 'field' => 'uid', + 'name table' => 'users', + 'name field' => 'name', + 'handler' => 'views_handler_filter_tracker_comment_user_uid' + ), + ); +} diff --git a/modules/tracker/views_handler_argument_tracker_comment_user_uid.inc b/modules/tracker/views_handler_argument_tracker_comment_user_uid.inc new file mode 100644 index 00000000..e614482d --- /dev/null +++ b/modules/tracker/views_handler_argument_tracker_comment_user_uid.inc @@ -0,0 +1,26 @@ +query->ensure_table('tracker_user'); + $this->query->add_where(0, "$tracker_user_alias.uid", $this->argument); + } + +} diff --git a/modules/tracker/views_handler_filter_tracker_boolean_operator.inc b/modules/tracker/views_handler_filter_tracker_boolean_operator.inc new file mode 100644 index 00000000..455e8242 --- /dev/null +++ b/modules/tracker/views_handler_filter_tracker_boolean_operator.inc @@ -0,0 +1,31 @@ + 0. + */ +class views_handler_filter_tracker_boolean_operator extends views_handler_filter_boolean_operator { + + /** + * Overrides views_handler_filter_boolean_operator::query(). + */ + function query() { + $this->ensure_my_table(); + $where = "$this->table_alias.$this->real_field "; + if (empty($this->value)) { + $where .= '= 0'; + if ($this->accept_null) { + $where = '(' . $where . " OR $this->table_alias.$this->real_field IS NULL)"; + } + } + else { + $where .= '= 1'; + } + $this->query->add_where_expression($this->options['group'], $where); + } + +} diff --git a/modules/tracker/views_handler_filter_tracker_comment_user_uid.inc b/modules/tracker/views_handler_filter_tracker_comment_user_uid.inc new file mode 100644 index 00000000..da6f65bc --- /dev/null +++ b/modules/tracker/views_handler_filter_tracker_comment_user_uid.inc @@ -0,0 +1,23 @@ +query->ensure_table('tracker_user'); + $this->query->add_where(0, "$tracker_user_alias.uid", $this->value); + } + +} diff --git a/modules/translation.views.inc b/modules/translation.views.inc new file mode 100644 index 00000000..b36c7a78 --- /dev/null +++ b/modules/translation.views.inc @@ -0,0 +1,121 @@ + 'tnid', + 'field' => 'tnid', + ); + + // The translation ID (nid of the "source" translation) + $data['node']['tnid'] = array( + 'group' => t('Content translation'), + 'title' => t('Translation set node ID'), + 'help' => t('The ID of the translation set the content belongs to.'), + 'field' => array( + 'handler' => 'views_handler_field_node', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_node_tnid', + 'name field' => 'title', // the field to display in the summary. + 'numeric' => TRUE, + 'validate type' => 'tnid', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'relationship' => array( + 'title' => t('Source translation'), + 'help' => t('The source that this content was translated from.'), + 'base' => 'node', + 'base field' => 'nid', + 'handler' => 'views_handler_relationship', + 'label' => t('Source translation'), + ), + ); + + // The source translation. + $data['node']['translation'] = array( + 'group' => t('Content translation'), + 'title' => t('Translations'), + 'help' => t('Versions of content in different languages.'), + 'relationship' => array( + 'title' => t('Translations'), + 'help' => t('Versions of content in different languages.'), + 'base' => 'node', + 'base field' => 'tnid', + 'relationship table' => 'node', + 'relationship field' => 'nid', + 'handler' => 'views_handler_relationship_translation', + 'label' => t('Translations'), + ), + ); + + // The source translation. + $data['node']['source_translation'] = array( + 'group' => t('Content translation'), + 'title' => t('Source translation'), + 'help' => t('Content that is either untranslated or is the original version of a translation set.'), + 'filter' => array( + 'handler' => 'views_handler_filter_node_tnid', + ), + ); + + // The source translation. + $data['node']['child_translation'] = array( + 'group' => t('Node translation'), + 'title' => t('Child translation'), + 'help' => t('Content that is a translation of a source translation.'), + 'filter' => array( + 'handler' => 'views_handler_filter_node_tnid_child', + ), + ); + + // Translation status + $data['node']['translate'] = array( + 'group' => t('Content translation'), + 'title' => t('Translation status'), + 'help' => t('The translation status of the content - whether or not the translation needs to be updated.'), + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Outdated'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // Translate node link. + $data['node']['translate_node'] = array( + 'group' => t('Content translation'), + 'title' => t('Translate link'), + 'help' => t('Provide a simple link to translate the node.'), + 'field' => array( + 'handler' => 'views_handler_field_node_link_translate', + ), + ); + + +} diff --git a/modules/translation/views_handler_argument_node_tnid.inc b/modules/translation/views_handler_argument_node_tnid.inc new file mode 100644 index 00000000..61e9ebab --- /dev/null +++ b/modules/translation/views_handler_argument_node_tnid.inc @@ -0,0 +1,26 @@ + $this->value)); + foreach ($result as $term) { + $titles[] = check_plain($term->title); + } + return $titles; + } +} diff --git a/modules/translation/views_handler_field_node_link_translate.inc b/modules/translation/views_handler_field_node_link_translate.inc new file mode 100644 index 00000000..3e30725c --- /dev/null +++ b/modules/translation/views_handler_field_node_link_translate.inc @@ -0,0 +1,29 @@ +get_value($values); + $node->status = 1; // unpublished nodes ignore access control + if (empty($node->language) || !translation_supported_type($node->type) || !node_access('view', $node) || !user_access('translate content')) { + return; + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/$node->nid/translate"; + $this->options['alter']['query'] = drupal_get_destination(); + + $text = !empty($this->options['text']) ? $this->options['text'] : t('translate'); + return $text; + } +} diff --git a/modules/translation/views_handler_field_node_translation_link.inc b/modules/translation/views_handler_field_node_translation_link.inc new file mode 100644 index 00000000..9d503692 --- /dev/null +++ b/modules/translation/views_handler_field_node_translation_link.inc @@ -0,0 +1,49 @@ +additional_fields['nid'] = 'nid'; + $this->additional_fields['tnid'] = 'tnid'; + $this->additional_fields['title'] = 'title'; + $this->additional_fields['language'] = 'language'; + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + $value = $this->get_value($values, 'tnid'); + return $this->render_link($this->sanitize_value($value), $values); + } + + function render_link($data, $values) { + global $language; + + $tnid = $this->get_value($values, 'tnid'); + // Only load translations if the node isn't in the current language. + if ($this->get_value($values, 'language') != $language->language) { + $translations = translation_node_get_translations($tnid); + if (isset($translations[$language->language])) { + $values->{$this->aliases['nid']} = $translations[$language->language]->nid; + $values->{$this->aliases['title']} = $translations[$language->language]->title; + } + } + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "node/" . $this->get_value($values, 'nid'); + return $this->get_value($values, 'title'); + } +} diff --git a/modules/translation/views_handler_filter_node_tnid.inc b/modules/translation/views_handler_filter_node_tnid.inc new file mode 100644 index 00000000..ed4d6a9a --- /dev/null +++ b/modules/translation/views_handler_filter_node_tnid.inc @@ -0,0 +1,45 @@ + 'radios', + '#title' => t('Include untranslated content'), + '#default_value' => $this->operator, + '#options' => array( + 1 => t('Yes'), + 0 => t('No'), + ), + ); + } + + function can_expose() { return FALSE; } + + function query() { + $table = $this->ensure_my_table(); + // Select for source translations (tnid = nid). Conditionally, also accept either untranslated nodes (tnid = 0). + $this->query->add_where_expression($this->options['group'], "$table.tnid = $table.nid" . ($this->operator ? " OR $table.tnid = 0" : '')); + } +} diff --git a/modules/translation/views_handler_filter_node_tnid_child.inc b/modules/translation/views_handler_filter_node_tnid_child.inc new file mode 100644 index 00000000..51316eb3 --- /dev/null +++ b/modules/translation/views_handler_filter_node_tnid_child.inc @@ -0,0 +1,22 @@ +ensure_my_table(); + $this->query->add_where_expression($this->options['group'], "$table.tnid <> $table.nid AND $table.tnid > 0"); + } +} diff --git a/modules/translation/views_handler_relationship_translation.inc b/modules/translation/views_handler_relationship_translation.inc new file mode 100644 index 00000000..509a9352 --- /dev/null +++ b/modules/translation/views_handler_relationship_translation.inc @@ -0,0 +1,103 @@ + 'current'); + + return $options; + } + + /** + * Add a translation selector. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $options = array( + 'all' => t('All'), + 'current' => t('Current language'), + 'default' => t('Default language'), + ); + $options = array_merge($options, locale_language_list()); + $form['language'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $this->options['language'], + '#title' => t('Translation option'), + '#description' => t('The translation options allows you to select which translation or translations in a translation set join on. Select "Current language" or "Default language" to join on the translation in the current or default language respectively. Select a specific language to join on a translation in that language. If you select "All", each translation will create a new row, which may appear to cause duplicates.'), + ); + } + + /** + * Called to implement a relationship in a query. + */ + function query() { + // Figure out what base table this relationship brings to the party. + $table_data = views_fetch_data($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensure_my_table(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $base_field; + $def['left_table'] = $this->table_alias; + $def['left_field'] = $this->field; + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + $def['extra'] = array(); + if ($this->options['language'] != 'all') { + switch ($this->options['language']) { + case 'current': + $def['extra'][] = array( + 'field' => 'language', + 'value' => '***CURRENT_LANGUAGE***', + ); + break; + case 'default': + $def['extra'][] = array( + 'field' => 'language', + 'value' => '***DEFAULT_LANGUAGE***', + ); + break; + // Other values will be the language codes. + default: + $def['extra'][] = array( + 'field' => 'language', + 'value' => $this->options['language'], + ); + break; + } + } + + if (!empty($def['join_handler']) && class_exists($def['join_handler'])) { + $join = new $def['join_handler']; + } + else { + $join = new views_join(); + } + + $join->definition = $def; + $join->construct(); + $join->adjusted = TRUE; + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->add_relationship($alias, $join, $this->definition['base'], $this->relationship); + } +} diff --git a/modules/user.views.inc b/modules/user.views.inc new file mode 100644 index 00000000..70771045 --- /dev/null +++ b/modules/user.views.inc @@ -0,0 +1,575 @@ + 'uid', + 'title' => t('User'), + 'help' => t('Users who have created accounts on your site.'), + 'access query tag' => 'user_access', + ); + $data['users']['table']['entity type'] = 'user'; + + + $data['users']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'uid', + ), + 'node_revision' => array( + 'table' => 'node_revision', + 'field' => 'uid', + ), + 'file' => array( + 'table' => 'file', + 'field' => 'uid', + ), + ); + + // uid + $data['users']['uid'] = array( + 'title' => t('Uid'), + 'help' => t('The user ID'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_user', + 'click sortable' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_user_uid', + 'name field' => 'name', // display this field in the summary + ), + 'filter' => array( + 'title' => t('Name'), + 'handler' => 'views_handler_filter_user_name', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'relationship' => array( + 'title' => t('Content authored'), + 'help' => t('Relate content to the user who created it. This relationship will create one record for each content item created by the user.'), + 'handler' => 'views_handler_relationship', + 'base' => 'node', + 'base field' => 'uid', + 'field' => 'uid', + 'label' => t('nodes'), + ), + ); + + // uid_raw + $data['users']['uid_raw'] = array( + 'help' => t('The raw numeric user ID.'), + 'real field' => 'uid', + 'filter' => array( + 'title' => t('The user ID'), + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // uid + $data['users']['uid_representative'] = array( + 'relationship' => array( + 'title' => t('Representative node'), + 'label' => t('Representative node'), + 'help' => t('Obtains a single representative node for each user, according to a chosen sort criterion.'), + 'handler' => 'views_handler_relationship_groupwise_max', + 'relationship field' => 'uid', + 'outer field' => 'users.uid', + 'argument table' => 'users', + 'argument field' => 'uid', + 'base' => 'node', + 'field' => 'nid', + ), + ); + + // uid + $data['users']['uid_current'] = array( + 'real field' => 'uid', + 'title' => t('Current'), + 'help' => t('Filter the view to the currently logged in user.'), + 'filter' => array( + 'handler' => 'views_handler_filter_user_current', + 'type' => 'yes-no', + ), + ); + + // name + $data['users']['name'] = array( + 'title' => t('Name'), // The item it appears as on the UI, + 'help' => t('The user or author name.'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_user_name', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + 'title' => t('Name (raw)'), + 'help' => t('The user or author name. This filter does not check if the user exists and allows partial matching. Does not utilize autocomplete.') + ), + ); + + // mail + // Note that this field implements field level access control. + $data['users']['mail'] = array( + 'title' => t('E-mail'), // The item it appears as on the UI, + 'help' => t('Email address for a given user. This field is normally not shown to users, so be cautious when using it.'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_user_mail', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + // language + $data['users']['language'] = array( + 'title' => t('Language'), // The item it appears as on the UI, + 'help' => t('Language of the user'), + 'field' => array( + 'handler' => 'views_handler_field_user_language', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_node_language', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_node_language', + ), + ); + + // picture + $data['users']['picture_fid']['moved to'] = array('users', 'picture'); + $data['users']['picture'] = array( + 'title' => t('Picture'), + 'help' => t("The user's picture, if allowed."), // The help that appears on the UI, + // Information for displaying the uid + 'field' => array( + 'handler' => 'views_handler_field_user_picture', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Has Avatar'), + 'type' => 'yes-no', + ), + ); + + // link + $data['users']['view_user'] = array( + 'field' => array( + 'title' => t('Link'), + 'help' => t('Provide a simple link to the user.'), + 'handler' => 'views_handler_field_user_link', + ), + ); + + // created field + $data['users']['created'] = array( + 'title' => t('Created date'), // The item it appears as on the UI, + 'help' => t('The date the user was created.'), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + $data['users']['created_fulldate'] = array( + 'title' => t('Created date'), + 'help' => t('Date in the form of CCYYMMDD.'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_fulldate', + ), + ); + + $data['users']['created_year_month'] = array( + 'title' => t('Created year + month'), + 'help' => t('Date in the form of YYYYMM.'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_year_month', + ), + ); + + $data['users']['timestamp_year']['moved to'] = array('users', 'created_year'); + $data['users']['created_year'] = array( + 'title' => t('Created year'), + 'help' => t('Date in the form of YYYY.'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_year', + ), + ); + + $data['users']['created_month'] = array( + 'title' => t('Created month'), + 'help' => t('Date in the form of MM (01 - 12).'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_month', + ), + ); + + $data['users']['created_day'] = array( + 'title' => t('Created day'), + 'help' => t('Date in the form of DD (01 - 31).'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_day', + ), + ); + + $data['users']['created_week'] = array( + 'title' => t('Created week'), + 'help' => t('Date in the form of WW (01 - 53).'), + 'argument' => array( + 'field' => 'created', + 'handler' => 'views_handler_argument_node_created_week', + ), + ); + + // access field + $data['users']['access'] = array( + 'title' => t('Last access'), // The item it appears as on the UI, + 'help' => t("The user's last access date."), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + // login field + $data['users']['login'] = array( + 'title' => t('Last login'), // The item it appears as on the UI, + 'help' => t("The user's last login date."), // The help that appears on the UI, + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + ); + + // active status + $data['users']['status'] = array( + 'title' => t('Active'), // The item it appears as on the UI, + 'help' => t('Whether a user is active or blocked.'), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_boolean', + 'click sortable' => TRUE, + 'output formats' => array( + 'active-blocked' => array(t('Active'), t('Blocked')), + ), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_boolean_operator', + 'label' => t('Active'), + 'type' => 'yes-no', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + ); + + // log field + $data['users']['signature'] = array( + 'title' => t('Signature'), // The item it appears as on the UI, + 'help' => t("The user's signature."), // The help that appears on the UI, + // Information for displaying a title as a field + 'field' => array( + 'handler' => 'views_handler_field_markup', + 'format' => filter_fallback_format(), + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + $data['users']['edit_node'] = array( + 'field' => array( + 'title' => t('Edit link'), + 'help' => t('Provide a simple link to edit the user.'), + 'handler' => 'views_handler_field_user_link_edit', + ), + ); + + $data['users']['cancel_node'] = array( + 'field' => array( + 'title' => t('Cancel link'), + 'help' => t('Provide a simple link to cancel the user.'), + 'handler' => 'views_handler_field_user_link_cancel', + ), + ); + + $data['users']['data'] = array( + 'title' => t('Data'), + 'help' => t('Provide serialized data of the user'), + 'field' => array( + 'handler' => 'views_handler_field_serialized', + ), + ); + + // ---------------------------------------------------------------------- + // users_roles table + + $data['users_roles']['table']['group'] = t('User'); + + // Explain how this table joins to others. + $data['users_roles']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_field' => 'uid', + 'field' => 'uid', + ), + ); + + $data['users_roles']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'uid', + ), + 'node_revision' => array( + 'table' => 'node_revision', + 'field' => 'uid', + ), + ); + + $data['users_roles']['rid'] = array( + 'title' => t('Roles'), + 'help' => t('Roles that a user belongs to.'), + 'field' => array( + 'handler' => 'views_handler_field_user_roles', + 'no group by' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_user_roles', + 'numeric' => TRUE, + 'allow empty' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_users_roles_rid', + 'name table' => 'role', + 'name field' => 'name', + 'empty field name' => t('No role'), + 'zero is null' => TRUE, + 'numeric' => TRUE, + ), + ); + + // ---------------------------------------------------------------------- + // role table + + $data['role']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_table' => 'users_roles', + 'left_field' => 'rid', + 'field' => 'rid', + ), + // needed for many to one helper sometimes + 'users_roles' => array( + 'left_field' => 'rid', + 'field' => 'rid', + ), + ); + + $data['role']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'uid', + ), + 'node_revision' => array( + 'table' => 'node_revision', + 'field' => 'uid', + ), + ); + + // permission table + $data['role_permission']['table']['group'] = t('User'); + $data['role_permission']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_table' => 'users_roles', + 'left_field' => 'rid', + 'field' => 'rid', + ), + ); + + $data['role_permission']['permission'] = array( + 'title' => t('Permission'), + 'help' => t('The user permissions.'), + 'field' => array( + 'handler' => 'views_handler_field_user_permissions', + 'no group by' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_user_permissions', + ), + ); + + // ---------------------------------------------------------------------- + // authmap table + + $data['authmap']['table']['group'] = t('User'); + $data['authmap']['table']['join'] = array( + // Directly links to users table. + 'users' => array( + 'left_field' => 'uid', + 'field' => 'uid', + ), + ); + + $data['authmap']['table']['default_relationship'] = array( + 'node' => array( + 'table' => 'node', + 'field' => 'uid', + ), + 'node_revision' => array( + 'table' => 'node_revision', + 'field' => 'uid', + ), + ); + + $data['authmap']['aid'] = array( + 'title' => t('Authmap ID'), + 'help' => t('The Authmap ID.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + 'numeric' => TRUE, + ), + 'argument' => array( + 'handler' => 'views_handler_argument_numeric', + 'numeric' => TRUE, + ), + ); + $data['authmap']['authname'] = array( + 'title' => t('Authentication name'), + 'help' => t('The unique authentication name.'), + 'field' => array( + 'handler' => 'views_handler_field', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + $data['authmap']['module'] = array( + 'title' => t('Authentication module'), + 'help' => t('The name of the module managing the authentication entry.'), + 'field' => array( + 'handler' => 'views_handler_field', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + 'argument' => array( + 'handler' => 'views_handler_argument_string', + ), + ); + + return $data; +} + +/** + * Implements hook_views_plugins(). + */ +function user_views_plugins() { + return array( + 'module' => 'views', // This just tells our themes are elsewhere. + 'row' => array( + 'user' => array( + 'title' => t('User'), + 'help' => t('Display the user with standard user view.'), + 'handler' => 'views_plugin_row_user_view', + 'base' => array('users'), // only works with 'users' as base. + 'uses options' => TRUE, + 'type' => 'normal', + 'help topic' => 'style-users', + ), + ), + 'argument default' => array( + 'user' => array( + 'title' => t('User ID from URL'), + 'handler' => 'views_plugin_argument_default_user', + 'path' => drupal_get_path('module', 'views') . '/modules/user', // not necessary for most modules + ), + 'current_user' => array( + 'title' => t('User ID from logged in user'), + 'handler' => 'views_plugin_argument_default_current_user', + 'path' => drupal_get_path('module', 'views') . '/modules/user', // not necessary for most modules + ), + ), + 'argument validator' => array( + 'user' => array( + 'title' => t('User'), + 'handler' => 'views_plugin_argument_validate_user', + 'path' => drupal_get_path('module', 'views') . '/modules/user', // not necessary for most modules + ), + ), + ); +} + +/** + * Allow replacement of current userid so we can cache these queries + */ +function user_views_query_substitutions($view) { + global $user; + return array('***CURRENT_USER***' => intval($user->uid)); +} diff --git a/modules/user/views_handler_argument_user_uid.inc b/modules/user/views_handler_argument_user_uid.inc new file mode 100644 index 00000000..6ab9167d --- /dev/null +++ b/modules/user/views_handler_argument_user_uid.inc @@ -0,0 +1,33 @@ +argument) { + return array(variable_get('anonymous', t('Anonymous'))); + } + + $titles = array(); + + $result = db_query("SELECT u.name FROM {users} u WHERE u.uid IN (:uids)", array(':uids' => $this->value)); + foreach ($result as $term) { + $titles[] = check_plain($term->name); + } + return $titles; + } +} diff --git a/modules/user/views_handler_argument_users_roles_rid.inc b/modules/user/views_handler_argument_users_roles_rid.inc new file mode 100644 index 00000000..31c58145 --- /dev/null +++ b/modules/user/views_handler_argument_users_roles_rid.inc @@ -0,0 +1,23 @@ + $this->value)); + foreach ($result as $term) { + $titles[] = check_plain($term->name); + } + return $titles; + } +} diff --git a/modules/user/views_handler_field_user.inc b/modules/user/views_handler_field_user.inc new file mode 100644 index 00000000..f6b15b5b --- /dev/null +++ b/modules/user/views_handler_field_user.inc @@ -0,0 +1,55 @@ +options['link_to_user'])) { + $this->additional_fields['uid'] = 'uid'; + } + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_to_user'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + /** + * Provide link to node option + */ + function options_form(&$form, &$form_state) { + $form['link_to_user'] = array( + '#title' => t('Link this field to its user'), + '#description' => t("Enable to override this field's links."), + '#type' => 'checkbox', + '#default_value' => $this->options['link_to_user'], + ); + parent::options_form($form, $form_state); + } + + function render_link($data, $values) { + if (!empty($this->options['link_to_user']) && user_access('access user profiles') && ($uid = $this->get_value($values, 'uid')) && $data !== NULL && $data !== '') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "user/" . $uid; + } + return $data; + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/user/views_handler_field_user_language.inc b/modules/user/views_handler_field_user_language.inc new file mode 100644 index 00000000..e29da31a --- /dev/null +++ b/modules/user/views_handler_field_user_language.inc @@ -0,0 +1,39 @@ +get_value($values, 'uid'); + if (!empty($this->options['link_to_user'])) { + $uid = $this->get_value($values, 'uid'); + if (user_access('access user profiles') && $uid) { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = 'user/' . $uid; + } + } + if (empty($data)) { + $lang = language_default(); + } + else { + $lang = language_list(); + $lang = $lang[$data]; + } + + return $this->sanitize_value($lang->name); + } + + function render($values) { + $value = $this->get_value($values); + return $this->render_link($this->sanitize_value($value), $values); + } +} diff --git a/modules/user/views_handler_field_user_link.inc b/modules/user/views_handler_field_user_link.inc new file mode 100644 index 00000000..03b5e0d4 --- /dev/null +++ b/modules/user/views_handler_field_user_link.inc @@ -0,0 +1,58 @@ +additional_fields['uid'] = 'uid'; + } + + function option_definition() { + $options = parent::option_definition(); + $options['text'] = array('default' => '', 'translatable' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + parent::options_form($form, $form_state); + } + + // An example of field level access control. + function access() { + return user_access('access user profiles'); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + $value = $this->get_value($values, 'uid'); + return $this->render_link($this->sanitize_value($value), $values); + } + + function render_link($data, $values) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('view'); + + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "user/" . $data; + + return $text; + } + +} diff --git a/modules/user/views_handler_field_user_link_cancel.inc b/modules/user/views_handler_field_user_link_cancel.inc new file mode 100644 index 00000000..9129c017 --- /dev/null +++ b/modules/user/views_handler_field_user_link_cancel.inc @@ -0,0 +1,33 @@ +{$this->aliases['uid']}; + + // Build a pseudo account object to be able to check the access. + $account = new stdClass(); + $account->uid = $uid; + + if ($uid && user_cancel_access($account)) { + $this->options['alter']['make_link'] = TRUE; + + $text = !empty($this->options['text']) ? $this->options['text'] : t('cancel'); + + $this->options['alter']['path'] = "user/$uid/cancel"; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } + } +} diff --git a/modules/user/views_handler_field_user_link_edit.inc b/modules/user/views_handler_field_user_link_edit.inc new file mode 100644 index 00000000..e37feae4 --- /dev/null +++ b/modules/user/views_handler_field_user_link_edit.inc @@ -0,0 +1,30 @@ +uid = $data; + + if ($data && user_edit_access($account)) { + $this->options['alter']['make_link'] = TRUE; + + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + + $this->options['alter']['path'] = "user/$data/edit"; + $this->options['alter']['query'] = drupal_get_destination(); + + return $text; + } + } +} diff --git a/modules/user/views_handler_field_user_mail.inc b/modules/user/views_handler_field_user_mail.inc new file mode 100644 index 00000000..82d19338 --- /dev/null +++ b/modules/user/views_handler_field_user_mail.inc @@ -0,0 +1,44 @@ + 'mailto'); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['link_to_user'] = array( + '#title' => t('Link this field'), + '#type' => 'radios', + '#options' => array( + 0 => t('No link'), + 'user' => t('To the user'), + 'mailto' => t("With a mailto:"), + ), + '#default_value' => $this->options['link_to_user'], + ); + } + + function render_link($data, $values) { + parent::render_link($data, $values); + + if ($this->options['link_to_user'] == 'mailto') { + $this->options['alter']['make_link'] = TRUE; + $this->options['alter']['path'] = "mailto:" . $data; + } + + return $data; + } +} diff --git a/modules/user/views_handler_field_user_name.inc b/modules/user/views_handler_field_user_name.inc new file mode 100644 index 00000000..45514519 --- /dev/null +++ b/modules/user/views_handler_field_user_name.inc @@ -0,0 +1,83 @@ +options['overwrite_anonymous']) || !empty($this->options['format_username'])) { + $this->additional_fields['uid'] = 'uid'; + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['overwrite_anonymous'] = array('default' => FALSE, 'bool' => TRUE); + $options['anonymous_text'] = array('default' => '', 'translatable' => TRUE); + $options['format_username'] = array('default' => TRUE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['format_username'] = array( + '#title' => t('Use formatted username'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['format_username']), + '#description' => t('If checked, the username will be formatted by the system. If unchecked, it will be displayed raw.'), + '#fieldset' => 'more', + ); + $form['overwrite_anonymous'] = array( + '#title' => t('Overwrite the value to display for anonymous users'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['overwrite_anonymous']), + '#description' => t('Enable to display different text for anonymous users.'), + '#fieldset' => 'more', + ); + $form['anonymous_text'] = array( + '#title' => t('Text to display for anonymous users'), + '#type' => 'textfield', + '#default_value' => $this->options['anonymous_text'], + '#dependency' => array( + 'edit-options-overwrite-anonymous' => array(1), + ), + '#fieldset' => 'more', + ); + + parent::options_form($form, $form_state); + } + + function render_link($data, $values) { + $account = new stdClass(); + $account->uid = $this->get_value($values, 'uid'); + $account->name = $this->get_value($values); + if (!empty($this->options['link_to_user']) || !empty($this->options['overwrite_anonymous'])) { + if (!empty($this->options['overwrite_anonymous']) && !$account->uid) { + // This is an anonymous user, and we're overriting the text. + return check_plain($this->options['anonymous_text']); + } + elseif (!empty($this->options['link_to_user'])) { + $account->name = $this->get_value($values); + return theme('username', array('account' => $account)); + } + } + // If we want a formatted username, do that. + if (!empty($this->options['format_username'])) { + return format_username($account); + } + // Otherwise, there's no special handling, so return the data directly. + return $data; + } +} diff --git a/modules/user/views_handler_field_user_permissions.inc b/modules/user/views_handler_field_user_permissions.inc new file mode 100644 index 00000000..edc9c446 --- /dev/null +++ b/modules/user/views_handler_field_user_permissions.inc @@ -0,0 +1,68 @@ +additional_fields['uid'] = array('table' => 'users', 'field' => 'uid'); + } + + function query() { + $this->add_additional_fields(); + $this->field_alias = $this->aliases['uid']; + } + + function pre_render(&$values) { + $uids = array(); + $this->items = array(); + + foreach ($values as $result) { + $uids[] = $this->get_value($result, NULL, TRUE); + } + + if ($uids) { + // Get a list of all the modules implementing a hook_permission() and sort by + // display name. + $module_info = system_get_info('module'); + $modules = array(); + foreach (module_implements('permission') as $module) { + $modules[$module] = $module_info[$module]['name']; + } + asort($modules); + + $permissions = module_invoke_all('permission'); + + $result = db_query("SELECT u.uid, u.rid, rp.permission FROM {role_permission} rp INNER JOIN {users_roles} u ON u.rid = rp.rid WHERE u.uid IN (:uids) AND rp.module IN (:modules) ORDER BY rp.permission", + array(':uids' => $uids, ':modules' => array_keys($modules))); + + foreach ($result as $perm) { + $this->items[$perm->uid][$perm->permission]['permission'] = $permissions[$perm->permission]['title']; + } + } + } + + function render_item($count, $item) { + return $item['permission']; + } + + /* + function document_self_tokens(&$tokens) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = t('The name of the role.'); + $tokens['[' . $this->options['id'] . '-rid' . ']'] = t('The role ID of the role.'); + } + + function add_self_tokens(&$tokens, $item) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = $item['role']; + $tokens['[' . $this->options['id'] . '-rid' . ']'] = $item['rid']; + } + */ +} diff --git a/modules/user/views_handler_field_user_picture.inc b/modules/user/views_handler_field_user_picture.inc new file mode 100644 index 00000000..babbae55 --- /dev/null +++ b/modules/user/views_handler_field_user_picture.inc @@ -0,0 +1,114 @@ +additional_fields['uid'] = 'uid'; + $this->additional_fields['name'] = 'name'; + $this->additional_fields['mail'] = 'mail'; + } + + function element_type($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) { + if ($inline) { + return 'span'; + } + if ($none_supported) { + if ($this->options['element_type'] === '0') { + return ''; + } + } + if ($this->options['element_type']) { + return check_plain($this->options['element_type']); + } + if ($default_empty) { + return ''; + } + if (isset($this->definition['element type'])) { + return $this->definition['element type']; + } + + return 'div'; + } + + function option_definition() { + $options = parent::option_definition(); + $options['link_photo_to_profile'] = array('default' => TRUE, 'bool' => TRUE); + $options['image_style'] = array('default' => ''); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['link_photo_to_profile'] = array( + '#title' => t("Link to user's profile"), + '#description' => t("Link the user picture to the user's profile"), + '#type' => 'checkbox', + '#default_value' => $this->options['link_photo_to_profile'], + ); + + if (module_exists('image')) { + $styles = image_styles(); + $style_options = array('' => t('Default')); + foreach ($styles as $style) { + $style_options[$style['name']] = $style['name']; + } + + $form['image_style'] = array( + '#title' => t('Image style'), + '#description' => t('Using Default will use the site-wide image style for user pictures set in the Account settings.', array('!account-settings' => url('admin/config/people/accounts', array('fragment' => 'edit-personalization')))), + '#type' => 'select', + '#options' => $style_options, + '#default_value' => $this->options['image_style'], + ); + } + } + + function render($values) { + if ($this->options['image_style'] && module_exists('image')) { + // @todo: Switch to always using theme('user_picture') when it starts + // supporting image styles. See http://drupal.org/node/1021564 + if ($picture_fid = $this->get_value($values)) { + $picture = file_load($picture_fid); + $picture_filepath = $picture->uri; + } + else { + $picture_filepath = variable_get('user_picture_default', ''); + } + if (file_valid_uri($picture_filepath)) { + $output = theme('image_style', array('style_name' => $this->options['image_style'], 'path' => $picture_filepath)); + if ($this->options['link_photo_to_profile'] && user_access('access user profiles')) { + $uid = $this->get_value($values, 'uid'); + $output = l($output, "user/$uid", array('html' => TRUE)); + } + } + else { + $output = ''; + } + } + else { + // Fake an account object. + $account = new stdClass(); + if ($this->options['link_photo_to_profile']) { + // Prevent template_preprocess_user_picture from adding a link + // by not setting the uid. + $account->uid = $this->get_value($values, 'uid'); + } + $account->name = $this->get_value($values, 'name'); + $account->mail = $this->get_value($values, 'mail'); + $account->picture = $this->get_value($values); + $output = theme('user_picture', array('account' => $account)); + } + + return $output; + } +} diff --git a/modules/user/views_handler_field_user_roles.inc b/modules/user/views_handler_field_user_roles.inc new file mode 100644 index 00000000..e6571cdb --- /dev/null +++ b/modules/user/views_handler_field_user_roles.inc @@ -0,0 +1,57 @@ +additional_fields['uid'] = array('table' => 'users', 'field' => 'uid'); + } + + function query() { + $this->add_additional_fields(); + $this->field_alias = $this->aliases['uid']; + } + + function pre_render(&$values) { + $uids = array(); + $this->items = array(); + + foreach ($values as $result) { + $uids[] = $this->get_value($result, NULL, TRUE); + } + + if ($uids) { + $result = db_query("SELECT u.uid, u.rid, r.name FROM {role} r INNER JOIN {users_roles} u ON u.rid = r.rid WHERE u.uid IN (:uids) ORDER BY r.name", + array(':uids' => $uids)); + foreach ($result as $role) { + $this->items[$role->uid][$role->rid]['role'] = check_plain($role->name); + $this->items[$role->uid][$role->rid]['rid'] = $role->rid; + } + } + } + + function render_item($count, $item) { + return $item['role']; + } + + function document_self_tokens(&$tokens) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = t('The name of the role.'); + $tokens['[' . $this->options['id'] . '-rid' . ']'] = t('The role ID of the role.'); + } + + function add_self_tokens(&$tokens, $item) { + if (!empty($item['role'])) { + $tokens['[' . $this->options['id'] . '-role' . ']'] = $item['role']; + $tokens['[' . $this->options['id'] . '-rid' . ']'] = $item['rid']; + } + } +} diff --git a/modules/user/views_handler_filter_user_current.inc b/modules/user/views_handler_filter_user_current.inc new file mode 100644 index 00000000..5f8fe4c8 --- /dev/null +++ b/modules/user/views_handler_filter_user_current.inc @@ -0,0 +1,36 @@ +value_value = t('Is the logged in user'); + } + + function query() { + $this->ensure_my_table(); + + $field = $this->table_alias . '.' . $this->real_field . ' '; + $or = db_or(); + + if (empty($this->value)) { + $or->condition($field, '***CURRENT_USER***', '<>'); + if ($this->accept_null) { + $or->isNull($field); + } + } + else { + $or->condition($field, '***CURRENT_USER***', '='); + } + $this->query->add_where($this->options['group'], $or); + } +} diff --git a/modules/user/views_handler_filter_user_name.inc b/modules/user/views_handler_filter_user_name.inc new file mode 100644 index 00000000..300607fe --- /dev/null +++ b/modules/user/views_handler_filter_user_name.inc @@ -0,0 +1,162 @@ +value) { + $result = db_query("SELECT * FROM {users} u WHERE uid IN (:uids)", array(':uids' => $this->value)); + foreach ($result as $account) { + if ($account->uid) { + $values[] = $account->name; + } + else { + $values[] = 'Anonymous'; // Intentionally NOT translated. + } + } + } + + sort($values); + $default_value = implode(', ', $values); + $form['value'] = array( + '#type' => 'textfield', + '#title' => t('Usernames'), + '#description' => t('Enter a comma separated list of user names.'), + '#default_value' => $default_value, + '#autocomplete_path' => 'admin/views/ajax/autocomplete/user', + ); + + if (!empty($form_state['exposed']) && !isset($form_state['input'][$this->options['expose']['identifier']])) { + $form_state['input'][$this->options['expose']['identifier']] = $default_value; + } + } + + function value_validate($form, &$form_state) { + $values = drupal_explode_tags($form_state['values']['options']['value']); + $uids = $this->validate_user_strings($form['value'], $values); + + if ($uids) { + $form_state['values']['options']['value'] = $uids; + } + } + + function accept_exposed_input($input) { + $rc = parent::accept_exposed_input($input); + + if ($rc) { + // If we have previously validated input, override. + if (isset($this->validated_exposed_input)) { + $this->value = $this->validated_exposed_input; + } + } + + return $rc; + } + + function exposed_validate(&$form, &$form_state) { + if (empty($this->options['exposed'])) { + return; + } + + if (empty($this->options['expose']['identifier'])) { + return; + } + + $identifier = $this->options['expose']['identifier']; + $input = $form_state['values'][$identifier]; + + if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { + $this->operator = $this->options['group_info']['group_items'][$input]['operator']; + $input = $this->options['group_info']['group_items'][$input]['value']; + } + + $values = drupal_explode_tags($input); + + if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) { + $uids = $this->validate_user_strings($form[$identifier], $values); + } + else { + $uids = FALSE; + } + + if ($uids) { + $this->validated_exposed_input = $uids; + } + } + + /** + * Validate the user string. Since this can come from either the form + * or the exposed filter, this is abstracted out a bit so it can + * handle the multiple input sources. + */ + function validate_user_strings(&$form, $values) { + $uids = array(); + $placeholders = array(); + $args = array(); + $results = array(); + foreach ($values as $value) { + if (strtolower($value) == 'anonymous') { + $uids[] = 0; + } + else { + $missing[strtolower($value)] = TRUE; + $args[] = $value; + $placeholders[] = "'%s'"; + } + } + + if (!$args) { + return $uids; + } + + $result = db_query("SELECT * FROM {users} WHERE name IN (:names)", array(':names' => $args)); + foreach ($result as $account) { + unset($missing[strtolower($account->name)]); + $uids[] = $account->uid; + } + + if ($missing) { + form_error($form, format_plural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', array_keys($missing))))); + } + + return $uids; + } + + function value_submit($form, &$form_state) { + // prevent array filter from removing our anonymous user. + } + + // Override to do nothing. + function get_value_options() { } + + function admin_summary() { + // set up $this->value_options for the parent summary + $this->value_options = array(); + + if ($this->value) { + $result = db_query("SELECT * FROM {users} u WHERE uid IN (:uids)", array(':uids' => $this->value)); + + foreach ($result as $account) { + if ($account->uid) { + $this->value_options[$account->uid] = $account->name; + } + else { + $this->value_options[$account->uid] = 'Anonymous'; // Intentionally NOT translated. + } + } + } + + return parent::admin_summary(); + } +} diff --git a/modules/user/views_handler_filter_user_permissions.inc b/modules/user/views_handler_filter_user_permissions.inc new file mode 100644 index 00000000..f999045d --- /dev/null +++ b/modules/user/views_handler_filter_user_permissions.inc @@ -0,0 +1,35 @@ +value_options = array(); + foreach ($modules as $module => $display_name) { + if ($permissions = module_invoke($module, 'permission')) { + foreach ($permissions as $perm => $perm_item) { + // @todo: group by module but views_handler_filter_many_to_one does not support this. + $this->value_options[$perm] = check_plain(strip_tags($perm_item['title'])); + } + } + } + } +} diff --git a/modules/user/views_handler_filter_user_roles.inc b/modules/user/views_handler_filter_user_roles.inc new file mode 100644 index 00000000..ab9b8a2f --- /dev/null +++ b/modules/user/views_handler_filter_user_roles.inc @@ -0,0 +1,28 @@ +value_options = user_roles(TRUE); + unset($this->value_options[DRUPAL_AUTHENTICATED_RID]); + } + + /** + * Override empty and not empty operator labels to be clearer for user roles. + */ + function operators() { + $operators = parent::operators(); + $operators['empty']['title'] = t("Only has the 'authenticated user' role"); + $operators['not empty']['title'] = t("Has roles in addition to 'authenticated user'"); + return $operators; + } +} diff --git a/modules/user/views_plugin_argument_default_current_user.inc b/modules/user/views_plugin_argument_default_current_user.inc new file mode 100644 index 00000000..e11c7022 --- /dev/null +++ b/modules/user/views_plugin_argument_default_current_user.inc @@ -0,0 +1,18 @@ +uid; + } +} diff --git a/modules/user/views_plugin_argument_default_user.inc b/modules/user/views_plugin_argument_default_user.inc new file mode 100644 index 00000000..bb104296 --- /dev/null +++ b/modules/user/views_plugin_argument_default_user.inc @@ -0,0 +1,77 @@ + '', 'bool' => TRUE, 'translatable' => FALSE); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['user'] = array( + '#type' => 'checkbox', + '#title' => t('Also look for a node and use the node author'), + '#default_value' => $this->options['user'], + ); + } + + function convert_options(&$options) { + if (!isset($options['user']) && isset($this->argument->options['default_argument_user'])) { + $options['user'] = $this->argument->options['default_argument_user']; + } + } + + function get_argument() { + foreach (range(1, 3) as $i) { + $user = menu_get_object('user', $i); + if (!empty($user)) { + return $user->uid; + } + } + + foreach (range(1, 3) as $i) { + $user = menu_get_object('user_uid_optional', $i); + if (!empty($user)) { + return $user->uid; + } + } + + if (!empty($this->options['user'])) { + foreach (range(1, 3) as $i) { + $node = menu_get_object('node', $i); + if (!empty($node)) { + return $node->uid; + } + } + } + + if (arg(0) == 'user' && is_numeric(arg(1))) { + return arg(1); + } + + if (!empty($this->options['user'])) { + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node) { + return $node->uid; + } + } + } + + // If the current page is a view that takes uid as an argument, return the uid. + $view = views_get_page_view(); + + if ($view && isset($view->argument['uid'])) { + return $view->argument['uid']->argument; + } + } +} diff --git a/modules/user/views_plugin_argument_validate_user.inc b/modules/user/views_plugin_argument_validate_user.inc new file mode 100644 index 00000000..b7270944 --- /dev/null +++ b/modules/user/views_plugin_argument_validate_user.inc @@ -0,0 +1,140 @@ + 'uid'); + $options['restrict_roles'] = array('default' => FALSE, 'bool' => TRUE); + $options['roles'] = array('default' => array()); + + return $options; + } + + function options_form(&$form, &$form_state) { + $form['type'] = array( + '#type' => 'radios', + '#title' => t('Type of user filter value to allow'), + '#options' => array( + 'uid' => t('Only allow numeric UIDs'), + 'name' => t('Only allow string usernames'), + 'either' => t('Allow both numeric UIDs and string usernames'), + ), + '#default_value' => $this->options['type'], + ); + + $form['restrict_roles'] = array( + '#type' => 'checkbox', + '#title' => t('Restrict user based on role'), + '#default_value' => $this->options['restrict_roles'], + ); + + $form['roles'] = array( + '#type' => 'checkboxes', + '#prefix' => '
    ', + '#suffix' => '
    ', + '#title' => t('Restrict to the selected roles'), + '#options' => array_map('check_plain', user_roles(TRUE)), + '#default_value' => $this->options['roles'], + '#description' => t('If no roles are selected, users from any role will be allowed.'), + '#dependency' => array( + 'edit-options-validate-options-user-restrict-roles' => array(1), + ), + ); + } + + function options_submit(&$form, &$form_state, &$options = array()) { + // filter trash out of the options so we don't store giant unnecessary arrays + $options['roles'] = array_filter($options['roles']); + } + + function convert_options(&$options) { + if (!isset($options['type']) && isset($this->argument->options['validate_user_argument_type'])) { + $options['type'] = $this->argument->options['validate_user_argument_type']; + $options['restrict_roles'] = $this->argument->options['validate_user_restrict_roles']; + $options['roles'] = $this->argument->options['validate_user_roles']; + } + } + + function validate_argument($argument) { + $type = $this->options['type']; + // is_numeric() can return false positives, so we ensure it's an integer. + // However, is_integer() will always fail, since $argument is a string. + if (is_numeric($argument) && $argument == (int)$argument) { + if ($type == 'uid' || $type == 'either') { + if ($argument == $GLOBALS['user']->uid) { + // If you assign an object to a variable in PHP, the variable + // automatically acts as a reference, not a copy, so we use + // clone to ensure that we don't actually mess with the + // real global $user object. + $account = clone $GLOBALS['user']; + } + $where = 'uid = :argument'; + } + } + else { + if ($type == 'name' || $type == 'either') { + $name = !empty($GLOBALS['user']->name) ? $GLOBALS['user']->name : variable_get('anonymous', t('Anonymous')); + if ($argument == $name) { + $account = clone $GLOBALS['user']; + } + $where = "name = :argument"; + } + } + + // If we don't have a WHERE clause, the argument is invalid. + if (empty($where)) { + return FALSE; + } + + if (!isset($account)) { + $query = "SELECT uid, name FROM {users} WHERE $where"; + $account = db_query($query, array(':argument' => $argument))->fetchObject(); + } + if (empty($account)) { + // User not found. + return FALSE; + } + + // See if we're filtering users based on roles. + if (!empty($this->options['restrict_roles']) && !empty($this->options['roles'])) { + $roles = $this->options['roles']; + $account->roles = array(); + $account->roles[] = $account->uid ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID; + $result = db_query('SELECT rid FROM {users_roles} WHERE uid = :uid', array(':uid' => $account->uid)); + foreach ($result as $role) { + $account->roles[] = $role->rid; + } + if (!(bool) array_intersect($account->roles, $roles)) { + return FALSE; + } + } + + $this->argument->argument = $account->uid; + $this->argument->validated_title = check_plain(format_username($account)); + return TRUE; + } + + function process_summary_arguments(&$args) { + // If the validation says the input is an username, we should reverse the + // argument so it works for example for generation summary urls. + $uids_arg_keys = array_flip($args); + if ($this->options['type'] == 'name') { + $users = user_load_multiple($args); + foreach ($users as $uid => $account) { + $args[$uids_arg_keys[$uid]] = $account->name; + } + } + } +} diff --git a/modules/user/views_plugin_row_user_view.inc b/modules/user/views_plugin_row_user_view.inc new file mode 100644 index 00000000..b48f4596 --- /dev/null +++ b/modules/user/views_plugin_row_user_view.inc @@ -0,0 +1,81 @@ + 'full'); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $options = $this->options_form_summary_options(); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => t('View mode'), + '#default_value' => $this->options['view_mode'], + ); + $form['help']['#markup'] = t("Display the user with standard user view. It might be necessary to add a user-profile.tpl.php in your themes template folder, because the default user-profilee template don't show the username per default.", array('@user-profile-api-link' => url('http://api.drupal.org/api/drupal/modules--user--user-profile.tpl.php/7'))); + } + + + /** + * Return the main options, which are shown in the summary title. + */ + function options_form_summary_options() { + $entity_info = entity_get_info('user'); + $options = array(); + if (!empty($entity_info['view modes'])) { + foreach ($entity_info['view modes'] as $mode => $settings) { + $options[$mode] = $settings['label']; + } + } + if (empty($options)) { + $options = array( + 'full' => t('User account') + ); + } + + return $options; + } + + function summary_title() { + $options = $this->options_form_summary_options(); + return check_plain($options[$this->options['view_mode']]); + } + + function pre_render($values) { + $uids = array(); + foreach ($values as $row) { + $uids[] = $row->{$this->field_alias}; + } + $this->users = user_load_multiple($uids); + } + + function render($row) { + $account = $this->users[$row->{$this->field_alias}]; + $account->view = $this->view; + $build = user_view($account, $this->options['view_mode']); + + return drupal_render($build); + } +} diff --git a/modules/views.views.inc b/modules/views.views.inc new file mode 100644 index 00000000..2029aa8b --- /dev/null +++ b/modules/views.views.inc @@ -0,0 +1,114 @@ + array(), + ); + + $data['views']['random'] = array( + 'title' => t('Random'), + 'help' => t('Randomize the display order.'), + 'sort' => array( + 'handler' => 'views_handler_sort_random', + ), + ); + + $data['views']['null'] = array( + 'title' => t('Null'), + 'help' => t('Allow a contextual filter value to be ignored. The query will not be altered by this contextual filter value. Can be used when contextual filter values come from the URL, and a part of the URL needs to be ignored.'), + 'argument' => array( + 'handler' => 'views_handler_argument_null', + ), + ); + + $data['views']['nothing'] = array( + 'title' => t('Custom text'), + 'help' => t('Provide custom text or link.'), + 'field' => array( + 'handler' => 'views_handler_field_custom', + ), + ); + + $data['views']['counter'] = array( + 'title' => t('View result counter'), + 'help' => t('Displays the actual position of the view result'), + 'field' => array( + 'handler' => 'views_handler_field_counter', + ), + ); + + $data['views']['area'] = array( + 'title' => t('Text area'), + 'help' => t('Provide markup text for the area.'), + 'area' => array( + 'handler' => 'views_handler_area_text', + ), + ); + + $data['views']['area_text_custom'] = array( + 'title' => t('Unfiltered text'), + 'help' => t('Add unrestricted, custom text or markup. This is similar to the custom text field.'), + 'area' => array( + 'handler' => 'views_handler_area_text_custom', + ), + ); + + $data['views']['view'] = array( + 'title' => t('View area'), + 'help' => t('Insert a view inside an area.'), + 'area' => array( + 'handler' => 'views_handler_area_view', + ), + ); + + $data['views']['result'] = array( + 'title' => t('Result summary'), + 'help' => t('Shows result summary, for example the items per page.'), + 'area' => array( + 'handler' => 'views_handler_area_result', + ), + ); + + if (module_exists('contextual')) { + $data['views']['contextual_links'] = array( + 'title' => t('Contextual Links'), + 'help' => t('Display fields in a contextual links menu.'), + 'field' => array( + 'handler' => 'views_handler_field_contextual_links', + ), + ); + } + + $data['views']['combine'] = array( + 'title' => t('Combine fields filter'), + 'help' => t('Combine two fields together and search by them.'), + 'filter' => array( + 'handler' => 'views_handler_filter_combine', + ), + ); + + if (module_invoke('ctools', 'api_version', '1.7.1')) { + $data['views']['expression'] = array( + 'title' => t('Math expression'), + 'help' => t('Evaluates a mathematical expression and displays it.'), + 'field' => array( + 'handler' => 'views_handler_field_math', + 'float' => TRUE, + ), + ); + } + + return $data; +} diff --git a/plugins/export_ui/views_ui.class.php b/plugins/export_ui/views_ui.class.php new file mode 100644 index 00000000..9d801382 --- /dev/null +++ b/plugins/export_ui/views_ui.class.php @@ -0,0 +1,447 @@ + 'template/%/add', + 'title' => 'Add from template', + 'page callback' => 'ctools_export_ui_switcher_page', + 'page arguments' => array($plugin['name'], 'add_template', $prefix_count + 2), + 'load arguments' => array($plugin['name']), + 'access callback' => 'ctools_export_ui_task_access', + 'access arguments' => array($plugin['name'], 'add_template', $prefix_count + 2), + 'type' => MENU_CALLBACK, + ); + + return parent::init($plugin); + } + + function hook_menu(&$items) { + // We are using our own 'edit' still, rather than having edit on this + // object (maybe in the future) so unset the edit callbacks: + + // Store this so we can put them back as sometimes they're needed + // again laster: + $stored_items = $this->plugin['menu']['items']; + // We leave these to make sure the operations still exist in the plugin so + // that the path finder. + unset($this->plugin['menu']['items']['edit']); + unset($this->plugin['menu']['items']['add']); + unset($this->plugin['menu']['items']['import']); + unset($this->plugin['menu']['items']['edit callback']); + + parent::hook_menu($items); + + $this->plugin['menu']['items'] = $stored_items; + } + + function load_item($item_name) { + return views_ui_cache_load($item_name); + } + + function list_form(&$form, &$form_state) { + $row_class = 'container-inline'; + if (!variable_get('views_ui_show_listing_filters', FALSE)) { + $row_class .= " element-invisible"; + } + + views_include('admin'); + + parent::list_form($form, $form_state); + + // ctools only has two rows. We want four. + // That's why we create our own structure. + $form['bottom row']['submit']['#attributes']['class'][] = 'js-hide'; + $form['first row'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + 'search' => $form['top row']['search'], + 'submit' => $form['bottom row']['submit'], + 'reset' => $form['bottom row']['reset'], + ); + $form['second row'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + 'storage' => $form['top row']['storage'], + 'disabled' => $form['top row']['disabled'], + ); + $form['third row'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + 'order' => $form['bottom row']['order'], + 'sort' => $form['bottom row']['sort'], + ); + unset($form['top row']); + unset($form['bottom row']); + + // Modify the look and contents of existing form elements. + $form['second row']['storage']['#title'] = ''; + $form['second row']['storage']['#options'] = array( + 'all' => t('All storage'), + t('Normal') => t('In database'), + t('Default') => t('In code'), + t('Overridden') => t('Database overriding code'), + ); + $form['second row']['disabled']['#title'] = ''; + $form['second row']['disabled']['#options']['all'] = t('All status'); + $form['third row']['sort']['#title'] = ''; + + // And finally, add our own. + $this->bases = array(); + foreach (views_fetch_base_tables() as $table => $info) { + $this->bases[$table] = $info['title']; + } + + $form['second row']['base'] = array( + '#type' => 'select', + '#options' => array_merge(array('all' => t('All types')), $this->bases), + '#default_value' => 'all', + '#weight' => -1, + ); + + $tags = array(); + if (isset($form_state['object']->items)) { + foreach ($form_state['object']->items as $name => $view) { + if (!empty($view->tag)) { + $view_tags = drupal_explode_tags($view->tag); + foreach ($view_tags as $tag) { + $tags[$tag] = $tag; + } + } + } + } + asort($tags); + + $form['second row']['tag'] = array( + '#type' => 'select', + '#title' => t('Filter'), + '#options' => array_merge(array('all' => t('All tags')), array('none' => t('No tags')), $tags), + '#default_value' => 'all', + '#weight' => -9, + ); + + $displays = array(); + foreach (views_fetch_plugin_data('display') as $id => $info) { + if (!empty($info['admin'])) { + $displays[$id] = $info['admin']; + } + } + asort($displays); + + $form['second row']['display'] = array( + '#type' => 'select', + '#options' => array_merge(array('all' => t('All displays')), $displays), + '#default_value' => 'all', + '#weight' => -1, + ); + } + + function list_filter($form_state, $view) { + // Don't filter by tags if all is set up. + if ($form_state['values']['tag'] != 'all') { + // If none is selected check whether the view has a tag. + if ($form_state['values']['tag'] == 'none') { + return !empty($view->tag); + } + else { + // Check whether the tag can be found in the views tag. + return strpos($view->tag, $form_state['values']['tag']) === FALSE; + } + } + if ($form_state['values']['base'] != 'all' && $form_state['values']['base'] != $view->base_table) { + return TRUE; + } + + return parent::list_filter($form_state, $view); + } + + function list_sort_options() { + return array( + 'disabled' => t('Enabled, name'), + 'name' => t('Name'), + 'path' => t('Path'), + 'tag' => t('Tag'), + 'storage' => t('Storage'), + ); + } + + + function list_build_row($view, &$form_state, $operations) { + if (!empty($view->human_name)) { + $title = $view->human_name; + } + else { + $title = $view->get_title(); + if (empty($title)) { + $title = $view->name; + } + } + + $paths = _views_ui_get_paths($view); + $paths = implode(", ", $paths); + + $base = !empty($this->bases[$view->base_table]) ? $this->bases[$view->base_table] : t('Broken'); + + $info = theme('views_ui_view_info', array('view' => $view, 'base' => $base)); + + // Reorder the operations so that enable is the default action for a templatic views + if (!empty($operations['enable'])) { + $operations = array('enable' => $operations['enable']) + $operations; + } + + // Set up sorting + switch ($form_state['values']['order']) { + case 'disabled': + $this->sorts[$view->name] = strtolower(empty($view->disabled) . $title); + break; + case 'name': + $this->sorts[$view->name] = strtolower($title); + break; + case 'path': + $this->sorts[$view->name] = strtolower($paths); + break; + case 'tag': + $this->sorts[$view->name] = strtolower($view->tag); + break; + case 'storage': + $this->sorts[$view->name] = strtolower($view->type . $title); + break; + } + + $ops = theme('links__ctools_dropbutton', array('links' => $operations, 'attributes' => array('class' => array('links', 'inline')))); + + $this->rows[$view->name] = array( + 'data' => array( + array('data' => $info, 'class' => array('views-ui-name')), + array('data' => check_plain($view->description), 'class' => array('views-ui-description')), + array('data' => check_plain($view->tag), 'class' => array('views-ui-tag')), + array('data' => $paths, 'class' => array('views-ui-path')), + array('data' => $ops, 'class' => array('views-ui-operations')), + ), + 'title' => t('Machine name: ') . check_plain($view->name), + 'class' => array(!empty($view->disabled) ? 'ctools-export-ui-disabled' : 'ctools-export-ui-enabled'), + ); + } + + function list_render(&$form_state) { + views_include('admin'); + views_ui_add_admin_css(); + if (empty($_REQUEST['js'])) { + views_ui_check_advanced_help(); + } + drupal_add_library('system', 'jquery.bbq'); + views_add_js('views-list'); + + $this->active = $form_state['values']['order']; + $this->order = $form_state['values']['sort']; + + $query = tablesort_get_query_parameters(); + + $header = array( + $this->tablesort_link(t('View name'), 'name', 'views-ui-name'), + array('data' => t('Description'), 'class' => array('views-ui-description')), + $this->tablesort_link(t('Tag'), 'tag', 'views-ui-tag'), + $this->tablesort_link(t('Path'), 'path', 'views-ui-path'), + array('data' => t('Operations'), 'class' => array('views-ui-operations')), + ); + + $table = array( + 'header' => $header, + 'rows' => $this->rows, + 'empty' => t('No views match the search criteria.'), + 'attributes' => array('id' => 'ctools-export-ui-list-items'), + ); + return theme('table', $table); + } + + function tablesort_link($label, $field, $class) { + $title = t('sort by @s', array('@s' => $label)); + $initial = 'asc'; + + if ($this->active == $field) { + $initial = ($this->order == 'asc') ? 'desc' : 'asc'; + $label .= theme('tablesort_indicator', array('style' => $initial)); + } + + $query['order'] = $field; + $query['sort'] = $initial; + $link_options = array( + 'html' => TRUE, + 'attributes' => array('title' => $title), + 'query' => $query, + ); + $link = l($label, $_GET['q'], $link_options); + if ($this->active == $field) { + $class .= ' active'; + } + + return array('data' => $link, 'class' => $class); + } + + function clone_page($js, $input, $item, $step = NULL) { + drupal_set_title($this->get_page_title('clone', $item)); + + $name = $item->{$this->plugin['export']['key']}; + + $form_state = array( + 'plugin' => $this->plugin, + 'object' => &$this, + 'ajax' => $js, + 'item' => $item, + 'op' => 'add', + 'form type' => 'clone', + 'original name' => $name, + 'rerender' => TRUE, + 'no_redirect' => TRUE, + 'step' => $step, + // Store these in case additional args are needed. + 'function args' => func_get_args(), + ); + + $output = drupal_build_form('views_ui_clone_form', $form_state); + if (!empty($form_state['executed'])) { + $item->name = $form_state['values']['name']; + $item->human_name = $form_state['values']['human_name']; + $item->vid = NULL; + views_ui_cache_set($item); + + drupal_goto(ctools_export_ui_plugin_menu_path($this->plugin, 'edit', $item->name)); + } + + return $output; + } + + function add_template_page($js, $input, $name, $step = NULL) { + $templates = views_get_all_templates(); + + if (empty($templates[$name])) { + return MENU_NOT_FOUND; + } + + $template = $templates[$name]; + + // The template description probably describes the template, not the + // view that will be created from it, but users aren't that likely to + // touch it. + if (!empty($template->description)) { + unset($template->description); + } + + $template->is_template = TRUE; + $template->type = t('Default'); + + $output = $this->clone_page($js, $input, $template, $step); + drupal_set_title(t('Create view from template @template', array('@template' => $template->get_human_name()))); + return $output; + } + + function set_item_state($state, $js, $input, $item) { + ctools_export_set_object_status($item, $state); + menu_rebuild(); + + if (!$js) { + drupal_goto(ctools_export_ui_plugin_base_path($this->plugin)); + } + else { + return $this->list_page($js, $input); + } + } + + function list_page($js, $input) { + // Remove filters values from session if filters are hidden. + if (!variable_get('views_ui_show_listing_filters', FALSE) && isset($_SESSION['ctools_export_ui'][$this->plugin['name']])) { + unset($_SESSION['ctools_export_ui'][$this->plugin['name']]); + } + + // wrap output in a div for CSS + $output = parent::list_page($js, $input); + if (is_string($output)) { + $output = '
    ' . $output . '
    '; + return $output; + } + } +} + +/** + * Form callback to edit an exportable item using the wizard + * + * This simply loads the object defined in the plugin and hands it off. + */ +function views_ui_clone_form($form, &$form_state) { + $counter = 1; + + if (!isset($form_state['item'])) { + $view = views_get_view($form_state['original name']); + } + else { + $view = $form_state['item']; + } + do { + if (empty($form_state['item']->is_template)) { + $name = format_plural($counter, 'Clone of', 'Clone @count of') . ' ' . $view->get_human_name(); + } + else { + $name = $view->get_human_name(); + if ($counter > 1) { + $name .= ' ' . $counter; + } + } + $counter++; + $machine_name = preg_replace('/[^a-z0-9_]+/', '_', drupal_strtolower($name)); + } while (ctools_export_crud_load($form_state['plugin']['schema'], $machine_name)); + + $form['human_name'] = array( + '#type' => 'textfield', + '#title' => t('View name'), + '#default_value' => $name, + '#size' => 32, + '#maxlength' => 255, + ); + + $form['name'] = array( + '#title' => t('View name'), + '#type' => 'machine_name', + '#required' => TRUE, + '#maxlength' => 128, + '#size' => 128, + '#machine_name' => array( + 'exists' => 'ctools_export_ui_edit_name_exists', + 'source' => array('human_name'), + ), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Continue'), + ); + + return $form; +} diff --git a/plugins/export_ui/views_ui.inc b/plugins/export_ui/views_ui.inc new file mode 100644 index 00000000..042fc79c --- /dev/null +++ b/plugins/export_ui/views_ui.inc @@ -0,0 +1,37 @@ + 'views_view', + 'access' => 'administer views', + + 'menu' => array( + 'menu item' => 'views', + 'menu title' => 'Views', + 'menu description' => 'Manage customized lists of content.', + ), + + 'title singular' => t('view'), + 'title singular proper' => t('View'), + 'title plural' => t('views'), + 'title plural proper' => t('Views'), + + 'handler' => 'views_ui', + + 'strings' => array( + 'confirmation' => array( + 'revert' => array( + 'information' => t('This action will permanently remove any customizations made to this view.'), + 'success' => t('The view has been reverted.'), + ), + 'delete' => array( + 'information' => t('This action will permanently remove the view from your database.'), + 'success' => t('The view has been deleted.'), + ), + ), + ), +); diff --git a/plugins/views_plugin_access.inc b/plugins/views_plugin_access.inc new file mode 100644 index 00000000..7f80d9b7 --- /dev/null +++ b/plugins/views_plugin_access.inc @@ -0,0 +1,96 @@ +view = &$view; + $this->display = &$display; + + if (is_object($display->handler)) { + $options = $display->handler->get_option('access'); + // Overlay incoming options on top of defaults + $this->unpack_options($this->options, $options); + } + } + + /** + * Retrieve the options when this is a new access + * control plugin + */ + function option_definition() { return array(); } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + function summary_title() { + return t('Unknown'); + } + + /** + * Determine if the current user has access or not. + */ + function access($account) { + // default to no access control. + return TRUE; + } + + /** + * Determine the access callback and arguments. + * + * This information will be embedded in the menu in order to reduce + * performance hits during menu item access testing, which happens + * a lot. + * + * @return an array; the first item should be the function to call, + * and the second item should be an array of arguments. The first + * item may also be TRUE (bool only) which will indicate no + * access control.) + */ + function get_access_callback() { + // default to no access control. + return TRUE; + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_access_none.inc b/plugins/views_plugin_access_none.inc new file mode 100644 index 00000000..d69fe8e4 --- /dev/null +++ b/plugins/views_plugin_access_none.inc @@ -0,0 +1,17 @@ +options['perm'], $account); + } + + function get_access_callback() { + return array('views_check_perm', array($this->options['perm'])); + } + + function summary_title() { + $permissions = module_invoke_all('permission'); + if (isset($permissions[$this->options['perm']])) { + return $permissions[$this->options['perm']]['title']; + } + + return t($this->options['perm']); + } + + + function option_definition() { + $options = parent::option_definition(); + $options['perm'] = array('default' => 'access content'); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $perms = array(); + $module_info = system_get_info('module'); + + // Get list of permissions + foreach (module_implements('permission') as $module) { + $permissions = module_invoke($module, 'permission'); + foreach ($permissions as $name => $perm) { + $perms[$module_info[$module]['name']][$name] = strip_tags($perm['title']); + } + } + + ksort($perms); + + $form['perm'] = array( + '#type' => 'select', + '#options' => $perms, + '#title' => t('Permission'), + '#default_value' => $this->options['perm'], + '#description' => t('Only users with the selected permission flag will be able to access this display. Note that users with "access all views" can see any view, regardless of other permissions.'), + ); + } +} diff --git a/plugins/views_plugin_access_role.inc b/plugins/views_plugin_access_role.inc new file mode 100644 index 00000000..b06812ea --- /dev/null +++ b/plugins/views_plugin_access_role.inc @@ -0,0 +1,66 @@ +options['role']), $account); + } + + function get_access_callback() { + return array('views_check_roles', array(array_filter($this->options['role']))); + } + + function summary_title() { + $count = count($this->options['role']); + if ($count < 1) { + return t('No role(s) selected'); + } + elseif ($count > 1) { + return t('Multiple roles'); + } + else { + $rids = views_ui_get_roles(); + $rid = reset($this->options['role']); + return check_plain($rids[$rid]); + } + } + + + function option_definition() { + $options = parent::option_definition(); + $options['role'] = array('default' => array()); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['role'] = array( + '#type' => 'checkboxes', + '#title' => t('Role'), + '#default_value' => $this->options['role'], + '#options' => array_map('check_plain', views_ui_get_roles()), + '#description' => t('Only the checked roles will be able to access this display. Note that users with "access all views" can see any view, regardless of role.'), + ); + } + + function options_validate(&$form, &$form_state) { + if (!array_filter($form_state['values']['access_options']['role'])) { + form_error($form['role'], t('You must select at least one role if type is "by role"')); + } + } + + function options_submit(&$form, &$form_state) { + // I hate checkboxes. + $form_state['values']['access_options']['role'] = array_filter($form_state['values']['access_options']['role']); + } +} diff --git a/plugins/views_plugin_argument_default.inc b/plugins/views_plugin_argument_default.inc new file mode 100644 index 00000000..2b877300 --- /dev/null +++ b/plugins/views_plugin_argument_default.inc @@ -0,0 +1,94 @@ +view = &$view; + $this->argument = &$argument; + + $this->convert_options($options); + $this->unpack_options($this->options, $options); + } + + /** + * Retrieve the options when this is a new access + * control plugin + */ + function option_definition() { return array(); } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state, &$options = array()) { } + + /** + * Determine if the administrator has the privileges to use this + * plugin + */ + function access() { return TRUE; } + + /** + * If we don't have access to the form but are showing it anyway, ensure that + * the form is safe and cannot be changed from user input. + * + * This is only called by child objects if specified in the options_form(), + * so it will not always be used. + */ + function check_access(&$form, $option_name) { + if (!$this->access()) { + $form[$option_name]['#disabled'] = TRUE; + $form[$option_name]['#value'] = $form[$this->option_name]['#default_value']; + $form[$option_name]['#description'] .= ' ' . t('Note: you do not have permission to modify this. If you change the default filter type, this setting will be lost and you will NOT be able to get it back.') . ''; + } + } + + /** + * Convert options from the older style. + * + * In Views 3, the method of storing default argument options has changed + * and each plugin now gets its own silo. This method can be used to + * move arguments from the old style to the new style. See + * views_plugin_argument_default_fixed for a good example of this method. + */ + function convert_options(&$options) { } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_argument_default_fixed.inc b/plugins/views_plugin_argument_default_fixed.inc new file mode 100644 index 00000000..38ede348 --- /dev/null +++ b/plugins/views_plugin_argument_default_fixed.inc @@ -0,0 +1,46 @@ + ''); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['argument'] = array( + '#type' => 'textfield', + '#title' => t('Fixed value'), + '#default_value' => $this->options['argument'], + ); + } + + /** + * Return the default argument. + */ + function get_argument() { + return $this->options['argument']; + } + + function convert_options(&$options) { + if (!isset($options['argument']) && isset($this->argument->options['default_argument_fixed'])) { + $options['argument'] = $this->argument->options['default_argument_fixed']; + } + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_argument_default_php.inc b/plugins/views_plugin_argument_default_php.inc new file mode 100644 index 00000000..c2fb14f5 --- /dev/null +++ b/plugins/views_plugin_argument_default_php.inc @@ -0,0 +1,57 @@ + ''); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['code'] = array( + '#type' => 'textarea', + '#title' => t('PHP contextual filter code'), + '#default_value' => $this->options['code'], + '#description' => t('Enter PHP code that returns a value to use for this filter. Do not use <?php ?>. You must return only a single value for just this filter. Some variables are available: the view object will be "$view". The argument handler will be "$argument", for example you may change the title used for substitutions for this argument by setting "argument->validated_title"".'), + ); + + // Only do this if using one simple standard form gadget + $this->check_access($form, 'code'); + } + + function convert_options(&$options) { + if (!isset($options['code']) && isset($this->argument->options['default_argument_php'])) { + $options['code'] = $this->argument->options['default_argument_php']; + } + } + + /** + * Only let users with PHP block visibility permissions set/modify this + * default plugin. + */ + function access() { + return user_access('use PHP for settings'); + } + + function get_argument() { + // set up variables to make it easier to reference during the argument. + $view = &$this->view; + $argument = &$this->argument; + ob_start(); + $result = eval($this->options['code']); + ob_end_clean(); + return $result; + } +} diff --git a/plugins/views_plugin_argument_default_raw.inc b/plugins/views_plugin_argument_default_raw.inc new file mode 100644 index 00000000..385ca912 --- /dev/null +++ b/plugins/views_plugin_argument_default_raw.inc @@ -0,0 +1,50 @@ + ''); + $options['use_alias'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + // Using range(1, 10) will create an array keyed 0-9, which allows arg() to + // properly function since it is also zero-based. + $form['index'] = array( + '#type' => 'select', + '#title' => t('Path component'), + '#default_value' => $this->options['index'], + '#options' => range(1, 10), + '#description' => t('The numbering starts from 1, e.g. on the page admin/structure/types, the 3rd path component is "types".'), + ); + $form['use_alias'] = array( + '#type' => 'checkbox', + '#title' => t('Use path alias'), + '#default_value' => $this->options['use_alias'], + '#description' => t('Use path alias instead of internal path.'), + ); + } + + function get_argument() { + $path = NULL; + if ($this->options['use_alias']) { + $path = drupal_get_path_alias(); + } + if ($arg = arg($this->options['index'], $path)) { + return $arg; + } + } +} diff --git a/plugins/views_plugin_argument_validate.inc b/plugins/views_plugin_argument_validate.inc new file mode 100644 index 00000000..07b49eeb --- /dev/null +++ b/plugins/views_plugin_argument_validate.inc @@ -0,0 +1,99 @@ +view = &$view; + $this->argument = &$argument; + + $this->convert_options($options); + $this->unpack_options($this->options, $options); + } + + /** + * Retrieve the options when this is a new access + * control plugin + */ + function option_definition() { return array(); } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state, &$options = array()) { } + + /** + * Convert options from the older style. + * + * In Views 3, the method of storing default argument options has changed + * and each plugin now gets its own silo. This method can be used to + * move arguments from the old style to the new style. See + * views_plugin_argument_default_fixed for a good example of this method. + */ + function convert_options(&$options) { } + + /** + * Determine if the administrator has the privileges to use this plugin + */ + function access() { return TRUE; } + + /** + * If we don't have access to the form but are showing it anyway, ensure that + * the form is safe and cannot be changed from user input. + * + * This is only called by child objects if specified in the options_form(), + * so it will not always be used. + */ + function check_access(&$form, $option_name) { + if (!$this->access()) { + $form[$option_name]['#disabled'] = TRUE; + $form[$option_name]['#value'] = $form[$this->option_name]['#default_value']; + $form[$option_name]['#description'] .= ' ' . t('Note: you do not have permission to modify this. If you change the default filter type, this setting will be lost and you will NOT be able to get it back.') . ''; + } + } + + function validate_argument($arg) { return TRUE; } + + /** + * Process the summary arguments for displaying. + * + * Some plugins alter the argument so it uses something else interal. + * For example the user validation set's the argument to the uid, + * for a faster query. But there are use cases where you want to use + * the old value again, for example the summary. + */ + function process_summary_arguments(&$args) { } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_argument_validate_numeric.inc b/plugins/views_plugin_argument_validate_numeric.inc new file mode 100644 index 00000000..049531bb --- /dev/null +++ b/plugins/views_plugin_argument_validate_numeric.inc @@ -0,0 +1,17 @@ + ''); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['code'] = array( + '#type' => 'textarea', + '#title' => t('PHP validate code'), + '#default_value' => $this->options['code'], + '#description' => t('Enter PHP code that returns TRUE or FALSE. No return is the same as FALSE, so be SURE to return something if you do not want to declare the argument invalid. Do not use <?php ?>. The argument to validate will be "$argument" and the view will be "$view". You may change the argument by setting "$handler->argument". You may change the title used for substitutions for this argument by setting "$handler->validated_title".'), + ); + + $this->check_access($form, 'code'); + } + + /** + * Only let users with PHP block visibility permissions set/modify this + * validate plugin. + */ + function access() { + return user_access('use PHP for settings'); + } + + function convert_options(&$options) { + if (!isset($options['code']) && isset($this->argument->options['validate_argument_php'])) { + $options['code'] = $this->argument->options['validate_argument_php']; + } + } + + function validate_argument($argument) { + // set up variables to make it easier to reference during the argument. + $view = &$this->view; + $handler = &$this->argument; + + ob_start(); + $result = eval($this->options['code']); + ob_end_clean(); + return $result; + } +} diff --git a/plugins/views_plugin_cache.inc b/plugins/views_plugin_cache.inc new file mode 100644 index 00000000..4d21701f --- /dev/null +++ b/plugins/views_plugin_cache.inc @@ -0,0 +1,313 @@ +view = &$view; + $this->display = &$display; + + if (is_object($display->handler)) { + $options = $display->handler->get_option('cache'); + // Overlay incoming options on top of defaults + $this->unpack_options($this->options, $options); + } + } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + function summary_title() { + return t('Unknown'); + } + + /** + * Determine the expiration time of the cache type, or NULL if no expire. + * + * Plugins must override this to implement expiration. + * + * @param $type + * The cache type, either 'query', 'result' or 'output'. + */ + function cache_expire($type) { } + + /** + * Determine expiration time in the cache table of the cache type + * or CACHE_PERMANENT if item shouldn't be removed automatically from cache. + * + * Plugins must override this to implement expiration in the cache table. + * + * @param $type + * The cache type, either 'query', 'result' or 'output'. + */ + function cache_set_expire($type) { + return CACHE_PERMANENT; + } + + + /** + * Save data to the cache. + * + * A plugin should override this to provide specialized caching behavior. + */ + function cache_set($type) { + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + break; + case 'results': + $data = array( + 'result' => $this->view->result, + 'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0, + 'current_page' => $this->view->get_current_page(), + ); + cache_set($this->get_results_key(), $data, $this->table, $this->cache_set_expire($type)); + break; + case 'output': + $this->gather_headers(); + $this->storage['output'] = $this->view->display_handler->output; + cache_set($this->get_output_key(), $this->storage, $this->table, $this->cache_set_expire($type)); + break; + } + } + + + /** + * Retrieve data from the cache. + * + * A plugin should override this to provide specialized caching behavior. + */ + function cache_get($type) { + $cutoff = $this->cache_expire($type); + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + return FALSE; + case 'results': + // Values to set: $view->result, $view->total_rows, $view->execute_time, + // $view->current_page. + if ($cache = cache_get($this->get_results_key(), $this->table)) { + if (!$cutoff || $cache->created > $cutoff) { + $this->view->result = $cache->data['result']; + $this->view->total_rows = $cache->data['total_rows']; + $this->view->set_current_page($cache->data['current_page']); + $this->view->execute_time = 0; + return TRUE; + } + } + return FALSE; + case 'output': + if ($cache = cache_get($this->get_output_key(), $this->table)) { + if (!$cutoff || $cache->created > $cutoff) { + $this->storage = $cache->data; + $this->view->display_handler->output = $cache->data['output']; + $this->restore_headers(); + return TRUE; + } + } + return FALSE; + } + } + + /** + * Clear out cached data for a view. + * + * We're just going to nuke anything related to the view, regardless of display, + * to be sure that we catch everything. Maybe that's a bad idea. + */ + function cache_flush() { + cache_clear_all($this->view->name . ':', $this->table, TRUE); + } + + /** + * Post process any rendered data. + * + * This can be valuable to be able to cache a view and still have some level of + * dynamic output. In an ideal world, the actual output will include HTML + * comment based tokens, and then the post process can replace those tokens. + * + * Example usage. If it is known that the view is a node view and that the + * primary field will be a nid, you can do something like this: + * + * + * + * And then in the post render, create an array with the text that should + * go there: + * + * strtr($output, array('', 'output for FIELD of nid 1'); + * + * All of the cached result data will be available in $view->result, as well, + * so all ids used in the query should be discoverable. + */ + function post_render(&$output) { } + + /** + * Start caching javascript, css and other out of band info. + * + * This takes a snapshot of the current system state so that we don't + * duplicate it. Later on, when gather_headers() is run, this information + * will be removed so that we don't hold onto it. + */ + function cache_start() { + $this->storage['head'] = drupal_add_html_head(); + $this->storage['css'] = drupal_add_css(); + $this->storage['js'] = drupal_add_js(); + $this->storage['headers'] = drupal_get_http_header(); + } + + /** + * Gather out of band data, compare it to what we started with and store the difference. + */ + function gather_headers() { + // Simple replacement for head + if (isset($this->storage['head'])) { + $this->storage['head'] = str_replace($this->storage['head'], '', drupal_add_html_head()); + } + else { + $this->storage['head'] = ''; + } + + // Slightly less simple for CSS: + $css = drupal_add_css(); + $css_start = isset($this->storage['css']) ? $this->storage['css'] : array(); + $this->storage['css'] = array_diff_assoc($css, $css_start); + + // Get javascript after/before views renders. + $js = drupal_add_js(); + $js_start = isset($this->storage['js']) ? $this->storage['js'] : array(); + // If there are any differences between the old and the new javascript then + // store them to be added later. + $this->storage['js'] = array_diff_assoc($js, $js_start); + + // Special case the settings key and get the difference of the data. + $settings = isset($js['settings']['data']) ? $js['settings']['data'] : array(); + $settings_start = isset($js_start['settings']['data']) ? $js_start['settings']['data'] : array(); + $this->storage['js']['settings'] = array_diff_assoc($settings, $settings_start); + + // Get difference of HTTP headers. + $this->storage['headers'] = array_diff_assoc(drupal_get_http_header(), $this->storage['headers']); + } + + /** + * Restore out of band data saved to cache. Copied from Panels. + */ + function restore_headers() { + if (!empty($this->storage['head'])) { + drupal_add_html_head($this->storage['head']); + } + if (!empty($this->storage['css'])) { + foreach ($this->storage['css'] as $args) { + drupal_add_css($args['data'], $args); + } + } + if (!empty($this->storage['js'])) { + foreach ($this->storage['js'] as $key => $args) { + if ($key !== 'settings') { + drupal_add_js($args['data'], $args); + } + else { + foreach ($args as $setting) { + drupal_add_js($setting, 'setting'); + } + } + } + } + if (!empty($this->storage['headers'])) { + foreach ($this->storage['headers'] as $name => $value) { + drupal_add_http_header($name, $value); + } + } + } + + function get_results_key() { + global $user; + + if (!isset($this->_results_key)) { + + $build_info = $this->view->build_info; + + $query_plugin = $this->view->display_handler->get_plugin('query'); + + foreach (array('query','count_query') as $index) { + // If the default query back-end is used generate SQL query strings from + // the query objects. + if ($build_info[$index] instanceof SelectQueryInterface) { + $query = clone $build_info[$index]; + $query->preExecute(); + $build_info[$index] = (string) $query; + } + } + $key_data = array( + 'build_info' => $build_info, + 'roles' => array_keys($user->roles), + 'super-user' => $user->uid == 1, // special caching for super user. + 'language' => $GLOBALS['language']->language, + 'base_url' => $GLOBALS['base_url'], + ); + foreach (array('exposed_info', 'page', 'sort', 'order', 'items_per_page', 'offset') as $key) { + if (isset($_GET[$key])) { + $key_data[$key] = $_GET[$key]; + } + } + + $this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data)); + } + + return $this->_results_key; + } + + function get_output_key() { + global $user; + if (!isset($this->_output_key)) { + $key_data = array( + 'result' => $this->view->result, + 'roles' => array_keys($user->roles), + 'super-user' => $user->uid == 1, // special caching for super user. + 'theme' => $GLOBALS['theme'], + 'language' => $GLOBALS['language']->language, + 'base_url' => $GLOBALS['base_url'], + ); + + $this->_output_key = $this->view->name . ':' . $this->display->id . ':output:' . md5(serialize($key_data)); + } + + return $this->_output_key; + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_cache_none.inc b/plugins/views_plugin_cache_none.inc new file mode 100644 index 00000000..9927a9d7 --- /dev/null +++ b/plugins/views_plugin_cache_none.inc @@ -0,0 +1,25 @@ + 3600); + $options['results_lifespan_custom'] = array('default' => 0); + $options['output_lifespan'] = array('default' => 3600); + $options['output_lifespan_custom'] = array('default' => 0); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $options = array(60, 300, 1800, 3600, 21600, 518400); + $options = drupal_map_assoc($options, 'format_interval'); + $options = array(-1 => t('Never cache')) + $options + array('custom' => t('Custom')); + + $form['results_lifespan'] = array( + '#type' => 'select', + '#title' => t('Query results'), + '#description' => t('The length of time raw query results should be cached.'), + '#options' => $options, + '#default_value' => $this->options['results_lifespan'], + ); + $form['results_lifespan_custom'] = array( + '#type' => 'textfield', + '#title' => t('Seconds'), + '#size' => '25', + '#maxlength' => '30', + '#description' => t('Length of time in seconds raw query results should be cached.'), + '#default_value' => $this->options['results_lifespan_custom'], + '#process' => array('form_process_select','ctools_dependent_process'), + '#dependency' => array( + 'edit-cache-options-results-lifespan' => array('custom'), + ), + ); + $form['output_lifespan'] = array( + '#type' => 'select', + '#title' => t('Rendered output'), + '#description' => t('The length of time rendered HTML output should be cached.'), + '#options' => $options, + '#default_value' => $this->options['output_lifespan'], + ); + $form['output_lifespan_custom'] = array( + '#type' => 'textfield', + '#title' => t('Seconds'), + '#size' => '25', + '#maxlength' => '30', + '#description' => t('Length of time in seconds rendered HTML output should be cached.'), + '#default_value' => $this->options['output_lifespan_custom'], + '#process' => array('form_process_select','ctools_dependent_process'), + '#dependency' => array( + 'edit-cache-options-output-lifespan' => array('custom'), + ), + ); + } + + function options_validate(&$form, &$form_state) { + $custom_fields = array('output_lifespan', 'results_lifespan'); + foreach ($custom_fields as $field) { + if ($form_state['values']['cache_options'][$field] == 'custom' && !is_numeric($form_state['values']['cache_options'][$field . '_custom'])) { + form_error($form[$field .'_custom'], t('Custom time values must be numeric.')); + } + } + } + + function summary_title() { + $results_lifespan = $this->get_lifespan('results'); + $output_lifespan = $this->get_lifespan('output'); + return format_interval($results_lifespan, 1) . '/' . format_interval($output_lifespan, 1); + } + + function get_lifespan($type) { + $lifespan = $this->options[$type . '_lifespan'] == 'custom' ? $this->options[$type . '_lifespan_custom'] : $this->options[$type . '_lifespan']; + return $lifespan; + } + + function cache_expire($type) { + $lifespan = $this->get_lifespan($type); + if ($lifespan) { + $cutoff = REQUEST_TIME - $lifespan; + return $cutoff; + } + else { + return FALSE; + } + } + + function cache_set_expire($type) { + $lifespan = $this->get_lifespan($type); + if ($lifespan) { + return time() + $lifespan; + } + else { + return CACHE_PERMANENT; + } + } +} diff --git a/plugins/views_plugin_display.inc b/plugins/views_plugin_display.inc new file mode 100644 index 00000000..34bc34f1 --- /dev/null +++ b/plugins/views_plugin_display.inc @@ -0,0 +1,3075 @@ +view = &$view; + $this->display = &$display; + + // Load extenders as soon as possible. + $this->extender = array(); + $extenders = views_get_enabled_display_extenders(); + // If you update to the dev version the registry might not be loaded yet. + if (!empty($extenders) && class_exists('views_plugin_display_extender')) { + foreach ($extenders as $extender) { + $plugin = views_get_plugin('display_extender', $extender); + if ($plugin) { + $plugin->init($this->view, $this); + $this->extender[$extender] = $plugin; + } + else { + vpr('Invalid display extender @extender', array('@handler' => $extender)); + } + } + } + + // Track changes that the user should know about. + $changed = FALSE; + + // Make some modifications: + if (!isset($options) && isset($display->display_options)) { + $options = $display->display_options; + } + + if ($this->is_default_display() && isset($options['defaults'])) { + unset($options['defaults']); + } + + // Cache for unpack_options, but not if we are in the ui. + static $unpack_options = array(); + if (empty($view->editing)) { + $cid = 'unpack_options:' . md5(serialize(array($this->options, $options))); + if (empty($unpack_options[$cid])) { + $cache = views_cache_get($cid, TRUE); + if (!empty($cache->data)) { + $this->options = $cache->data; + } + else { + $this->unpack_options($this->options, $options); + views_cache_set($cid, $this->options, TRUE); + } + $unpack_options[$cid] = $this->options; + } + else { + $this->options = $unpack_options[$cid]; + } + } + else { + $this->unpack_options($this->options, $options); + } + + // Translate changed settings: + $items_per_page = $this->get_option('items_per_page'); + $offset = $this->get_option('offset'); + $use_pager = $this->get_option('use_pager'); + $pager = $this->get_option('pager'); + // Check if the pager options were already converted. + // The pager settings of a Views 2.x view specifying 10 items with an + // offset of 0 and no pager is the same as of a Views 3.x view with + // default settings. In this case, the only way to determine which case we + // are dealing with is checking the API version but that's only available + // for exported Views as it's not stored in the database. + // If you would like to change this code, really take care that you thought + // of every possibility. + // @TODO: Provide a way to convert the database views as well. + if (((!empty($items_per_page) && $items_per_page != 10) || !empty($offset) || !empty($use_pager)) + || (!empty($view->api_version) && $view->api_version == 2)) { + // Find out the right pager type. + // If the view "use pager" it's a normal/full pager. + if ($use_pager) { + $type = 'full'; + } + // If it does not use pager, but 0 items per page it should not page + // else it should display just a certain amount of items. + else { + $type = $items_per_page ? 'some' : 'none'; + } + + // Setup the pager options. + $pager = array( + 'type' => $type, + 'options' => array( + 'offset' => intval($offset) + ), + ); + + if ($items_per_page) { + $pager['options']['items_per_page'] = $items_per_page; + } + // Setup the pager element. + if ($id = $this->get_option('pager_element')) { + $pager['options']['id'] = $id; + } + + // Unset the previous options + // After edit and save the view they will be erased + $this->set_option('items_per_page', NULL); + $this->set_option('offset', NULL); + $this->set_option('use_pager', NULL); + $this->set_option('pager', $pager); + $changed = TRUE; + } + + + // Plugable headers, footer and empty texts are + // not compatible with previous version of views + // This code converts old values into a configured handler for each area + foreach (array('header', 'footer', 'empty') as $area) { + $converted = FALSE; + if (isset($this->options[$area]) && !is_array($this->options[$area])) { + if (!empty($this->options[$area])) { + $content = $this->get_option($area); + if (!empty($content) && !is_array($content)) { + $format = $this->get_option($area . '_format'); + $options = array( + 'id' => 'area', + 'table' => 'views', + 'field' => 'area', + 'label' => '', + 'relationship' => 'none', + 'group_type' => 'group', + 'content' => $content, + 'format' => !empty($format) ? $format : filter_default_format(), + ); + + if ($area != 'empty' && $empty = $this->get_option($area . '_empty')) { + $options['empty'] = $empty; + } + $this->set_option($area, array('text' => $options)); + $converted = TRUE; + $changed = TRUE; + } + } + // Ensure that options are at least an empty array + if (!$converted) { + $this->set_option($area, array()); + } + } + } + + // Convert distinct setting from display to query settings. + $distinct = $this->get_option('distinct'); + if (!empty($distinct)) { + $query_settings = $this->get_option('query'); + $query_settings['options']['distinct'] = $distinct; + $this->set_option('query', $query_settings); + // Clear the values + $this->set_option('distinct', NULL); + $changed = TRUE; + } + + // Convert field language settings. + $query_options = $this->get_option('query'); + if (isset($query_options['options']['field_language'])) { + $this->set_option('field_language', $query_options['options']['field_language']); + unset($query_options['options']['field_language']); + $changed = TRUE; + } + if (isset($query_options['options']['field_language_add_to_query'])) { + $this->set_option('field_language_add_to_query', $query_options['options']['field_language_add_to_query']); + unset($query_options['options']['field_language_add_to_query']); + $changed = TRUE; + } + $this->set_option('query', $query_options); + + // Convert filter groups. + $filter_groups = $this->get_option('filter_groups'); + // Only convert if it wasn't converted yet, which is the case if there is a 0 group. + if (isset($filter_groups['groups'][0])) { + // Update filter groups. + $filter_groups ['groups'] = views_array_key_plus($filter_groups['groups']); + $this->set_option('filter_groups', $filter_groups); + // Update the filter group on each filter. + $filters = $this->get_option('filters'); + foreach ($filters as &$filter) { + if (isset($filter['group'])) { + $filter['group']++; + } + else { + $filter['group'] = 1; + } + } + $this->set_option('filters', $filters); + $changed = TRUE; + } + + // Filter groups were allowed to be rewritten without its filters, so + // before this update the view was using the default values. To be sure that + // the existing view isn't broken, don't use this overridden values but copy + // them from the default display. Only do this if the filters are overridden + // but the filter_groups are not marked as so. + if (!$this->is_default_display() && !$this->options['defaults']['filters'] && $this->options['defaults']['filter_groups']) { + // Set filter_groups to be overridden and save the value in the + // display_options as well. + $this->options['defaults']['filter_groups'] = FALSE; + $this->display->display_options['defaults']['filter_groups'] = $this->options['defaults']['filter_groups']; + // Copy the filter_groups from the default, and add them to the + // display_options as well. $this->default_display is not initialized at + // this point. + $this->options['filter_groups'] = $this->view->display['default']->handler->options['filter_groups']; + $this->display->display_options['filter_groups'] = $this->options['filter_groups']; + + $changed = TRUE; + } + + // Mark the view as changed so the user has a chance to save it. + if ($changed) { + $this->view->changed = TRUE; + } + } + + function destroy() { + parent::destroy(); + + foreach ($this->handlers as $type => $handlers) { + foreach ($handlers as $id => $handler) { + if (is_object($handler)) { + $this->handlers[$type][$id]->destroy(); + } + } + } + + if (isset($this->default_display)) { + unset($this->default_display); + } + + foreach ($this->extender as $extender) { + $extender->destroy(); + } + } + + /** + * Determine if this display is the 'default' display which contains + * fallback settings + */ + function is_default_display() { return FALSE; } + + /** + * Determine if this display uses exposed filters, so the view + * will know whether or not to build them. + */ + function uses_exposed() { + if (!isset($this->has_exposed)) { + foreach ($this->handlers as $type => $value) { + foreach ($this->view->$type as $id => $handler) { + if ($handler->can_expose() && $handler->is_exposed()) { + // one is all we need; if we find it, return true. + $this->has_exposed = TRUE; + return TRUE; + } + } + } + $pager = $this->get_plugin('pager'); + if (isset($pager) && $pager->uses_exposed()) { + $this->has_exposed = TRUE; + return TRUE; + } + $this->has_exposed = FALSE; + } + + return $this->has_exposed; + } + + /** + * Determine if this display should display the exposed + * filters widgets, so the view will know whether or not + * to render them. + * + * Regardless of what this function + * returns, exposed filters will not be used nor + * displayed unless uses_exposed() returns TRUE. + */ + function displays_exposed() { + return TRUE; + } + + /** + * Does the display use AJAX? + */ + function use_ajax() { + if (!empty($this->definition['use ajax'])) { + return $this->get_option('use_ajax'); + } + return FALSE; + } + + /** + * Does the display have a pager enabled? + */ + function use_pager() { + $pager = $this->get_plugin('pager'); + if ($pager) { + return $pager->use_pager(); + } + } + + /** + * Does the display have a more link enabled? + */ + function use_more() { + if (!empty($this->definition['use more'])) { + return $this->get_option('use_more'); + } + return FALSE; + } + + /** + * Does the display have groupby enabled? + */ + function use_group_by() { + return $this->get_option('group_by'); + } + + /** + * Should the enabled display more link be shown when no more items? + */ + function use_more_always() { + if (!empty($this->definition['use more'])) { + return $this->get_option('use_more_always'); + } + return FALSE; + } + + /** + * Does the display have custom link text? + */ + function use_more_text() { + if (!empty($this->definition['use more'])) { + return $this->get_option('use_more_text'); + } + return FALSE; + } + + /** + * Can this display accept attachments? + */ + function accept_attachments() { + if (empty($this->definition['accept attachments'])) { + return FALSE; + } + if (!empty($this->view->argument) && $this->get_option('hide_attachment_summary')) { + foreach ($this->view->argument as $argument_id => $argument) { + if ($argument->needs_style_plugin() && empty($argument->argument_validated)) { + return FALSE; + } + } + } + return TRUE; + } + + /** + * Allow displays to attach to other views. + */ + function attach_to($display_id) { } + + /** + * Static member function to list which sections are defaultable + * and what items each section contains. + */ + function defaultable_sections($section = NULL) { + $sections = array( + 'access' => array('access', 'access_options'), + 'access_options' => array('access', 'access_options'), + 'cache' => array('cache', 'cache_options'), + 'cache_options' => array('cache', 'cache_options'), + 'title' => array('title'), + 'css_class' => array('css_class'), + 'use_ajax' => array('use_ajax'), + 'hide_attachment_summary' => array('hide_attachment_summary'), + 'hide_admin_links' => array('hide_admin_links'), + 'group_by' => array('group_by'), + 'query' => array('query'), + 'use_more' => array('use_more', 'use_more_always', 'use_more_text'), + 'use_more_always' => array('use_more', 'use_more_always', 'use_more_text'), + 'use_more_text' => array('use_more', 'use_more_always', 'use_more_text'), + 'link_display' => array('link_display', 'link_url'), + + // Force these to cascade properly. + 'style_plugin' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'), + 'style_options' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'), + 'row_plugin' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'), + 'row_options' => array('style_plugin', 'style_options', 'row_plugin', 'row_options'), + + 'pager' => array('pager', 'pager_options'), + 'pager_options' => array('pager', 'pager_options'), + + 'exposed_form' => array('exposed_form', 'exposed_form_options'), + 'exposed_form_options' => array('exposed_form', 'exposed_form_options'), + + // These guys are special + 'header' => array('header'), + 'footer' => array('footer'), + 'empty' => array('empty'), + 'relationships' => array('relationships'), + 'fields' => array('fields'), + 'sorts' => array('sorts'), + 'arguments' => array('arguments'), + 'filters' => array('filters', 'filter_groups'), + 'filter_groups' => array('filters', 'filter_groups'), + ); + + // If the display cannot use a pager, then we cannot default it. + if (empty($this->definition['use pager'])) { + unset($sections['pager']); + unset($sections['items_per_page']); + } + + foreach ($this->extender as $extender) { + $extender->defaultable_sections($sections, $section); + } + + if ($section) { + if (!empty($sections[$section])) { + return $sections[$section]; + } + } + else { + return $sections; + } + } + + function option_definition() { + $options = array( + 'defaults' => array( + 'default' => array( + 'access' => TRUE, + 'cache' => TRUE, + 'query' => TRUE, + 'title' => TRUE, + 'css_class' => TRUE, + + 'display_description' => FALSE, + 'use_ajax' => TRUE, + 'hide_attachment_summary' => TRUE, + 'hide_admin_links' => FALSE, + 'pager' => TRUE, + 'pager_options' => TRUE, + 'use_more' => TRUE, + 'use_more_always' => TRUE, + 'use_more_text' => TRUE, + 'exposed_form' => TRUE, + 'exposed_form_options' => TRUE, + + 'link_display' => TRUE, + 'link_url' => '', + 'group_by' => TRUE, + + 'style_plugin' => TRUE, + 'style_options' => TRUE, + 'row_plugin' => TRUE, + 'row_options' => TRUE, + + 'header' => TRUE, + 'footer' => TRUE, + 'empty' => TRUE, + + 'relationships' => TRUE, + 'fields' => TRUE, + 'sorts' => TRUE, + 'arguments' => TRUE, + 'filters' => TRUE, + 'filter_groups' => TRUE, + ), + 'export' => FALSE, + ), + + 'title' => array( + 'default' => '', + 'translatable' => TRUE, + ), + 'enabled' => array( + 'default' => TRUE, + 'translatable' => FALSE, + 'bool' => TRUE, + ), + 'display_comment' => array( + 'default' => '', + ), + 'css_class' => array( + 'default' => '', + 'translatable' => FALSE, + ), + 'display_description' => array( + 'default' => '', + 'translatable' => TRUE, + ), + 'use_ajax' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'hide_attachment_summary' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'hide_admin_links' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + // This is legacy code: + // Items_per/offset/use_pager is moved to the pager plugin + // but the automatic update path needs this items defined, so don't remove it. + // @see views_plugin_display::init() + 'items_per_page' => array( + 'default' => 10, + ), + 'offset' => array( + 'default' => 0, + ), + 'use_pager' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'use_more' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'use_more_always' => array( + 'default' => FALSE, + 'bool' => TRUE, + 'export' => 'export_option_always', + ), + 'use_more_text' => array( + 'default' => 'more', + 'translatable' => TRUE, + ), + 'link_display' => array( + 'default' => '', + ), + 'link_url' => array( + 'default' => '', + ), + 'group_by' => array( + 'default' => FALSE, + 'bool' => TRUE, + ), + 'field_language' => array( + 'default' => '***CURRENT_LANGUAGE***', + ), + 'field_language_add_to_query' => array( + 'default' => 1, + ), + + // These types are all plugins that can have individual settings + // and therefore need special handling. + 'access' => array( + 'contains' => array( + 'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), + ), + ), + 'cache' => array( + 'contains' => array( + 'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), + ), + ), + 'query' => array( + 'contains' => array( + 'type' => array('default' => 'views_query', 'export' => 'export_plugin'), + 'options' => array('default' => array(), 'export' => FALSE), + ), + ), + // Note that exposed_form plugin has options in a separate array, + // while access and cache do not. access and cache are legacy and + // that pattern should not be repeated, but it is left as is to + // reduce the need to modify older views. Let's consider the + // pattern used here to be the template from which future plugins + // should be copied. + 'exposed_form' => array( + 'contains' => array( + 'type' => array('default' => 'basic', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), + 'options' => array('default' => array(), 'export' => FALSE), + ), + ), + 'pager' => array( + 'contains' => array( + 'type' => array('default' => 'full', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), + 'options' => array('default' => array(), 'export' => FALSE), + ), + ), + + // Note that the styles have their options completely independent. + // Like access and cache above, this is a legacy pattern and + // should not be repeated. + 'style_plugin' => array( + 'default' => 'default', + 'export' => 'export_style', + 'unpack_translatable' => 'unpack_style', + ), + 'style_options' => array( + 'default' => array(), + 'export' => FALSE, + ), + 'row_plugin' => array( + 'default' => 'fields', + 'export' => 'export_style', + 'unpack_translatable' => 'unpack_style', + ), + 'row_options' => array( + 'default' => array(), + 'export' => FALSE, + ), + + 'exposed_block' => array( + 'default' => FALSE, + ), + + 'header' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + ), + 'footer' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + ), + 'empty' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + ), + + // We want these to export last. + // These are the 5 handler types. + 'relationships' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + + ), + 'fields' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + ), + 'sorts' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + ), + 'arguments' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + ), + 'filter_groups' => array( + 'contains' => array( + 'operator' => array('default' => 'AND'), + 'groups' => array('default' => array(1 => 'AND')), + ), + ), + 'filters' => array( + 'default' => array(), + 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_handler', + ), + ); + + if (empty($this->definition['use pager'])) { + $options['defaults']['default']['use_pager'] = FALSE; + $options['defaults']['default']['items_per_page'] = FALSE; + $options['defaults']['default']['offset'] = FALSE; + $options['defaults']['default']['pager'] = FALSE; + $options['pager']['contains']['type']['default'] = 'some'; + } + + if ($this->is_default_display()) { + unset($options['defaults']); + } + + foreach ($this->extender as $extender) { + $extender->options_definition_alter($options); + } + + return $options; + } + + /** + * Check to see if the display has a 'path' field. + * + * This is a pure function and not just a setting on the definition + * because some displays (such as a panel pane) may have a path based + * upon configuration. + * + * By default, displays do not have a path. + */ + function has_path() { return FALSE; } + + /** + * Check to see if the display has some need to link to another display. + * + * For the most part, displays without a path will use a link display. However, + * sometimes displays that have a path might also need to link to another display. + * This is true for feeds. + */ + function uses_link_display() { return !$this->has_path(); } + + /** + * Check to see if the display can put the exposed formin a block. + * + * By default, displays that do not have a path cannot disconnect + * the exposed form and put it in a block, because the form has no + * place to go and Views really wants the forms to go to a specific + * page. + */ + function uses_exposed_form_in_block() { return $this->has_path(); } + + /** + * Check to see which display to use when creating links within + * a view using this display. + */ + function get_link_display() { + $display_id = $this->get_option('link_display'); + // If unknown, pick the first one. + if (empty($display_id) || empty($this->view->display[$display_id])) { + foreach ($this->view->display as $display_id => $display) { + if (!empty($display->handler) && $display->handler->has_path()) { + return $display_id; + } + } + } + else { + return $display_id; + } + // fall-through returns NULL + } + + /** + * Return the base path to use for this display. + * + * This can be overridden for displays that do strange things + * with the path. + */ + function get_path() { + if ($this->has_path()) { + return $this->get_option('path'); + } + + $display_id = $this->get_link_display(); + if ($display_id && !empty($this->view->display[$display_id]) && is_object($this->view->display[$display_id]->handler)) { + return $this->view->display[$display_id]->handler->get_path(); + } + + if ($this->get_option('link_display') == 'custom_url' && $link_url = $this->get_option('link_url')) { + return $link_url; + } + } + + function get_url() { + return $this->view->get_url(); + } + + /** + * Check to see if the display needs a breadcrumb + * + * By default, displays do not need breadcrumbs + */ + function uses_breadcrumb() { return FALSE; } + + /** + * Determine if a given option is set to use the default display or the + * current display + * + * @return + * TRUE for the default display + */ + function is_defaulted($option) { + return !$this->is_default_display() && !empty($this->default_display) && !empty($this->options['defaults'][$option]); + } + + /** + * Intelligently get an option either from this display or from the + * default display, if directed to do so. + */ + function get_option($option) { + if ($this->is_defaulted($option)) { + return $this->default_display->get_option($option); + } + + if (array_key_exists($option, $this->options)) { + return $this->options[$option]; + } + } + + /** + * Determine if the display's style uses fields. + */ + function uses_fields() { + $plugin = $this->get_plugin(); + if ($plugin) { + return $plugin->uses_fields(); + } + } + + /** + * Get the instance of a plugin, for example style or row. + * + * @param string $type + * The type of the plugin. + * @param string $name + * The name of the plugin defined in hook_views_plugins. + * + * @return views_plugin|FALSE + */ + function get_plugin($type = 'style', $name = NULL) { + static $cache = array(); + if (!isset($cache[$type][$name])) { + switch ($type) { + case 'style': + case 'row': + $option_name = $type . '_plugin'; + $options = $this->get_option($type . '_options'); + if (!$name) { + $name = $this->get_option($option_name); + } + + break; + case 'query': + $views_data = views_fetch_data($this->view->base_table); + $name = !empty($views_data['table']['base']['query class']) ? $views_data['table']['base']['query class'] : 'views_query'; + default: + $option_name = $type; + $options = $this->get_option($type); + if (!$name) { + $name = $options['type']; + } + + // access & cache store their options as siblings with the + // type; all others use an 'options' array. + if ($type != 'access' && $type != 'cache') { + $options = $options['options']; + } + } + $plugin = views_get_plugin($type, $name); + + if (!$plugin) { + return; + } + if ($type != 'query') { + $plugin->init($this->view, $this->display, $options); + } + else { + $display_id = $this->is_defaulted($option_name) ? $this->display->id : 'default'; + $plugin->localization_keys = array($display_id, $type); + + if (!isset($this->base_field)) { + $views_data = views_fetch_data($this->view->base_table); + $this->view->base_field = !empty($views_data['table']['base']['field']) ? $views_data['table']['base']['field'] : ''; + } + $plugin->init($this->view->base_table, $this->view->base_field, $options); + } + $cache[$type][$name] = $plugin; + } + + return $cache[$type][$name]; + } + + /** + * Get the handler object for a single handler. + */ + function &get_handler($type, $id) { + if (!isset($this->handlers[$type])) { + $this->get_handlers($type); + } + + if (isset($this->handlers[$type][$id])) { + return $this->handlers[$type][$id]; + } + + // So we can return a reference. + $null = NULL; + return $null; + } + + /** + * Get a full array of handlers for $type. This caches them. + */ + function get_handlers($type) { + if (!isset($this->handlers[$type])) { + $this->handlers[$type] = array(); + $types = views_object_types(); + $plural = $types[$type]['plural']; + + foreach ($this->get_option($plural) as $id => $info) { + // If this is during form submission and there are temporary options + // which can only appear if the view is in the edit cache, use those + // options instead. This is used for AJAX multi-step stuff. + if (isset($_POST['form_id']) && isset($this->view->temporary_options[$type][$id])) { + $info = $this->view->temporary_options[$type][$id]; + } + + if ($info['id'] != $id) { + $info['id'] = $id; + } + + // If aggregation is on, the group type might override the actual + // handler that is in use. This piece of code checks that and, + // if necessary, sets the override handler. + $override = NULL; + if ($this->use_group_by() && !empty($info['group_type'])) { + if (empty($this->view->query)) { + $this->view->init_query(); + } + $aggregate = $this->view->query->get_aggregation_info(); + if (!empty($aggregate[$info['group_type']]['handler'][$type])) { + $override = $aggregate[$info['group_type']]['handler'][$type]; + } + } + + if (!empty($types[$type]['type'])) { + $handler_type = $types[$type]['type']; + } + else { + $handler_type = $type; + } + + $handler = views_get_handler($info['table'], $info['field'], $handler_type, $override); + if ($handler) { + // Special override for area types so they know where they come from. + if ($handler_type == 'area') { + $handler->handler_type = $type; + } + + $handler->init($this->view, $info); + $this->handlers[$type][$id] = &$handler; + } + + // Prevent reference problems. + unset($handler); + } + } + + return $this->handlers[$type]; + } + + /** + * Retrieve a list of fields for the current display with the + * relationship associated if it exists. + * + * @param $groupable_only + * Return only an array of field labels from handler that return TRUE + * from use_string_group_by method. + */ + function get_field_labels() { + // Use func_get_arg so the function signature isn't amended + // but we can still pass TRUE into the function to filter + // by groupable handlers. + $args = func_get_args(); + $groupable_only = isset($args[0]) ? $args[0] : FALSE; + + $options = array(); + foreach ($this->get_handlers('relationship') as $relationship => $handler) { + if ($label = $handler->label()) { + $relationships[$relationship] = $label; + } + else { + $relationships[$relationship] = $handler->ui_name(); + } + } + + foreach ($this->get_handlers('field') as $id => $handler) { + if ($groupable_only && !$handler->use_string_group_by()) { + // Continue to next handler if it's not groupable. + continue; + } + if ($label = $handler->label()) { + $options[$id] = $label; + } + else { + $options[$id] = $handler->ui_name(); + } + if (!empty($handler->options['relationship']) && !empty($relationships[$handler->options['relationship']])) { + $options[$id] = '(' . $relationships[$handler->options['relationship']] . ') ' . $options[$id]; + } + } + return $options; + } + + /** + * Intelligently set an option either from this display or from the + * default display, if directed to do so. + */ + function set_option($option, $value) { + if ($this->is_defaulted($option)) { + return $this->default_display->set_option($option, $value); + } + + // Set this in two places: On the handler where we'll notice it + // but also on the display object so it gets saved. This should + // only be a temporary fix. + $this->display->display_options[$option] = $value; + return $this->options[$option] = $value; + } + + /** + * Set an option and force it to be an override. + */ + function override_option($option, $value) { + $this->set_override($option, FALSE); + $this->set_option($option, $value); + } + + /** + * Because forms may be split up into sections, this provides + * an easy URL to exactly the right section. Don't override this. + */ + function option_link($text, $section, $class = '', $title = '') { + views_add_js('ajax'); + if (!empty($class)) { + $text = '' . $text . ''; + } + + if (!trim($text)) { + $text = t('Broken field'); + } + + if (empty($title)) { + $title = $text; + } + + return l($text, 'admin/structure/views/nojs/display/' . $this->view->name . '/' . $this->display->id . '/' . $section, array('attributes' => array('class' => 'views-ajax-link ' . $class, 'title' => $title, 'id' => drupal_html_id('views-' . $this->display->id . '-' . $section)), 'html' => TRUE)); + } + + /** + * Returns to tokens for arguments. + * + * This function is similar to views_handler_field::get_render_tokens() + * but without fields tokens. + */ + function get_arguments_tokens() { + $tokens = array(); + if (!empty($this->view->build_info['substitutions'])) { + $tokens = $this->view->build_info['substitutions']; + } + $count = 0; + foreach ($this->view->display_handler->get_handlers('argument') as $arg => $handler) { + $token = '%' . ++$count; + if (!isset($tokens[$token])) { + $tokens[$token] = ''; + } + + // Use strip tags as there should never be HTML in the path. + // However, we need to preserve special characters like " that + // were removed by check_plain(). + $tokens['!' . $count] = isset($this->view->args[$count - 1]) ? strip_tags(decode_entities($this->view->args[$count - 1])) : ''; + } + + return $tokens; + } + + /** + * Provide the default summary for options in the views UI. + * + * This output is returned as an array. + */ + function options_summary(&$categories, &$options) { + $categories = array( + 'title' => array( + 'title' => t('Title'), + 'column' => 'first', + ), + 'format' => array( + 'title' => t('Format'), + 'column' => 'first', + ), + 'filters' => array( + 'title' => t('Filters'), + 'column' => 'first', + ), + 'fields' => array( + 'title' => t('Fields'), + 'column' => 'first', + ), + 'pager' => array( + 'title' => t('Pager'), + 'column' => 'second', + ), + 'exposed' => array( + 'title' => t('Exposed form'), + 'column' => 'third', + 'build' => array( + '#weight' => 1, + ), + ), + 'access' => array( + 'title' => '', + 'column' => 'second', + 'build' => array( + '#weight' => -5, + ), + ), + 'other' => array( + 'title' => t('Other'), + 'column' => 'third', + 'build' => array( + '#weight' => 2, + ), + ), + ); + + if ($this->display->id != 'default') { + $options['display_id'] = array( + 'category' => 'other', + 'title' => t('Machine Name'), + 'value' => !empty($this->display->new_id) ? check_plain($this->display->new_id) : check_plain($this->display->id), + 'desc' => t('Change the machine name of this display.'), + ); + } + + $display_comment = check_plain(drupal_substr($this->get_option('display_comment'), 0, 10)); + $options['display_comment'] = array( + 'category' => 'other', + 'title' => t('Comment'), + 'value' => !empty($display_comment) ? $display_comment : t('No comment'), + 'desc' => t('Comment or document this display.'), + ); + + $title = strip_tags($this->get_option('title')); + if (!$title) { + $title = t('None'); + } + + $options['title'] = array( + 'category' => 'title', + 'title' => t('Title'), + 'value' => $title, + 'desc' => t('Change the title that this display will use.'), + ); + + $style_plugin = views_fetch_plugin_data('style', $this->get_option('style_plugin')); + $style_plugin_instance = $this->get_plugin('style'); + $style_summary = empty($style_plugin['title']) ? t('Missing style plugin') : $style_plugin_instance->summary_title(); + $style_title = empty($style_plugin['title']) ? t('Missing style plugin') : $style_plugin_instance->plugin_title(); + + $style = ''; + + $options['style_plugin'] = array( + 'category' => 'format', + 'title' => t('Format'), + 'value' => $style_title, + 'setting' => $style_summary, + 'desc' => t('Change the way content is formatted.'), + ); + + // This adds a 'Settings' link to the style_options setting if the style has options. + if (!empty($style_plugin['uses options'])) { + $options['style_plugin']['links']['style_options'] = t('Change settings for this format'); + } + + if (!empty($style_plugin['uses row plugin'])) { + $row_plugin = views_fetch_plugin_data('row', $this->get_option('row_plugin')); + $row_plugin_instance = $this->get_plugin('row'); + $row_summary = empty($row_plugin['title']) ? t('Missing style plugin') : $row_plugin_instance->summary_title(); + $row_title = empty($row_plugin['title']) ? t('Missing style plugin') : $row_plugin_instance->plugin_title(); + + $options['row_plugin'] = array( + 'category' => 'format', + 'title' => t('Show'), + 'value' => $row_title, + 'setting' => $row_summary, + 'desc' => t('Change the way each row in the view is styled.'), + ); + // This adds a 'Settings' link to the row_options setting if the row style has options. + if (!empty($row_plugin['uses options'])) { + $options['row_plugin']['links']['row_options'] = t('Change settings for this style'); + } + } + if (!empty($this->definition['use ajax'])) { + $options['use_ajax'] = array( + 'category' => 'other', + 'title' => t('Use AJAX'), + 'value' => $this->get_option('use_ajax') ? t('Yes') : t('No'), + 'desc' => t('Change whether or not this display will use AJAX.'), + ); + } + if (!empty($this->definition['accept attachments'])) { + $options['hide_attachment_summary'] = array( + 'category' => 'other', + 'title' => t('Hide attachments in summary'), + 'value' => $this->get_option('hide_attachment_summary') ? t('Yes') : t('No'), + 'desc' => t('Change whether or not to display attachments when displaying a contextual filter summary.'), + ); + } + if (!isset($this->definition['contextual links locations']) || !empty($this->definition['contextual links locations'])) { + $options['hide_admin_links'] = array( + 'category' => 'other', + 'title' => t('Hide contextual links'), + 'value' => $this->get_option('hide_admin_links') ? t('Yes') : t('No'), + 'desc' => t('Change whether or not to display contextual links for this view.'), + ); + } + + $pager_plugin = $this->get_plugin('pager'); + if (!$pager_plugin) { + // default to the no pager plugin. + $pager_plugin = views_get_plugin('pager', 'none'); + } + + $pager_str = $pager_plugin->summary_title(); + + $options['pager'] = array( + 'category' => 'pager', + 'title' => t('Use pager'), + 'value' => $pager_plugin->plugin_title(), + 'setting' => $pager_str, + 'desc' => t("Change this display's pager setting."), + ); + + // If pagers aren't allowed, change the text of the item: + if (empty($this->definition['use pager'])) { + $options['pager']['title'] = t('Items to display'); + } + + if (!empty($pager_plugin->definition['uses options'])) { + $options['pager']['links']['pager_options'] = t('Change settings for this pager type.'); + } + + if (!empty($this->definition['use more'])) { + $options['use_more'] = array( + 'category' => 'pager', + 'title' => t('More link'), + 'value' => $this->get_option('use_more') ? t('Yes') : t('No'), + 'desc' => t('Specify whether this display will provide a "more" link.'), + ); + } + + $this->view->init_query(); + if ($this->view->query->get_aggregation_info()) { + $options['group_by'] = array( + 'category' => 'other', + 'title' => t('Use aggregation'), + 'value' => $this->get_option('group_by') ? t('Yes') : t('No'), + 'desc' => t('Allow grouping and aggregation (calculation) of fields.'), + ); + } + + $options['query'] = array( + 'category' => 'other', + 'title' => t('Query settings'), + 'value' => t('Settings'), + 'desc' => t('Allow to set some advanced settings for the query plugin'), + ); + + $languages = array( + '***CURRENT_LANGUAGE***' => t("Current user's language"), + '***DEFAULT_LANGUAGE***' => t("Default site language"), + LANGUAGE_NONE => t('Language neutral'), + ); + if (module_exists('locale')) { + $languages = array_merge($languages, locale_language_list()); + } + $field_language = array(); + $options['field_language'] = array( + 'category' => 'other', + 'title' => t('Field Language'), + 'value' => $languages[$this->get_option('field_language')], + 'desc' => t('All fields which support translations will be displayed in the selected language.'), + ); + + $access_plugin = $this->get_plugin('access'); + if (!$access_plugin) { + // default to the no access control plugin. + $access_plugin = views_get_plugin('access', 'none'); + } + + $access_str = $access_plugin->summary_title(); + + $options['access'] = array( + 'category' => 'access', + 'title' => t('Access'), + 'value' => $access_plugin->plugin_title(), + 'setting' => $access_str, + 'desc' => t('Specify access control type for this display.'), + ); + + if (!empty($access_plugin->definition['uses options'])) { + $options['access']['links']['access_options'] = t('Change settings for this access type.'); + } + + $cache_plugin = $this->get_plugin('cache'); + if (!$cache_plugin) { + // default to the no cache control plugin. + $cache_plugin = views_get_plugin('cache', 'none'); + } + + $cache_str = $cache_plugin->summary_title(); + + $options['cache'] = array( + 'category' => 'other', + 'title' => t('Caching'), + 'value' => $cache_plugin->plugin_title(), + 'setting' => $cache_str, + 'desc' => t('Specify caching type for this display.'), + ); + + if (!empty($cache_plugin->definition['uses options'])) { + $options['cache']['links']['cache_options'] = t('Change settings for this caching type.'); + } + + if (!empty($access_plugin->definition['uses options'])) { + $options['access']['links']['access_options'] = t('Change settings for this access type.'); + } + + if ($this->uses_link_display()) { + $display_id = $this->get_link_display(); + $link_display = empty($this->view->display[$display_id]) ? t('None') : check_plain($this->view->display[$display_id]->display_title); + $link_display = $this->get_option('link_display') == 'custom_url' ? t('Custom URL') : $link_display; + $options['link_display'] = array( + 'category' => 'other', + 'title' => t('Link display'), + 'value' => $link_display, + 'desc' => t('Specify which display or custom url this display will link to.'), + ); + } + + if ($this->uses_exposed_form_in_block()) { + $options['exposed_block'] = array( + 'category' => 'exposed', + 'title' => t('Exposed form in block'), + 'value' => $this->get_option('exposed_block') ? t('Yes') : t('No'), + 'desc' => t('Allow the exposed form to appear in a block instead of the view.'), + ); + } + + $exposed_form_plugin = $this->get_plugin('exposed_form'); + if (!$exposed_form_plugin) { + // default to the no cache control plugin. + $exposed_form_plugin = views_get_plugin('exposed_form', 'basic'); + } + + $exposed_form_str = $exposed_form_plugin->summary_title(); + + $options['exposed_form'] = array( + 'category' => 'exposed', + 'title' => t('Exposed form style'), + 'value' => $exposed_form_plugin->plugin_title(), + 'setting' => $exposed_form_str, + 'desc' => t('Select the kind of exposed filter to use.'), + ); + + if (!empty($exposed_form_plugin->definition['uses options'])) { + $options['exposed_form']['links']['exposed_form_options'] = t('Exposed form settings for this exposed form style.'); + } + + $css_class = check_plain(trim($this->get_option('css_class'))); + if (!$css_class) { + $css_class = t('None'); + } + + $options['css_class'] = array( + 'category' => 'other', + 'title' => t('CSS class'), + 'value' => $css_class, + 'desc' => t('Change the CSS class name(s) that will be added to this display.'), + ); + + $options['analyze-theme'] = array( + 'category' => 'other', + 'title' => t('Theme'), + 'value' => t('Information'), + 'desc' => t('Get information on how to theme this display'), + ); + + foreach ($this->extender as $extender) { + $extender->options_summary($categories, $options); + } + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + if ($this->defaultable_sections($form_state['section'])) { + views_ui_standard_display_dropdown($form, $form_state, $form_state['section']); + } + $form['#title'] = check_plain($this->display->display_title) . ': '; + + // Set the 'section' to hilite on the form. + // If it's the item we're looking at is pulling from the default display, + // reflect that. Don't use is_defaulted since we want it to show up even + // on the default display. + if (!empty($this->options['defaults'][$form_state['section']])) { + $form['#section'] = 'default-' . $form_state['section']; + } + else { + $form['#section'] = $this->display->id . '-' . $form_state['section']; + } + + switch ($form_state['section']) { + case 'display_id': + $form['#title'] .= t('The machine name of this display'); + $form['display_id'] = array( + '#type' => 'textfield', + '#description' => t('This is machine name of the display.'), + '#default_value' => !empty($this->display->new_id) ? $this->display->new_id : $this->display->id, + '#required' => TRUE, + '#size' => 64, + ); + break; + case 'display_title': + $form['#title'] .= t('The name and the description of this display'); + $form['display_title'] = array( + '#title' => t('Name'), + '#type' => 'textfield', + '#description' => t('This name will appear only in the administrative interface for the View.'), + '#default_value' => $this->display->display_title, + ); + $form['display_description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#description' => t('This description will appear only in the administrative interface for the View.'), + '#default_value' => $this->get_option('display_description'), + ); + break; + case 'display_comment': + $form['#title'] .= t("This display's comments"); + $form['display_comment'] = array( + '#type' => 'textarea', + '#description' => t('This value will be seen and used only within the Views UI and can be used to document this display. You can use this to provide notes for other or future maintainers of your site about how or why this display is configured.'), + '#default_value' => $this->get_option('display_comment'), + ); + break; + case 'title': + $form['#title'] .= t('The title of this view'); + $form['title'] = array( + '#type' => 'textfield', + '#description' => t('This title will be displayed with the view, wherever titles are normally displayed; i.e, as the page title, block title, etc.'), + '#default_value' => $this->get_option('title'), + ); + break; + case 'css_class': + $form['#title'] .= t('CSS class'); + $form['css_class'] = array( + '#type' => 'textfield', + '#description' => t('The CSS class names will be added to the view. This enables you to use specific CSS code for each view. You may define multiples classes separated by spaces.'), + '#default_value' => $this->get_option('css_class'), + ); + break; + case 'use_ajax': + $form['#title'] .= t('Use AJAX when available to load this view'); + $form['description'] = array( + '#markup' => '
    ' . t('If set, this view will use an AJAX mechanism for paging, table sorting and exposed filters. This means the entire page will not refresh. It is not recommended that you use this if this view is the main content of the page as it will prevent deep linking to specific pages, but it is very useful for side content.') . '
    ', + ); + $form['use_ajax'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->get_option('use_ajax') ? 1 : 0, + ); + break; + case 'hide_attachment_summary': + $form['#title'] .= t('Hide attachments when displaying a contextual filter summary'); + $form['hide_attachment_summary'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->get_option('hide_attachment_summary') ? 1 : 0, + ); + break; + case 'hide_admin_links': + $form['#title'] .= t('Hide contextual links on this view.'); + $form['hide_admin_links'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->get_option('hide_admin_links') ? 1 : 0, + ); + break; + case 'use_more': + $form['#title'] .= t('Add a more link to the bottom of the display.'); + $form['use_more'] = array( + '#type' => 'checkbox', + '#title' => t('Create more link'), + '#description' => t("This will add a more link to the bottom of this view, which will link to the page view. If you have more than one page view, the link will point to the display specified in 'Link display' section under advanced. You can override the url at the link display setting."), + '#default_value' => $this->get_option('use_more'), + ); + $form['use_more_always'] = array( + '#type' => 'checkbox', + '#title' => t("Display 'more' link only if there is more content"), + '#description' => t("Leave this unchecked to display the 'more' link even if there are no more items to display."), + '#default_value' => !$this->get_option('use_more_always'), + '#dependency' => array( + 'edit-use-more' => array(TRUE), + ), + ); + $form['use_more_text'] = array( + '#type' => 'textfield', + '#title' => t('More link text'), + '#description' => t("The text to display for the more link."), + '#default_value' => $this->get_option('use_more_text'), + '#dependency' => array( + 'edit-use-more' => array(TRUE), + ), + ); + break; + case 'group_by': + $form['#title'] .= t('Allow grouping and aggregation (calculation) of fields.'); + $form['group_by'] = array( + '#type' => 'checkbox', + '#title' => t('Aggregate'), + '#description' => t('If enabled, some fields may become unavailable. All fields that are selected for grouping will be collapsed to one record per distinct value. Other fields which are selected for aggregation will have the function run on them. For example, you can group nodes on title and count the number of nids in order to get a list of duplicate titles.'), + '#default_value' => $this->get_option('group_by'), + ); + break; + case 'access': + $form['#title'] .= t('Access restrictions'); + $form['access'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#tree' => TRUE, + ); + + $access = $this->get_option('access'); + $form['access']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('access', NULL, array($this->view->base_table)), + '#default_value' => $access['type'], + ); + + $access_plugin = views_fetch_plugin_data('access', $access['type']); + if (!empty($access_plugin['uses options'])) { + $form['markup'] = array( + '#prefix' => '
    ', + '#markup' => t('You may also adjust the !settings for the currently selected access restriction.', array('!settings' => $this->option_link(t('settings'), 'access_options'))), + '#suffix' => '
    ', + ); + } + + break; + case 'access_options': + $access = $this->get_option('access'); + $plugin = $this->get_plugin('access'); + $form['#title'] .= t('Access options'); + if ($plugin) { + $form['#help_topic'] = $plugin->definition['help topic']; + $form['#help_module'] = $plugin->definition['module']; + + $form['access_options'] = array( + '#tree' => TRUE, + ); + $form['access_options']['type'] = array( + '#type' => 'value', + '#value' => $access['type'], + ); + $plugin->options_form($form['access_options'], $form_state); + } + break; + case 'cache': + $form['#title'] .= t('Caching'); + $form['cache'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#tree' => TRUE, + ); + + $cache = $this->get_option('cache'); + $form['cache']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('cache', NULL, array($this->view->base_table)), + '#default_value' => $cache['type'], + ); + + $cache_plugin = views_fetch_plugin_data('cache', $cache['type']); + if (!empty($cache_plugin['uses options'])) { + $form['markup'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#markup' => t('You may also adjust the !settings for the currently selected cache mechanism.', array('!settings' => $this->option_link(t('settings'), 'cache_options'))), + ); + } + break; + case 'cache_options': + $cache = $this->get_option('cache'); + $plugin = $this->get_plugin('cache'); + $form['#title'] .= t('Caching options'); + if ($plugin) { + $form['#help_topic'] = $plugin->definition['help topic']; + $form['#help_module'] = $plugin->definition['module']; + + $form['cache_options'] = array( + '#tree' => TRUE, + ); + $form['cache_options']['type'] = array( + '#type' => 'value', + '#value' => $cache['type'], + ); + $plugin->options_form($form['cache_options'], $form_state); + } + break; + case 'query': + $query_options = $this->get_option('query'); + $plugin_name = $query_options['type']; + + $form['#title'] .= t('Query options'); + $this->view->init_query(); + if ($this->view->query) { + if (isset($this->view->query->definition['help topic'])) { + $form['#help_topic'] = $this->view->query->definition['help topic']; + } + + if (isset($this->view->query->definition['module'])) { + $form['#help_module'] = $this->view->query->definition['module']; + } + + $form['query'] = array( + '#tree' => TRUE, + 'type' => array( + '#type' => 'value', + '#value' => $plugin_name, + ), + 'options' => array( + '#tree' => TRUE, + ), + ); + + $this->view->query->options_form($form['query']['options'], $form_state); + } + break; + case 'field_language': + $form['#title'] .= t('Field Language'); + + $entities = entity_get_info(); + $entity_tables = array(); + $has_translation_handlers = FALSE; + foreach ($entities as $type => $entity_info) { + $entity_tables[] = $entity_info['base table']; + + if (!empty($entity_info['translation'])) { + $has_translation_handlers = TRUE; + } + } + + // Doesn't make sense to show a field setting here if we aren't querying + // an entity base table. Also, we make sure that there's at least one + // entity type with a translation handler attached. + if (in_array($this->view->base_table, $entity_tables) && $has_translation_handlers) { + $languages = array( + '***CURRENT_LANGUAGE***' => t("Current user's language"), + '***DEFAULT_LANGUAGE***' => t("Default site language"), + LANGUAGE_NONE => t('Language neutral'), + ); + $languages = array_merge($languages, views_language_list()); + + $form['field_language'] = array( + '#type' => 'select', + '#title' => t('Field Language'), + '#description' => t('All fields which support translations will be displayed in the selected language.'), + '#options' => $languages, + '#default_value' => $this->get_option('field_language'), + ); + $form['field_language_add_to_query'] = array( + '#type' => 'checkbox', + '#title' => t('When needed, add the field language condition to the query'), + '#default_value' => $this->get_option('field_language_add_to_query'), + ); + } + else { + $form['field_language']['#markup'] = t("You don't have translatable entity types."); + } + break; + case 'style_plugin': + $form['#title'] .= t('How should this view be styled'); + $form['#help_topic'] = 'style'; + $form['style_plugin'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('style', $this->get_style_type(), array($this->view->base_table)), + '#default_value' => $this->get_option('style_plugin'), + '#description' => t('If the style you choose has settings, be sure to click the settings button that will appear next to it in the View summary.'), + ); + + $style_plugin = views_fetch_plugin_data('style', $this->get_option('style_plugin')); + if (!empty($style_plugin['uses options'])) { + $form['markup'] = array( + '#markup' => '
    ' . t('You may also adjust the !settings for the currently selected style.', array('!settings' => $this->option_link(t('settings'), 'style_options'))) . '
    ', + ); + } + + break; + case 'style_options': + $form['#title'] .= t('Style options'); + $style = TRUE; + $type = 'style_plugin'; + $name = $this->get_option('style_plugin'); + + case 'row_options': + if (!isset($name)) { + $name = $this->get_option('row_plugin'); + } + // if row, $style will be empty. + if (empty($style)) { + $form['#title'] .= t('Row style options'); + $type = 'row_plugin'; + } + $plugin = $this->get_plugin(empty($style) ? 'row' : 'style'); + if ($plugin) { + if (isset($plugin->definition['help topic'])) { + $form['#help_topic'] = $plugin->definition['help topic']; + $form['#help_module'] = $plugin->definition['module']; + } + $form[$form_state['section']] = array( + '#tree' => TRUE, + ); + $plugin->options_form($form[$form_state['section']], $form_state); + } + break; + case 'row_plugin': + $form['#title'] .= t('How should each row in this view be styled'); + $form['#help_topic'] = 'style-row'; + $form['row_plugin'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('row', $this->get_style_type(), array($this->view->base_table)), + '#default_value' => $this->get_option('row_plugin'), + ); + + $row_plugin = views_fetch_plugin_data('row', $this->get_option('row_plugin')); + if (!empty($row_plugin['uses options'])) { + $form['markup'] = array( + '#markup' => '
    ' . t('You may also adjust the !settings for the currently selected row style.', array('!settings' => $this->option_link(t('settings'), 'row_options'))) . '
    ', + ); + } + + break; + case 'link_display': + $form['#title'] .= t('Which display to use for path'); + foreach ($this->view->display as $display_id => $display) { + if ($display->handler->has_path()) { + $options[$display_id] = $display->display_title; + } + } + $options['custom_url'] = t('Custom URL'); + if (count($options)) { + $form['link_display'] = array( + '#type' => 'radios', + '#options' => $options, + '#description' => t("Which display to use to get this display's path for things like summary links, rss feed links, more links, etc."), + '#default_value' => $this->get_option('link_display'), + ); + } + + $options = array(); + $count = 0; // This lets us prepare the key as we want it printed. + foreach ($this->view->display_handler->get_handlers('argument') as $arg => $handler) { + $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->ui_name())); + $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->ui_name())); + } + + // Default text. + // We have some options, so make a list. + $output = ''; + if (!empty($options)) { + $output = t('

    The following tokens are available for this link.

    '); + foreach (array_keys($options) as $type) { + if (!empty($options[$type])) { + $items = array(); + foreach ($options[$type] as $key => $value) { + $items[] = $key . ' == ' . $value; + } + $output .= theme('item_list', + array( + 'items' => $items, + 'type' => $type + )); + } + } + } + + $form['link_url'] = array( + '#type' => 'textfield', + '#title' => t('Custom URL'), + '#default_value' => $this->get_option('link_url'), + '#description' => t('A Drupal path or external URL the more link will point to. Note that this will override the link display setting above.') . $output, + '#dependency' => array('radio:link_display' => array('custom_url')), + ); + break; + case 'analyze-theme': + $form['#title'] .= t('Theming information'); + $form['#help_topic'] = 'analyze-theme'; + + if (isset($_POST['theme'])) { + $this->theme = $_POST['theme']; + } + elseif (empty($this->theme)) { + $this->theme = variable_get('theme_default', 'bartik'); + } + + if (isset($GLOBALS['theme']) && $GLOBALS['theme'] == $this->theme) { + $this->theme_registry = theme_get_registry(); + $theme_engine = $GLOBALS['theme_engine']; + } + else { + $themes = list_themes(); + $theme = $themes[$this->theme]; + + // Find all our ancestor themes and put them in an array. + $base_theme = array(); + $ancestor = $this->theme; + while ($ancestor && isset($themes[$ancestor]->base_theme)) { + $ancestor = $themes[$ancestor]->base_theme; + $base_theme[] = $themes[$ancestor]; + } + + // The base themes should be initialized in the right order. + $base_theme = array_reverse($base_theme); + + // This code is copied directly from _drupal_theme_initialize() + $theme_engine = NULL; + + // Initialize the theme. + if (isset($theme->engine)) { + // Include the engine. + include_once DRUPAL_ROOT . '/' . $theme->owner; + + $theme_engine = $theme->engine; + if (function_exists($theme_engine . '_init')) { + foreach ($base_theme as $base) { + call_user_func($theme_engine . '_init', $base); + } + call_user_func($theme_engine . '_init', $theme); + } + } + else { + // include non-engine theme files + foreach ($base_theme as $base) { + // Include the theme file or the engine. + if (!empty($base->owner)) { + include_once DRUPAL_ROOT . '/' . $base->owner; + } + } + // and our theme gets one too. + if (!empty($theme->owner)) { + include_once DRUPAL_ROOT . '/' . $theme->owner; + } + } + $this->theme_registry = _theme_load_registry($theme, $base_theme, $theme_engine); + } + + // If there's a theme engine involved, we also need to know its extension + // so we can give the proper filename. + $this->theme_extension = '.tpl.php'; + if (isset($theme_engine)) { + $extension_function = $theme_engine . '_extension'; + if (function_exists($extension_function)) { + $this->theme_extension = $extension_function(); + } + } + + $funcs = array(); + // Get theme functions for the display. Note that some displays may + // not have themes. The 'feed' display, for example, completely + // delegates to the style. + if (!empty($this->definition['theme'])) { + $funcs[] = $this->option_link(t('Display output'), 'analyze-theme-display') . ': ' . $this->format_themes($this->theme_functions()); + $themes = $this->additional_theme_functions(); + if ($themes) { + foreach ($themes as $theme) { + $funcs[] = $this->option_link(t('Alternative display output'), 'analyze-theme-display') . ': ' . $this->format_themes($theme); + } + } + } + + $plugin = $this->get_plugin(); + if ($plugin) { + $funcs[] = $this->option_link(t('Style output'), 'analyze-theme-style') . ': ' . $this->format_themes($plugin->theme_functions(), $plugin->additional_theme_functions()); + $themes = $plugin->additional_theme_functions(); + if ($themes) { + foreach ($themes as $theme) { + $funcs[] = $this->option_link(t('Alternative style'), 'analyze-theme-style') . ': ' . $this->format_themes($theme); + } + } + + if ($plugin->uses_row_plugin()) { + $row_plugin = $this->get_plugin('row'); + if ($row_plugin) { + $funcs[] = $this->option_link(t('Row style output'), 'analyze-theme-row') . ': ' . $this->format_themes($row_plugin->theme_functions()); + $themes = $row_plugin->additional_theme_functions(); + if ($themes) { + foreach ($themes as $theme) { + $funcs[] = $this->option_link(t('Alternative row style'), 'analyze-theme-row') . ': ' . $this->format_themes($theme); + } + } + } + } + + if ($plugin->uses_fields()) { + foreach ($this->get_handlers('field') as $id => $handler) { + $funcs[] = $this->option_link(t('Field @field (ID: @id)', array('@field' => $handler->ui_name(), '@id' => $id)), 'analyze-theme-field') . ': ' . $this->format_themes($handler->theme_functions()); + } + } + } + + $form['important'] = array( + '#markup' => '

    ' . t('This section lists all possible templates for the display plugin and for the style plugins, ordered roughly from the least specific to the most specific. The active template for each plugin -- which is the most specific template found on the system -- is highlighted in bold.') . '

    ', + ); + + if (isset($this->view->display[$this->view->current_display]->new_id)) { + $form['important']['new_id'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#value' => t("Important! You have changed the display's machine name. Anything that attached to this display specifically, such as theming, may stop working until it is updated. To see theme suggestions for it, you need to save the view."), + ); + } + + foreach (list_themes() as $key => $theme) { + if (!empty($theme->info['hidden'])) { + continue; + } + $options[$key] = $theme->info['name']; + } + + $form['box'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + $form['box']['theme'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => $this->theme, + ); + + $form['box']['change'] = array( + '#type' => 'submit', + '#value' => t('Change theme'), + '#submit' => array('views_ui_edit_display_form_change_theme'), + ); + + $form['analysis'] = array( + '#markup' => '
    ' . theme('item_list', array('items' => $funcs)) . '
    ', + ); + + $form['rescan_button'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + $form['rescan_button']['button'] = array( + '#type' => 'submit', + '#value' => t('Rescan template files'), + '#submit' => array('views_ui_config_item_form_rescan'), + ); + $form['rescan_button']['markup'] = array( + '#markup' => '
    ' . t("Important! When adding, removing, or renaming template files, it is necessary to make Drupal aware of the changes by making it rescan the files on your system. By clicking this button you clear Drupal's theme registry and thereby trigger this rescanning process. The highlighted templates above will then reflect the new state of your system.") . '
    ', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-display': + $form['#title'] .= t('Theming information (display)'); + $output = '

    ' . t('Back to !info.', array('!info' => $this->option_link(t('theming information'), 'analyze-theme'))) . '

    '; + + if (empty($this->definition['theme'])) { + $output .= t('This display has no theming information'); + } + else { + $output .= '

    ' . t('This is the default theme template used for this display.') . '

    '; + $output .= '
    ' . check_plain(file_get_contents('./' . $this->definition['theme path'] . '/' . strtr($this->definition['theme'], '_', '-') . '.tpl.php')) . '
    '; + } + + if (!empty($this->definition['additional themes'])) { + foreach ($this->definition['additional themes'] as $theme => $type) { + $output .= '

    ' . t('This is an alternative template for this display.') . '

    '; + $output .= '
    ' . check_plain(file_get_contents('./' . $this->definition['theme path'] . '/' . strtr($theme, '_', '-') . '.tpl.php')) . '
    '; + } + } + + $form['analysis'] = array( + '#markup' => '
    ' . $output . '
    ', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-style': + $form['#title'] .= t('Theming information (style)'); + $output = '

    ' . t('Back to !info.', array('!info' => $this->option_link(t('theming information'), 'analyze-theme'))) . '

    '; + + $plugin = $this->get_plugin(); + + if (empty($plugin->definition['theme'])) { + $output .= t('This display has no style theming information'); + } + else { + $output .= '

    ' . t('This is the default theme template used for this style.') . '

    '; + $output .= '
    ' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($plugin->definition['theme'], '_', '-') . '.tpl.php')) . '
    '; + } + + if (!empty($plugin->definition['additional themes'])) { + foreach ($plugin->definition['additional themes'] as $theme => $type) { + $output .= '

    ' . t('This is an alternative template for this style.') . '

    '; + $output .= '
    ' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($theme, '_', '-') . '.tpl.php')) . '
    '; + } + } + + $form['analysis'] = array( + '#markup' => '
    ' . $output . '
    ', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-row': + $form['#title'] .= t('Theming information (row style)'); + $output = '

    ' . t('Back to !info.', array('!info' => $this->option_link(t('theming information'), 'analyze-theme'))) . '

    '; + + $plugin = $this->get_plugin('row'); + + if (empty($plugin->definition['theme'])) { + $output .= t('This display has no row style theming information'); + } + else { + $output .= '

    ' . t('This is the default theme template used for this row style.') . '

    '; + $output .= '
    ' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($plugin->definition['theme'], '_', '-') . '.tpl.php')) . '
    '; + } + + if (!empty($plugin->definition['additional themes'])) { + foreach ($plugin->definition['additional themes'] as $theme => $type) { + $output .= '

    ' . t('This is an alternative template for this row style.') . '

    '; + $output .= '
    ' . check_plain(file_get_contents('./' . $plugin->definition['theme path'] . '/' . strtr($theme, '_', '-') . '.tpl.php')) . '
    '; + } + } + + $form['analysis'] = array( + '#markup' => '
    ' . $output . '
    ', + ); + + $form_state['ok_button'] = TRUE; + break; + case 'analyze-theme-field': + $form['#title'] .= t('Theming information (row style)'); + $output = '

    ' . t('Back to !info.', array('!info' => $this->option_link(t('theming information'), 'analyze-theme'))) . '

    '; + + $output .= '

    ' . t('This is the default theme template used for this row style.') . '

    '; + + // Field templates aren't registered the normal way...and they're always + // this one, anyhow. + $output .= '
    ' . check_plain(file_get_contents(drupal_get_path('module', 'views') . '/theme/views-view-field.tpl.php')) . '
    '; + + $form['analysis'] = array( + '#markup' => '
    ' . $output . '
    ', + ); + $form_state['ok_button'] = TRUE; + break; + + case 'exposed_block': + $form['#title'] .= t('Put the exposed form in a block'); + $form['description'] = array( + '#markup' => '
    ' . t('If set, any exposed widgets will not appear with this view. Instead, a block will be made available to the Drupal block administration system, and the exposed form will appear there. Note that this block must be enabled manually, Views will not enable it for you.') . '
    ', + ); + $form['exposed_block'] = array( + '#type' => 'radios', + '#options' => array(1 => t('Yes'), 0 => t('No')), + '#default_value' => $this->get_option('exposed_block') ? 1 : 0, + ); + break; + case 'exposed_form': + $form['#title'] .= t('Exposed Form'); + $form['exposed_form'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#tree' => TRUE, + ); + + $exposed_form = $this->get_option('exposed_form'); + $form['exposed_form']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('exposed_form', NULL, array($this->view->base_table)), + '#default_value' => $exposed_form['type'], + ); + + $exposed_form_plugin = views_fetch_plugin_data('exposed_form', $exposed_form['type']); + if (!empty($exposed_form_plugin['uses options'])) { + $form['markup'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#markup' => t('You may also adjust the !settings for the currently selected style.', array('!settings' => $this->option_link(t('settings'), 'exposed_form_options'))), + ); + } + break; + case 'exposed_form_options': + $plugin = $this->get_plugin('exposed_form'); + $form['#title'] .= t('Exposed form options'); + if ($plugin) { + $form['#help_topic'] = $plugin->definition['help topic']; + + $form['exposed_form_options'] = array( + '#tree' => TRUE, + ); + $plugin->options_form($form['exposed_form_options'], $form_state); + } + break; + case 'pager': + $form['#title'] .= t('Select which pager, if any, to use for this view'); + $form['pager'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#tree' => TRUE, + ); + + $pager = $this->get_option('pager'); + $form['pager']['type'] = array( + '#type' => 'radios', + '#options' => views_fetch_plugin_names('pager', empty($this->definition['use pager']) ? 'basic' : NULL, array($this->view->base_table)), + '#default_value' => $pager['type'], + ); + + $pager_plugin = views_fetch_plugin_data('pager', $pager['type'], array($this->view->base_table)); + if (!empty($pager_plugin['uses options'])) { + $form['markup'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#markup' => t('You may also adjust the !settings for the currently selected pager.', array('!settings' => $this->option_link(t('settings'), 'pager_options'))), + ); + } + + break; + case 'pager_options': + $plugin = $this->get_plugin('pager'); + $form['#title'] .= t('Pager options'); + if ($plugin) { + $form['#help_topic'] = $plugin->definition['help topic']; + + $form['pager_options'] = array( + '#tree' => TRUE, + ); + $plugin->options_form($form['pager_options'], $form_state); + } + break; + } + + foreach ($this->extender as $extender) { + $extender->options_form($form, $form_state); + } + } + + /** + * Format a list of theme templates for output by the theme info helper. + */ + function format_themes($themes) { + $registry = $this->theme_registry; + $extension = $this->theme_extension; + + $output = ''; + $picked = FALSE; + foreach ($themes as $theme) { + $template = strtr($theme, '_', '-') . $extension; + if (!$picked && !empty($registry[$theme])) { + $template_path = isset($registry[$theme]['path']) ? $registry[$theme]['path'] . '/' : './'; + if (file_exists($template_path . $template)) { + $hint = t('File found in folder @template-path', array('@template-path' => $template_path)); + $template = '' . $template . ''; + } + else { + $template = '' . $template . ' ' . t('(File not found, in folder @template-path)', array('@template-path' => $template_path)) . ''; + } + $picked = TRUE; + } + $fixed[] = $template; + } + + return implode(', ', array_reverse($fixed)); + } + + /** + * Validate the options form. + */ + function options_validate(&$form, &$form_state) { + switch ($form_state['section']) { + case 'display_title': + if (empty($form_state['values']['display_title'])) { + form_error($form['display_title'], t('Display title may not be empty.')); + } + break; + case 'css_class': + $css_class = $form_state['values']['css_class']; + if (preg_match('/[^a-zA-Z0-9-_ ]/', $css_class)) { + form_error($form['css_class'], t('CSS classes must be alphanumeric or dashes only.')); + } + break; + case 'display_id': + if ($form_state['values']['display_id']) { + if (preg_match('/[^a-z0-9_]/', $form_state['values']['display_id'])) { + form_error($form['display_id'], t('Display name must be letters, numbers, or underscores only.')); + } + + foreach ($this->view->display as $id => $display) { + if ($id != $this->view->current_display && ($form_state['values']['display_id'] == $id || (isset($display->new_id) && $form_state['values']['display_id'] == $display->new_id))) { + form_error($form['display_id'], t('Display id should be unique.')); + } + } + } + break; + case 'style_options': + $style = TRUE; + case 'row_options': + // if row, $style will be empty. + $plugin = $this->get_plugin(empty($style) ? 'row' : 'style'); + if ($plugin) { + $plugin->options_validate($form[$form_state['section']], $form_state); + } + break; + case 'access_options': + $plugin = $this->get_plugin('access'); + if ($plugin) { + $plugin->options_validate($form['access_options'], $form_state); + } + break; + case 'query': + if ($this->view->query) { + $this->view->query->options_validate($form['query'], $form_state); + } + break; + case 'cache_options': + $plugin = $this->get_plugin('cache'); + if ($plugin) { + $plugin->options_validate($form['cache_options'], $form_state); + } + break; + case 'exposed_form_options': + $plugin = $this->get_plugin('exposed_form'); + if ($plugin) { + $plugin->options_validate($form['exposed_form_options'], $form_state); + } + break; + case 'pager_options': + $plugin = $this->get_plugin('pager'); + if ($plugin) { + $plugin->options_validate($form['pager_options'], $form_state); + } + break; + } + + foreach ($this->extender as $extender) { + $extender->options_validate($form, $form_state); + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function options_submit(&$form, &$form_state) { + // Not sure I like this being here, but it seems (?) like a logical place. + $cache_plugin = $this->get_plugin('cache'); + if ($cache_plugin) { + $cache_plugin->cache_flush(); + } + + $section = $form_state['section']; + switch ($section) { + case 'display_id': + if (isset($form_state['values']['display_id'])) { + $this->display->new_id = $form_state['values']['display_id']; + } + break; + case 'display_title': + $this->display->display_title = $form_state['values']['display_title']; + $this->set_option('display_description', $form_state['values']['display_description']); + break; + case 'access': + $access = $this->get_option('access'); + if ($access['type'] != $form_state['values']['access']['type']) { + $plugin = views_get_plugin('access', $form_state['values']['access']['type']); + if ($plugin) { + $access = array('type' => $form_state['values']['access']['type']); + $this->set_option('access', $access); + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('access_options')); + } + } + } + + break; + case 'access_options': + $plugin = views_get_plugin('access', $form_state['values'][$section]['type']); + if ($plugin) { + $plugin->options_submit($form['access_options'], $form_state); + $this->set_option('access', $form_state['values'][$section]); + } + break; + case 'cache': + $cache = $this->get_option('cache'); + if ($cache['type'] != $form_state['values']['cache']['type']) { + $plugin = views_get_plugin('cache', $form_state['values']['cache']['type']); + if ($plugin) { + $cache = array('type' => $form_state['values']['cache']['type']); + $this->set_option('cache', $cache); + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('cache_options')); + } + } + } + + break; + case 'cache_options': + $plugin = views_get_plugin('cache', $form_state['values'][$section]['type']); + if ($plugin) { + $plugin->options_submit($form['cache_options'], $form_state); + $this->set_option('cache', $form_state['values'][$section]); + } + break; + case 'query': + $plugin = $this->get_plugin('query'); + if ($plugin) { + $plugin->options_submit($form['query']['options'], $form_state); + $this->set_option('query', $form_state['values'][$section]); + } + break; + + case 'link_display': + $this->set_option('link_url', $form_state['values']['link_url']); + case 'title': + case 'css_class': + case 'display_comment': + $this->set_option($section, $form_state['values'][$section]); + break; + case 'field_language': + $this->set_option('field_language', $form_state['values']['field_language']); + $this->set_option('field_language_add_to_query', $form_state['values']['field_language_add_to_query']); + break; + case 'use_ajax': + case 'hide_attachment_summary': + case 'hide_admin_links': + $this->set_option($section, (bool)$form_state['values'][$section]); + break; + case 'use_more': + $this->set_option($section, intval($form_state['values'][$section])); + $this->set_option('use_more_always', !intval($form_state['values']['use_more_always'])); + $this->set_option('use_more_text', $form_state['values']['use_more_text']); + case 'distinct': + $this->set_option($section, $form_state['values'][$section]); + break; + case 'group_by': + $this->set_option($section, $form_state['values'][$section]); + break; + case 'row_plugin': + // This if prevents resetting options to default if they don't change + // the plugin. + if ($this->get_option($section) != $form_state['values'][$section]) { + $plugin = views_get_plugin('row', $form_state['values'][$section]); + if ($plugin) { + $this->set_option($section, $form_state['values'][$section]); + $this->set_option('row_options', array()); + + // send ajax form to options page if we use it. + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('row_options')); + } + } + } + break; + case 'style_plugin': + // This if prevents resetting options to default if they don't change + // the plugin. + if ($this->get_option($section) != $form_state['values'][$section]) { + $plugin = views_get_plugin('style', $form_state['values'][$section]); + if ($plugin) { + $this->set_option($section, $form_state['values'][$section]); + $this->set_option('style_options', array()); + // send ajax form to options page if we use it. + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('style_options')); + } + } + } + break; + case 'style_options': + $style = TRUE; + case 'row_options': + // if row, $style will be empty. + $plugin = $this->get_plugin(empty($style) ? 'row' : 'style'); + if ($plugin) { + $plugin->options_submit($form['options'][$section], $form_state); + } + $this->set_option($section, $form_state['values'][$section]); + break; + case 'exposed_block': + $this->set_option($section, (bool) $form_state['values'][$section]); + break; + case 'exposed_form': + $exposed_form = $this->get_option('exposed_form'); + if ($exposed_form['type'] != $form_state['values']['exposed_form']['type']) { + $plugin = views_get_plugin('exposed_form', $form_state['values']['exposed_form']['type']); + if ($plugin) { + $exposed_form = array('type' => $form_state['values']['exposed_form']['type'], 'options' => array()); + $this->set_option('exposed_form', $exposed_form); + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('exposed_form_options')); + } + } + } + + break; + case 'exposed_form_options': + $plugin = $this->get_plugin('exposed_form'); + if ($plugin) { + $exposed_form = $this->get_option('exposed_form'); + $plugin->options_submit($form['exposed_form_options'], $form_state); + $exposed_form['options'] = $form_state['values'][$section]; + $this->set_option('exposed_form', $exposed_form); + } + break; + case 'pager': + $pager = $this->get_option('pager'); + if ($pager['type'] != $form_state['values']['pager']['type']) { + $plugin = views_get_plugin('pager', $form_state['values']['pager']['type']); + if ($plugin) { + // Because pagers have very similar options, let's allow pagers to + // try to carry the options over. + $plugin->init($this->view, $this->display, $pager['options']); + + $pager = array('type' => $form_state['values']['pager']['type'], 'options' => $plugin->options); + $this->set_option('pager', $pager); + if (!empty($plugin->definition['uses options'])) { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('pager_options')); + } + } + } + + break; + case 'pager_options': + $plugin = $this->get_plugin('pager'); + if ($plugin) { + $pager = $this->get_option('pager'); + $plugin->options_submit($form['pager_options'], $form_state); + $pager['options'] = $form_state['values'][$section]; + $this->set_option('pager', $pager); + } + break; + } + + foreach ($this->extender as $extender) { + $extender->options_submit($form, $form_state); + } + } + + /** + * If override/revert was clicked, perform the proper toggle. + */ + function options_override($form, &$form_state) { + $this->set_override($form_state['section']); + } + + /** + * Flip the override setting for the given section. + * + * @param string $section + * Which option should be marked as overridden, for example "filters". + * @param bool $new_state + * Select the new state of the option. + * - TRUE: Revert to default. + * - FALSE: Mark it as overridden. + */ + function set_override($section, $new_state = NULL) { + $options = $this->defaultable_sections($section); + if (!$options) { + return; + } + + if (!isset($new_state)) { + $new_state = empty($this->options['defaults'][$section]); + } + + // For each option that is part of this group, fix our settings. + foreach ($options as $option) { + if ($new_state) { + // Revert to defaults. + unset($this->options[$option]); + unset($this->display->display_options[$option]); + } + else { + // copy existing values into our display. + $this->options[$option] = $this->get_option($option); + $this->display->display_options[$option] = $this->options[$option]; + } + $this->options['defaults'][$option] = $new_state; + $this->display->display_options['defaults'][$option] = $new_state; + } + } + + /** + * Inject anything into the query that the display handler needs. + */ + function query() { + foreach ($this->extender as $extender) { + $extender->query(); + } + } + + /** + * Not all display plugins will support filtering + */ + function render_filters() { } + + /** + * Not all display plugins will suppert pager rendering. + */ + function render_pager() { + return TRUE; + } + + /** + * Render the 'more' link + */ + function render_more_link() { + if ($this->use_more() && ($this->use_more_always() || (!empty($this->view->query->pager) && $this->view->query->pager->has_more_records()))) { + $path = $this->get_path(); + + if ($this->get_option('link_display') == 'custom_url' && $override_path = $this->get_option('link_url')) { + $tokens = $this->get_arguments_tokens(); + $path = strtr($override_path, $tokens); + } + + if ($path) { + if (empty($override_path)) { + $path = $this->view->get_url(NULL, $path); + } + $url_options = array(); + if (!empty($this->view->exposed_raw_input)) { + $url_options['query'] = $this->view->exposed_raw_input; + } + $theme = views_theme_functions('views_more', $this->view, $this->display); + $path = check_url(url($path, $url_options)); + + return theme($theme, array('more_url' => $path, 'link_text' => check_plain($this->use_more_text()), 'view' => $this->view)); + } + } + } + + + /** + * Legacy functions. + */ + + /** + * Render the header of the view. + */ + function render_header() { + $empty = !empty($this->view->result); + return $this->render_area('header', $empty); + } + + /** + * Render the footer of the view. + */ + function render_footer() { + $empty = !empty($this->view->result); + return $this->render_area('footer', $empty); + } + + function render_empty() { + return $this->render_area('empty'); + } + + /** + * If this display creates a block, implement one of these. + */ + function hook_block_list($delta = 0, $edit = array()) { return array(); } + + /** + * If this display creates a page with a menu item, implement it here. + */ + function hook_menu() { return array(); } + + /** + * Render this display. + */ + function render() { + return theme($this->theme_functions(), array('view' => $this->view)); + } + + function render_area($area, $empty = FALSE) { + $return = ''; + foreach ($this->get_handlers($area) as $area) { + $return .= $area->render($empty); + } + return $return; + } + + + /** + * Determine if the user has access to this display of the view. + */ + function access($account = NULL) { + if (!isset($account)) { + global $user; + $account = $user; + } + + // Full override. + if (user_access('access all views', $account)) { + return TRUE; + } + + $plugin = $this->get_plugin('access'); + if ($plugin) { + return $plugin->access($account); + } + + // fallback to all access if no plugin. + return TRUE; + } + + /** + * Set up any variables on the view prior to execution. These are separated + * from execute because they are extremely common and unlikely to be + * overridden on an individual display. + */ + function pre_execute() { + $this->view->set_use_ajax($this->use_ajax()); + if ($this->use_more() && !$this->use_more_always()) { + $this->view->get_total_rows = TRUE; + } + $this->view->init_handlers(); + if ($this->uses_exposed()) { + $exposed_form = $this->get_plugin('exposed_form'); + $exposed_form->pre_execute(); + } + + foreach ($this->extender as $extender) { + $extender->pre_execute(); + } + + if ($this->get_option('hide_admin_links')) { + $this->view->hide_admin_links = TRUE; + } + } + + /** + * When used externally, this is how a view gets run and returns + * data in the format required. + * + * The base class cannot be executed. + */ + function execute() { } + + /** + * Fully render the display for the purposes of a live preview or + * some other AJAXy reason. + */ + function preview() { return $this->view->render(); } + + /** + * Displays can require a certain type of style plugin. By default, they will + * be 'normal'. + */ + function get_style_type() { return 'normal'; } + + /** + * Make sure the display and all associated handlers are valid. + * + * @return + * Empty array if the display is valid; an array of error strings if it is not. + */ + function validate() { + $errors = array(); + // Make sure displays that use fields HAVE fields. + if ($this->uses_fields()) { + $fields = FALSE; + foreach ($this->get_handlers('field') as $field) { + if (empty($field->options['exclude'])) { + $fields = TRUE; + } + } + + if (!$fields) { + $errors[] = t('Display "@display" uses fields but there are none defined for it or all are excluded.', array('@display' => $this->display->display_title)); + } + } + + if ($this->has_path() && !$this->get_option('path')) { + $errors[] = t('Display "@display" uses a path but the path is undefined.', array('@display' => $this->display->display_title)); + } + + // Validate style plugin + $style = $this->get_plugin(); + if (empty($style)) { + $errors[] = t('Display "@display" has an invalid style plugin.', array('@display' => $this->display->display_title)); + } + else { + $result = $style->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + } + + // Validate query plugin. + $query = $this->get_plugin('query'); + $result = $query->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + + // Validate handlers + foreach (views_object_types() as $type => $info) { + foreach ($this->get_handlers($type) as $handler) { + $result = $handler->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + } + } + + return $errors; + } + + /** + * Check if the provided identifier is unique. + * + * @param string $id + * The id of the handler which is checked. + * @param string $identifier + * The actual get identifier configured in the exposed settings. + * + * @return bool + * Returns whether the identifier is unique on all handlers. + * + */ + function is_identifier_unique($id, $identifier) { + foreach (views_object_types() as $type => $info) { + foreach ($this->get_handlers($type) as $key => $handler) { + if ($handler->can_expose() && $handler->is_exposed()) { + if ($handler->is_a_group()) { + if ($id != $key && $identifier == $handler->options['group_info']['identifier']) { + return FALSE; + } + } + else { + if ($id != $key && $identifier == $handler->options['expose']['identifier']) { + return FALSE; + } + } + } + } + } + return TRUE; + } + + /** + * Provide the block system with any exposed widget blocks for this display. + */ + function get_special_blocks() { + $blocks = array(); + + if ($this->uses_exposed_form_in_block()) { + $delta = '-exp-' . $this->view->name . '-' . $this->display->id; + $desc = t('Exposed form: @view-@display_id', array('@view' => $this->view->name, '@display_id' => $this->display->id)); + + $blocks[$delta] = array( + 'info' => $desc, + 'cache' => DRUPAL_NO_CACHE, + ); + } + + return $blocks; + } + + /** + * Render any special blocks provided for this display. + */ + function view_special_blocks($type) { + if ($type == '-exp') { + // avoid interfering with the admin forms. + if (arg(0) == 'admin' && arg(1) == 'structure' && arg(2) == 'views') { + return; + } + $this->view->init_handlers(); + + if ($this->uses_exposed() && $this->get_option('exposed_block')) { + $exposed_form = $this->get_plugin('exposed_form'); + return array( + 'content' => $exposed_form->render_exposed_form(TRUE), + ); + } + } + } + + /** + * Override of export_option() + * + * Because displays do not want to export options that are NOT overridden from the + * default display, we need some special handling during the export process. + */ + function export_option($indent, $prefix, $storage, $option, $definition, $parents) { + // The $prefix is wrong because we store our actual options a little differently: + $prefix = '$handler->display->display_options'; + $output = ''; + if (!$parents && !$this->is_default_display()) { + // Do not export items that are not overridden. + if ($this->is_defaulted($option)) { + return; + } + + // If this is not defaulted and is overrideable, flip the switch to say this + // is overridden. + if ($this->defaultable_sections($option)) { + $output .= $indent . $prefix . "['defaults']['$option'] = FALSE;\n"; + } + } + + $output .= parent::export_option($indent, $prefix, $storage, $option, $definition, $parents); + return $output; + } + + /** + * Special method to export items that have handlers. + * + * This method was specified in the option_definition() as the method to utilize to + * export fields, filters, sort criteria, relationships and arguments. This passes + * the export off to the individual handlers so that they can export themselves + * properly. + */ + function export_handler($indent, $prefix, $storage, $option, $definition, $parents) { + $output = ''; + + // cut the 's' off because the data is stored as the plural form but we need + // the singular form. Who designed that anyway? Oh yeah, I did. :( + if ($option != 'header' && $option != 'footer' && $option != 'empty') { + $type = substr($option, 0, -1); + } + else { + $type = $option; + } + $types = views_object_types(); + foreach ($storage[$option] as $id => $info) { + if (!empty($types[$type]['type'])) { + $handler_type = $types[$type]['type']; + } + else { + $handler_type = $type; + } + // If aggregation is on, the group type might override the actual + // handler that is in use. This piece of code checks that and, + // if necessary, sets the override handler. + $override = NULL; + if ($this->use_group_by() && !empty($info['group_type'])) { + if (empty($this->view->query)) { + $this->view->init_query(); + } + $aggregate = $this->view->query->get_aggregation_info(); + if (!empty($aggregate[$info['group_type']]['handler'][$type])) { + $override = $aggregate[$info['group_type']]['handler'][$type]; + } + } + $handler = views_get_handler($info['table'], $info['field'], $handler_type, $override); + if ($handler) { + $handler->init($this->view, $info); + $output .= $indent . '/* ' . $types[$type]['stitle'] . ': ' . $handler->ui_name() . " */\n"; + $output .= $handler->export_options($indent, $prefix . "['$option']['$id']"); + } + + // Prevent reference problems. + unset($handler); + } + + return $output; + } + + /** + * Special handling for the style export. + * + * Styles are stored as style_plugin and style_options or row_plugin and + * row_options accordingly. The options are told not to export, and the + * export for the plugin should export both. + */ + function export_style($indent, $prefix, $storage, $option, $definition, $parents) { + $output = ''; + $style_plugin = $this->get_plugin(); + if ($option == 'style_plugin') { + $type = 'style'; + $options_field = 'style_options'; + $plugin = $style_plugin; + } + else { + if (!$style_plugin || !$style_plugin->uses_row_plugin()) { + return; + } + + $type = 'row'; + $options_field = 'row_options'; + $plugin = $this->get_plugin('row'); + // If the style plugin doesn't use row plugins, don't even bother. + } + + if ($plugin) { + // Write which plugin to use. + $value = $this->get_option($option); + $output .= $indent . $prefix . "['$option'] = '$value';\n"; + + // Pass off to the plugin to export itself. + $output .= $plugin->export_options($indent, $prefix . "['$options_field']"); + } + + return $output; + } + + /** + * Special handling for plugin export + * + * Plugins other than styles are stored in array with 'type' being the key + * to the plugin. For modern plugins, the options are stored in the 'options' + * array, but for legacy plugins (access and cache) options are stored as + * siblings to the type. + */ + function export_plugin($indent, $prefix, $storage, $option, $definition, $parents) { + $output = ''; + $plugin_type = end($parents); + $plugin = $this->get_plugin($plugin_type); + if ($plugin) { + // Write which plugin to use. + $value = $storage[$option]; + $new_prefix = $prefix . "['$plugin_type']"; + + $output .= $indent . $new_prefix . "['$option'] = '$value';\n"; + + if ($plugin_type != 'access' && $plugin_type!= 'cache') { + $new_prefix .= "['options']"; + } + + // Pass off to the plugin to export itself. + $output .= $plugin->export_options($indent, $new_prefix); + } + + return $output; + } + + function unpack_style($indent, $prefix, $storage, $option, $definition, $parents) { + $output = ''; + $style_plugin = $this->get_plugin(); + if ($option == 'style_plugin') { + $type = 'style'; + $options_field = 'style_options'; + $plugin = $style_plugin; + } + else { + if (!$style_plugin || !$style_plugin->uses_row_plugin()) { + return; + } + + $type = 'row'; + $options_field = 'row_options'; + $plugin = $this->get_plugin('row'); + // If the style plugin doesn't use row plugins, don't even bother. + } + + if ($plugin) { + return $plugin->unpack_translatables($translatable, $parents); + } + } + + /** + * Special handling for plugin unpacking. + */ + function unpack_plugin(&$translatable, $storage, $option, $definition, $parents) { + $plugin_type = end($parents); + $plugin = $this->get_plugin($plugin_type); + if ($plugin) { + // Write which plugin to use. + return $plugin->unpack_translatables($translatable, $parents); + } + } + + /** + * Special method to unpack items that have handlers. + * + * This method was specified in the option_definition() as the method to utilize to + * export fields, filters, sort criteria, relationships and arguments. This passes + * the export off to the individual handlers so that they can export themselves + * properly. + */ + function unpack_handler(&$translatable, $storage, $option, $definition, $parents) { + $output = ''; + + // cut the 's' off because the data is stored as the plural form but we need + // the singular form. Who designed that anyway? Oh yeah, I did. :( + if ($option != 'header' && $option != 'footer' && $option != 'empty') { + $type = substr($option, 0, -1); + } + else { + $type = $option; + } + $types = views_object_types(); + foreach ($storage[$option] as $id => $info) { + if (!empty($types[$type]['type'])) { + $handler_type = $types[$type]['type']; + } + else { + $handler_type = $type; + } + $handler = views_get_handler($info['table'], $info['field'], $handler_type); + if ($handler) { + $handler->init($this->view, $info); + $handler->unpack_translatables($translatable, array_merge($parents, array($type, $info['table'], $info['id']))); + } + + // Prevent reference problems. + unset($handler); + } + + return $output; + } + + /** + * Provide some helpful text for the arguments. + * The result should contain of an array with + * - filter value present: The title of the fieldset in the argument + * where you can configure what should be done with a given argument. + * - filter value not present: The tiel of the fieldset in the argument + * where you can configure what should be done if the argument does not + * exist. + * - description: A description about how arguments comes to the display. + * For example blocks don't get it from url. + */ + function get_argument_text() { + return array( + 'filter value not present' => t('When the filter value is NOT available'), + 'filter value present' => t('When the filter value IS available or a default is provided'), + 'description' => t("This display does not have a source for contextual filters, so no contextual filter value will be available unless you select 'Provide default'."), + ); + } + + /** + * Provide some helpful text for pagers. + * + * The result should contain of an array within + * - items per page title + */ + function get_pager_text() { + return array( + 'items per page title' => t('Items to display'), + 'items per page description' => t('The number of items to display. Enter 0 for no limit.') + ); + } +} + + +/** + * @} + */ diff --git a/plugins/views_plugin_display_attachment.inc b/plugins/views_plugin_display_attachment.inc new file mode 100644 index 00000000..91c8d1f9 --- /dev/null +++ b/plugins/views_plugin_display_attachment.inc @@ -0,0 +1,282 @@ + array()); + $options['attachment_position'] = array('default' => 'before'); + $options['inherit_arguments'] = array('default' => TRUE, 'bool' => TRUE); + $options['inherit_exposed_filters'] = array('default' => FALSE, 'bool' => TRUE); + $options['inherit_pager'] = array('default' => FALSE, 'bool' => TRUE); + $options['render_pager'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function execute() { + return $this->view->render($this->display->id); + } + + function attachment_positions($position = NULL) { + $positions = array( + 'before' => t('Before'), + 'after' => t('After'), + 'both' => t('Both'), + ); + + if ($position) { + return $positions[$position]; + } + + return $positions; + } + + /** + * Provide the summary for attachment options in the views UI. + * + * This output is returned as an array. + */ + function options_summary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::options_summary($categories, $options); + + $categories['attachment'] = array( + 'title' => t('Attachment settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $displays = array_filter($this->get_option('displays')); + if (count($displays) > 1) { + $attach_to = t('Multiple displays'); + } + elseif (count($displays) == 1) { + $display = array_shift($displays); + if (!empty($this->view->display[$display])) { + $attach_to = check_plain($this->view->display[$display]->display_title); + } + } + + if (!isset($attach_to)) { + $attach_to = t('Not defined'); + } + + $options['displays'] = array( + 'category' => 'attachment', + 'title' => t('Attach to'), + 'value' => $attach_to, + ); + + $options['attachment_position'] = array( + 'category' => 'attachment', + 'title' => t('Attachment position'), + 'value' => $this->attachment_positions($this->get_option('attachment_position')), + ); + + $options['inherit_arguments'] = array( + 'category' => 'attachment', + 'title' => t('Inherit contextual filters'), + 'value' => $this->get_option('inherit_arguments') ? t('Yes') : t('No'), + ); + + $options['inherit_exposed_filters'] = array( + 'category' => 'attachment', + 'title' => t('Inherit exposed filters'), + 'value' => $this->get_option('inherit_exposed_filters') ? t('Yes') : t('No'), + ); + + $options['inherit_pager'] = array( + 'category' => 'pager', + 'title' => t('Inherit pager'), + 'value' => $this->get_option('inherit_pager') ? t('Yes') : t('No'), + ); + + $options['render_pager'] = array( + 'category' => 'pager', + 'title' => t('Render pager'), + 'value' => $this->get_option('render_pager') ? t('Yes') : t('No'), + ); + + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_form($form, $form_state); + + switch ($form_state['section']) { + case 'inherit_arguments': + $form['#title'] .= t('Inherit contextual filters'); + $form['inherit_arguments'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit'), + '#description' => t('Should this display inherit its contextual filter values from the parent display to which it is attached?'), + '#default_value' => $this->get_option('inherit_arguments'), + ); + break; + case 'inherit_exposed_filters': + $form['#title'] .= t('Inherit exposed filters'); + $form['inherit_exposed_filters'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit'), + '#description' => t('Should this display inherit its exposed filter values from the parent display to which it is attached?'), + '#default_value' => $this->get_option('inherit_exposed_filters'), + ); + break; + case 'inherit_pager': + $form['#title'] .= t('Inherit pager'); + $form['inherit_pager'] = array( + '#type' => 'checkbox', + '#title' => t('Inherit'), + '#description' => t('Should this display inherit its paging values from the parent display to which it is attached?'), + '#default_value' => $this->get_option('inherit_pager'), + ); + break; + case 'render_pager': + $form['#title'] .= t('Render pager'); + $form['render_pager'] = array( + '#type' => 'checkbox', + '#title' => t('Render'), + '#description' => t('Should this display render the pager values? This is only meaningful if inheriting a pager.'), + '#default_value' => $this->get_option('render_pager'), + ); + break; + case 'attachment_position': + $form['#title'] .= t('Position'); + $form['attachment_position'] = array( + '#type' => 'radios', + '#description' => t('Attach before or after the parent display?'), + '#options' => $this->attachment_positions(), + '#default_value' => $this->get_option('attachment_position'), + ); + break; + case 'displays': + $form['#title'] .= t('Attach to'); + $displays = array(); + foreach ($this->view->display as $display_id => $display) { + if (!empty($display->handler) && $display->handler->accept_attachments()) { + $displays[$display_id] = $display->display_title; + } + } + $form['displays'] = array( + '#type' => 'checkboxes', + '#description' => t('Select which display or displays this should attach to.'), + '#options' => $displays, + '#default_value' => $this->get_option('displays'), + ); + break; + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function options_submit(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_submit($form, $form_state); + switch ($form_state['section']) { + case 'inherit_arguments': + case 'inherit_pager': + case 'render_pager': + case 'inherit_exposed_filters': + case 'attachment_position': + case 'displays': + $this->set_option($form_state['section'], $form_state['values'][$form_state['section']]); + break; + } + } + + /** + * Attach to another view. + */ + function attach_to($display_id) { + $displays = $this->get_option('displays'); + + if (empty($displays[$display_id])) { + return; + } + + if (!$this->access()) { + return; + } + + // Get a fresh view because our current one has a lot of stuff on it because it's + // already been executed. + $view = $this->view->clone_view(); + $view->original_args = $view->args; + + $args = $this->get_option('inherit_arguments') ? $this->view->args : array(); + $view->set_arguments($args); + $view->set_display($this->display->id); + if ($this->get_option('inherit_pager')) { + $view->display_handler->use_pager = $this->view->display[$display_id]->handler->use_pager(); + $view->display_handler->set_option('pager', $this->view->display[$display_id]->handler->get_option('pager')); + } + + $attachment = $view->execute_display($this->display->id, $args); + + switch ($this->get_option('attachment_position')) { + case 'before': + $this->view->attachment_before .= $attachment; + break; + case 'after': + $this->view->attachment_after .= $attachment; + break; + case 'both': + $this->view->attachment_before .= $attachment; + $this->view->attachment_after .= $attachment; + break; + } + + $view->destroy(); + } + + /** + * Attachment displays only use exposed widgets if + * they are set to inherit the exposed filter settings + * of their parent display. + */ + function uses_exposed() { + if (!empty($this->options['inherit_exposed_filters']) && parent::uses_exposed()) { + return TRUE; + } + return FALSE; + } + + /** + * If an attachment is set to inherit the exposed filter + * settings from its parent display, then don't render and + * display a second set of exposed filter widgets. + */ + function displays_exposed() { + return $this->options['inherit_exposed_filters'] ? FALSE : TRUE; + } + + function use_pager() { + return !empty($this->use_pager); + } + + function render_pager() { + return !empty($this->use_pager) && $this->get_option('render_pager'); + } +} diff --git a/plugins/views_plugin_display_block.inc b/plugins/views_plugin_display_block.inc new file mode 100644 index 00000000..c903a9b6 --- /dev/null +++ b/plugins/views_plugin_display_block.inc @@ -0,0 +1,243 @@ + '', 'translatable' => TRUE); + $options['block_caching'] = array('default' => DRUPAL_NO_CACHE); + + return $options; + } + + /** + * The default block handler doesn't support configurable items, + * but extended block handlers might be able to do interesting + * stuff with it. + */ + function execute_hook_block_list($delta = 0, $edit = array()) { + $delta = $this->view->name . '-' . $this->display->id; + $desc = $this->get_option('block_description'); + + if (empty($desc)) { + if ($this->display->display_title == $this->definition['title']) { + $desc = t('View: !view', array('!view' => $this->view->get_human_name())); + } + else { + $desc = t('View: !view: !display', array('!view' => $this->view->get_human_name(), '!display' => $this->display->display_title)); + } + } + return array( + $delta => array( + 'info' => $desc, + 'cache' => $this->get_cache_type() + ), + ); + } + + /** + * The display block handler returns the structure necessary for a block. + */ + function execute() { + // Prior to this being called, the $view should already be set to this + // display, and arguments should be set on the view. + $info['content'] = $this->view->render(); + $info['subject'] = filter_xss_admin($this->view->get_title()); + if (!empty($this->view->result) || $this->get_option('empty') || !empty($this->view->style_plugin->definition['even empty'])) { + return $info; + } + } + + /** + * Provide the summary for page options in the views UI. + * + * This output is returned as an array. + */ + function options_summary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::options_summary($categories, $options); + + $categories['block'] = array( + 'title' => t('Block settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $block_description = strip_tags($this->get_option('block_description')); + if (empty($block_description)) { + $block_description = t('None'); + } + + $options['block_description'] = array( + 'category' => 'block', + 'title' => t('Block name'), + 'value' => views_ui_truncate($block_description, 24), + ); + + $types = $this->block_caching_modes(); + $options['block_caching'] = array( + 'category' => 'other', + 'title' => t('Block caching'), + 'value' => $types[$this->get_cache_type()], + ); + } + + /** + * Provide a list of core's block caching modes. + */ + function block_caching_modes() { + return array( + DRUPAL_NO_CACHE => t('Do not cache'), + DRUPAL_CACHE_GLOBAL => t('Cache once for everything (global)'), + DRUPAL_CACHE_PER_PAGE => t('Per page'), + DRUPAL_CACHE_PER_ROLE => t('Per role'), + DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE => t('Per role per page'), + DRUPAL_CACHE_PER_USER => t('Per user'), + DRUPAL_CACHE_PER_USER | DRUPAL_CACHE_PER_PAGE => t('Per user per page'), + ); + } + + /** + * Provide a single method to figure caching type, keeping a sensible default + * for when it's unset. + */ + function get_cache_type() { + $cache_type = $this->get_option('block_caching'); + if (empty($cache_type)) { + $cache_type = DRUPAL_NO_CACHE; + } + return $cache_type; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_form($form, $form_state); + + switch ($form_state['section']) { + case 'block_description': + $form['#title'] .= t('Block admin description'); + $form['block_description'] = array( + '#type' => 'textfield', + '#description' => t('This will appear as the name of this block in administer >> structure >> blocks.'), + '#default_value' => $this->get_option('block_description'), + ); + break; + case 'block_caching': + $form['#title'] .= t('Block caching type'); + + $form['block_caching'] = array( + '#type' => 'radios', + '#description' => t("This sets the default status for Drupal's built-in block caching method; this requires that caching be turned on in block administration, and be careful because you have little control over when this cache is flushed."), + '#options' => $this->block_caching_modes(), + '#default_value' => $this->get_cache_type(), + ); + break; + case 'exposed_form_options': + $this->view->init_handlers(); + if (!$this->uses_exposed() && parent::uses_exposed()) { + $form['exposed_form_options']['warning'] = array( + '#weight' => -10, + '#markup' => '
    ' . t('Exposed filters in block displays require "Use AJAX" to be set to work correctly.') . '
    ', + ); + } + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function options_submit(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_submit($form, $form_state); + switch ($form_state['section']) { + case 'display_id': + $this->update_block_bid($form_state['view']->name, $this->display->id, $this->display->new_id); + break; + case 'block_description': + $this->set_option('block_description', $form_state['values']['block_description']); + break; + case 'block_caching': + $this->set_option('block_caching', $form_state['values']['block_caching']); + $this->save_block_cache($form_state['view']->name . '-'. $form_state['display_id'], $form_state['values']['block_caching']); + break; + } + } + + /** + * Block views use exposed widgets only if AJAX is set. + */ + function uses_exposed() { + if ($this->use_ajax()) { + return parent::uses_exposed(); + } + return FALSE; + } + + /** + * Update the block delta when you change the machine readable name of the display. + */ + function update_block_bid($name, $old_delta, $delta) { + $old_hashes = $hashes = variable_get('views_block_hashes', array()); + + $old_delta = $name . '-' . $old_delta; + $delta = $name . '-' . $delta; + if (strlen($old_delta) >= 32) { + $old_delta = md5($old_delta); + unset($hashes[$old_delta]); + } + if (strlen($delta) >= 32) { + $md5_delta = md5($delta); + $hashes[$md5_delta] = $delta; + $delta = $md5_delta; + } + + // Maybe people don't have block module installed, so let's skip this. + if (db_table_exists('block')) { + db_update('block') + ->fields(array('delta' => $delta)) + ->condition('delta', $old_delta) + ->execute(); + } + + // Update the hashes if needed. + if ($hashes != $old_hashes) { + variable_set('views_block_hashes', $hashes); + } + } + + /** + * Save the block cache setting in the blocks table if this block allready + * exists in the blocks table. Dirty fix untill http://drupal.org/node/235673 gets in. + */ + function save_block_cache($delta, $cache_setting) { + if (strlen($delta) >= 32) { + $delta = md5($delta); + } + if (db_table_exists('block') && $bid = db_query("SELECT bid FROM {block} WHERE module = 'views' AND delta = :delta", array( + ':delta' => $delta))->fetchField()) { + db_update('block') + ->fields(array( + 'cache' => $cache_setting, + )) + ->condition('module','views') + ->condition('delta', $delta) + ->execute(); + } + } +} diff --git a/plugins/views_plugin_display_default.inc b/plugins/views_plugin_display_default.inc new file mode 100644 index 00000000..4b1fc086 --- /dev/null +++ b/plugins/views_plugin_display_default.inc @@ -0,0 +1,57 @@ +execute_display('default', $args); + * @endcode + * + * For more complex usages, a view can be partially built: + * @code + * $view->set_arguments($args); + * $view->build('default'); // Build the query + * $view->pre_execute(); // Pre-execute the query. + * $view->execute(); // Run the query + * $output = $view->render(); // Render the view + * @endcode + * + * If short circuited at any point, look in $view->build_info for + * information about the query. After execute, look in $view->result + * for the array of objects returned from db_query. + * + * You can also do: + * @code + * $view->set_arguments($args); + * $output = $view->render('default'); // Render the view + * @endcode + * + * This illustrates that render is smart enough to call build and execute + * if these items have not already been accomplished. + * + * Note that execute also must accomplish other tasks, such + * as setting page titles, breadcrumbs, and generating exposed filter + * data if necessary. + */ + function execute() { + return $this->view->render($this->display->id); + } +} diff --git a/plugins/views_plugin_display_embed.inc b/plugins/views_plugin_display_embed.inc new file mode 100644 index 00000000..8b25cf96 --- /dev/null +++ b/plugins/views_plugin_display_embed.inc @@ -0,0 +1,14 @@ +view = $view; + $this->display = $display; + } + + + /** + * Provide a form to edit options for this plugin. + */ + function options_definition_alter(&$options) { } + + /** + * Provide a form to edit options for this plugin. + */ + function options_form(&$form, &$form_state) { } + + /** + * Validate the options form. + */ + function options_validate(&$form, &$form_state) { } + + /** + * Handle any special handling on the validate form. + */ + function options_submit(&$form, &$form_state) { } + + /** + * Set up any variables on the view prior to execution. + */ + function pre_execute() { } + + /** + * Inject anything into the query that the display_extender handler needs. + */ + function query() { } + + /** + * Provide the default summary for options in the views UI. + * + * This output is returned as an array. + */ + function options_summary(&$categories, &$options) { } + + /** + * Static member function to list which sections are defaultable + * and what items each section contains. + */ + function defaultable_sections(&$sections, $section = NULL) { } +} diff --git a/plugins/views_plugin_display_feed.inc b/plugins/views_plugin_display_feed.inc new file mode 100644 index 00000000..bfd220c5 --- /dev/null +++ b/plugins/views_plugin_display_feed.inc @@ -0,0 +1,222 @@ +get_style_type(), array($view->base_table)); + $default_row_plugin = key($row_plugins); + if ($this->options['row_plugin'] == '') { + $this->options['row_plugin'] = $default_row_plugin; + } + } + + function uses_breadcrumb() { return FALSE; } + function get_style_type() { return 'feed'; } + + /** + * Feeds do not go through the normal page theming mechanism. Instead, they + * go through their own little theme function and then return NULL so that + * Drupal believes that the page has already rendered itself...which it has. + */ + function execute() { + $output = $this->view->render(); + if (empty($output)) { + return drupal_not_found(); + } + print $output; + } + + function preview() { + if (!empty($this->view->live_preview)) { + return '
    ' . check_plain($this->view->render()) . '
    '; + } + return $this->view->render(); + } + + /** + * Instead of going through the standard views_view.tpl.php, delegate this + * to the style handler. + */ + function render() { + return $this->view->style_plugin->render($this->view->result); + } + + function defaultable_sections($section = NULL) { + if (in_array($section, array('style_options', 'style_plugin', 'row_options', 'row_plugin',))) { + return FALSE; + } + + $sections = parent::defaultable_sections($section); + + // Tell views our sitename_title option belongs in the title section. + if ($section == 'title') { + $sections[] = 'sitename_title'; + } + elseif (!$section) { + $sections['title'][] = 'sitename_title'; + } + return $sections; + } + + function option_definition() { + $options = parent::option_definition(); + + $options['displays'] = array('default' => array()); + + // Overrides for standard stuff: + $options['style_plugin']['default'] = 'rss'; + $options['style_options']['default'] = array('description' => ''); + $options['sitename_title']['default'] = FALSE; + $options['row_plugin']['default'] = ''; + $options['defaults']['default']['style_plugin'] = FALSE; + $options['defaults']['default']['style_options'] = FALSE; + $options['defaults']['default']['row_plugin'] = FALSE; + $options['defaults']['default']['row_options'] = FALSE; + + return $options; + } + + function options_summary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::options_summary($categories, $options); + + // Since we're childing off the 'page' type, we'll still *call* our + // category 'page' but let's override it so it says feed settings. + $categories['page'] = array( + 'title' => t('Feed settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + if ($this->get_option('sitename_title')) { + $options['title']['value'] = t('Using the site name'); + } + + // I don't think we want to give feeds menus directly. + unset($options['menu']); + + $displays = array_filter($this->get_option('displays')); + if (count($displays) > 1) { + $attach_to = t('Multiple displays'); + } + elseif (count($displays) == 1) { + $display = array_shift($displays); + if (!empty($this->view->display[$display])) { + $attach_to = check_plain($this->view->display[$display]->display_title); + } + } + + if (!isset($attach_to)) { + $attach_to = t('None'); + } + + $options['displays'] = array( + 'category' => 'page', + 'title' => t('Attach to'), + 'value' => $attach_to, + ); + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + // It is very important to call the parent function here. + parent::options_form($form, $form_state); + + switch ($form_state['section']) { + case 'title': + $title = $form['title']; + // A little juggling to move the 'title' field beyond our checkbox. + unset($form['title']); + $form['sitename_title'] = array( + '#type' => 'checkbox', + '#title' => t('Use the site name for the title'), + '#default_value' => $this->get_option('sitename_title'), + ); + $form['title'] = $title; + $form['title']['#dependency'] = array('edit-sitename-title' => array(FALSE)); + break; + case 'displays': + $form['#title'] .= t('Attach to'); + $displays = array(); + foreach ($this->view->display as $display_id => $display) { + if (!empty($display->handler) && $display->handler->accept_attachments()) { + $displays[$display_id] = $display->display_title; + } + } + $form['displays'] = array( + '#type' => 'checkboxes', + '#description' => t('The feed icon will be available only to the selected displays.'), + '#options' => $displays, + '#default_value' => $this->get_option('displays'), + ); + break; + case 'path': + $form['path']['#description'] = t('This view will be displayed by visiting this path on your site. It is recommended that the path be something like "path/%/%/feed" or "path/%/%/rss.xml", putting one % in the path for each contextual filter you have defined in the view.'); + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function options_submit(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_submit($form, $form_state); + switch ($form_state['section']) { + case 'title': + $this->set_option('sitename_title', $form_state['values']['sitename_title']); + break; + case 'displays': + $this->set_option($form_state['section'], $form_state['values'][$form_state['section']]); + break; + } + } + + /** + * Attach to another view. + */ + function attach_to($display_id) { + $displays = $this->get_option('displays'); + if (empty($displays[$display_id])) { + return; + } + + // Defer to the feed style; it may put in meta information, and/or + // attach a feed icon. + $plugin = $this->get_plugin(); + if ($plugin) { + $clone = $this->view->clone_view(); + $clone->set_display($this->display->id); + $clone->build_title(); + $plugin->attach_to($display_id, $this->get_path(), $clone->get_title()); + + // Clean up + $clone->destroy(); + unset($clone); + } + } + + function uses_link_display() { + return TRUE; + } +} diff --git a/plugins/views_plugin_display_page.inc b/plugins/views_plugin_display_page.inc new file mode 100644 index 00000000..6ab95b71 --- /dev/null +++ b/plugins/views_plugin_display_page.inc @@ -0,0 +1,557 @@ + ''); + $options['menu'] = array( + 'contains' => array( + 'type' => array('default' => 'none'), + // Do not translate menu and title as menu system will. + 'title' => array('default' => '', 'translatable' => FALSE), + 'description' => array('default' => '', 'translatable' => FALSE), + 'weight' => array('default' => 0), + 'name' => array('default' => variable_get('menu_default_node_menu', 'navigation')), + 'context' => array('default' => ''), + ), + ); + $options['tab_options'] = array( + 'contains' => array( + 'type' => array('default' => 'none'), + // Do not translate menu and title as menu system will. + 'title' => array('default' => '', 'translatable' => FALSE), + 'description' => array('default' => '', 'translatable' => FALSE), + 'weight' => array('default' => 0), + 'name' => array('default' => 'navigation'), + ), + ); + + return $options; + } + + /** + * Add this display's path information to Drupal's menu system. + */ + function execute_hook_menu($callbacks) { + $items = array(); + // Replace % with the link to our standard views argument loader + // views_arg_load -- which lives in views.module + + $bits = explode('/', $this->get_option('path')); + $page_arguments = array($this->view->name, $this->display->id); + $this->view->init_handlers(); + $view_arguments = $this->view->argument; + + // Replace % with %views_arg for menu autoloading and add to the + // page arguments so the argument actually comes through. + foreach ($bits as $pos => $bit) { + if ($bit == '%') { + $argument = array_shift($view_arguments); + if (!empty($argument->options['specify_validation']) && $argument->options['validate']['type'] != 'none') { + $bits[$pos] = '%views_arg'; + } + $page_arguments[] = $pos; + } + } + + $path = implode('/', $bits); + + $access_plugin = $this->get_plugin('access'); + if (!isset($access_plugin)) { + $access_plugin = views_get_plugin('access', 'none'); + } + + // Get access callback might return an array of the callback + the dynamic arguments. + $access_plugin_callback = $access_plugin->get_access_callback(); + + if (is_array($access_plugin_callback)) { + $access_arguments = array(); + + // Find the plugin arguments. + $access_plugin_method = array_shift($access_plugin_callback); + $access_plugin_arguments = array_shift($access_plugin_callback); + if (!is_array($access_plugin_arguments)) { + $access_plugin_arguments = array(); + } + + $access_arguments[0] = array($access_plugin_method, &$access_plugin_arguments); + + // Move the plugin arguments to the access arguments array. + $i = 1; + foreach ($access_plugin_arguments as $key => $value) { + if (is_int($value)) { + $access_arguments[$i] = $value; + $access_plugin_arguments[$key] = $i; + $i++; + } + } + } + else { + $access_arguments = array($access_plugin_callback); + } + + if ($path) { + $items[$path] = array( + // default views page entry + 'page callback' => 'views_page', + 'page arguments' => $page_arguments, + // Default access check (per display) + 'access callback' => 'views_access', + 'access arguments' => $access_arguments, + // Identify URL embedded arguments and correlate them to a handler + 'load arguments' => array($this->view->name, $this->display->id, '%index'), + ); + $menu = $this->get_option('menu'); + if (empty($menu)) { + $menu = array('type' => 'none'); + } + // Set the title and description if we have one. + if ($menu['type'] != 'none') { + $items[$path]['title'] = $menu['title']; + $items[$path]['description'] = $menu['description']; + } + + if (isset($menu['weight'])) { + $items[$path]['weight'] = intval($menu['weight']); + } + + switch ($menu['type']) { + case 'none': + default: + $items[$path]['type'] = MENU_CALLBACK; + break; + case 'normal': + $items[$path]['type'] = MENU_NORMAL_ITEM; + // Insert item into the proper menu + $items[$path]['menu_name'] = $menu['name']; + break; + case 'tab': + $items[$path]['type'] = MENU_LOCAL_TASK; + break; + case 'default tab': + $items[$path]['type'] = MENU_DEFAULT_LOCAL_TASK; + break; + } + + // Add context for contextual links. + // @see menu_contextual_links() + if (!empty($menu['context'])) { + $items[$path]['context'] = MENU_CONTEXT_INLINE; + } + + // If this is a 'default' tab, check to see if we have to create teh + // parent menu item. + if ($menu['type'] == 'default tab') { + $tab_options = $this->get_option('tab_options'); + if (!empty($tab_options['type']) && $tab_options['type'] != 'none') { + $bits = explode('/', $path); + // Remove the last piece. + $bit = array_pop($bits); + + // we can't do this if they tried to make the last path bit variable. + // @todo: We can validate this. + if ($bit != '%views_arg' && !empty($bits)) { + $default_path = implode('/', $bits); + $items[$default_path] = array( + // default views page entry + 'page callback' => 'views_page', + 'page arguments' => $page_arguments, + // Default access check (per display) + 'access callback' => 'views_access', + 'access arguments' => $access_arguments, + // Identify URL embedded arguments and correlate them to a handler + 'load arguments' => array($this->view->name, $this->display->id, '%index'), + 'title' => $tab_options['title'], + 'description' => $tab_options['description'], + 'menu_name' => $tab_options['name'], + ); + switch ($tab_options['type']) { + default: + case 'normal': + $items[$default_path]['type'] = MENU_NORMAL_ITEM; + break; + case 'tab': + $items[$default_path]['type'] = MENU_LOCAL_TASK; + break; + } + if (isset($tab_options['weight'])) { + $items[$default_path]['weight'] = intval($tab_options['weight']); + } + } + } + } + } + + return $items; + } + + /** + * The display page handler returns a normal view, but it also does + * a drupal_set_title for the page, and does a views_set_page_view + * on the view. + */ + function execute() { + // Let the world know that this is the page view we're using. + views_set_page_view($this->view); + + // Prior to this being called, the $view should already be set to this + // display, and arguments should be set on the view. + $this->view->build(); + if (!empty($this->view->build_info['fail'])) { + return drupal_not_found(); + } + + if (!empty($this->view->build_info['denied'])) { + return drupal_access_denied(); + } + + $this->view->get_breadcrumb(TRUE); + + + // And now render the view. + $render = $this->view->render(); + + // First execute the view so it's possible to get tokens for the title. + // And the title, which is much easier. + drupal_set_title(filter_xss_admin($this->view->get_title()), PASS_THROUGH); + return $render; + } + + /** + * Provide the summary for page options in the views UI. + * + * This output is returned as an array. + */ + function options_summary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::options_summary($categories, $options); + + $categories['page'] = array( + 'title' => t('Page settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $path = strip_tags($this->get_option('path')); + if (empty($path)) { + $path = t('No path is set'); + } + else { + $path = '/' . $path; + } + + $options['path'] = array( + 'category' => 'page', + 'title' => t('Path'), + 'value' => views_ui_truncate($path, 24), + ); + + $menu = $this->get_option('menu'); + if (!is_array($menu)) { + $menu = array('type' => 'none'); + } + switch($menu['type']) { + case 'none': + default: + $menu_str = t('No menu'); + break; + case 'normal': + $menu_str = t('Normal: @title', array('@title' => $menu['title'])); + break; + case 'tab': + case 'default tab': + $menu_str = t('Tab: @title', array('@title' => $menu['title'])); + break; + } + + $options['menu'] = array( + 'category' => 'page', + 'title' => t('Menu'), + 'value' => views_ui_truncate($menu_str, 24), + ); + + // This adds a 'Settings' link to the style_options setting if the style has options. + if ($menu['type'] == 'default tab') { + $options['menu']['setting'] = t('Parent menu item'); + $options['menu']['links']['tab_options'] = t('Change settings for the parent menu'); + } + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_form($form, $form_state); + + switch ($form_state['section']) { + case 'path': + $form['#title'] .= t('The menu path or URL of this view'); + $form['#help_topic'] = 'path'; + $form['path'] = array( + '#type' => 'textfield', + '#description' => t('This view will be displayed by visiting this path on your site. You may use "%" in your URL to represent values that will be used for contextual filters: For example, "node/%/feed".'), + '#default_value' => $this->get_option('path'), + '#field_prefix' => '' . url(NULL, array('absolute' => TRUE)) . (variable_get('clean_url', 0) ? '' : '?q='), + '#field_suffix' => '‎', + '#attributes' => array('dir'=>'ltr'), + ); + break; + case 'menu': + $form['#title'] .= t('Menu item entry'); + $form['#help_topic'] = 'menu'; + $form['menu'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#tree' => TRUE, + ); + $menu = $this->get_option('menu'); + if (empty($menu)) { + $menu = array('type' => 'none', 'title' => '', 'weight' => 0); + } + $form['menu']['type'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#title' => t('Type'), + '#type' => 'radios', + '#options' => array( + 'none' => t('No menu entry'), + 'normal' => t('Normal menu entry'), + 'tab' => t('Menu tab'), + 'default tab' => t('Default menu tab') + ), + '#default_value' => $menu['type'], + ); + $form['menu']['title'] = array( + '#prefix' => '
    ', + '#title' => t('Title'), + '#type' => 'textfield', + '#default_value' => $menu['title'], + '#description' => t('If set to normal or tab, enter the text to use for the menu item.'), + '#dependency' => array('radio:menu[type]' => array('normal', 'tab', 'default tab')), + ); + $form['menu']['description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#default_value' => $menu['description'], + '#description' => t("If set to normal or tab, enter the text to use for the menu item's description."), + '#dependency' => array('radio:menu[type]' => array('normal', 'tab', 'default tab')), + ); + + // Only display the menu selector if menu module is enabled. + if (module_exists('menu')) { + $form['menu']['name'] = array( + '#title' => t('Menu'), + '#type' => 'select', + '#options' => menu_get_menus(), + '#default_value' => $menu['name'], + '#description' => t('Insert item into an available menu.'), + '#dependency' => array('radio:menu[type]' => array('normal', 'tab')), + ); + } + else { + $form['menu']['name'] = array( + '#type' => 'value', + '#value' => $menu['name'], + ); + $form['menu']['markup'] = array( + '#markup' => t('Menu selection requires the activation of menu module.'), + ); + } + $form['menu']['weight'] = array( + '#title' => t('Weight'), + '#type' => 'textfield', + '#default_value' => isset($menu['weight']) ? $menu['weight'] : 0, + '#description' => t('The lower the weight the higher/further left it will appear.'), + '#dependency' => array('radio:menu[type]' => array('normal', 'tab', 'default tab')), + ); + $form['menu']['context'] = array( + '#title' => t('Context'), + '#suffix' => '
    ', + '#type' => 'checkbox', + '#default_value' => !empty($menu['context']), + '#description' => t('Displays the link in contextual links'), + '#dependency' => array('radio:menu[type]' => array('tab')), + ); + break; + case 'tab_options': + $form['#title'] .= t('Default tab options'); + $tab_options = $this->get_option('tab_options'); + if (empty($tab_options)) { + $tab_options = array('type' => 'none', 'title' => '', 'weight' => 0); + } + + $form['tab_markup'] = array( + '#markup' => '
    ' . t('When providing a menu item as a tab, Drupal needs to know what the parent menu item of that tab will be. Sometimes the parent will already exist, but other times you will need to have one created. The path of a parent item will always be the same path with the last part left off. i.e, if the path to this view is foo/bar/baz, the parent path would be foo/bar.') . '
    ', + ); + + $form['tab_options'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#tree' => TRUE, + ); + $form['tab_options']['type'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + '#title' => t('Parent menu item'), + '#type' => 'radios', + '#options' => array('none' => t('Already exists'), 'normal' => t('Normal menu item'), 'tab' => t('Menu tab')), + '#default_value' => $tab_options['type'], + ); + $form['tab_options']['title'] = array( + '#prefix' => '
    ', + '#title' => t('Title'), + '#type' => 'textfield', + '#default_value' => $tab_options['title'], + '#description' => t('If creating a parent menu item, enter the title of the item.'), + '#dependency' => array('radio:tab_options[type]' => array('normal', 'tab')), + ); + $form['tab_options']['description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#default_value' => $tab_options['description'], + '#description' => t('If creating a parent menu item, enter the description of the item.'), + '#dependency' => array('radio:tab_options[type]' => array('normal', 'tab')), + ); + // Only display the menu selector if menu module is enabled. + if (module_exists('menu')) { + $form['tab_options']['name'] = array( + '#title' => t('Menu'), + '#type' => 'select', + '#options' => menu_get_menus(), + '#default_value' => $tab_options['name'], + '#description' => t('Insert item into an available menu.'), + '#dependency' => array('radio:tab_options[type]' => array('normal')), + ); + } + else { + $form['tab_options']['name'] = array( + '#type' => 'value', + '#value' => $tab_options['name'], + ); + $form['tab_options']['markup'] = array( + '#markup' => t('Menu selection requires the activation of menu module.'), + ); + } + $form['tab_options']['weight'] = array( + '#suffix' => '
    ', + '#title' => t('Tab weight'), + '#type' => 'textfield', + '#default_value' => $tab_options['weight'], + '#size' => 5, + '#description' => t('If the parent menu item is a tab, enter the weight of the tab. The lower the number, the more to the left it will be.'), + '#dependency' => array('radio:tab_options[type]' => array('tab')), + ); + break; + } + } + + function options_validate(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_validate($form, $form_state); + switch ($form_state['section']) { + case 'path': + if (strpos($form_state['values']['path'], '$arg') !== FALSE) { + form_error($form['path'], t('"$arg" is no longer supported. Use % instead.')); + } + + if (strpos($form_state['values']['path'], '%') === 0) { + form_error($form['path'], t('"%" may not be used for the first segment of a path.')); + } + + // automatically remove '/' and trailing whitespace from path. + $form_state['values']['path'] = trim($form_state['values']['path'], '/ '); + break; + case 'menu': + $path = $this->get_option('path'); + if ($form_state['values']['menu']['type'] == 'normal' && strpos($path, '%') !== FALSE) { + form_error($form['menu']['type'], t('Views cannot create normal menu items for paths with a % in them.')); + } + + if ($form_state['values']['menu']['type'] == 'default tab' || $form_state['values']['menu']['type'] == 'tab') { + $bits = explode('/', $path); + $last = array_pop($bits); + if ($last == '%') { + form_error($form['menu']['type'], t('A display whose path ends with a % cannot be a tab.')); + } + } + + if ($form_state['values']['menu']['type'] != 'none' && empty($form_state['values']['menu']['title'])) { + form_error($form['menu']['title'], t('Title is required for this menu type.')); + } + break; + } + } + + function options_submit(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_submit($form, $form_state); + switch ($form_state['section']) { + case 'path': + $this->set_option('path', $form_state['values']['path']); + break; + case 'menu': + $this->set_option('menu', $form_state['values']['menu']); + // send ajax form to options page if we use it. + if ($form_state['values']['menu']['type'] == 'default tab') { + views_ui_add_form_to_stack('display', $this->view, $this->display->id, array('tab_options')); + } + break; + case 'tab_options': + $this->set_option('tab_options', $form_state['values']['tab_options']); + break; + } + } + + function validate() { + $errors = parent::validate(); + + $menu = $this->get_option('menu'); + if (!empty($menu['type']) && $menu['type'] != 'none' && empty($menu['title'])) { + $errors[] = t('Display @display is set to use a menu but the menu link text is not set.', array('@display' => $this->display->display_title)); + } + + if ($menu['type'] == 'default tab') { + $tab_options = $this->get_option('tab_options'); + if (!empty($tab_options['type']) && $tab_options['type'] != 'none' && empty($tab_options['title'])) { + $errors[] = t('Display @display is set to use a parent menu but the parent menu link text is not set.', array('@display' => $this->display->display_title)); + } + } + + return $errors; + } + + function get_argument_text() { + return array( + 'filter value not present' => t('When the filter value is NOT in the URL'), + 'filter value present' => t('When the filter value IS in the URL or a default is provided'), + 'description' => t('The contextual filter values is provided by the URL.'), + ); + } + + function get_pager_text() { + return array( + 'items per page title' => t('Items per page'), + 'items per page description' => t('The number of items to display per page. Enter 0 for no limit.') + ); + } +} diff --git a/plugins/views_plugin_exposed_form.inc b/plugins/views_plugin_exposed_form.inc new file mode 100644 index 00000000..343eee81 --- /dev/null +++ b/plugins/views_plugin_exposed_form.inc @@ -0,0 +1,334 @@ +view = &$view; + $this->display = &$display; + + $this->unpack_options($this->options, $options); + } + + function option_definition() { + $options = parent::option_definition(); + $options['submit_button'] = array('default' => 'Apply', 'translatable' => TRUE); + $options['reset_button'] = array('default' => FALSE, 'bool' => TRUE); + $options['reset_button_label'] = array('default' => 'Reset', 'translatable' => TRUE); + $options['exposed_sorts_label'] = array('default' => 'Sort by', 'translatable' => TRUE); + $options['expose_sort_order'] = array('default' => TRUE, 'bool' => TRUE); + $options['sort_asc_label'] = array('default' => 'Asc', 'translatable' => TRUE); + $options['sort_desc_label'] = array('default' => 'Desc', 'translatable' => TRUE); + $options['autosubmit'] = array('default' => FALSE, 'bool' => TRUE); + $options['autosubmit_hide'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['submit_button'] = array( + '#type' => 'textfield', + '#title' => t('Submit button text'), + '#description' => t('Text to display in the submit button of the exposed form.'), + '#default_value' => $this->options['submit_button'], + '#required' => TRUE, + ); + + $form['reset_button'] = array ( + '#type' => 'checkbox', + '#title' => t('Include reset button'), + '#description' => t('If checked the exposed form will provide a button to reset all the applied exposed filters'), + '#default_value' => $this->options['reset_button'], + ); + + $form['reset_button_label'] = array( + '#type' => 'textfield', + '#title' => t('Reset button label'), + '#description' => t('Text to display in the reset button of the exposed form.'), + '#default_value' => $this->options['reset_button_label'], + '#required' => TRUE, + '#dependency' => array( + 'edit-exposed-form-options-reset-button' => array(1) + ), + ); + + $form['exposed_sorts_label'] = array( + '#type' => 'textfield', + '#title' => t('Exposed sorts label'), + '#description' => t('Text to display as the label of the exposed sort select box.'), + '#default_value' => $this->options['exposed_sorts_label'], + '#required' => TRUE, + ); + + $form['expose_sort_order'] = array( + '#type' => 'checkbox', + '#title' => t('Expose sort order'), + '#description' => t('Allow the user to choose the sort order. If sort order is not exposed, the sort criteria settings for each sort will determine its order.'), + '#default_value' => $this->options['expose_sort_order'], + ); + + $form['sort_asc_label'] = array( + '#type' => 'textfield', + '#title' => t('Ascending'), + '#description' => t('Text to use when exposed sort is ordered ascending.'), + '#default_value' => $this->options['sort_asc_label'], + '#required' => TRUE, + '#dependency' => array('edit-exposed-form-options-expose-sort-order' => array(TRUE)), + ); + + $form['sort_desc_label'] = array( + '#type' => 'textfield', + '#title' => t('Descending'), + '#description' => t('Text to use when exposed sort is ordered descending.'), + '#default_value' => $this->options['sort_desc_label'], + '#required' => TRUE, + '#dependency' => array('edit-exposed-form-options-expose-sort-order' => array(TRUE)), + ); + + $form['autosubmit'] = array( + '#type' => 'checkbox', + '#title' => t('Autosubmit'), + '#description' => t('Automatically submit the form once an element is changed.'), + '#default_value' => $this->options['autosubmit'], + ); + + $form['autosubmit_hide'] = array( + '#type' => 'checkbox', + '#title' => t('Hide submit button'), + '#description' => t('Hide submit button if javascript is enabled.'), + '#default_value' => $this->options['autosubmit_hide'], + '#dependency' => array( + 'edit-exposed-form-options-autosubmit' => array(1), + ), + ); + } + + /** + * Render the exposed filter form. + * + * This actually does more than that; because it's using FAPI, the form will + * also assign data to the appropriate handlers for use in building the + * query. + */ + function render_exposed_form($block = FALSE) { + // Deal with any exposed filters we may have, before building. + $form_state = array( + 'view' => &$this->view, + 'display' => &$this->display, + 'method' => 'get', + 'rerender' => TRUE, + 'no_redirect' => TRUE, + 'always_process' => TRUE, + ); + + // Some types of displays (eg. attachments) may wish to use the exposed + // filters of their parent displays instead of showing an additional + // exposed filter form for the attachment as well as that for the parent. + if (!$this->view->display_handler->displays_exposed() || (!$block && $this->view->display_handler->get_option('exposed_block'))) { + unset($form_state['rerender']); + } + + if (!empty($this->ajax)) { + $form_state['ajax'] = TRUE; + } + + $form_state['exposed_form_plugin'] = $this; + $form = drupal_build_form('views_exposed_form', $form_state); + $output = drupal_render($form); + + if (!$this->view->display_handler->displays_exposed() || (!$block && $this->view->display_handler->get_option('exposed_block'))) { + return ""; + } + else { + return $output; + } + } + + function query() { + $view = $this->view; + $exposed_data = isset($view->exposed_data) ? $view->exposed_data : array(); + $sort_by = isset($exposed_data['sort_by']) ? $exposed_data['sort_by'] : NULL; + if (!empty($sort_by)) { + // Make sure the original order of sorts is preserved + // (e.g. a sticky sort is often first) + if (isset($view->sort[$sort_by])) { + $view->query->orderby = array(); + foreach ($view->sort as $key => $sort) { + if (!$sort->is_exposed()) { + $sort->query(); + } + else if ($key == $sort_by) { + if (isset($exposed_data['sort_order']) && in_array($exposed_data['sort_order'], array('ASC', 'DESC'))) { + $sort->options['order'] = $exposed_data['sort_order']; + } + $sort->set_relationship(); + $sort->query(); + } + } + } + } + } + + function pre_render($values) { } + + function post_render(&$output) { } + + function pre_execute() { } + + function post_execute() { } + + function exposed_form_alter(&$form, &$form_state) { + if (!empty($this->options['reset_button'])) { + $form['reset'] = array( + '#value' => $this->options['reset_button_label'], + '#type' => 'submit', + ); + } + + $form['submit']['#value'] = $this->options['submit_button']; + // Check if there is exposed sorts for this view + $exposed_sorts = array(); + foreach ($this->view->sort as $id => $handler) { + if ($handler->can_expose() && $handler->is_exposed()) { + $exposed_sorts[$id] = check_plain($handler->options['expose']['label']); + } + } + + if (count($exposed_sorts)) { + $form['sort_by'] = array( + '#type' => 'select', + '#options' => $exposed_sorts, + '#title' => $this->options['exposed_sorts_label'], + ); + $sort_order = array( + 'ASC' => $this->options['sort_asc_label'], + 'DESC' => $this->options['sort_desc_label'], + ); + if (isset($form_state['input']['sort_by']) && isset($this->view->sort[$form_state['input']['sort_by']])) { + $default_sort_order = $this->view->sort[$form_state['input']['sort_by']]->options['order']; + } else { + $first_sort = reset($this->view->sort); + $default_sort_order = $first_sort->options['order']; + } + + if (!isset($form_state['input']['sort_by'])) { + $keys = array_keys($exposed_sorts); + $form_state['input']['sort_by'] = array_shift($keys); + } + + if ($this->options['expose_sort_order']) { + $form['sort_order'] = array( + '#type' => 'select', + '#options' => $sort_order, + '#title' => t('Order'), + '#default_value' => $default_sort_order, + ); + } + $form['submit']['#weight'] = 10; + if (isset($form['reset'])) { + $form['reset']['#weight'] = 10; + } + } + + $pager = $this->view->display_handler->get_plugin('pager'); + if ($pager) { + $pager->exposed_form_alter($form, $form_state); + $form_state['pager_plugin'] = $pager; + } + + + // Apply autosubmit values. + if (!empty($this->options['autosubmit'])) { + $form = array_merge_recursive($form, array('#attributes' => array('class' => array('ctools-auto-submit-full-form')))); + $form['submit']['#attributes']['class'][] = 'ctools-use-ajax'; + $form['submit']['#attributes']['class'][] = 'ctools-auto-submit-click'; + $form['#attached']['js'][] = drupal_get_path('module', 'ctools') . '/js/auto-submit.js'; + + if (!empty($this->options['autosubmit_hide'])) { + $form['submit']['#attributes']['class'][] = 'js-hide'; + } + } + } + + function exposed_form_validate(&$form, &$form_state) { + if (isset($form_state['pager_plugin'])) { + $form_state['pager_plugin']->exposed_form_validate($form, $form_state); + } + } + + /** + * This function is executed when exposed form is submited. + * + * @param $form + * Nested array of form elements that comprise the form. + * @param $form_state + * A keyed array containing the current state of the form. + * @param $exclude + * Nested array of keys to exclude of insert into + * $view->exposed_raw_input + */ + function exposed_form_submit(&$form, &$form_state, &$exclude) { + if (!empty($form_state['values']['op']) && $form_state['values']['op'] == $this->options['reset_button_label']) { + $this->reset_form($form, $form_state); + } + if (isset($form_state['pager_plugin'])) { + $form_state['pager_plugin']->exposed_form_submit($form, $form_state, $exclude); + $exclude[] = 'pager_plugin'; + } + } + + function reset_form(&$form, &$form_state) { + // _SESSION is not defined for users who are not logged in. + + // If filters are not overridden, store the 'remember' settings on the + // default display. If they are, store them on this display. This way, + // multiple displays in the same view can share the same filters and + // remember settings. + $display_id = ($this->view->display_handler->is_defaulted('filters')) ? 'default' : $this->view->current_display; + + if (isset($_SESSION['views'][$this->view->name][$display_id])) { + unset($_SESSION['views'][$this->view->name][$display_id]); + } + + // Set the form to allow redirect. + if (empty($this->view->live_preview)) { + $form_state['no_redirect'] = FALSE; + } + else { + $form_state['rebuild'] = TRUE; + $this->view->exposed_data = array(); + } + + $form_state['redirect'] = current_path(); + $form_state['values'] = array(); + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_exposed_form_basic.inc b/plugins/views_plugin_exposed_form_basic.inc new file mode 100644 index 00000000..73ae54aa --- /dev/null +++ b/plugins/views_plugin_exposed_form_basic.inc @@ -0,0 +1,13 @@ + 'Select any filter and click on Apply to see results', 'translatable' => TRUE); + $options['text_input_required_format'] = array('default' => NULL); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['text_input_required'] = array( + '#type' => 'text_format', + '#title' => t('Text on demand'), + '#description' => t('Text to display instead of results until the user selects and applies an exposed filter.'), + '#default_value' => $this->options['text_input_required'], + '#format' => isset($this->options['text_input_required_format']) ? $this->options['text_input_required_format'] : filter_default_format(), + '#wysiwyg' => FALSE, + ); + } + + function options_submit(&$form, &$form_state) { + $form_state['values']['exposed_form_options']['text_input_required_format'] = $form_state['values']['exposed_form_options']['text_input_required']['format']; + $form_state['values']['exposed_form_options']['text_input_required'] = $form_state['values']['exposed_form_options']['text_input_required']['value']; + parent::options_submit($form, $form_state); + } + + function exposed_filter_applied() { + static $cache = NULL; + if (!isset($cache)) { + $view = $this->view; + if (is_array($view->filter) && count($view->filter)) { + foreach ($view->filter as $filter_id => $filter) { + if ($filter->is_exposed()) { + $identifier = $filter->options['expose']['identifier']; + if (isset($view->exposed_input[$identifier])) { + $cache = TRUE; + return $cache; + } + } + } + } + $cache = FALSE; + } + + return $cache; + } + + function pre_render($values) { + if (!$this->exposed_filter_applied()) { + $options = array( + 'id' => 'area', + 'table' => 'views', + 'field' => 'area', + 'label' => '', + 'relationship' => 'none', + 'group_type' => 'group', + 'content' => $this->options['text_input_required'], + 'format' => $this->options['text_input_required_format'], + ); + $handler = views_get_handler('views', 'area', 'area'); + $handler->init($this->view, $options); + $this->display->handler->handlers['empty'] = array( + 'area' => $handler, + ); + $this->display->handler->set_option('empty', array('text' => $options)); + } + } + + function query() { + if (!$this->exposed_filter_applied()) { + // We return with no query; this will force the empty text. + $this->view->built = TRUE; + $this->view->executed = TRUE; + $this->view->result = array(); + } + else { + parent::query(); + } + } + +} diff --git a/plugins/views_plugin_localization.inc b/plugins/views_plugin_localization.inc new file mode 100644 index 00000000..08caf9e0 --- /dev/null +++ b/plugins/views_plugin_localization.inc @@ -0,0 +1,171 @@ +view = &$view; + } + + /** + * Translate a string / text with format + * + * The $source parameter is an array with the following elements: + * - value, source string + * - format, input format in case the text has some format to be applied + * - keys. An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + * + * @param $source + * Full data for the string to be translated. + * + * @return string + * Translated string / text + */ + function translate($source) { + // Allow other modules to make changes to the string before and after translation + $source['pre_process'] = $this->invoke_translation_process($source, 'pre'); + $source['translation'] = $this->translate_string($source['value'], $source['keys'], $source['format']); + $source['post_process'] = $this->invoke_translation_process($source, 'post'); + return $source['translation']; + } + + /** + * Translate a string. + * + * @param $string + * The string to be translated. + * @param $keys + * An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + * @param $format + * The input format of the string. This is optional. + */ + function translate_string($string, $keys = array(), $format = '') {} + + /** + * Save string source for translation. + * + * @param $source + * Full data for the string to be translated. + */ + function save($source) { + // Allow other modules to make changes to the string before saving + $source['pre_process'] = $this->invoke_translation_process($source, 'pre'); + $this->save_string($source['value'], $source['keys'], isset($source['format']) ? $source['format'] : ''); + } + + /** + * Save a string for translation + * + * @param $string + * The string to be translated. + * @param $keys + * An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + * @param $format + * The input format of the string. This is optional. + */ + function save_string($string, $keys = array(), $format = '') {} + + /** + * Delete a string. + * + * @param $source + * Full data for the string to be translated. + */ + function delete($source) { } + + /** + * Collect strings to be exported to code. + * + * @param $source + * Full data for the string to be translated. + */ + function export($source) { } + + /** + * Render any collected exported strings to code. + * + * @param $indent + * An optional indentation for prettifying nested code. + */ + function export_render($indent = ' ') { } + + /** + * Invoke hook_translation_pre_process() or hook_translation_post_process(). + * + * Like node_invoke_nodeapi(), this function is needed to enable both passing + * by reference and fetching return values. + */ + function invoke_translation_process(&$value, $op) { + $return = array(); + $hook = 'translation_' . $op . '_process'; + foreach (module_implements($hook) as $module) { + $function = $module . '_' . $hook; + $result = $function($value); + if (isset($result)) { + $return[$module] = $result; + } + } + return $return; + } + + function process_locale_strings($op) { + $this->view->init_display(); + + foreach ($this->view->display as $display_id => $display) { + $translatable = array(); + // Special handling for display title. + if (isset($display->display_title)) { + $translatable[] = array('value' => $display->display_title, 'keys' => array('display_title')); + } + // Unpack handlers. + if (is_object($this->view->display[$display_id]->handler)) { + $this->view->display[$display_id]->handler->unpack_translatables($translatable); + } + foreach ($translatable as $data) { + $data['keys'] = array_merge(array($this->view->name, $display_id), $data['keys']); + switch ($op) { + case 'save': + $this->save($data); + break; + case 'delete': + $this->delete($data); + break; + case 'export': + $this->export($data); + break; + } + } + } + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_localization_core.inc b/plugins/views_plugin_localization_core.inc new file mode 100644 index 00000000..87443ca0 --- /dev/null +++ b/plugins/views_plugin_localization_core.inc @@ -0,0 +1,109 @@ +language == 'en') { + $changed = TRUE; + $languages = language_list(); + $cached_language = $language; + unset($languages['en']); + if (!empty($languages)) { + $language = current($languages); + } + } + + t($string); + + if (isset($cached_language)) { + $language = $cached_language; + } + return TRUE; + } + + /** + * Delete a string. + * + * Deletion is not supported. + * + * @param $source + * Full data for the string to be translated. + */ + function delete($source) { + return FALSE; + } + + /** + * Collect strings to be exported to code. + * + * String identifiers are not supported so strings are anonymously in an array. + * + * @param $source + * Full data for the string to be translated. + */ + function export($source) { + if (!empty($source['value'])) { + $this->export_strings[] = $source['value']; + } + } + + /** + * Render any collected exported strings to code. + * + * @param $indent + * An optional indentation for prettifying nested code. + */ + function export_render($indent = ' ') { + $output = ''; + if (!empty($this->export_strings)) { + $this->export_strings = array_unique($this->export_strings); + $output = $indent . '$translatables[\'' . $this->view->name . '\'] = array(' . "\n"; + foreach ($this->export_strings as $string) { + $output .= $indent . " t('" . str_replace("'", "\'", $string) . "'),\n"; + } + $output .= $indent . ");\n"; + } + return $output; + } +} diff --git a/plugins/views_plugin_localization_none.inc b/plugins/views_plugin_localization_none.inc new file mode 100644 index 00000000..620352a2 --- /dev/null +++ b/plugins/views_plugin_localization_none.inc @@ -0,0 +1,36 @@ +view = &$view; + $this->display = &$display; + + $this->unpack_options($this->options, $options); + } + + /** + * Get how many items per page this pager will display. + * + * All but the leanest pagers should probably return a value here, so + * most pagers will not need to override this method. + */ + function get_items_per_page() { + return isset($this->options['items_per_page']) ? $this->options['items_per_page'] : 0; + } + + /** + * Set how many items per page this pager will display. + * + * This is mostly used for things that will override the value. + */ + function set_items_per_page($items) { + $this->options['items_per_page'] = $items; + } + + /** + * Get the page offset, or how many items to skip. + * + * Even pagers that don't actually page can skip items at the beginning, + * so few pagers will need to override this method. + */ + function get_offset() { + return isset($this->options['offset']) ? $this->options['offset'] : 0; + } + + /** + * Set the page offset, or how many items to skip. + */ + function set_offset($offset) { + $this->options['offset'] = $offset; + } + + /** + * Get the current page. + * + * If NULL, we do not know what the current page is. + */ + function get_current_page() { + return $this->current_page; + } + + /** + * Set the current page. + * + * @param $number + * If provided, the page number will be set to this. If NOT provided, + * the page number will be set from the global page array. + */ + function set_current_page($number = NULL) { + if (!is_numeric($number) || $number < 0) { + $number = 0; + } + $this->current_page = $number; + } + + /** + * Get the total number of items. + * + * If NULL, we do not yet know what the total number of items are. + */ + function get_total_items() { + return $this->total_items; + } + + /** + * Get the pager id, if it exists + */ + function get_pager_id() { + return isset($this->options['id']) ? $this->options['id'] : 0; + } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * pager plugin. + */ + function summary_title() { + return t('Unknown'); + } + + /** + * Determine if this pager actually uses a pager. + * + * Only a couple of very specific pagers will set this to false. + */ + function use_pager() { + return TRUE; + } + + /** + * Determine if a pager needs a count query. + * + * If a pager needs a count query, a simple query + */ + function use_count_query() { + return TRUE; + } + + /** + * Execute the count query, which will be done just prior to the query + * itself being executed. + */ + function execute_count_query(&$count_query) { + $this->total_items = $count_query->execute()->fetchField(); + if (!empty($this->options['offset'])) { + $this->total_items -= $this->options['offset']; + } + + $this->update_page_info(); + return $this->total_items; + } + + /** + * If there are pagers that need global values set, this method can + * be used to set them. It will be called when the count query is run. + */ + function update_page_info() { + + } + + /** + * Modify the query for paging + * + * This is called during the build phase and can directly modify the query. + */ + function query() { } + + /** + * Perform any needed actions just prior to the query executing. + */ + function pre_execute(&$query) { } + + /** + * Perform any needed actions just after the query executing. + */ + function post_execute(&$result) { } + + /** + * Perform any needed actions just before rendering. + */ + function pre_render(&$result) { } + + /** + * Render the pager. + * + * Called during the view render process, this will render the + * pager. + * + * @param $input + * Any extra GET parameters that should be retained, such as exposed + * input. + */ + function render($input) { } + + /** + * Determine if there are more records available. + * + * This is primarily used to control the display of a more link. + */ + function has_more_records() { + return $this->get_items_per_page() + && $this->total_items > (intval($this->current_page) + 1) * $this->get_items_per_page(); + } + + function exposed_form_alter(&$form, &$form_state) { } + + function exposed_form_validate(&$form, &$form_state) { } + + function exposed_form_submit(&$form, &$form_state, &$exclude) { } + + function uses_exposed() { + return FALSE; + } + + function items_per_page_exposed() { + return FALSE; + } + + function offset_exposed() { + return FALSE; + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_pager_full.inc b/plugins/views_plugin_pager_full.inc new file mode 100644 index 00000000..d4c22038 --- /dev/null +++ b/plugins/views_plugin_pager_full.inc @@ -0,0 +1,422 @@ +options['offset'])) { + return format_plural($this->options['items_per_page'], '@count item, skip @skip', 'Paged, @count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], '@count item', 'Paged, @count items', array('@count' => $this->options['items_per_page'])); + } + + function option_definition() { + $options = parent::option_definition(); + $options['items_per_page'] = array('default' => 10); + $options['offset'] = array('default' => 0); + $options['id'] = array('default' => 0); + $options['total_pages'] = array('default' => ''); + // Use the same default quantity that core uses by default. + $options['quantity'] = array('default' => 9); + $options['expose'] = array( + 'contains' => array( + 'items_per_page' => array('default' => FALSE, 'bool' => TRUE), + 'items_per_page_label' => array('default' => 'Items per page', 'translatable' => TRUE), + 'items_per_page_options' => array('default' => '5, 10, 20, 40, 60'), + 'items_per_page_options_all' => array('default' => FALSE, 'bool' => TRUE), + 'items_per_page_options_all_label' => array('default' => '- All -', 'translatable' => TRUE), + + 'offset' => array('default' => FALSE, 'bool' => TRUE), + 'offset_label' => array('default' => 'Offset', 'translatable' => TRUE), + ), + ); + $options['tags'] = array( + 'contains' => array( + 'first' => array('default' => '« first', 'translatable' => TRUE), + 'previous' => array('default' => '‹ previous', 'translatable' => TRUE), + 'next' => array('default' => 'next ›', 'translatable' => TRUE), + 'last' => array('default' => 'last »', 'translatable' => TRUE), + ), + ); + return $options; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $pager_text = $this->display->handler->get_pager_text(); + $form['items_per_page'] = array( + '#title' => $pager_text['items per page title'], + '#type' => 'textfield', + '#description' => $pager_text['items per page description'], + '#default_value' => $this->options['items_per_page'], + ); + + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + + $form['id'] = array( + '#type' => 'textfield', + '#title' => t('Pager ID'), + '#description' => t("Unless you're experiencing problems with pagers related to this view, you should leave this at 0. If using multiple pagers on one page you may need to set this number to a higher value so as not to conflict within the ?page= array. Large values will add a lot of commas to your URLs, so avoid if possible."), + '#default_value' => $this->options['id'], + ); + + $form['total_pages'] = array( + '#type' => 'textfield', + '#title' => t('Number of pages'), + '#description' => t('The total number of pages. Leave empty to show all pages.'), + '#default_value' => $this->options['total_pages'], + ); + + $form['quantity'] = array( + '#type' => 'textfield', + '#title' => t('Number of pager links visible'), + '#description' => t('Specify the number of links to pages to display in the pager.'), + '#default_value' => $this->options['quantity'], + ); + + $form['tags'] = array ( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#tree' => TRUE, + '#title' => t('Tags'), + '#input' => TRUE, + '#description' => t('A lists of labels for the controls in the pager'), + ); + + $form['tags']['first'] = array( + '#type' => 'textfield', + '#title' => t('Text for "first"-link'), + '#description' => t('Text for "first"-link'), + '#default_value' => $this->options['tags']['first'], + ); + + $form['tags']['previous'] = array( + '#type' => 'textfield', + '#title' => t('Text for "previous"-link'), + '#description' => t('Text for "previous"-link'), + '#default_value' => $this->options['tags']['previous'], + ); + + $form['tags']['next'] = array( + '#type' => 'textfield', + '#title' => t('Text for "next"-link'), + '#description' => t('Text for "next"-link'), + '#default_value' => $this->options['tags']['next'], + ); + + $form['tags']['last'] = array( + '#type' => 'textfield', + '#title' => t('Text for "last"-link'), + '#description' => t('Text for "last"-link'), + '#default_value' => $this->options['tags']['last'], + ); + + $form['expose'] = array ( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#tree' => TRUE, + '#title' => t('Exposed options'), + '#input' => TRUE, + '#description' => t('Exposing this options allows users to define their values in a exposed form when view is displayed'), + ); + + $form['expose']['items_per_page'] = array( + '#type' => 'checkbox', + '#title' => t('Expose items per page'), + '#description' => t('When checked, users can determine how many items per page show in a view'), + '#default_value' => $this->options['expose']['items_per_page'], + ); + + $form['expose']['items_per_page_label'] = array( + '#type' => 'textfield', + '#title' => t('Items per page label'), + '#required' => TRUE, + '#description' => t('Label to use in the exposed items per page form element.'), + '#default_value' => $this->options['expose']['items_per_page_label'], + '#dependency' => array( + 'edit-pager-options-expose-items-per-page' => array(1) + ), + ); + + $form['expose']['items_per_page_options'] = array( + '#type' => 'textfield', + '#title' => t('Exposed items per page options'), + '#required' => TRUE, + '#description' => t('Set between which values the user can choose when determining the items per page. Separated by comma.'), + '#default_value' => $this->options['expose']['items_per_page_options'], + '#dependency' => array( + 'edit-pager-options-expose-items-per-page' => array(1) + ), + ); + + + $form['expose']['items_per_page_options_all'] = array( + '#type' => 'checkbox', + '#title' => t('Include all items option'), + '#description' => t('If checked, an extra item will be included to items per page to display all items'), + '#default_value' => $this->options['expose']['items_per_page_options_all'], + ); + + $form['expose']['items_per_page_options_all_label'] = array( + '#type' => 'textfield', + '#title' => t('All items label'), + '#description' => t('Which label will be used to display all items'), + '#default_value' => $this->options['expose']['items_per_page_options_all_label'], + '#dependency' => array( + 'edit-items-per-page-options-all' => array(1), + ), + ); + + $form['expose']['offset'] = array( + '#type' => 'checkbox', + '#title' => t('Expose Offset'), + '#description' => t('When checked, users can determine how many items should be skipped at the beginning.'), + '#default_value' => $this->options['expose']['offset'], + ); + + $form['expose']['offset_label'] = array( + '#type' => 'textfield', + '#title' => t('Offset label'), + '#required' => TRUE, + '#description' => t('Label to use in the exposed offset form element.'), + '#default_value' => $this->options['expose']['offset_label'], + '#dependency' => array( + 'edit-pager-options-expose-offset' => array(1) + ), + ); + } + + function options_validate(&$form, &$form_state) { + // Only accept integer values. + $error = FALSE; + $exposed_options = $form_state['values']['pager_options']['expose']['items_per_page_options']; + if (strpos($exposed_options, '.') !== FALSE) { + $error = TRUE; + } + $options = explode(',',$exposed_options); + if (!$error && is_array($options)) { + foreach ($options as $option) { + if (!is_numeric($option) || intval($option) == 0) { + $error = TRUE; + } + } + } + else { + $error = TRUE; + } + if ($error) { + form_set_error('pager_options][expose][items_per_page_options', t('Please insert a list of integer numeric values separated by commas: e.g: 10, 20, 50, 100')); + } + + // Take sure that the items_per_page is part of the expose settings. + if (!empty($form_state['values']['pager_options']['expose']['items_per_page']) && !empty($form_state['values']['pager_options']['items_per_page'])) { + $items_per_page = $form_state['values']['pager_options']['items_per_page']; + if (array_search($items_per_page, $options) === FALSE) { + form_set_error('pager_options][expose][items_per_page_options', t('Please insert the items per page (@items_per_page) from above.', + array('@items_per_page' => $items_per_page)) + ); + } + } + } + + function query() { + if ($this->items_per_page_exposed()) { + if (!empty($_GET['items_per_page']) && $_GET['items_per_page'] > 0) { + $this->options['items_per_page'] = $_GET['items_per_page']; + } + elseif (!empty($_GET['items_per_page']) && $_GET['items_per_page'] == 'All' && $this->options['expose']['items_per_page_options_all']) { + $this->options['items_per_page'] = 0; + } + } + if ($this->offset_exposed()) { + if (isset($_GET['offset']) && $_GET['offset'] >= 0) { + $this->options['offset'] = $_GET['offset']; + } + } + + $limit = $this->options['items_per_page']; + $offset = $this->current_page * $this->options['items_per_page'] + $this->options['offset']; + if (!empty($this->options['total_pages'])) { + if ($this->current_page >= $this->options['total_pages']) { + $limit = $this->options['items_per_page']; + $offset = $this->options['total_pages'] * $this->options['items_per_page']; + } + } + + $this->view->query->set_limit($limit); + $this->view->query->set_offset($offset); + } + + function render($input) { + $pager_theme = views_theme_functions('pager', $this->view, $this->display); + // The 0, 1, 3, 4 index are correct. See theme_pager documentation. + $tags = array( + 0 => $this->options['tags']['first'], + 1 => $this->options['tags']['previous'], + 3 => $this->options['tags']['next'], + 4 => $this->options['tags']['last'], + ); + $output = theme($pager_theme, array( + 'tags' => $tags, + 'element' => $this->options['id'], + 'parameters' => $input, + 'quantity' => $this->options['quantity'], + )); + return $output; + } + + /** + * Set the current page. + * + * @param $number + * If provided, the page number will be set to this. If NOT provided, + * the page number will be set from the global page array. + */ + function set_current_page($number = NULL) { + if (isset($number)) { + $this->current_page = $number; + return; + } + + // If the current page number was not specified, extract it from the global + // page array. + global $pager_page_array; + + if (empty($pager_page_array)) { + $pager_page_array = array(); + } + + // Fill in missing values in the global page array, in case the global page + // array hasn't been initialized before. + $page = isset($_GET['page']) ? explode(',', $_GET['page']) : array(); + + for ($i = 0; $i <= $this->options['id'] || $i < count($pager_page_array); $i++) { + $pager_page_array[$i] = empty($page[$i]) ? 0 : $page[$i]; + } + + $this->current_page = intval($pager_page_array[$this->options['id']]); + + if ($this->current_page < 0) { + $this->current_page = 0; + } + } + + function get_pager_total() { + if ($items_per_page = intval($this->get_items_per_page())) { + return ceil($this->total_items / $items_per_page); + } + else { + return 1; + } + } + + /** + * Update global paging info. + * + * This is called after the count query has been run to set the total + * items available and to update the current page if the requested + * page is out of range. + */ + function update_page_info() { + if (!empty($this->options['total_pages'])) { + if (($this->options['total_pages'] * $this->options['items_per_page']) < $this->total_items) { + $this->total_items = $this->options['total_pages'] * $this->options['items_per_page']; + } + } + + // Don't set pager settings for items per page = 0. + $items_per_page = $this->get_items_per_page(); + if (!empty($items_per_page)) { + // Dump information about what we already know into the globals. + global $pager_page_array, $pager_total, $pager_total_items, $pager_limits; + // Set the limit. + $pager_limits[$this->options['id']] = $this->options['items_per_page']; + // Set the item count for the pager. + $pager_total_items[$this->options['id']] = $this->total_items; + // Calculate and set the count of available pages. + $pager_total[$this->options['id']] = $this->get_pager_total(); + + // See if the requested page was within range: + if ($this->current_page < 0) { + $this->current_page = 0; + } + else if ($this->current_page >= $pager_total[$this->options['id']]) { + // Pages are numbered from 0 so if there are 10 pages, the last page is 9. + $this->current_page = $pager_total[$this->options['id']] - 1; + } + + // Put this number in to guarantee that we do not generate notices when the pager + // goes to look for it later. + $pager_page_array[$this->options['id']] = $this->current_page; + } + } + + function uses_exposed() { + return $this->items_per_page_exposed() || $this->offset_exposed(); + } + + function items_per_page_exposed() { + return !empty($this->options['expose']['items_per_page']); + } + + function offset_exposed() { + return !empty($this->options['expose']['offset']); + } + + function exposed_form_alter(&$form, &$form_state) { + if ($this->items_per_page_exposed()) { + $options = explode(',', $this->options['expose']['items_per_page_options']); + $sanitized_options = array(); + if (is_array($options)) { + foreach ($options as $option) { + $sanitized_options[intval($option)] = intval($option); + } + if (!empty($this->options['expose']['items_per_page_options_all']) && !empty($this->options['expose']['items_per_page_options_all_label'])) { + $sanitized_options['All'] = $this->options['expose']['items_per_page_options_all_label']; + } + $form['items_per_page'] = array( + '#type' => 'select', + '#title' => $this->options['expose']['items_per_page_label'], + '#options' => $sanitized_options, + '#default_value' => $this->get_items_per_page(), + ); + } + } + + if ($this->offset_exposed()) { + $form['offset'] = array( + '#type' => 'textfield', + '#size' => 10, + '#maxlength' => 10, + '#title' => $this->options['expose']['offset_label'], + '#default_value' => $this->get_offset(), + ); + } + } + + function exposed_form_validate(&$form, &$form_state) { + if (!empty($form_state['values']['offset']) && trim($form_state['values']['offset'])) { + if (!is_numeric($form_state['values']['offset']) || $form_state['values']['offset'] < 0) { + form_set_error('offset', t('Offset must be an number greather or equal than 0.')); + } + } + } +} diff --git a/plugins/views_plugin_pager_mini.inc b/plugins/views_plugin_pager_mini.inc new file mode 100644 index 00000000..bec48c89 --- /dev/null +++ b/plugins/views_plugin_pager_mini.inc @@ -0,0 +1,70 @@ +options['offset'])) { + return format_plural($this->options['items_per_page'], 'Mini pager, @count item, skip @skip', 'Mini pager, @count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], 'Mini pager, @count item', 'Mini pager, @count items', array('@count' => $this->options['items_per_page'])); + } + + /** + * Overrides views_plugin_pager_full::option_definition(). + * + * Overrides the full pager options form by deleting unused settings. + */ + function option_definition() { + $options = parent::option_definition(); + + unset($options['quantity']); + unset($options['tags']['first']); + unset($options['tags']['last']); + $options['tags']['previous']['default'] = '‹‹'; + $options['tags']['next']['default'] = '››'; + + return $options; + } + + /** + * Overrides views_plugin_pager_full::options_form(). + * + * Overrides the full pager options form by deleting unused settings. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + unset($form['quantity']); + unset($form['tags']['first']); + unset($form['tags']['last']); + } + + /** + * Overrides views_plugin_pager_full::render(). + * + * Overrides the full pager renderer by changing the theme function + * and leaving out variables that are not used in the mini pager. + */ + function render($input) { + $pager_theme = views_theme_functions('views_mini_pager', $this->view, $this->display); + // The 1, 3 index are correct. + // @see theme_pager(). + $tags = array( + 1 => $this->options['tags']['previous'], + 3 => $this->options['tags']['next'], + ); + return theme($pager_theme, array( + 'tags' => $tags, + 'element' => $this->options['id'], + 'parameters' => $input, + )); + } +} diff --git a/plugins/views_plugin_pager_none.inc b/plugins/views_plugin_pager_none.inc new file mode 100644 index 00000000..12b96d04 --- /dev/null +++ b/plugins/views_plugin_pager_none.inc @@ -0,0 +1,75 @@ +set_items_per_page(0); + } + + function summary_title() { + if (!empty($this->options['offset'])) { + return t('All items, skip @skip', array('@skip' => $this->options['offset'])); + } + return t('All items'); + } + + function option_definition() { + $options = parent::option_definition(); + $options['offset'] = array('default' => 0); + + return $options; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + } + + function use_pager() { + return FALSE; + } + + function use_count_query() { + return FALSE; + } + + function get_items_per_page() { + return 0; + } + + function execute_count_query(&$count_query) { + // If we are displaying all items, never count. But we can update the count in post_execute. + } + + function post_execute(&$result) { + $this->total_items = count($result); + } + + function query() { + // The only query modifications we might do are offsets. + if (!empty($this->options['offset'])) { + $this->view->query->set_offset($this->options['offset']); + } + } +} diff --git a/plugins/views_plugin_pager_some.inc b/plugins/views_plugin_pager_some.inc new file mode 100644 index 00000000..09452ce3 --- /dev/null +++ b/plugins/views_plugin_pager_some.inc @@ -0,0 +1,62 @@ +options['offset'])) { + return format_plural($this->options['items_per_page'], '@count item, skip @skip', '@count items, skip @skip', array('@count' => $this->options['items_per_page'], '@skip' => $this->options['offset'])); + } + return format_plural($this->options['items_per_page'], '@count item', '@count items', array('@count' => $this->options['items_per_page'])); + } + + function option_definition() { + $options = parent::option_definition(); + $options['items_per_page'] = array('default' => 10); + $options['offset'] = array('default' => 0); + + return $options; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $pager_text = $this->display->handler->get_pager_text(); + $form['items_per_page'] = array( + '#title' => $pager_text['items per page title'], + '#type' => 'textfield', + '#description' => $pager_text['items per page description'], + '#default_value' => $this->options['items_per_page'], + ); + + $form['offset'] = array( + '#type' => 'textfield', + '#title' => t('Offset'), + '#description' => t('The number of items to skip. For example, if this field is 3, the first 3 items will be skipped and not displayed.'), + '#default_value' => $this->options['offset'], + ); + } + + function use_pager() { + return FALSE; + } + + function use_count_query() { + return FALSE; + } + + function query() { + $this->view->query->set_limit($this->options['items_per_page']); + $this->view->query->set_offset($this->options['offset']); + } +} diff --git a/plugins/views_plugin_query.inc b/plugins/views_plugin_query.inc new file mode 100644 index 00000000..d39ed989 --- /dev/null +++ b/plugins/views_plugin_query.inc @@ -0,0 +1,185 @@ +base_table = $base_table; + $this->base_field = $base_field; + $this->unpack_options($this->options, $options); + } + + /** + * Generate a query and a countquery from all of the information supplied + * to the object. + * + * @param $get_count + * Provide a countquery if this is true, otherwise provide a normal query. + */ + function query($get_count = FALSE) { } + + /** + * Let modules modify the query just prior to finalizing it. + * + * @param view $view + * The view which is executed. + */ + function alter(&$view) { } + + /** + * Builds the necessary info to execute the query. + * + * @param view $view + * The view which is executed. + */ + function build(&$view) { } + + /** + * Executes the query and fills the associated view object with according + * values. + * + * Values to set: $view->result, $view->total_rows, $view->execute_time, + * $view->pager['current_page']. + * + * $view->result should contain an array of objects. The array must use a + * numeric index starting at 0. + * + * @param view $view + * The view which is executed. + */ + function execute(&$view) { } + + /** + * Add a signature to the query, if such a thing is feasible. + * + * This signature is something that can be used when perusing query logs to + * discern where particular queries might be coming from. + * + * @param view $view + * The view which is executed. + */ + function add_signature(&$view) { } + + /** + * Get aggregation info for group by queries. + * + * If NULL, aggregation is not allowed. + */ + function get_aggregation_info() { } + + /** + * Add settings for the ui. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + } + + function options_validate(&$form, &$form_state) { } + + function options_submit(&$form, &$form_state) { } + + function summary_title() { + return t('Settings'); + } + + /** + * Set a LIMIT on the query, specifying a maximum number of results. + */ + function set_limit($limit) { + $this->limit = $limit; + } + + /** + * Set an OFFSET on the query, specifying a number of results to skip + */ + function set_offset($offset) { + $this->offset = $offset; + } + + /** + * Render the pager, if necessary. + */ + function render_pager($exposed_input) { + if (!empty($this->pager) && $this->pager->use_pager()) { + return $this->pager->render($exposed_input); + } + + return ''; + } + + /** + * Create a new grouping for the WHERE or HAVING clause. + * + * @param $type + * Either 'AND' or 'OR'. All items within this group will be added + * to the WHERE clause with this logical operator. + * @param $group + * An ID to use for this group. If unspecified, an ID will be generated. + * @param $where + * 'where' or 'having'. + * + * @return $group + * The group ID generated. + */ + function set_where_group($type = 'AND', $group = NULL, $where = 'where') { + // Set an alias. + $groups = &$this->$where; + + if (!isset($group)) { + $group = empty($groups) ? 1 : max(array_keys($groups)) + 1; + } + + // Create an empty group + if (empty($groups[$group])) { + $groups[$group] = array('conditions' => array(), 'args' => array()); + } + + $groups[$group]['type'] = strtoupper($type); + return $group; + } + + /** + * Control how all WHERE and HAVING groups are put together. + * + * @param $type + * Either 'AND' or 'OR' + */ + function set_group_operator($type = 'AND') { + $this->group_operator = strtoupper($type); + } + + /** + * Returns the according entity objects for the given query results. + */ + function get_result_entities($results, $relationship = NULL) { + return FALSE; + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_query_default.inc b/plugins/views_plugin_query_default.inc new file mode 100644 index 00000000..030c5ea0 --- /dev/null +++ b/plugins/views_plugin_query_default.inc @@ -0,0 +1,1657 @@ +base_table = $base_table; // Predefine these above, for clarity. + $this->base_field = $base_field; + $this->relationships[$base_table] = array( + 'link' => NULL, + 'table' => $base_table, + 'alias' => $base_table, + 'base' => $base_table + ); + + // init the table queue with our primary table. + $this->table_queue[$base_table] = array( + 'alias' => $base_table, + 'table' => $base_table, + 'relationship' => $base_table, + 'join' => NULL, + ); + + // init the tables with our primary table + $this->tables[$base_table][$base_table] = array( + 'count' => 1, + 'alias' => $base_table, + ); + +/** + * -- we no longer want the base field to appear automatigically. + if ($base_field) { + $this->fields[$base_field] = array( + 'table' => $base_table, + 'field' => $base_field, + 'alias' => $base_field, + ); + } + */ + + $this->count_field = array( + 'table' => $base_table, + 'field' => $base_field, + 'alias' => $base_field, + 'count' => TRUE, + ); + } + + // ---------------------------------------------------------------- + // Utility methods to set flags and data. + + /** + * Set the view to be distinct. + * + * There are either distinct per base field or distinct in the pure sql way, + * based on $pure_distinct. + * + * @param bool $value + * Should the view by distincted. + * @param bool $pure_distinct + * Should only the sql keyword be added. + */ + function set_distinct($value = TRUE, $pure_distinct = FALSE) { + if (!(isset($this->no_distinct) && $value)) { + $this->distinct = $value; + $this->pure_distinct = $pure_distinct; + } + } + + /** + * Set what field the query will count() on for paging. + */ + function set_count_field($table, $field, $alias = NULL) { + if (empty($alias)) { + $alias = $table . '_' . $field; + } + $this->count_field = array( + 'table' => $table, + 'field' => $field, + 'alias' => $alias, + 'count' => TRUE, + ); + } + + /** + * Set the table header; used for click-sorting because it's needed + * info to modify the ORDER BY clause. + */ + function set_header($header) { + $this->header = $header; + } + + function option_definition() { + $options = parent::option_definition(); + $options['disable_sql_rewrite'] = array( + 'default' => FALSE, + 'translatable' => FALSE, + 'bool' => TRUE, + ); + $options['distinct'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + $options['pure_distinct'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + $options['slave'] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + $options['query_comment'] = array( + 'default' => '', + ); + $options['query_tags'] = array( + 'default' => array(), + ); + + return $options; + } + + /** + * Add settings for the ui. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['disable_sql_rewrite'] = array( + '#title' => t('Disable SQL rewriting'), + '#description' => t('Disabling SQL rewriting will disable node_access checks as well as other modules that implement hook_query_alter().'), + '#type' => 'checkbox', + '#default_value' => !empty($this->options['disable_sql_rewrite']), + '#suffix' => '
    ' . t('WARNING: Disabling SQL rewriting means that node access security is disabled. This may allow users to see data they should not be able to see if your view is misconfigured. Please use this option only if you understand and accept this security risk.') . '
    ', + ); + $form['distinct'] = array( + '#type' => 'checkbox', + '#title' => t('Distinct'), + '#description' => t('This will make the view display only distinct items. If there are multiple identical items, each will be displayed only once. You can use this to try and remove duplicates from a view, though it does not always work. Note that this can slow queries down, so use it with caution.'), + '#default_value' => !empty($this->options['distinct']), + ); + $form['pure_distinct'] = array( + '#type' => 'checkbox', + '#title' => t('Pure Distinct'), + '#description' => t('This will prevent views from adding the base column to the distinct field. If this is not selected and the base column is a primary key, then a non-pure distinct will not function properly because the primary key is always unique.'), + '#default_value' => !empty($this->options['pure_distinct']), + '#dependency' => array('edit-query-options-distinct' => '1'), + ); + $form['slave'] = array( + '#type' => 'checkbox', + '#title' => t('Use Slave Server'), + '#description' => t('This will make the query attempt to connect to a slave server if available. If no slave server is defined or available, it will fall back to the default server.'), + '#default_value' => !empty($this->options['slave']), + ); + $form['query_comment'] = array( + '#type' => 'textfield', + '#title' => t('Query Comment'), + '#description' => t('If set, this comment will be embedded in the query and passed to the SQL server. This can be helpful for logging or debugging.'), + '#default_value' => $this->options['query_comment'], + ); + $form['query_tags'] = array( + '#type' => 'textfield', + '#title' => t('Query Tags'), + '#description' => t('If set, these tags will be appended to the query and can be used to identify the query in a module. This can be helpful for altering queries.'), + '#default_value' => implode(', ', $this->options['query_tags']), + '#element_validate' => array('views_element_validate_tags'), + ); + } + + /** + * Special submit handling. + */ + function options_submit(&$form, &$form_state) { + $element = array('#parents' => array('query', 'options', 'query_tags')); + $value = explode(',', drupal_array_get_nested_value($form_state['values'], $element['#parents'])); + $value = array_filter(array_map('trim', $value)); + form_set_value($element, $value, $form_state); + } + + // ---------------------------------------------------------------- + // Table/join adding + + /** + * A relationship is an alternative endpoint to a series of table + * joins. Relationships must be aliases of the primary table and + * they must join either to the primary table or to a pre-existing + * relationship. + * + * An example of a relationship would be a nodereference table. + * If you have a nodereference named 'book_parent' which links to a + * parent node, you could set up a relationship 'node_book_parent' + * to 'node'. Then, anything that links to 'node' can link to + * 'node_book_parent' instead, thus allowing all properties of + * both nodes to be available in the query. + * + * @param $alias + * What this relationship will be called, and is also the alias + * for the table. + * @param views_join $join + * A views_join object (or derived object) to join the alias in. + * @param $base + * The name of the 'base' table this relationship represents; this + * tells the join search which path to attempt to use when finding + * the path to this relationship. + * @param $link_point + * If this relationship links to something other than the primary + * table, specify that table here. For example, a 'track' node + * might have a relationship to an 'album' node, which might + * have a relationship to an 'artist' node. + */ + function add_relationship($alias, $join, $base, $link_point = NULL) { + if (empty($link_point)) { + $link_point = $this->base_table; + } + elseif (!array_key_exists($link_point, $this->relationships)) { + return FALSE; + } + + // Make sure $alias isn't already used; if it, start adding stuff. + $alias_base = $alias; + $count = 1; + while (!empty($this->relationships[$alias])) { + $alias = $alias_base . '_' . $count++; + } + + // Make sure this join is adjusted for our relationship. + if ($link_point && isset($this->relationships[$link_point])) { + $join = $this->adjust_join($join, $link_point); + } + + // Add the table directly to the queue to avoid accidentally marking + // it. + $this->table_queue[$alias] = array( + 'table' => $join->table, + 'num' => 1, + 'alias' => $alias, + 'join' => $join, + 'relationship' => $link_point, + ); + + $this->relationships[$alias] = array( + 'link' => $link_point, + 'table' => $join->table, + 'base' => $base, + ); + + $this->tables[$this->base_table][$alias] = array( + 'count' => 1, + 'alias' => $alias, + ); + + return $alias; + } + + /** + * Add a table to the query, ensuring the path exists. + * + * This function will test to ensure that the path back to the primary + * table is valid and exists; if you do not wish for this testing to + * occur, use $query->queue_table() instead. + * + * @param $table + * The name of the table to add. It needs to exist in the global table + * array. + * @param $relationship + * An alias of a table; if this is set, the path back to this table will + * be tested prior to adding the table, making sure that all intermediary + * tables exist and are properly aliased. If set to NULL the path to + * the primary table will be ensured. If the path cannot be made, the + * table will NOT be added. + * @param views_join $join + * In some join configurations this table may actually join back through + * a different method; this is most likely to be used when tracing + * a hierarchy path. (node->parent->parent2->parent3). This parameter + * will specify how this table joins if it is not the default. + * @param $alias + * A specific alias to use, rather than the default alias. + * + * @return $alias + * The alias of the table; this alias can be used to access information + * about the table and should always be used to refer to the table when + * adding parts to the query. Or FALSE if the table was not able to be + * added. + */ + function add_table($table, $relationship = NULL, $join = NULL, $alias = NULL) { + if (!$this->ensure_path($table, $relationship, $join)) { + return FALSE; + } + + if ($join && $relationship) { + $join = $this->adjust_join($join, $relationship); + } + + return $this->queue_table($table, $relationship, $join, $alias); + } + + /** + * Add a table to the query without ensuring the path. + * + * This is a pretty internal function to Views and add_table() or + * ensure_table() should be used instead of this one, unless you are + * absolutely sure this is what you want. + * + * @param $table + * The name of the table to add. It needs to exist in the global table + * array. + * @param $relationship + * The primary table alias this table is related to. If not set, the + * primary table will be used. + * @param views_join $join + * In some join configurations this table may actually join back through + * a different method; this is most likely to be used when tracing + * a hierarchy path. (node->parent->parent2->parent3). This parameter + * will specify how this table joins if it is not the default. + * @param $alias + * A specific alias to use, rather than the default alias. + * + * @return $alias + * The alias of the table; this alias can be used to access information + * about the table and should always be used to refer to the table when + * adding parts to the query. Or FALSE if the table was not able to be + * added. + */ + function queue_table($table, $relationship = NULL, $join = NULL, $alias = NULL) { + // If the alias is set, make sure it doesn't already exist. + if (isset($this->table_queue[$alias])) { + return $alias; + } + + if (empty($relationship)) { + $relationship = $this->base_table; + } + + if (!array_key_exists($relationship, $this->relationships)) { + return FALSE; + } + + if (!$alias && $join && $relationship && !empty($join->adjusted) && $table != $join->table) { + if ($relationship == $this->base_table) { + $alias = $table; + } + else { + $alias = $relationship . '_' . $table; + } + } + + // Check this again to make sure we don't blow up existing aliases for already + // adjusted joins. + if (isset($this->table_queue[$alias])) { + return $alias; + } + + $alias = $this->mark_table($table, $relationship, $alias); + + // If no alias is specified, give it the default. + if (!isset($alias)) { + $alias = $this->tables[$relationship][$table]['alias'] . $this->tables[$relationship][$table]['count']; + } + + // If this is a relationship based table, add a marker with + // the relationship as a primary table for the alias. + if ($table != $alias) { + $this->mark_table($alias, $this->base_table, $alias); + } + + // If no join is specified, pull it from the table data. + if (!isset($join)) { + $join = $this->get_join_data($table, $this->relationships[$relationship]['base']); + if (empty($join)) { + return FALSE; + } + + $join = $this->adjust_join($join, $relationship); + } + + $this->table_queue[$alias] = array( + 'table' => $table, + 'num' => $this->tables[$relationship][$table]['count'], + 'alias' => $alias, + 'join' => $join, + 'relationship' => $relationship, + ); + + return $alias; + } + + function mark_table($table, $relationship, $alias) { + // Mark that this table has been added. + if (empty($this->tables[$relationship][$table])) { + if (!isset($alias)) { + $alias = ''; + if ($relationship != $this->base_table) { + // double underscore will help prevent accidental name + // space collisions. + $alias = $relationship . '__'; + } + $alias .= $table; + } + $this->tables[$relationship][$table] = array( + 'count' => 1, + 'alias' => $alias, + ); + } + else { + $this->tables[$relationship][$table]['count']++; + } + + return $alias; + } + + /** + * Ensure a table exists in the queue; if it already exists it won't + * do anything, but if it doesn't it will add the table queue. It will ensure + * a path leads back to the relationship table. + * + * @param $table + * The unaliased name of the table to ensure. + * @param $relationship + * The relationship to ensure the table links to. Each relationship will + * get a unique instance of the table being added. If not specified, + * will be the primary table. + * @param views_join $join + * A views_join object (or derived object) to join the alias in. + * + * @return + * The alias used to refer to this specific table, or NULL if the table + * cannot be ensured. + */ + function ensure_table($table, $relationship = NULL, $join = NULL) { + // ensure a relationship + if (empty($relationship)) { + $relationship = $this->base_table; + } + + // If the relationship is the primary table, this actually be a relationship + // link back from an alias. We store all aliases along with the primary table + // to detect this state, because eventually it'll hit a table we already + // have and that's when we want to stop. + if ($relationship == $this->base_table && !empty($this->tables[$relationship][$table])) { + return $this->tables[$relationship][$table]['alias']; + } + + if (!array_key_exists($relationship, $this->relationships)) { + return FALSE; + } + + if ($table == $this->relationships[$relationship]['base']) { + return $relationship; + } + + // If we do not have join info, fetch it. + if (!isset($join)) { + $join = $this->get_join_data($table, $this->relationships[$relationship]['base']); + } + + // If it can't be fetched, this won't work. + if (empty($join)) { + return; + } + + // Adjust this join for the relationship, which will ensure that the 'base' + // table it links to is correct. Tables adjoined to a relationship + // join to a link point, not the base table. + $join = $this->adjust_join($join, $relationship); + + if ($this->ensure_path($table, $relationship, $join)) { + // Attempt to eliminate redundant joins. If this table's + // relationship and join exactly matches an existing table's + // relationship and join, we do not have to join to it again; + // just return the existing table's alias. See + // http://groups.drupal.org/node/11288 for details. + // + // This can be done safely here but not lower down in + // queue_table(), because queue_table() is also used by + // add_table() which requires the ability to intentionally add + // the same table with the same join multiple times. For + // example, a view that filters on 3 taxonomy terms using AND + // needs to join taxonomy_term_data 3 times with the same join. + + // scan through the table queue to see if a matching join and + // relationship exists. If so, use it instead of this join. + + // TODO: Scanning through $this->table_queue results in an + // O(N^2) algorithm, and this code runs every time the view is + // instantiated (Views 2 does not currently cache queries). + // There are a couple possible "improvements" but we should do + // some performance testing before picking one. + foreach ($this->table_queue as $queued_table) { + // In PHP 4 and 5, the == operation returns TRUE for two objects + // if they are instances of the same class and have the same + // attributes and values. + if ($queued_table['relationship'] == $relationship && $queued_table['join'] == $join) { + return $queued_table['alias']; + } + } + + return $this->queue_table($table, $relationship, $join); + } + } + + /** + * Make sure that the specified table can be properly linked to the primary + * table in the JOINs. This function uses recursion. If the tables + * needed to complete the path back to the primary table are not in the + * query they will be added, but additional copies will NOT be added + * if the table is already there. + */ + function ensure_path($table, $relationship = NULL, $join = NULL, $traced = array(), $add = array()) { + if (!isset($relationship)) { + $relationship = $this->base_table; + } + + if (!array_key_exists($relationship, $this->relationships)) { + return FALSE; + } + + // If we do not have join info, fetch it. + if (!isset($join)) { + $join = $this->get_join_data($table, $this->relationships[$relationship]['base']); + } + + // If it can't be fetched, this won't work. + if (empty($join)) { + return FALSE; + } + + // Does a table along this path exist? + if (isset($this->tables[$relationship][$table]) || + ($join && $join->left_table == $relationship) || + ($join && $join->left_table == $this->relationships[$relationship]['table'])) { + + // Make sure that we're linking to the correct table for our relationship. + foreach (array_reverse($add) as $table => $path_join) { + $this->queue_table($table, $relationship, $this->adjust_join($path_join, $relationship)); + } + return TRUE; + } + + // Have we been this way? + if (isset($traced[$join->left_table])) { + // we looped. Broked. + return FALSE; + } + + // Do we have to add this table? + $left_join = $this->get_join_data($join->left_table, $this->relationships[$relationship]['base']); + if (!isset($this->tables[$relationship][$join->left_table])) { + $add[$join->left_table] = $left_join; + } + + // Keep looking. + $traced[$join->left_table] = TRUE; + return $this->ensure_path($join->left_table, $relationship, $left_join, $traced, $add); + } + + /** + * Fix a join to adhere to the proper relationship; the left table can vary + * based upon what relationship items are joined in on. + */ + function adjust_join($join, $relationship) { + if (!empty($join->adjusted)) { + return $join; + } + + if (empty($relationship) || empty($this->relationships[$relationship])) { + return $join; + } + + // Adjusts the left table for our relationship. + if ($relationship != $this->base_table) { + // If we're linking to the primary table, the relationship to use will + // be the prior relationship. Unless it's a direct link. + + // Safety! Don't modify an original here. + $join = clone $join; + + // Do we need to try to ensure a path? + if ($join->left_table != $this->relationships[$relationship]['table'] && + $join->left_table != $this->relationships[$relationship]['base'] && + !isset($this->tables[$relationship][$join->left_table]['alias'])) { + $this->ensure_table($join->left_table, $relationship); + } + + // First, if this is our link point/anchor table, just use the relationship + if ($join->left_table == $this->relationships[$relationship]['table']) { + $join->left_table = $relationship; + } + // then, try the base alias. + elseif (isset($this->tables[$relationship][$join->left_table]['alias'])) { + $join->left_table = $this->tables[$relationship][$join->left_table]['alias']; + } + // But if we're already looking at an alias, use that instead. + elseif (isset($this->table_queue[$relationship]['alias'])) { + $join->left_table = $this->table_queue[$relationship]['alias']; + } + } + + $join->adjusted = TRUE; + return $join; + } + + /** + * Retrieve join data from the larger join data cache. + * + * @param $table + * The table to get the join information for. + * @param $base_table + * The path we're following to get this join. + * + * @return views_join + * A views_join object or child object, if one exists. + */ + function get_join_data($table, $base_table) { + // Check to see if we're linking to a known alias. If so, get the real + // table's data instead. + if (!empty($this->table_queue[$table])) { + $table = $this->table_queue[$table]['table']; + } + return views_get_table_join($table, $base_table); + } + + /** + * Get the information associated with a table. + * + * If you need the alias of a table with a particular relationship, use + * ensure_table(). + */ + function get_table_info($table) { + if (!empty($this->table_queue[$table])) { + return $this->table_queue[$table]; + } + + // In rare cases we might *only* have aliased versions of the table. + if (!empty($this->tables[$this->base_table][$table])) { + $alias = $this->tables[$this->base_table][$table]['alias']; + if (!empty($this->table_queue[$alias])) { + return $this->table_queue[$alias]; + } + } + } + + /** + * Add a field to the query table, possibly with an alias. This will + * automatically call ensure_table to make sure the required table + * exists, *unless* $table is unset. + * + * @param $table + * The table this field is attached to. If NULL, it is assumed this will + * be a formula; otherwise, ensure_table is used to make sure the + * table exists. + * @param $field + * The name of the field to add. This may be a real field or a formula. + * @param $alias + * The alias to create. If not specified, the alias will be $table_$field + * unless $table is NULL. When adding formulae, it is recommended that an + * alias be used. + * @param $params + * An array of parameters additional to the field that will control items + * such as aggregation functions and DISTINCT. + * + * @return $name + * The name that this field can be referred to as. Usually this is the alias. + */ + function add_field($table, $field, $alias = '', $params = array()) { + // We check for this specifically because it gets a special alias. + if ($table == $this->base_table && $field == $this->base_field && empty($alias)) { + $alias = $this->base_field; + } + + if ($table && empty($this->table_queue[$table])) { + $this->ensure_table($table); + } + + if (!$alias && $table) { + $alias = $table . '_' . $field; + } + + // Make sure an alias is assigned + $alias = $alias ? $alias : $field; + + // PostgreSQL truncates aliases to 63 characters: http://drupal.org/node/571548 + + // We limit the length of the original alias up to 60 characters + // to get a unique alias later if its have duplicates + $alias = strtolower(substr($alias, 0, 60)); + + // Create a field info array. + $field_info = array( + 'field' => $field, + 'table' => $table, + 'alias' => $alias, + ) + $params; + + // Test to see if the field is actually the same or not. Due to + // differing parameters changing the aggregation function, we need + // to do some automatic alias collision detection: + $base = $alias; + $counter = 0; + while (!empty($this->fields[$alias]) && $this->fields[$alias] != $field_info) { + $field_info['alias'] = $alias = $base . '_' . ++$counter; + } + + if (empty($this->fields[$alias])) { + $this->fields[$alias] = $field_info; + } + + // Keep track of all aliases used. + $this->field_aliases[$table][$field] = $alias; + + return $alias; + } + + /** + * Remove all fields that may've been added; primarily used for summary + * mode where we're changing the query because we didn't get data we needed. + */ + function clear_fields() { + $this->fields = array(); + } + + /** + * Add a simple WHERE clause to the query. The caller is responsible for + * ensuring that all fields are fully qualified (TABLE.FIELD) and that + * the table already exists in the query. + * + * @param $group + * The WHERE group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $field + * The name of the field to check. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For more + * complex options, it is an array. The meaning of each element in the array is + * dependent on the $operator. + * @param $operator + * The comparison operator, such as =, <, or >=. It also accepts more complex + * options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is an array + * = otherwise. If $field is a string you have to use 'formula' here. + * + * The $field, $value and $operator arguments can also be passed in with a + * single DatabaseCondition object, like this: + * @code + * $this->query->add_where( + * $this->options['group'], + * db_or() + * ->condition($field, $value, 'NOT IN') + * ->condition($field, $value, 'IS NULL') + * ); + * @endcode + * + * @see QueryConditionInterface::condition() + * @see DatabaseCondition + */ + function add_where($group, $field, $value = NULL, $operator = NULL) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->where[$group])) { + $this->set_where_group('AND', $group); + } + + $this->where[$group]['conditions'][] = array( + 'field' => $field, + 'value' => $value, + 'operator' => $operator, + ); + } + + /** + * Add a complex WHERE clause to the query. + * + * The caller is reponsible for ensuring that all fields are fully qualified + * (TABLE.FIELD) and that the table already exists in the query. + * Internally the dbtng method "where" is used. + * + * @param $group + * The WHERE group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $snippet + * The snippet to check. This can be either a column or + * a complex expression like "UPPER(table.field) = 'value'" + * @param $args + * An associative array of arguments. + * + * @see QueryConditionInterface::where() + */ + function add_where_expression($group, $snippet, $args = array()) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->where[$group])) { + $this->set_where_group('AND', $group); + } + + $this->where[$group]['conditions'][] = array( + 'field' => $snippet, + 'value' => $args, + 'operator' => 'formula', + ); + } + + /** + * Add a simple HAVING clause to the query. + * + * The caller is responsible for ensuring that all fields are fully qualified + * (TABLE.FIELD) and that the table and an appropriate GROUP BY already exist in the query. + * Internally the dbtng method "havingCondition" is used. + * + * @param $group + * The HAVING group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $field + * The name of the field to check. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For more + * complex options, it is an array. The meaning of each element in the array is + * dependent on the $operator. + * @param $operator + * The comparison operator, such as =, <, or >=. It also accepts more complex + * options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is an array + * = otherwise. If $field is a string you have to use 'formula' here. + * + * @see SelectQueryInterface::havingCondition() + */ + function add_having($group, $field, $value = NULL, $operator = NULL) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->having[$group])) { + $this->set_where_group('AND', $group, 'having'); + } + + // Add the clause and the args. + $this->having[$group]['conditions'][] = array( + 'field' => $field, + 'value' => $value, + 'operator' => $operator, + ); + } + + /** + * Add a complex HAVING clause to the query. + * The caller is responsible for ensuring that all fields are fully qualified + * (TABLE.FIELD) and that the table and an appropriate GROUP BY already exist in the query. + * Internally the dbtng method "having" is used. + * + * @param $group + * The HAVING group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param $snippet + * The snippet to check. This can be either a column or + * a complex expression like "COUNT(table.field) > 3" + * @param $args + * An associative array of arguments. + * + * @see QueryConditionInterface::having() + */ + function add_having_expression($group, $snippet, $args = array()) { + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + + // Check for a group. + if (!isset($this->having[$group])) { + $this->set_where_group('AND', $group, 'having'); + } + + // Add the clause and the args. + $this->having[$group]['conditions'][] = array( + 'field' => $snippet, + 'value' => $args, + 'operator' => 'formula', + ); + } + + /** + * Add an ORDER BY clause to the query. + * + * @param $table + * The table this field is part of. If a formula, enter NULL. + * If you want to orderby random use "rand" as table and nothing else. + * @param $field + * The field or formula to sort on. If already a field, enter NULL + * and put in the alias. + * @param $order + * Either ASC or DESC. + * @param $alias + * The alias to add the field as. In SQL, all fields in the order by + * must also be in the SELECT portion. If an $alias isn't specified + * one will be generated for from the $field; however, if the + * $field is a formula, this alias will likely fail. + * @param $params + * Any params that should be passed through to the add_field. + */ + function add_orderby($table, $field = NULL, $order = 'ASC', $alias = '', $params = array()) { + // Only ensure the table if it's not the special random key. + // @todo: Maybe it would make sense to just add a add_orderby_rand or something similar. + if ($table && $table != 'rand') { + $this->ensure_table($table); + } + + // Only fill out this aliasing if there is a table; + // otherwise we assume it is a formula. + if (!$alias && $table) { + $as = $table . '_' . $field; + } + else { + $as = $alias; + } + + if ($field) { + $as = $this->add_field($table, $field, $as, $params); + } + + $this->orderby[] = array( + 'field' => $as, + 'direction' => strtoupper($order) + ); + + /** + * -- removing, this should be taken care of by field adding now. + * -- leaving commented because I am unsure. + // If grouping, all items in the order by must also be in the + // group by clause. Check $table to ensure that this is not a + // formula. + if ($this->groupby && $table) { + $this->add_groupby($as); + } + */ + } + + /** + * Add a simple GROUP BY clause to the query. The caller is responsible + * for ensuring that the fields are fully qualified and the table is properly + * added. + */ + function add_groupby($clause) { + // Only add it if it's not already in there. + if (!in_array($clause, $this->groupby)) { + $this->groupby[] = $clause; + } + } + + /** + * Returns the alias for the given field added to $table. + * + * @see views_plugin_query_default::add_field() + */ + function get_field_alias($table_alias, $field) { + return isset($this->field_aliases[$table_alias][$field]) ? $this->field_aliases[$table_alias][$field] : FALSE; + } + + /** + * Adds a query tag to the sql object. + * + * @see SelectQuery::addTag() + */ + function add_tag($tag) { + $this->tags[] = $tag; + } + + /** + * Generates a unique placeholder used in the db query. + */ + function placeholder($base = 'views') { + static $placeholders = array(); + if (!isset($placeholders[$base])) { + $placeholders[$base] = 0; + return ':' . $base; + } + else { + return ':' . $base . ++$placeholders[$base]; + } + } + + /** + * Construct the "WHERE" or "HAVING" part of the query. + * + * As views has to wrap the conditions from arguments with AND, a special + * group is wrapped around all conditions. This special group has the ID 0. + * There is other code in filters which makes sure that the group IDs are + * higher than zero. + * + * @param $where + * 'where' or 'having'. + */ + function build_condition($where = 'where') { + $has_condition = FALSE; + $has_arguments = FALSE; + $has_filter = FALSE; + + $main_group = db_and(); + $filter_group = $this->group_operator == 'OR' ? db_or() : db_and(); + + foreach ($this->$where as $group => $info) { + + if (!empty($info['conditions'])) { + $sub_group = $info['type'] == 'OR' ? db_or() : db_and(); + foreach ($info['conditions'] as $key => $clause) { + // DBTNG doesn't support to add the same subquery twice to the main + // query and the count query, so clone the subquery to have two instances + // of the same object. - http://drupal.org/node/1112854 + if (is_object($clause['value']) && $clause['value'] instanceof SelectQuery) { + $clause['value'] = clone $clause['value']; + } + if ($clause['operator'] == 'formula') { + $has_condition = TRUE; + $sub_group->where($clause['field'], $clause['value']); + } + else { + $has_condition = TRUE; + $sub_group->condition($clause['field'], $clause['value'], $clause['operator']); + } + } + + // Add the item to the filter group. + if ($group != 0) { + $has_filter = TRUE; + $filter_group->condition($sub_group); + } + else { + $has_arguments = TRUE; + $main_group->condition($sub_group); + } + } + } + + if ($has_filter) { + $main_group->condition($filter_group); + } + + if (!$has_arguments && $has_condition) { + return $filter_group; + } + if ($has_arguments && $has_condition) { + return $main_group; + } + } + + /** + * Build fields array. + */ + function compile_fields($fields_array, $query) { + $non_aggregates = array(); + foreach ($fields_array as $field) { + $string = ''; + if (!empty($field['table'])) { + $string .= $field['table'] . '.'; + } + $string .= $field['field']; + $fieldname = (!empty($field['alias']) ? $field['alias'] : $string); + + if (!empty($field['distinct'])) { + throw new Exception("Column-level distinct is not supported anymore."); + } + + if (!empty($field['count'])) { + // Retained for compatibility + $field['function'] = 'count'; + // It seems there's no way to abstract the table+column reference + // without adding a field, aliasing, and then using the alias. + } + + if (!empty($field['function'])) { + $info = $this->get_aggregation_info(); + if (!empty($info[$field['function']]['method']) && function_exists($info[$field['function']]['method'])) { + $string = $info[$field['function']]['method']($field['function'], $string); + $placeholders = !empty($field['placeholders']) ? $field['placeholders'] : array(); + $query->addExpression($string, $fieldname, $placeholders); + } + + $this->has_aggregate = TRUE; + } + // This is a formula, using no tables. + elseif (empty($field['table'])) { + $non_aggregates[] = $fieldname; + $placeholders = !empty($field['placeholders']) ? $field['placeholders'] : array(); + $query->addExpression($string, $fieldname, $placeholders); + } + + elseif ($this->distinct && !in_array($fieldname, $this->groupby)) { + // d7cx: This code was there, apparently needed for PostgreSQL + // $string = db_driver() == 'pgsql' ? "FIRST($string)" : $string; + $query->addField(!empty($field['table']) ? $field['table'] : $this->base_table, $field['field'], $fieldname); + } + elseif (empty($field['aggregate'])) { + $non_aggregates[] = $fieldname; + $query->addField(!empty($field['table']) ? $field['table'] : $this->base_table, $field['field'], $fieldname); + } + + // @TODO Remove this old code. + if (!empty($field['distinct']) && empty($field['function'])) { + $distinct[] = $string; + } + else { + $fields[] = $string; + } + + if ($this->get_count_optimized) { + // We only want the first field in this case. + break; + } + } + return array( + $non_aggregates, + ); + } + + /** + * Generate a query and a countquery from all of the information supplied + * to the object. + * + * @param $get_count + * Provide a countquery if this is true, otherwise provide a normal query. + */ + function query($get_count = FALSE) { + // Check query distinct value. + if (empty($this->no_distinct) && $this->distinct && !empty($this->fields)) { + if ($this->pure_distinct === FALSE){ + $base_field_alias = $this->add_field($this->base_table, $this->base_field); + $this->add_groupby($base_field_alias); + } + $distinct = TRUE; + } + + /** + * An optimized count query includes just the base field instead of all the fields. + * Determine of this query qualifies by checking for a groupby or distinct. + */ + $fields_array = $this->fields; + if ($get_count && !$this->groupby) { + foreach ($fields_array as $field) { + if (!empty($field['distinct']) || !empty($field['function'])) { + $this->get_count_optimized = FALSE; + break; + } + } + } + else { + $this->get_count_optimized = FALSE; + } + if (!isset($this->get_count_optimized)) { + $this->get_count_optimized = TRUE; + } + + $options = array(); + $target = 'default'; + $key = 'default'; + // Detect an external database and set the + if (isset($this->view->base_database)) { + $key = $this->view->base_database; + } + + // Set the slave target if the slave option is set + if (!empty($this->options['slave'])) { + $target = 'slave'; + } + + // Go ahead and build the query. + // db_select doesn't support to specify the key, so use getConnection directly. + $query = Database::getConnection($target, $key) + ->select($this->base_table, $this->base_table, $options) + ->addTag('views') + ->addTag('views_' . $this->view->name); + + // Add the tags added to the view itself. + foreach ($this->tags as $tag) { + $query->addTag($tag); + } + + if (!empty($distinct)) { + $query->distinct(); + } + + $joins = $where = $having = $orderby = $groupby = ''; + $fields = $distinct = array(); + + // Add all the tables to the query via joins. We assume all LEFT joins. + foreach ($this->table_queue as $table) { + if (is_object($table['join'])) { + $table['join']->build_join($query, $table, $this); + } + } + + $this->has_aggregate = FALSE; + $non_aggregates = array(); + + list($non_aggregates) = $this->compile_fields($fields_array, $query); + + if (count($this->having)) { + $this->has_aggregate = TRUE; + } + if ($this->has_aggregate && (!empty($this->groupby) || !empty($non_aggregates))) { + $groupby = array_unique(array_merge($this->groupby, $non_aggregates)); + foreach ($groupby as $field) { + $query->groupBy($field); + } + if (!empty($this->having) && $condition = $this->build_condition('having')) { + $query->havingCondition($condition); + } + } + + if (!$this->get_count_optimized) { + // we only add the orderby if we're not counting. + if ($this->orderby) { + foreach ($this->orderby as $order) { + if ($order['field'] == 'rand_') { + $query->orderRandom(); + } + else { + $query->orderBy($order['field'], $order['direction']); + } + } + } + } + + if (!empty($this->where) && $condition = $this->build_condition('where')) { + $query->condition($condition); + } + + // Add a query comment. + if (!empty($this->options['query_comment'])) { + $query->comment($this->options['query_comment']); + } + + // Add the query tags. + if (!empty($this->options['query_tags'])) { + foreach ($this->options['query_tags'] as $tag) { + $query->addTag($tag); + } + } + + // Add all query substitutions as metadata. + $query->addMetaData('views_substitutions', module_invoke_all('views_query_substitutions', $this)); + + return $query; + } + + /** + * Get the arguments attached to the WHERE and HAVING clauses of this query. + */ + function get_where_args() { + $args = array(); + foreach ($this->where as $group => $where) { + $args = array_merge($args, $where['args']); + } + foreach ($this->having as $group => $having) { + $args = array_merge($args, $having['args']); + } + return $args; + } + + /** + * Let modules modify the query just prior to finalizing it. + */ + function alter(&$view) { + foreach (module_implements('views_query_alter') as $module) { + $function = $module . '_views_query_alter'; + $function($view, $this); + } + } + + /** + * Builds the necessary info to execute the query. + */ + function build(&$view) { + // Make the query distinct if the option was set. + if (!empty($this->options['distinct'])) { + $this->set_distinct(TRUE, !empty($this->options['pure_distinct'])); + } + + // Store the view in the object to be able to use it later. + $this->view = $view; + + $view->init_pager(); + + // Let the pager modify the query to add limits. + $this->pager->query(); + + $view->build_info['query'] = $this->query(); + $view->build_info['count_query'] = $this->query(TRUE); + } + + /** + * Executes the query and fills the associated view object with according + * values. + * + * Values to set: $view->result, $view->total_rows, $view->execute_time, + * $view->current_page. + */ + function execute(&$view) { + $external = FALSE; // Whether this query will run against an external database. + $query = $view->build_info['query']; + $count_query = $view->build_info['count_query']; + + $query->addMetaData('view', $view); + $count_query->addMetaData('view', $view); + + if (empty($this->options['disable_sql_rewrite'])) { + $base_table_data = views_fetch_data($this->base_table); + if (isset($base_table_data['table']['base']['access query tag'])) { + $access_tag = $base_table_data['table']['base']['access query tag']; + $query->addTag($access_tag); + $count_query->addTag($access_tag); + } + } + + $items = array(); + if ($query) { + $additional_arguments = module_invoke_all('views_query_substitutions', $view); + + // Count queries must be run through the preExecute() method. + // If not, then hook_query_node_access_alter() may munge the count by + // adding a distinct against an empty query string + // (e.g. COUNT DISTINCT(1) ...) and no pager will return. + // See pager.inc > PagerDefault::execute() + // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7 + // See http://drupal.org/node/1046170. + $count_query->preExecute(); + + // Build the count query. + $count_query = $count_query->countQuery(); + + // Add additional arguments as a fake condition. + // XXX: this doesn't work... because PDO mandates that all bound arguments + // are used on the query. TODO: Find a better way to do this. + if (!empty($additional_arguments)) { + // $query->where('1 = 1', $additional_arguments); + // $count_query->where('1 = 1', $additional_arguments); + } + + $start = microtime(TRUE); + + + try { + if ($this->pager->use_count_query() || !empty($view->get_total_rows)) { + $this->pager->execute_count_query($count_query); + } + + // Let the pager modify the query to add limits. + $this->pager->pre_execute($query); + + if (!empty($this->limit) || !empty($this->offset)) { + // We can't have an offset without a limit, so provide a very large limit instead. + $limit = intval(!empty($this->limit) ? $this->limit : 999999); + $offset = intval(!empty($this->offset) ? $this->offset : 0); + $query->range($offset, $limit); + } + + $result = $query->execute(); + + $view->result = array(); + foreach ($result as $item) { + $view->result[] = $item; + } + + $this->pager->post_execute($view->result); + + if ($this->pager->use_count_query() || !empty($view->get_total_rows)) { + $view->total_rows = $this->pager->get_total_items(); + } + } + catch (Exception $e) { + $view->result = array(); + if (!empty($view->live_preview)) { + drupal_set_message($e->getMessage(), 'error'); + } + else { + vpr('Exception in @human_name[@view_name]: @message', array('@human_name' => $view->human_name, '@view_name' => $view->name, '@message' => $e->getMessage())); + } + } + + } + else { + $start = microtime(TRUE); + } + $view->execute_time = microtime(TRUE) - $start; + } + + function add_signature(&$view) { + $view->query->add_field(NULL, "'" . $view->name . ':' . $view->current_display . "'", 'view_name'); + } + + function get_aggregation_info() { + // @todo -- need a way to get database specific and customized aggregation + // functions into here. + return array( + 'group' => array( + 'title' => t('Group results together'), + 'is aggregate' => FALSE, + ), + 'count' => array( + 'title' => t('Count'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'field' => 'views_handler_field_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'count_distinct' => array( + 'title' => t('Count DISTINCT'), + 'method' => 'views_query_default_aggregation_method_distinct', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'field' => 'views_handler_field_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'sum' => array( + 'title' => t('Sum'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'avg' => array( + 'title' => t('Average'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'min' => array( + 'title' => t('Minimum'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'max' => array( + 'title' => t('Maximum'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ), + 'stddev_pop' => array( + 'title' => t('Standard deviation'), + 'method' => 'views_query_default_aggregation_method_simple', + 'handler' => array( + 'argument' => 'views_handler_argument_group_by_numeric', + 'filter' => 'views_handler_filter_group_by_numeric', + 'sort' => 'views_handler_sort_group_by_numeric', + ), + ) + ); + } + + /** + * Returns the according entity objects for the given query results. + * + */ + function get_result_entities($results, $relationship = NULL) { + $base_table = $this->base_table; + $base_table_alias = $base_table; + + if (!empty($relationship)) { + foreach ($this->view->relationship as $current) { + if ($current->alias == $relationship) { + $base_table = $current->definition['base']; + $base_table_alias = $relationship; + break; + } + } + } + $table_data = views_fetch_data($base_table); + + // Bail out if the table has not specified the according entity-type. + if (!isset($table_data['table']['entity type'])) { + return FALSE; + } + $entity_type = $table_data['table']['entity type']; + $info = entity_get_info($entity_type); + $id_alias = $this->get_field_alias($base_table_alias, $info['entity keys']['id']); + + // Assemble the ids of the entities to load. + $ids = array(); + foreach ($results as $key => $result) { + if (isset($result->$id_alias)) { + $ids[$key] = $result->$id_alias; + } + } + + $entities = entity_load($entity_type, $ids); + // Re-key the array by row-index. + $result = array(); + foreach ($ids as $key => $id) { + $result[$key] = isset($entities[$id]) ? $entities[$id] : FALSE; + } + return array($entity_type, $result); + } +} + +function views_query_default_aggregation_method_simple($group_type, $field) { + return strtoupper($group_type) . '(' . $field . ')'; +} + +function views_query_default_aggregation_method_distinct($group_type, $field) { + $group_type = str_replace('_distinct', '', $group_type); + return strtoupper($group_type) . '(DISTINCT ' . $field . ')'; +} + +/** + * Validation callback for query tags. + */ +function views_element_validate_tags($element, &$form_state) { + $values = array_map('trim', explode(',', $element['#value'])); + foreach ($values as $value) { + if (preg_match("/[^a-z_]/", $value)) { + form_error($element, t('The query tags may only contain lower-case alphabetical characters and underscores.')); + return; + } + } +} diff --git a/plugins/views_plugin_row.inc b/plugins/views_plugin_row.inc new file mode 100644 index 00000000..157cc26d --- /dev/null +++ b/plugins/views_plugin_row.inc @@ -0,0 +1,152 @@ +view = &$view; + $this->display = &$display; + + // Overlay incoming options on top of defaults + $this->unpack_options($this->options, isset($options) ? $options : $display->handler->get_option('row_options')); + } + + function uses_fields() { + return !empty($this->definition['uses fields']); + } + + + function option_definition() { + $options = parent::option_definition(); + if (isset($this->base_table)) { + $options['relationship'] = array('default' => 'none'); + } + + return $options; + } + + /** + * Provide a form for setting options. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + if (isset($this->base_table)) { + $view = &$form_state['view']; + + // A whole bunch of code to figure out what relationships are valid for + // this item. + $relationships = $view->display_handler->get_option('relationships'); + $relationship_options = array(); + + foreach ($relationships as $relationship) { + $relationship_handler = views_get_handler($relationship['table'], $relationship['field'], 'relationship'); + + // If this relationship is valid for this type, add it to the list. + $data = views_fetch_data($relationship['table']); + $base = $data[$relationship['field']]['relationship']['base']; + if ($base == $this->base_table) { + $relationship_handler->init($view, $relationship); + $relationship_options[$relationship['id']] = $relationship_handler->label(); + } + } + + if (!empty($relationship_options)) { + $relationship_options = array_merge(array('none' => t('Do not use a relationship')), $relationship_options); + $rel = empty($this->options['relationship']) ? 'none' : $this->options['relationship']; + if (empty($relationship_options[$rel])) { + // Pick the first relationship. + $rel = key($relationship_options); + } + + $form['relationship'] = array( + '#type' => 'select', + '#title' => t('Relationship'), + '#options' => $relationship_options, + '#default_value' => $rel, + ); + } + else { + $form['relationship'] = array( + '#type' => 'value', + '#value' => 'none', + ); + } + } + } + + /** + * Validate the options form. + */ + function options_validate(&$form, &$form_state) { } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function options_submit(&$form, &$form_state) { } + + function query() { + if (isset($this->base_table)) { + if (isset($this->options['relationship']) && isset($this->view->relationship[$this->options['relationship']])) { + $relationship = $this->view->relationship[$this->options['relationship']]; + $this->field_alias = $this->view->query->add_field($relationship->alias, $this->base_field); + } + else { + $this->field_alias = $this->view->query->add_field($this->base_table, $this->base_field); + } + } + } + + /** + * Allow the style to do stuff before each row is rendered. + * + * @param $result + * The full array of results from the query. + */ + function pre_render($result) { } + + /** + * Render a row object. This usually passes through to a theme template + * of some form, but not always. + * + * @param stdClass $row + * A single row of the query result, so an element of $view->result. + * + * @return string + * The rendered output of a single row, used by the style plugin. + */ + function render($row) { + return theme($this->theme_functions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $row, + 'field_alias' => isset($this->field_alias) ? $this->field_alias : '', + )); + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_row_fields.inc b/plugins/views_plugin_row_fields.inc new file mode 100644 index 00000000..b1c02e1f --- /dev/null +++ b/plugins/views_plugin_row_fields.inc @@ -0,0 +1,86 @@ + array()); + $options['separator'] = array('default' => ''); + $options['hide_empty'] = array('default' => FALSE, 'bool' => TRUE); + $options['default_field_elements'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + /** + * Provide a form for setting options. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $options = $this->display->handler->get_field_labels(); + + if (empty($this->options['inline'])) { + $this->options['inline'] = array(); + } + + $form['default_field_elements'] = array( + '#type' => 'checkbox', + '#title' => t('Provide default field wrapper elements'), + '#default_value' => $this->options['default_field_elements'], + '#description' => t('If not checked, fields that are not configured to customize their HTML elements will get no wrappers at all for their field, label and field + label wrappers. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.'), + ); + + $form['inline'] = array( + '#type' => 'checkboxes', + '#title' => t('Inline fields'), + '#options' => $options, + '#default_value' => $this->options['inline'], + '#description' => t('Inline fields will be displayed next to each other rather than one after another. Note that some fields will ignore this if they are block elements, particularly body fields and other formatted HTML.'), + '#dependency' => array( + 'edit-row-options-default-field-elements' => array(1), + ), + '#prefix' => '
    ', + '#suffix' => '
    ', + + ); + + $form['separator'] = array( + '#title' => t('Separator'), + '#type' => 'textfield', + '#size' => 10, + '#default_value' => isset($this->options['separator']) ? $this->options['separator'] : '', + '#description' => t('The separator may be placed between inline fields to keep them from squishing up next to each other. You can use HTML in this field.'), + '#dependency' => array( + 'edit-row-options-default-field-elements' => array(1), + ), + ); + + $form['hide_empty'] = array( + '#type' => 'checkbox', + '#title' => t('Hide empty fields'), + '#default_value' => $this->options['hide_empty'], + '#description' => t('Do not display fields, labels or markup for fields that are empty.'), + ); + + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function options_submit(&$form, &$form_state) { + $form_state['values']['row_options']['inline'] = array_filter($form_state['values']['row_options']['inline']); + } +} diff --git a/plugins/views_plugin_row_rss_fields.inc b/plugins/views_plugin_row_rss_fields.inc new file mode 100644 index 00000000..9355e833 --- /dev/null +++ b/plugins/views_plugin_row_rss_fields.inc @@ -0,0 +1,180 @@ + ''); + $options['link_field'] = array('default' => ''); + $options['description_field'] = array('default' => ''); + $options['creator_field'] = array('default' => ''); + $options['date_field'] = array('default' => ''); + $options['guid_field_options']['guid_field'] = array('default' => ''); + $options['guid_field_options']['guid_field_is_permalink'] = array('default' => TRUE, 'bool' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $initial_labels = array('' => t('- None -')); + $view_fields_labels = $this->display->handler->get_field_labels(); + $view_fields_labels = array_merge($initial_labels, $view_fields_labels); + + $form['title_field'] = array( + '#type' => 'select', + '#title' => t('Title field'), + '#description' => t('The field that is going to be used as the RSS item title for each row.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['title_field'], + '#required' => TRUE, + ); + $form['link_field'] = array( + '#type' => 'select', + '#title' => t('Link field'), + '#description' => t('The field that is going to be used as the RSS item link for each row. This must be a drupal relative path.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['link_field'], + '#required' => TRUE, + ); + $form['description_field'] = array( + '#type' => 'select', + '#title' => t('Description field'), + '#description' => t('The field that is going to be used as the RSS item description for each row.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['description_field'], + '#required' => TRUE, + ); + $form['creator_field'] = array( + '#type' => 'select', + '#title' => t('Creator field'), + '#description' => t('The field that is going to be used as the RSS item creator for each row.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['creator_field'], + '#required' => TRUE, + ); + $form['date_field'] = array( + '#type' => 'select', + '#title' => t('Publication date field'), + '#description' => t('The field that is going to be used as the RSS item pubDate for each row. It needs to be in RFC 2822 format.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['date_field'], + '#required' => TRUE, + ); + $form['guid_field_options'] = array( + '#type' => 'fieldset', + '#title' => t('GUID settings'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + $form['guid_field_options']['guid_field'] = array( + '#type' => 'select', + '#title' => t('GUID field'), + '#description' => t('The globally unique identifier of the RSS item.'), + '#options' => $view_fields_labels, + '#default_value' => $this->options['guid_field_options']['guid_field'], + '#required' => TRUE, + ); + $form['guid_field_options']['guid_field_is_permalink'] = array( + '#type' => 'checkbox', + '#title' => t('GUID is permalink'), + '#description' => t('The RSS item GUID is a permalink.'), + '#default_value' => $this->options['guid_field_options']['guid_field_is_permalink'], + ); + } + + function validate() { + $errors = parent::validate(); + $required_options = array('title_field', 'link_field', 'description_field', 'creator_field', 'date_field'); + foreach ($required_options as $required_option) { + if (empty($this->options[$required_option])) { + $errors[] = t('Row style plugin requires specifying which views fields to use for RSS item.'); + break; + } + } + // Once more for guid. + if (empty($this->options['guid_field_options']['guid_field'])) { + $errors[] = t('Row style plugin requires specifying which views fields to use for RSS item.'); + } + return $errors; + } + + function render($row) { + static $row_index; + if (!isset($row_index)) { + $row_index = 0; + } + if (function_exists('rdf_get_namespaces')) { + // Merge RDF namespaces in the XML namespaces in case they are used + // further in the RSS content. + $xml_rdf_namespaces = array(); + foreach (rdf_get_namespaces() as $prefix => $uri) { + $xml_rdf_namespaces['xmlns:' . $prefix] = $uri; + } + $this->view->style_plugin->namespaces += $xml_rdf_namespaces; + } + + // Create the RSS item object. + $item = new stdClass(); + $item->title = $this->get_field($row_index, $this->options['title_field']); + $item->link = url($this->get_field($row_index, $this->options['link_field']), array('absolute' => TRUE)); + $item->description = $this->get_field($row_index, $this->options['description_field']); + $item->elements = array( + array('key' => 'pubDate', 'value' => $this->get_field($row_index, $this->options['date_field'])), + array( + 'key' => 'dc:creator', + 'value' => $this->get_field($row_index, $this->options['creator_field']), + 'namespace' => array('xmlns:dc' => 'http://purl.org/dc/elements/1.1/'), + ), + ); + $guid_is_permalink_string = 'false'; + $item_guid = $this->get_field($row_index, $this->options['guid_field_options']['guid_field']); + if ($this->options['guid_field_options']['guid_field_is_permalink']) { + $guid_is_permalink_string = 'true'; + $item_guid = url($item_guid, array('absolute' => TRUE)); + } + $item->elements[] = array( + 'key' => 'guid', + 'value' => $item_guid, + 'attributes' => array('isPermaLink' => $guid_is_permalink_string), + ); + + $row_index++; + + foreach ($item->elements as $element) { + if (isset($element['namespace'])) { + $this->view->style_plugin->namespaces = array_merge($this->view->style_plugin->namespaces, $element['namespace']); + } + } + + return theme($this->theme_functions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'row' => $item, + 'field_alias' => isset($this->field_alias) ? $this->field_alias : '', + )); + } + + /** + * Retrieves a views field value from the style plugin. + * + * @param $index + * The index count of the row as expected by views_plugin_style::get_field(). + * @param $field_id + * The ID assigned to the required field in the display. + */ + function get_field($index, $field_id) { + if (empty($this->view->style_plugin) || !is_object($this->view->style_plugin) || empty($field_id)) { + return ''; + } + return $this->view->style_plugin->get_field($index, $field_id); + } + +} diff --git a/plugins/views_plugin_style.inc b/plugins/views_plugin_style.inc new file mode 100644 index 00000000..d355e1bc --- /dev/null +++ b/plugins/views_plugin_style.inc @@ -0,0 +1,598 @@ +view = &$view; + $this->display = &$display; + + // Overlay incoming options on top of defaults + $this->unpack_options($this->options, isset($options) ? $options : $display->handler->get_option('style_options')); + + if ($this->uses_row_plugin() && $display->handler->get_option('row_plugin')) { + $this->row_plugin = $display->handler->get_plugin('row'); + } + + $this->options += array( + 'grouping' => array(), + ); + + $this->definition += array( + 'uses grouping' => TRUE, + ); + } + + function destroy() { + parent::destroy(); + + if (isset($this->row_plugin)) { + $this->row_plugin->destroy(); + } + } + + /** + * Return TRUE if this style also uses a row plugin. + */ + function uses_row_plugin() { + return !empty($this->definition['uses row plugin']); + } + + /** + * Return TRUE if this style also uses a row plugin. + */ + function uses_row_class() { + return !empty($this->definition['uses row class']); + } + + /** + * Return TRUE if this style also uses fields. + * + * @return bool + */ + function uses_fields() { + // If we use a row plugin, ask the row plugin. Chances are, we don't + // care, it does. + $row_uses_fields = FALSE; + if ($this->uses_row_plugin() && !empty($this->row_plugin)) { + $row_uses_fields = $this->row_plugin->uses_fields(); + } + // Otherwise, check the definition or the option. + return $row_uses_fields || !empty($this->definition['uses fields']) || !empty($this->options['uses_fields']); + } + + /** + * Return TRUE if this style uses tokens. + * + * Used to ensure we don't fetch tokens when not needed for performance. + */ + function uses_tokens() { + if ($this->uses_row_class()) { + $class = $this->options['row_class']; + if (strpos($class, '[') !== FALSE || strpos($class, '!') !== FALSE || strpos($class, '%') !== FALSE) { + return TRUE; + } + } + } + + /** + * Return the token replaced row class for the specified row. + */ + function get_row_class($row_index) { + if ($this->uses_row_class()) { + $class = $this->options['row_class']; + if ($this->uses_fields() && $this->view->field) { + $class = strip_tags($this->tokenize_value($class, $row_index)); + } + + $classes = explode(' ', $class); + foreach ($classes as &$class) { + $class = drupal_clean_css_identifier($class); + } + return implode(' ', $classes); + } + } + + /** + * Take a value and apply token replacement logic to it. + */ + function tokenize_value($value, $row_index) { + if (strpos($value, '[') !== FALSE || strpos($value, '!') !== FALSE || strpos($value, '%') !== FALSE) { + $fake_item = array( + 'alter_text' => TRUE, + 'text' => $value, + ); + + // Row tokens might be empty, for example for node row style. + $tokens = isset($this->row_tokens[$row_index]) ? $this->row_tokens[$row_index] : array(); + if (!empty($this->view->build_info['substitutions'])) { + $tokens += $this->view->build_info['substitutions']; + } + + if ($tokens) { + $value = strtr($value, $tokens); + } + } + + return $value; + } + + /** + * Should the output of the style plugin be rendered even if it's a empty view. + */ + function even_empty() { + return !empty($this->definition['even empty']); + } + + function option_definition() { + $options = parent::option_definition(); + $options['grouping'] = array('default' => array()); + if ($this->uses_row_class()) { + $options['row_class'] = array('default' => ''); + $options['default_row_class'] = array('default' => TRUE, 'bool' => TRUE); + $options['row_class_special'] = array('default' => TRUE, 'bool' => TRUE); + } + $options['uses_fields'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + // Only fields-based views can handle grouping. Style plugins can also exclude + // themselves from being groupable by setting their "use grouping" definiton + // key to FALSE. + // @TODO: Document "uses grouping" in docs.php when docs.php is written. + if ($this->uses_fields() && $this->definition['uses grouping']) { + $options = array('' => t('- None -')); + $field_labels = $this->display->handler->get_field_labels(TRUE); + $options += $field_labels; + // If there are no fields, we can't group on them. + if (count($options) > 1) { + // This is for backward compability, when there was just a single select form. + if (is_string($this->options['grouping'])) { + $grouping = $this->options['grouping']; + $this->options['grouping'] = array(); + $this->options['grouping'][0]['field'] = $grouping; + } + if (isset($this->options['group_rendered']) && is_string($this->options['group_rendered'])) { + $this->options['grouping'][0]['rendered'] = $this->options['group_rendered']; + unset($this->options['group_rendered']); + } + + $c = count($this->options['grouping']); + // Add a form for every grouping, plus one. + for ($i = 0; $i <= $c; $i++) { + $grouping = !empty($this->options['grouping'][$i]) ? $this->options['grouping'][$i] : array(); + $grouping += array('field' => '', 'rendered' => TRUE, 'rendered_strip' => FALSE); + $form['grouping'][$i]['field'] = array( + '#type' => 'select', + '#title' => t('Grouping field Nr.@number', array('@number' => $i + 1)), + '#options' => $options, + '#default_value' => $grouping['field'], + '#description' => t('You may optionally specify a field by which to group the records. Leave blank to not group.'), + ); + $form['grouping'][$i]['rendered'] = array( + '#type' => 'checkbox', + '#title' => t('Use rendered output to group rows'), + '#default_value' => $grouping['rendered'], + '#description' => t('If enabled the rendered output of the grouping field is used to group the rows.'), + '#dependency' => array( + 'edit-style-options-grouping-' . $i . '-field' => array_keys($field_labels), + ) + ); + $form['grouping'][$i]['rendered_strip'] = array( + '#type' => 'checkbox', + '#title' => t('Remove tags from rendered output'), + '#default_value' => $grouping['rendered_strip'], + '#dependency' => array( + 'edit-style-options-grouping-' . $i . '-field' => array_keys($field_labels), + ) + ); + } + } + } + + if ($this->uses_row_class()) { + $form['row_class'] = array( + '#title' => t('Row class'), + '#description' => t('The class to provide on each row.'), + '#type' => 'textfield', + '#default_value' => $this->options['row_class'], + ); + + if ($this->uses_fields()) { + $form['row_class']['#description'] .= ' ' . t('You may use field tokens from as per the "Replacement patterns" used in "Rewrite the output of this field" for all fields.'); + } + + $form['default_row_class'] = array( + '#title' => t('Add views row classes'), + '#description' => t('Add the default row classes like views-row-1 to the output. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.'), + '#type' => 'checkbox', + '#default_value' => $this->options['default_row_class'], + ); + $form['row_class_special'] = array( + '#title' => t('Add striping (odd/even), first/last row classes'), + '#description' => t('Add css classes to the first and last line, as well as odd/even classes for striping.'), + '#type' => 'checkbox', + '#default_value' => $this->options['row_class_special'], + ); + } + + if (!$this->uses_fields() || !empty($this->options['uses_fields'])) { + $form['uses_fields'] = array( + '#type' => 'checkbox', + '#title' => t('Force using fields'), + '#description' => t('If neither the row nor the style plugin supports fields, this field allows to enable them, so you can for example use groupby.'), + '#default_value' => $this->options['uses_fields'], + ); + } + } + + function options_validate(&$form, &$form_state) { + // Don't run validation on style plugins without the grouping setting. + if (isset($form_state['values']['style_options']['grouping'])) { + // Don't save grouping if no field is specified. + foreach ($form_state['values']['style_options']['grouping'] as $index => $grouping) { + if (empty($grouping['field'])) { + unset($form_state['values']['style_options']['grouping'][$index]); + } + } + } + } + + /** + * Called by the view builder to see if this style handler wants to + * interfere with the sorts. If so it should build; if it returns + * any non-TRUE value, normal sorting will NOT be added to the query. + */ + function build_sort() { return TRUE; } + + /** + * Called by the view builder to let the style build a second set of + * sorts that will come after any other sorts in the view. + */ + function build_sort_post() { } + + /** + * Allow the style to do stuff before each row is rendered. + * + * @param $result + * The full array of results from the query. + */ + function pre_render($result) { + if (!empty($this->row_plugin)) { + $this->row_plugin->pre_render($result); + } + } + + /** + * Render the display in this style. + */ + function render() { + if ($this->uses_row_plugin() && empty($this->row_plugin)) { + debug('views_plugin_style_default: Missing row plugin'); + return; + } + + // Group the rows according to the grouping instructions, if specified. + $sets = $this->render_grouping( + $this->view->result, + $this->options['grouping'], + TRUE + ); + + return $this->render_grouping_sets($sets); + } + + /** + * Render the grouping sets. + * + * Plugins may override this method if they wish some other way of handling + * grouping. + * + * @param $sets + * Array containing the grouping sets to render. + * @param $level + * Integer indicating the hierarchical level of the grouping. + * + * @return string + * Rendered output of given grouping sets. + */ + function render_grouping_sets($sets, $level = 0) { + $output = ''; + foreach ($sets as $set) { + $row = reset($set['rows']); + // Render as a grouping set. + if (is_array($row) && isset($row['group'])) { + $output .= theme(views_theme_functions('views_view_grouping', $this->view, $this->display), + array( + 'view' => $this->view, + 'grouping' => $this->options['grouping'][$level], + 'grouping_level' => $level, + 'rows' => $set['rows'], + 'title' => $set['group']) + ); + } + // Render as a record set. + else { + if ($this->uses_row_plugin()) { + foreach ($set['rows'] as $index => $row) { + $this->view->row_index = $index; + $set['rows'][$index] = $this->row_plugin->render($row); + } + } + + $output .= theme($this->theme_functions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'grouping_level' => $level, + 'rows' => $set['rows'], + 'title' => $set['group']) + ); + } + } + unset($this->view->row_index); + return $output; + } + + /** + * Group records as needed for rendering. + * + * @param $records + * An array of records from the view to group. + * @param $groupings + * An array of grouping instructions on which fields to group. If empty, the + * result set will be given a single group with an empty string as a label. + * @param $group_rendered + * Boolean value whether to use the rendered or the raw field value for + * grouping. If set to NULL the return is structured as before + * Views 7.x-3.0-rc2. After Views 7.x-3.0 this boolean is only used if + * $groupings is an old-style string or if the rendered option is missing + * for a grouping instruction. + * @return + * The grouped record set. + * A nested set structure is generated if multiple grouping fields are used. + * + * @code + * array( + * 'grouping_field_1:grouping_1' => array( + * 'group' => 'grouping_field_1:content_1', + * 'rows' => array( + * 'grouping_field_2:grouping_a' => array( + * 'group' => 'grouping_field_2:content_a', + * 'rows' => array( + * $row_index_1 => $row_1, + * $row_index_2 => $row_2, + * // ... + * ) + * ), + * ), + * ), + * 'grouping_field_1:grouping_2' => array( + * // ... + * ), + * ) + * @endcode + */ + function render_grouping($records, $groupings = array(), $group_rendered = NULL) { + // This is for backward compability, when $groupings was a string containing + // the ID of a single field. + if (is_string($groupings)) { + $rendered = $group_rendered === NULL ? TRUE : $group_rendered; + $groupings = array(array('field' => $groupings, 'rendered' => $rendered)); + } + + // Make sure fields are rendered + $this->render_fields($this->view->result); + $sets = array(); + if ($groupings) { + foreach ($records as $index => $row) { + // Iterate through configured grouping fields to determine the + // hierarchically positioned set where the current row belongs to. + // While iterating, parent groups, that do not exist yet, are added. + $set = &$sets; + foreach ($groupings as $info) { + $field = $info['field']; + $rendered = isset($info['rendered']) ? $info['rendered'] : $group_rendered; + $rendered_strip = isset($info['rendered_strip']) ? $info['rendered_strip'] : FALSE; + $grouping = ''; + $group_content = ''; + // Group on the rendered version of the field, not the raw. That way, + // we can control any special formatting of the grouping field through + // the admin or theme layer or anywhere else we'd like. + if (isset($this->view->field[$field])) { + $group_content = $this->get_field($index, $field); + if ($this->view->field[$field]->options['label']) { + $group_content = $this->view->field[$field]->options['label'] . ': ' . $group_content; + } + if ($rendered) { + $grouping = $group_content; + if ($rendered_strip) { + $group_content = $grouping = strip_tags(htmlspecialchars_decode($group_content)); + } + } + else { + $grouping = $this->get_field_value($index, $field); + // Not all field handlers return a scalar value, + // e.g. views_handler_field_field. + if (!is_scalar($grouping)) { + $grouping = md5(serialize($grouping)); + } + } + } + + // Create the group if it does not exist yet. + if (empty($set[$grouping])) { + $set[$grouping]['group'] = $group_content; + $set[$grouping]['rows'] = array(); + } + + // Move the set reference into the row set of the group we just determined. + $set = &$set[$grouping]['rows']; + } + // Add the row to the hierarchically positioned row set we just determined. + $set[$index] = $row; + } + } + else { + // Create a single group with an empty grouping field. + $sets[''] = array( + 'group' => '', + 'rows' => $records, + ); + } + + // If this parameter isn't explicitely set modify the output to be fully + // backward compatible to code before Views 7.x-3.0-rc2. + // @TODO Remove this as soon as possible e.g. October 2020 + if ($group_rendered === NULL) { + $old_style_sets = array(); + foreach ($sets as $group) { + $old_style_sets[$group['group']] = $group['rows']; + } + $sets = $old_style_sets; + } + + return $sets; + } + + /** + * Render all of the fields for a given style and store them on the object. + * + * @param $result + * The result array from $view->result + */ + function render_fields($result) { + if (!$this->uses_fields()) { + return; + } + + if (!isset($this->rendered_fields)) { + $this->rendered_fields = array(); + $this->view->row_index = 0; + $keys = array_keys($this->view->field); + + // If all fields have a field::access FALSE there might be no fields, so + // there is no reason to execute this code. + if (!empty($keys)) { + foreach ($result as $count => $row) { + $this->view->row_index = $count; + foreach ($keys as $id) { + $this->rendered_fields[$count][$id] = $this->view->field[$id]->theme($row); + } + + $this->row_tokens[$count] = $this->view->field[$id]->get_render_tokens(array()); + } + } + unset($this->view->row_index); + } + + return $this->rendered_fields; + } + + /** + * Get a rendered field. + * + * @param $index + * The index count of the row. + * @param $field + * The id of the field. + */ + function get_field($index, $field) { + if (!isset($this->rendered_fields)) { + $this->render_fields($this->view->result); + } + + if (isset($this->rendered_fields[$index][$field])) { + return $this->rendered_fields[$index][$field]; + } + } + + /** + * Get the raw field value. + * + * @param $index + * The index count of the row. + * @param $field + * The id of the field. + */ + function get_field_value($index, $field) { + $this->view->row_index = $index; + $value = $this->view->field[$field]->get_value($this->view->result[$index]); + unset($this->view->row_index); + return $value; + } + + function validate() { + $errors = parent::validate(); + + if ($this->uses_row_plugin()) { + $plugin = $this->display->handler->get_plugin('row'); + if (empty($plugin)) { + $errors[] = t('Style @style requires a row style but the row plugin is invalid.', array('@style' => $this->definition['title'])); + } + else { + $result = $plugin->validate(); + if (!empty($result) && is_array($result)) { + $errors = array_merge($errors, $result); + } + } + } + return $errors; + } + + function query() { + parent::query(); + if (isset($this->row_plugin)) { + $this->row_plugin->query(); + } + } +} + +/** + * @} + */ diff --git a/plugins/views_plugin_style_default.inc b/plugins/views_plugin_style_default.inc new file mode 100644 index 00000000..a18f6ccf --- /dev/null +++ b/plugins/views_plugin_style_default.inc @@ -0,0 +1,25 @@ + '4'); + $options['alignment'] = array('default' => 'horizontal'); + $options['fill_single_line'] = array('default' => TRUE, 'bool' => TRUE); + $options['summary'] = array('default' => ''); + $options['caption'] = array('default' => ''); + + return $options; + } + + /** + * Render the given style. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['columns'] = array( + '#type' => 'textfield', + '#title' => t('Number of columns'), + '#default_value' => $this->options['columns'], + '#required' => TRUE, + '#element_validate' => array('views_element_validate_integer'), + ); + $form['alignment'] = array( + '#type' => 'radios', + '#title' => t('Alignment'), + '#options' => array('horizontal' => t('Horizontal'), 'vertical' => t('Vertical')), + '#default_value' => $this->options['alignment'], + '#description' => t('Horizontal alignment will place items starting in the upper left and moving right. Vertical alignment will place items starting in the upper left and moving down.'), + ); + + $form['fill_single_line'] = array( + '#type' => 'checkbox', + '#title' => t('Fill up single line'), + '#description' => t('If you disable this option, a grid with only one row will have the same number of table cells (
    ) as items. Disabling it can cause problems with your CSS.'), + '#default_value' => !empty($this->options['fill_single_line']), + ); + + $form['caption'] = array( + '#type' => 'textfield', + '#title' => t('Short description of table'), + '#description' => t('Include a caption for better accessibility of your table.'), + '#default_value' => $this->options['caption'], + ); + + $form['summary'] = array( + '#type' => 'textfield', + '#title' => t('Table summary'), + '#description' => t('This value will be displayed as table-summary attribute in the html. Use this to give a summary of complex tables.'), + '#default_value' => $this->options['summary'], + ); + } +} diff --git a/plugins/views_plugin_style_jump_menu.inc b/plugins/views_plugin_style_jump_menu.inc new file mode 100644 index 00000000..a07567c0 --- /dev/null +++ b/plugins/views_plugin_style_jump_menu.inc @@ -0,0 +1,159 @@ + FALSE, 'bool' => TRUE); + $options['path'] = array('default' => ''); + $options['text'] = array('default' => 'Go', 'translatable' => TRUE); + $options['label'] = array('default' => '', 'translatable' => TRUE); + $options['choose'] = array('default' => '- Choose -', 'translatable' => TRUE); + $options['default_value'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Render the given style. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $handlers = $this->display->handler->get_handlers('field'); + if (empty($handlers)) { + $form['error_markup'] = array( + '#markup' => t('You need at least one field before you can configure your jump menu settings'), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + return; + } + + $form['markup'] = array( + '#markup' => t('To properly configure a jump menu, you must select one field that will represent the path to utilize. You should then set that field to exclude. All other displayed fields will be part of the menu. Please note that all HTML will be stripped from this output as select boxes cannot show HTML.'), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + + foreach ($handlers as $id => $handler) { + $options[$id] = $handler->ui_name(); + } + + $form['path'] = array( + '#type' => 'select', + '#title' => t('Path field'), + '#options' => $options, + '#default_value' => $this->options['path'], + ); + + $form['hide'] = array( + '#type' => 'checkbox', + '#title' => t('Hide the "Go" button'), + '#default_value' => !empty($this->options['hide']), + '#description' => t('If hidden, this button will only be hidden for users with javascript and the page will automatically jump when the select is changed.'), + ); + + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Button text'), + '#default_value' => $this->options['text'], + ); + + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Selector label'), + '#default_value' => $this->options['label'], + '#description' => t('The text that will appear as the the label of the selector element. If blank no label tag will be used.'), + ); + + $form['choose'] = array( + '#type' => 'textfield', + '#title' => t('Choose text'), + '#default_value' => $this->options['choose'], + '#description' => t('The text that will appear as the selected option in the jump menu.'), + ); + + $form['default_value'] = array( + '#type' => 'checkbox', + '#title' => t('Select the current contextual filter value'), + '#default_value' => !empty($this->options['default_value']), + '#description' => t('If checked, the current path will be displayed as the default option in the jump menu, if applicable.'), + ); + } + + /** + * Render the display in this style. + * + * This is overridden so that we can render our grouping specially. + */ + function render() { + $sets = $this->render_grouping($this->view->result, $this->options['grouping']); + + // Turn this all into an $options array for the jump menu. + $this->view->row_index = 0; + $options = array(); + $paths = array(); + + foreach ($sets as $title => $records) { + foreach ($records as $row_index => $row) { + $this->view->row_index = $row_index; + $path = strip_tags(decode_entities($this->get_field($this->view->row_index, $this->options['path']))); + // Putting a '/' in front messes up url() so let's take that out + // so users don't shoot themselves in the foot. + $base_path = base_path(); + if (strpos($path, $base_path) === 0) { + $path = drupal_substr($path, drupal_strlen($base_path)); + } + + // use drupal_parse_url() to preserve query and fragment in case the user + // wants to do fun tricks. + $url_options = drupal_parse_url($path); + + $path = url($url_options['path'], $url_options); + $field = strip_tags(decode_entities($this->row_plugin->render($row))); + $key = md5($path . $field) . "::" . $path; + if ($title) { + $options[$title][$key] = $field; + } + else { + $options[$key] = $field; + } + $paths[$path] = $key; + $this->view->row_index++; + } + } + unset($this->view->row_index); + + $default_value = ''; + if ($this->options['default_value'] && !empty($paths[url($_GET['q'])])) { + $default_value = $paths[url($_GET['q'])]; + } + + ctools_include('jump-menu'); + $settings = array( + 'hide' => $this->options['hide'], + 'button' => $this->options['text'], + 'title' => $this->options['label'], + 'choose' => $this->options['choose'], + 'default_value' => $default_value, + ); + + $form = drupal_get_form('ctools_jump_menu', $options, $settings); + return $form; + } + + function render_set($title, $records) { + $options = array(); + $fields = $this->rendered_fields; + } +} diff --git a/plugins/views_plugin_style_list.inc b/plugins/views_plugin_style_list.inc new file mode 100644 index 00000000..2a1dadb8 --- /dev/null +++ b/plugins/views_plugin_style_list.inc @@ -0,0 +1,53 @@ + 'ul'); + $options['class'] = array('default' => ''); + $options['wrapper_class'] = array('default' => 'item-list'); + + return $options; + } + + /** + * Render the given style. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['type'] = array( + '#type' => 'radios', + '#title' => t('List type'), + '#options' => array('ul' => t('Unordered list'), 'ol' => t('Ordered list')), + '#default_value' => $this->options['type'], + ); + $form['wrapper_class'] = array( + '#title' => t('Wrapper class'), + '#description' => t('The class to provide on the wrapper, outside the list.'), + '#type' => 'textfield', + '#size' => '30', + '#default_value' => $this->options['wrapper_class'], + ); + $form['class'] = array( + '#title' => t('List class'), + '#description' => t('The class to provide on the list element itself.'), + '#type' => 'textfield', + '#size' => '30', + '#default_value' => $this->options['class'], + ); + } +} diff --git a/plugins/views_plugin_style_mapping.inc b/plugins/views_plugin_style_mapping.inc new file mode 100644 index 00000000..fb60a03f --- /dev/null +++ b/plugins/views_plugin_style_mapping.inc @@ -0,0 +1,125 @@ +define_mapping() as $key => $value) { + $default = !empty($value['#multiple']) ? array() : ''; + $options['mapping']['contains'][$key] = array( + 'default' => isset($value['#default_value']) ? $value['#default_value'] : $default, + ); + if (!empty($value['#toggle'])) { + $options['mapping']['contains']["toggle_$key"] = array( + 'default' => FALSE, + 'bool' => TRUE, + ); + } + } + + return $options; + } + + /** + * Overrides views_plugin_style::options_form(). + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Get the mapping. + $mapping = $this->define_mapping(); + + // Restrict the list of defaults to the mapping, in case they have changed. + $options = array_intersect_key($this->options['mapping'], $mapping); + + // Get the labels of the fields added to this display. + $field_labels = $this->display->handler->get_field_labels(); + + // Provide some default values. + $defaults = array( + '#type' => 'select', + '#required' => FALSE, + '#multiple' => FALSE, + ); + + // For each mapping, add a select element to the form. + foreach ($options as $key => $value) { + // If the field is optional, add a 'None' value to the top of the options. + $field_options = array(); + $required = !empty($mapping[$key]['#required']); + if (!$required && empty($mapping[$key]['#multiple'])) { + $field_options = array('' => t('- None -')); + } + $field_options += $field_labels; + + // Optionally filter the available fields. + if (isset($mapping[$key]['#filter'])) { + $this->view->init_handlers(); + $this::$mapping[$key]['#filter']($field_options); + unset($mapping[$key]['#filter']); + } + + // These values must always be set. + $overrides = array( + '#options' => $field_options, + '#default_value' => $options[$key], + ); + + // Optionally allow the select to be toggleable. + if (!empty($mapping[$key]['#toggle'])) { + $form['mapping']["toggle_$key"] = array( + '#type' => 'checkbox', + '#title' => t('Use a custom %field_name', array('%field_name' => strtolower($mapping[$key]['#title']))), + '#default_value' => $this->options['mapping']["toggle_$key"], + ); + $overrides['#states']['visible'][':input[name="style_options[mapping][' . "toggle_$key" . ']"]'] = array('checked' => TRUE); + } + + $form['mapping'][$key] = $overrides + $mapping[$key] + $defaults; + } + } + + /** + * Overrides views_plugin_style::render(). + * + * Provides the mapping definition as an available variable. + */ + function render() { + return theme($this->theme_functions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'rows' => $this->view->result, + 'mapping' => $this->define_mapping(), + )); + } + +} diff --git a/plugins/views_plugin_style_rss.inc b/plugins/views_plugin_style_rss.inc new file mode 100644 index 00000000..27106a89 --- /dev/null +++ b/plugins/views_plugin_style_rss.inc @@ -0,0 +1,123 @@ +view->display[$display_id]->handler; + $url_options = array(); + $input = $this->view->get_exposed_input(); + if ($input) { + $url_options['query'] = $input; + } + $url_options['absolute'] = TRUE; + + $url = url($this->view->get_url(NULL, $path), $url_options); + if ($display->has_path()) { + if (empty($this->preview)) { + drupal_add_feed($url, $title); + } + } + else { + if (empty($this->view->feed_icon)) { + $this->view->feed_icon = ''; + } + + $this->view->feed_icon .= theme('feed_icon', array('url' => $url, 'title' => $title)); + drupal_add_html_head_link(array( + 'rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => $title, + 'href' => $url + )); + } + } + + function option_definition() { + $options = parent::option_definition(); + + $options['description'] = array('default' => '', 'translatable' => TRUE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['description'] = array( + '#type' => 'textfield', + '#title' => t('RSS description'), + '#default_value' => $this->options['description'], + '#description' => t('This will appear in the RSS feed itself.'), + '#maxlength' => 1024, + ); + } + + /** + * Return an array of additional XHTML elements to add to the channel. + * + * @return + * An array that can be passed to format_xml_elements(). + */ + function get_channel_elements() { + return array(); + } + + /** + * Get RSS feed description. + * + * @return string + * The string containing the description with the tokens replaced. + */ + function get_description() { + $description = $this->options['description']; + + // Allow substitutions from the first row. + $description = $this->tokenize_value($description, 0); + + return $description; + } + + function render() { + if (empty($this->row_plugin)) { + vpr('views_plugin_style_default: Missing row plugin'); + return; + } + $rows = ''; + + // This will be filled in by the row plugin and is used later on in the + // theming output. + $this->namespaces = array('xmlns:dc' => 'http://purl.org/dc/elements/1.1/'); + + // Fetch any additional elements for the channel and merge in their + // namespaces. + $this->channel_elements = $this->get_channel_elements(); + foreach ($this->channel_elements as $element) { + if (isset($element['namespace'])) { + $this->namespaces = array_merge($this->namespaces, $element['namespace']); + } + } + + foreach ($this->view->result as $row_index => $row) { + $this->view->row_index = $row_index; + $rows .= $this->row_plugin->render($row); + } + + $output = theme($this->theme_functions(), + array( + 'view' => $this->view, + 'options' => $this->options, + 'rows' => $rows + )); + unset($this->view->row_index); + return $output; + } +} diff --git a/plugins/views_plugin_style_summary.inc b/plugins/views_plugin_style_summary.inc new file mode 100644 index 00000000..5081dd62 --- /dev/null +++ b/plugins/views_plugin_style_summary.inc @@ -0,0 +1,76 @@ + ''); + $options['count'] = array('default' => TRUE, 'bool' => TRUE); + $options['override'] = array('default' => FALSE, 'bool' => TRUE); + $options['items_per_page'] = array('default' => 25); + + return $options; + } + + function query() { + if (!empty($this->options['override'])) { + $this->view->set_items_per_page(intval($this->options['items_per_page'])); + } + } + + function options_form(&$form, &$form_state) { + $form['base_path'] = array( + '#type' => 'textfield', + '#title' => t('Base path'), + '#default_value' => $this->options['base_path'], + '#description' => t('Define the base path for links in this summary + view, i.e. http://example.com/your_view_path/archive. + Do not include beginning and ending forward slash. If this value + is empty, views will use the first path found as the base path, + in page displays, or / if no path could be found.'), + ); + $form['count'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['count']), + '#title' => t('Display record count with link'), + ); + $form['override'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['override']), + '#title' => t('Override number of items to display'), + ); + + $form['items_per_page'] = array( + '#type' => 'textfield', + '#title' => t('Items to display'), + '#default_value' => $this->options['items_per_page'], + '#dependency' => array( + 'edit-options-summary-options-' . str_replace('_', '-', $this->definition['name']) . '-override' => array(1) + ), + ); + } + + function render() { + $rows = array(); + foreach ($this->view->result as $row) { + // @todo: Include separator as an option. + $rows[] = $row; + } + + return theme($this->theme_functions(), array( + 'view' => $this->view, + 'options' => $this->options, + 'rows' => $rows + )); + } +} diff --git a/plugins/views_plugin_style_summary_jump_menu.inc b/plugins/views_plugin_style_summary_jump_menu.inc new file mode 100644 index 00000000..d5100df0 --- /dev/null +++ b/plugins/views_plugin_style_summary_jump_menu.inc @@ -0,0 +1,138 @@ + ''); + $options['count'] = array('default' => TRUE, 'bool' => TRUE); + $options['hide'] = array('default' => FALSE, 'bool' => TRUE); + $options['text'] = array('default' => 'Go', 'translatable' => TRUE); + $options['label'] = array('default' => '', 'translatable' => TRUE); + $options['choose'] = array('default' => '- Choose -', 'translatable' => TRUE); + $options['default_value'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + function query() { + // Copy the offset option. + $pager = array( + 'type' => 'none', + 'options' => $this->display->handler->options['pager']['options'], + ); + $this->display->handler->set_option('pager', $pager); + } + + function options_form(&$form, &$form_state) { + $form['base_path'] = array( + '#type' => 'textfield', + '#title' => t('Base path'), + '#default_value' => $this->options['base_path'], + '#description' => t('Define the base path for links in this summary + view, i.e. http://example.com/your_view_path/archive. + Do not include beginning and ending forward slash. If this value + is empty, views will use the first path found as the base path, + in page displays, or / if no path could be found.'), + ); + $form['count'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['count']), + '#title' => t('Display record count with link'), + ); + + $form['hide'] = array( + '#type' => 'checkbox', + '#title' => t('Hide the "Go" button'), + '#default_value' => !empty($this->options['hide']), + '#description' => t('If hidden, this button will only be hidden for users with javascript and the page will automatically jump when the select is changed.'), + ); + + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Button text'), + '#default_value' => $this->options['text'], + ); + + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Selector label'), + '#default_value' => $this->options['label'], + '#description' => t('The text that will appear as the the label of the selector element. If blank no label tag will be used.'), + ); + + $form['choose'] = array( + '#type' => 'textfield', + '#title' => t('Choose text'), + '#default_value' => $this->options['choose'], + '#description' => t('The text that will appear as the selected option in the jump menu.'), + ); + + $form['default_value'] = array( + '#type' => 'checkbox', + '#title' => t('Select the current contextual filter value'), + '#default_value' => !empty($this->options['default_value']), + '#description' => t('If checked, the current contextual filter value will be displayed as the default option in the jump menu, if applicable.'), + ); + } + + function render() { + $argument = $this->view->argument[$this->view->build_info['summary_level']]; + + $url_options = array(); + + if (!empty($this->view->exposed_raw_input)) { + $url_options['query'] = $this->view->exposed_raw_input; + } + + $options = array(); + $default_value = ''; + $row_args = array(); + foreach ($this->view->result as $id => $row) { + $row_args[$id] = $argument->summary_argument($row); + } + $argument->process_summary_arguments($row_args); + + foreach ($this->view->result as $id => $row) { + $args = $this->view->args; + $args[$argument->position] = $row_args[$id]; + $base_path = NULL; + if (!empty($argument->options['summary_options']['base_path'])) { + $base_path = $argument->options['summary_options']['base_path']; + } + $path = url($this->view->get_url($args, $base_path), $url_options); + $summary_value = strip_tags($argument->summary_name($row)); + $key = md5($path . $summary_value) . "::" . $path; + + $options[$key] = $summary_value; + if (!empty($this->options['count'])) { + $options[$key] .= ' (' . intval($row->{$argument->count_alias}) . ')'; + } + if ($this->options['default_value'] && $_GET['q'] == $this->view->get_url($args)) { + $default_value = $key; + } + } + + ctools_include('jump-menu'); + $settings = array( + 'hide' => $this->options['hide'], + 'button' => $this->options['text'], + 'title' => $this->options['label'], + 'choose' => $this->options['choose'], + 'default_value' => $default_value, + ); + + $form = drupal_get_form('ctools_jump_menu', $options, $settings); + return drupal_render($form); + } +} diff --git a/plugins/views_plugin_style_summary_unformatted.inc b/plugins/views_plugin_style_summary_unformatted.inc new file mode 100644 index 00000000..fc466245 --- /dev/null +++ b/plugins/views_plugin_style_summary_unformatted.inc @@ -0,0 +1,34 @@ + FALSE, 'bool' => TRUE); + $options['separator'] = array('default' => ''); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $form['inline'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['inline']), + '#title' => t('Display items inline'), + ); + $form['separator'] = array( + '#type' => 'textfield', + '#title' => t('Separator'), + '#default_value' => $this->options['separator'], + ); + } +} diff --git a/plugins/views_plugin_style_table.inc b/plugins/views_plugin_style_table.inc new file mode 100644 index 00000000..45ed9763 --- /dev/null +++ b/plugins/views_plugin_style_table.inc @@ -0,0 +1,307 @@ + array()); + $options['default'] = array('default' => ''); + $options['info'] = array('default' => array()); + $options['override'] = array('default' => TRUE, 'bool' => TRUE); + $options['sticky'] = array('default' => FALSE, 'bool' => TRUE); + $options['order'] = array('default' => 'asc'); + $options['caption'] = array('default' => '', 'translatable' => TRUE); + $options['summary'] = array('default' => '', 'translatable' => TRUE); + $options['empty_table'] = array('default' => FALSE, 'bool' => TRUE); + + return $options; + } + + /** + * Determine if we should provide sorting based upon $_GET inputs + * + * @return bool + */ + function build_sort() { + if (!isset($_GET['order']) && ($this->options['default'] == -1 || empty($this->view->field[$this->options['default']]))) { + return TRUE; + } + + // If a sort we don't know anything about gets through, exit gracefully. + if (isset($_GET['order']) && empty($this->view->field[$_GET['order']])) { + return TRUE; + } + + // Let the builder know whether or not we're overriding the default sorts. + return empty($this->options['override']); + } + + /** + * Add our actual sort criteria + */ + function build_sort_post() { + if (!isset($_GET['order'])) { + // check for a 'default' clicksort. If there isn't one, exit gracefully. + if (empty($this->options['default'])) { + return; + } + $sort = $this->options['default']; + if (!empty($this->options['info'][$sort]['default_sort_order'])) { + $this->order = $this->options['info'][$sort]['default_sort_order']; + } + else { + $this->order = !empty($this->options['order']) ? $this->options['order'] : 'asc'; + } + } + else { + $sort = $_GET['order']; + // Store the $order for later use. + $this->order = !empty($_GET['sort']) ? strtolower($_GET['sort']) : 'asc'; + } + + // If a sort we don't know anything about gets through, exit gracefully. + if (empty($this->view->field[$sort])) { + return; + } + + // Ensure $this->order is valid. + if ($this->order != 'asc' && $this->order != 'desc') { + $this->order = 'asc'; + } + + // Store the $sort for later use. + $this->active = $sort; + + // Tell the field to click sort. + $this->view->field[$sort]->click_sort($this->order); + } + + /** + * Normalize a list of columns based upon the fields that are + * available. This compares the fields stored in the style handler + * to the list of fields actually in the view, removing fields that + * have been removed and adding new fields in their own column. + * + * - Each field must be in a column. + * - Each column must be based upon a field, and that field + * is somewhere in the column. + * - Any fields not currently represented must be added. + * - Columns must be re-ordered to match the fields. + * + * @param $columns + * An array of all fields; the key is the id of the field and the + * value is the id of the column the field should be in. + * @param $fields + * The fields to use for the columns. If not provided, they will + * be requested from the current display. The running render should + * send the fields through, as they may be different than what the + * display has listed due to access control or other changes. + * + * @return array + * An array of all the sanitized columns. + */ + function sanitize_columns($columns, $fields = NULL) { + $sanitized = array(); + if ($fields === NULL) { + $fields = $this->display->handler->get_option('fields'); + } + // Preconfigure the sanitized array so that the order is retained. + foreach ($fields as $field => $info) { + // Set to itself so that if it isn't touched, it gets column + // status automatically. + $sanitized[$field] = $field; + } + + foreach ($columns as $field => $column) { + // first, make sure the field still exists. + if (!isset($sanitized[$field])) { + continue; + } + + // If the field is the column, mark it so, or the column + // it's set to is a column, that's ok + if ($field == $column || $columns[$column] == $column && !empty($sanitized[$column])) { + $sanitized[$field] = $column; + } + // Since we set the field to itself initially, ignoring + // the condition is ok; the field will get its column + // status back. + } + + return $sanitized; + } + + /** + * Render the given style. + */ + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + $handlers = $this->display->handler->get_handlers('field'); + if (empty($handlers)) { + $form['error_markup'] = array( + '#markup' => '
    ' . t('You need at least one field before you can configure your table settings') . '
    ', + ); + return; + } + + $form['override'] = array( + '#type' => 'checkbox', + '#title' => t('Override normal sorting if click sorting is used'), + '#default_value' => !empty($this->options['override']), + ); + + $form['sticky'] = array( + '#type' => 'checkbox', + '#title' => t('Enable Drupal style "sticky" table headers (Javascript)'), + '#default_value' => !empty($this->options['sticky']), + '#description' => t('(Sticky header effects will not be active for preview below, only on live output.)'), + ); + + $form['caption'] = array( + '#type' => 'textfield', + '#title' => t('Short description of table'), + '#description' => t('Include a caption for better accessibility of your table.'), + '#default_value' => $this->options['caption'], + '#maxlength' => 255, + ); + + $form['summary'] = array( + '#type' => 'textfield', + '#title' => t('Table summary'), + '#description' => t('This value will be displayed as table-summary attribute in the html. Use this to give a summary of complex tables.'), + '#default_value' => $this->options['summary'], + '#maxlength' => 255, + ); + + // Note: views UI registers this theme handler on our behalf. Your module + // will have to register your theme handlers if you do stuff like this. + $form['#theme'] = 'views_ui_style_plugin_table'; + + $columns = $this->sanitize_columns($this->options['columns']); + + // Create an array of allowed columns from the data we know: + $field_names = $this->display->handler->get_field_labels(); + + if (isset($this->options['default'])) { + $default = $this->options['default']; + if (!isset($columns[$default])) { + $default = -1; + } + } + else { + $default = -1; + } + + foreach ($columns as $field => $column) { + $safe = str_replace(array('][', '_', ' '), '-', $field); + // the $id of the column for dependency checking. + $id = 'edit-style-options-columns-' . $safe; + + $form['columns'][$field] = array( + '#type' => 'select', + '#options' => $field_names, + '#default_value' => $column, + ); + if ($handlers[$field]->click_sortable()) { + $form['info'][$field]['sortable'] = array( + '#type' => 'checkbox', + '#default_value' => !empty($this->options['info'][$field]['sortable']), + '#dependency' => array($id => array($field)), + ); + $form['info'][$field]['default_sort_order'] = array( + '#type' => 'select', + '#options' => array('asc' => t('Ascending'), 'desc' => t('Descending')), + '#default_value' => !empty($this->options['info'][$field]['default_sort_order']) ? $this->options['info'][$field]['default_sort_order'] : 'asc', + '#dependency_count' => 2, + '#dependency' => array($id => array($field), 'edit-style-options-info-' . $safe . '-sortable' => array(1)), + ); + // Provide an ID so we can have such things. + $radio_id = drupal_html_id('edit-default-' . $field); + $form['default'][$field] = array( + '#type' => 'radio', + '#return_value' => $field, + '#parents' => array('style_options', 'default'), + '#id' => $radio_id, + // because 'radio' doesn't fully support '#id' =( + '#attributes' => array('id' => $radio_id), + '#default_value' => $default, + '#dependency' => array($id => array($field)), + ); + } + $form['info'][$field]['align'] = array( + '#type' => 'select', + '#default_value' => !empty($this->options['info'][$field]['align']) ? $this->options['info'][$field]['align'] : '', + '#options' => array( + '' => t('None'), + 'views-align-left' => t('Left'), + 'views-align-center' => t('Center'), + 'views-align-right' => t('Right'), + ), + '#dependency' => array($id => array($field)), + ); + $form['info'][$field]['separator'] = array( + '#type' => 'textfield', + '#size' => 10, + '#default_value' => isset($this->options['info'][$field]['separator']) ? $this->options['info'][$field]['separator'] : '', + '#dependency' => array($id => array($field)), + ); + $form['info'][$field]['empty_column'] = array( + '#type' => 'checkbox', + '#default_value' => isset($this->options['info'][$field]['empty_column']) ? $this->options['info'][$field]['empty_column'] : FALSE, + '#dependency' => array($id => array($field)), + ); + + // markup for the field name + $form['info'][$field]['name'] = array( + '#markup' => $field_names[$field], + ); + } + + // Provide a radio for no default sort + $form['default'][-1] = array( + '#type' => 'radio', + '#return_value' => -1, + '#parents' => array('style_options', 'default'), + '#id' => 'edit-default-0', + '#default_value' => $default, + ); + + $form['empty_table'] = array( + '#type' => 'checkbox', + '#title' => t('Show the empty text in the table'), + '#default_value' => $this->options['empty_table'], + '#description' => t('Per default the table is hidden for an empty view. With this option it is posible to show an empty table with the text in it.'), + ); + + $form['description_markup'] = array( + '#markup' => '
    ' . t('Place fields into columns; you may combine multiple fields into the same column. If you do, the separator in the column specified will be used to separate the fields. Check the sortable box to make that column click sortable, and check the default sort radio to determine which column will be sorted by default, if any. You may control column order and field labels in the fields section.') . '
    ', + ); + } + + function even_empty() { + return parent::even_empty() || !empty($this->options['empty_table']); + } +} diff --git a/plugins/views_wizard/comment.inc b/plugins/views_wizard/comment.inc new file mode 100644 index 00000000..a9107884 --- /dev/null +++ b/plugins/views_wizard/comment.inc @@ -0,0 +1,44 @@ + 'comment', + 'base_table' => 'comment', + 'created_column' => 'created', + 'form_wizard_class' => array( + 'file' => 'views_ui_comment_views_wizard.class.php', + 'class' => 'ViewsUiCommentViewsWizard', + ), + 'title' => t('Comments'), + 'filters' => array( + 'status' => array( + 'value' => COMMENT_PUBLISHED, + 'table' => 'comment', + 'field' => 'status', + ), + 'status_node' => array( + 'value' => NODE_PUBLISHED, + 'table' => 'node', + 'field' => 'status', + 'relationship' => 'nid', + ), + ), + 'path_field' => array( + 'id' => 'cid', + 'table' => 'comment', + 'field' => 'cid', + 'exclude' => TRUE, + 'link_to_comment' => FALSE, + 'alter' => array( + 'alter_text' => 1, + 'text' => 'comment/[cid]#comment-[cid]', + ), + ), + ); +} diff --git a/plugins/views_wizard/file_managed.inc b/plugins/views_wizard/file_managed.inc new file mode 100644 index 00000000..049ce1b5 --- /dev/null +++ b/plugins/views_wizard/file_managed.inc @@ -0,0 +1,26 @@ + 'file_managed', + 'base_table' => 'file_managed', + 'created_column' => 'timestamp', + 'form_wizard_class' => array( + 'file' => 'views_ui_file_managed_views_wizard.class.php', + 'class' => 'ViewsUiFileManagedViewsWizard', + ), + 'title' => t('Files'), + 'filters' => array( + ), + 'path_field' => array( + 'id' => 'uri', + 'table' => 'file_managed', + 'field' => 'uri', + 'exclude' => TRUE, + 'file_download_path' => TRUE, + ), +); diff --git a/plugins/views_wizard/node.inc b/plugins/views_wizard/node.inc new file mode 100644 index 00000000..ccca48de --- /dev/null +++ b/plugins/views_wizard/node.inc @@ -0,0 +1,42 @@ + 'node', + 'base_table' => 'node', + 'created_column' => 'created', + 'available_sorts' => array( + 'title:DESC' => t('Title') + ), + 'form_wizard_class' => array( + 'file' => 'views_ui_node_views_wizard.class.php', + 'class' => 'ViewsUiNodeViewsWizard', + ), + 'title' => t('Content'), + 'filters' => array( + 'status' => array( + 'value' => NODE_PUBLISHED, + 'table' => 'node', + 'field' => 'status', + ), + ), + 'path_field' => array( + 'id' => 'nid', + 'table' => 'node', + 'field' => 'nid', + 'exclude' => TRUE, + 'link_to_node' => FALSE, + 'alter' => array( + 'alter_text' => 1, + 'text' => 'node/[nid]', + ), + ), +); + +if (module_exists('statistics')) { + $plugin['available_sorts']['node_counter-totalcount:DESC'] = t('Number of hits'); +} diff --git a/plugins/views_wizard/node_revision.inc b/plugins/views_wizard/node_revision.inc new file mode 100644 index 00000000..ddf1d61c --- /dev/null +++ b/plugins/views_wizard/node_revision.inc @@ -0,0 +1,43 @@ + 'node_revision', + 'base_table' => 'node_revision', + 'created_column' => 'timestamp', + 'form_wizard_class' => array( + 'file' => 'views_ui_node_revision_views_wizard.class.php', + 'class' => 'ViewsUiNodeRevisionViewsWizard', + ), + 'title' => t('Content revisions'), + 'filters' => array( + 'status' => array( + 'value' => '1', + 'table' => 'node', // @todo - unclear if this should be node or node_revision + 'field' => 'status', + ), + ), + 'path_field' => array( + 'id' => 'vid', + 'table' => 'node_revision', + 'field' => 'vid', + 'exclude' => TRUE, + 'alter' => array( + 'alter_text' => 1, + 'text' => 'node/[nid]/revisions/[vid]/view', + ), + ), + 'path_fields_supplemental' => array( + array( + 'id' => 'nid', + 'table' => 'node', + 'field' => 'nid', + 'exclude' => TRUE, + 'link_to_node' => FALSE, + ), + ), +); diff --git a/plugins/views_wizard/taxonomy_term.inc b/plugins/views_wizard/taxonomy_term.inc new file mode 100644 index 00000000..599e3543 --- /dev/null +++ b/plugins/views_wizard/taxonomy_term.inc @@ -0,0 +1,30 @@ + 'taxonomy_term', + 'base_table' => 'taxonomy_term_data', + 'form_wizard_class' => array( + 'file' => 'views_ui_taxonomy_term_views_wizard.class.php', + 'class' => 'ViewsUiTaxonomyTermViewsWizard', + ), + 'title' => t('Taxonomy terms'), + 'filters' => array( + ), + 'path_field' => array( + 'id' => 'tid', + 'table' => 'taxonomy_term_data', + 'field' => 'tid', + 'exclude' => TRUE, + 'alter' => array( + 'alter_text' => 1, + 'text' => 'taxonomy/term/[tid]', + ), + ), + ); +} diff --git a/plugins/views_wizard/users.inc b/plugins/views_wizard/users.inc new file mode 100644 index 00000000..176a9e1e --- /dev/null +++ b/plugins/views_wizard/users.inc @@ -0,0 +1,35 @@ + 'users', + 'base_table' => 'users', + 'created_column' => 'created', + 'form_wizard_class' => array( + 'file' => 'views_ui_users_views_wizard.class.php', + 'class' => 'ViewsUiUsersViewsWizard', + ), + 'title' => t('Users'), + 'filters' => array( + 'status' => array( + 'value' => '1', + 'table' => 'users', + 'field' => 'status', + ), + ), + 'path_field' => array( + 'id' => 'uid', + 'table' => 'users', + 'field' => 'uid', + 'exclude' => TRUE, + 'link_to_user' => FALSE, + 'alter' => array( + 'alter_text' => 1, + 'text' => 'user/[uid]', + ), + ), +); diff --git a/plugins/views_wizard/views_ui_base_views_wizard.class.php b/plugins/views_wizard/views_ui_base_views_wizard.class.php new file mode 100644 index 00000000..8893ab81 --- /dev/null +++ b/plugins/views_wizard/views_ui_base_views_wizard.class.php @@ -0,0 +1,929 @@ + NULL, + 'expose' => array('operator' => FALSE), + 'group' => 1, + ); + + function __construct($plugin) { + $this->base_table = $plugin['base_table']; + $default = $this->filter_defaults; + + if (isset($plugin['filters'])) { + foreach ($plugin['filters'] as $name => $info) { + $default['id'] = $name; + $plugin['filters'][$name] = $info + $default; + } + } + + $this->plugin = $plugin; + + $entities = entity_get_info(); + foreach ($entities as $entity_type => $entity_info) { + if (isset($entity_info['base table']) && $this->base_table == $entity_info['base table']) { + $this->entity_info = $entity_info; + $this->entity_type = $entity_type; + } + } + } + + function build_form($form, &$form_state) { + $style_options = views_fetch_plugin_names('style', 'normal', array($this->base_table)); + $feed_row_options = views_fetch_plugin_names('row', 'feed', array($this->base_table)); + $path_prefix = url(NULL, array('absolute' => TRUE)) . (variable_get('clean_url', 0) ? '' : '?q='); + + // Add filters and sorts which apply to the view as a whole. + $this->build_filters($form, $form_state); + $this->build_sorts($form, $form_state); + + $form['displays']['page'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('views-attachment', 'fieldset-no-legend'),), + '#tree' => TRUE, + ); + $form['displays']['page']['create'] = array( + '#title' => t('Create a page'), + '#type' => 'checkbox', + '#attributes' => array('class' => array('strong')), + '#default_value' => TRUE, + '#id' => 'edit-page-create', + ); + + // All options for the page display are included in this container so they + // can be hidden en masse when the "Create a page" checkbox is unchecked. + $form['displays']['page']['options'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('options-set'),), + '#dependency' => array( + 'edit-page-create' => array(1), + ), + '#pre_render' => array('ctools_dependent_pre_render'), + '#prefix' => '
    ', + '#suffix' => '
    ', + '#parents' => array('page'), + ); + + $form['displays']['page']['options']['title'] = array( + '#title' => t('Page title'), + '#type' => 'textfield', + ); + $form['displays']['page']['options']['path'] = array( + '#title' => t('Path'), + '#type' => 'textfield', + '#field_prefix' => $path_prefix, + ); + $form['displays']['page']['options']['style'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('container-inline', 'fieldset-no-legend')), + ); + + // Create the dropdown for choosing the display format. + $form['displays']['page']['options']['style']['style_plugin'] = array( + '#title' => t('Display format'), + '#help_topic' => 'style', + '#type' => 'select', + '#options' => $style_options, + ); + $style_form = &$form['displays']['page']['options']['style']; + $style_form['style_plugin']['#default_value'] = views_ui_get_selected($form_state, array('page', 'style', 'style_plugin'), 'default', $style_form['style_plugin']); + // Changing this dropdown updates $form['displays']['page']['options'] via + // AJAX. + views_ui_add_ajax_trigger($style_form, 'style_plugin', array('displays', 'page', 'options')); + + $this->build_form_style($form, $form_state, 'page'); + $form['displays']['page']['options']['items_per_page'] = array( + '#title' => t('Items to display'), + '#type' => 'textfield', + '#default_value' => '10', + '#size' => 5, + '#element_validate' => array('views_element_validate_integer'), + ); + $form['displays']['page']['options']['pager'] = array( + '#title' => t('Use a pager'), + '#type' => 'checkbox', + '#default_value' => TRUE, + ); + $form['displays']['page']['options']['link'] = array( + '#title' => t('Create a menu link'), + '#type' => 'checkbox', + '#id' => 'edit-page-link', + ); + $form['displays']['page']['options']['link_properties'] = array( + '#type' => 'container', + '#dependency' => array( + 'edit-page-link' => array(1), + ), + '#pre_render' => array('ctools_dependent_pre_render'), + '#prefix' => '', + ); + if (module_exists('menu')) { + $menu_options = menu_get_menus(); + } + else { + // These are not yet translated. + $menu_options = menu_list_system_menus(); + foreach ($menu_options as $name => $title) { + $menu_options[$name] = t($title); + } + } + $form['displays']['page']['options']['link_properties']['menu_name'] = array( + '#title' => t('Menu'), + '#type' => 'select', + '#options' => $menu_options, + ); + $form['displays']['page']['options']['link_properties']['title'] = array( + '#title' => t('Link text'), + '#type' => 'textfield', + ); + // Only offer a feed if we have at least one available feed row style. + if ($feed_row_options) { + $form['displays']['page']['options']['feed'] = array( + '#title' => t('Include an RSS feed'), + '#type' => 'checkbox', + '#id' => 'edit-page-feed', + ); + $form['displays']['page']['options']['feed_properties'] = array( + '#type' => 'container', + '#dependency' => array( + 'edit-page-feed' => array(1), + ), + '#pre_render' => array('ctools_dependent_pre_render'), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + $form['displays']['page']['options']['feed_properties']['path'] = array( + '#title' => t('Feed path'), + '#type' => 'textfield', + '#field_prefix' => $path_prefix, + ); + // This will almost never be visible. + $form['displays']['page']['options']['feed_properties']['row_plugin'] = array( + '#title' => t('Feed row style'), + '#type' => 'select', + '#options' => $feed_row_options, + '#default_value' => key($feed_row_options), + '#access' => (count($feed_row_options) > 1), + '#dependency' => array( + 'edit-page-feed' => array(1), + ), + '#pre_render' => array('ctools_dependent_pre_render'), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + } + + $form['displays']['block'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('views-attachment', 'fieldset-no-legend'),), + '#tree' => TRUE, + ); + $form['displays']['block']['create'] = array( + '#title' => t('Create a block'), + '#type' => 'checkbox', + '#attributes' => array('class' => array('strong')), + '#id' => 'edit-block-create', + ); + + // All options for the block display are included in this container so they + // can be hidden en masse when the "Create a block" checkbox is unchecked. + $form['displays']['block']['options'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('options-set'),), + '#dependency' => array( + 'edit-block-create' => array(1), + ), + '#pre_render' => array('ctools_dependent_pre_render'), + '#prefix' => '
    ', + '#suffix' => '
    ', + '#parents' => array('block'), + ); + + $form['displays']['block']['options']['title'] = array( + '#title' => t('Block title'), + '#type' => 'textfield', + ); + $form['displays']['block']['options']['style'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('container-inline', 'fieldset-no-legend')), + ); + + // Create the dropdown for choosing the display format. + $form['displays']['block']['options']['style']['style_plugin'] = array( + '#title' => t('Display format'), + '#help_topic' => 'style', + '#type' => 'select', + '#options' => $style_options, + ); + $style_form = &$form['displays']['block']['options']['style']; + $style_form['style_plugin']['#default_value'] = views_ui_get_selected($form_state, array('block', 'style', 'style_plugin'), 'default', $style_form['style_plugin']); + // Changing this dropdown updates $form['displays']['block']['options'] via + // AJAX. + views_ui_add_ajax_trigger($style_form, 'style_plugin', array('displays', 'block', 'options')); + + $this->build_form_style($form, $form_state, 'block'); + $form['displays']['block']['options']['items_per_page'] = array( + '#title' => t('Items per page'), + '#type' => 'textfield', + '#default_value' => '5', + '#size' => 5, + '#element_validate' => array('views_element_validate_integer'), + ); + $form['displays']['block']['options']['pager'] = array( + '#title' => t('Use a pager'), + '#type' => 'checkbox', + '#default_value' => FALSE, + ); + + return $form; + } + + /** + * Build the part of the form that builds the display format options. + */ + protected function build_form_style(&$form, &$form_state, $type) { + $style_form =& $form['displays'][$type]['options']['style']; + $style = $style_form['style_plugin']['#default_value']; + $style_plugin = views_get_plugin('style', $style); + if (isset($style_plugin) && $style_plugin->uses_row_plugin()) { + $options = $this->row_style_options($type); + $style_form['row_plugin'] = array( + '#type' => 'select', + '#title' => t('of'), + '#options' => $options, + '#access' => count($options) > 1, + ); + // For the block display, the default value should be "titles (linked)", + // if it's available (since that's the most common use case). + $block_with_linked_titles_available = ($type == 'block' && isset($options['titles_linked'])); + $default_value = $block_with_linked_titles_available ? 'titles_linked' : key($options); + $style_form['row_plugin']['#default_value'] = views_ui_get_selected($form_state, array($type, 'style', 'row_plugin'), $default_value, $style_form['row_plugin']); + // Changing this dropdown updates the individual row options via AJAX. + views_ui_add_ajax_trigger($style_form, 'row_plugin', array('displays', $type, 'options', 'style', 'row_options')); + + // This is the region that can be updated by AJAX. The base class doesn't + // add anything here, but child classes can. + $style_form['row_options'] = array( + '#theme_wrappers' => array('container'), + ); + } + elseif ($style_plugin->uses_fields()) { + $style_form['row_plugin'] = array('#markup' => '' . t('of fields') . ''); + } + } + + /** + * Add possible row style options. + * + * Per default use fields with base field. + */ + protected function row_style_options($type) { + $data = views_fetch_data($this->base_table); + // Get all available row plugins by default. + $options = views_fetch_plugin_names('row', 'normal', array($this->base_table)); + return $options; + } + + /** + * Build the part of the form that allows the user to select the view's filters. + * + * By default, this adds "of type" and "tagged with" filters (when they are + * available). + */ + protected function build_filters(&$form, &$form_state) { + // Find all the fields we are allowed to filter by. + $fields = views_fetch_fields($this->base_table, 'filter'); + + $entity_info = $this->entity_info; + // If the current base table support bundles and has more than one (like user). + if (isset($entity_info['bundle keys']) && isset($entity_info['bundles'])) { + // Get all bundles and their human readable names. + $options = array('all' => t('All')); + foreach ($entity_info['bundles'] as $type => $bundle) { + $options[$type] = $bundle['label']; + } + $form['displays']['show']['type'] = array( + '#type' => 'select', + '#title' => t('of type'), + '#options' => $options, + ); + $selected_bundle = views_ui_get_selected($form_state, array('show', 'type'), 'all', $form['displays']['show']['type']); + $form['displays']['show']['type']['#default_value'] = $selected_bundle; + // Changing this dropdown updates the entire content of $form['displays'] + // via AJAX, since each bundle might have entirely different fields + // attached to it, etc. + views_ui_add_ajax_trigger($form['displays']['show'], 'type', array('displays')); + } + + // Check if we are allowed to filter by taxonomy, and if so, add the + // "tagged with" filter to the view. + // + // We construct this filter using taxonomy_index.tid (which limits the + // filtering to a specific vocabulary) rather than taxonomy_term_data.name + // (which matches terms in any vocabulary). This is because it is a more + // commonly-used filter that works better with the autocomplete UI, and + // also to avoid confusion with other vocabularies on the site that may + // have terms with the same name but are not used for free tagging. + // + // The downside is that if there *is* more than one vocabulary on the site + // that is used for free tagging, the wizard will only be able to make the + // "tagged with" filter apply to one of them (see below for the method it + // uses to choose). + if (isset($fields['taxonomy_index.tid'])) { + // Check if this view will be displaying fieldable entities. + if (!empty($entity_info['fieldable'])) { + // Find all "tag-like" taxonomy fields associated with the view's + // entities. If a particular entity type (i.e., bundle) has been + // selected above, then we only search for taxonomy fields associated + // with that bundle. Otherwise, we use all bundles. + $bundles = array_keys($entity_info['bundles']); + // Double check that this is a real bundle before using it (since above + // we added a dummy option 'all' to the bundle list on the form). + if (isset($selected_bundle) && in_array($selected_bundle, $bundles)) { + $bundles = array($selected_bundle); + } + $tag_fields = array(); + foreach ($bundles as $bundle) { + foreach (field_info_instances($this->entity_type, $bundle) as $instance) { + // We define "tag-like" taxonomy fields as ones that use the + // "Autocomplete term widget (tagging)" widget. + if ($instance['widget']['type'] == 'taxonomy_autocomplete') { + $tag_fields[] = $instance['field_name']; + } + } + } + $tag_fields = array_unique($tag_fields); + if (!empty($tag_fields)) { + // If there is more than one "tag-like" taxonomy field available to + // the view, we can only make our filter apply to one of them (as + // described above). We choose 'field_tags' if it is available, since + // that is created by the Standard install profile in core and also + // commonly used by contrib modules; thus, it is most likely to be + // associated with the "main" free-tagging vocabulary on the site. + if (in_array('field_tags', $tag_fields)) { + $tag_field_name = 'field_tags'; + } + else { + $tag_field_name = reset($tag_fields); + } + // Add the autocomplete textfield to the wizard. + $form['displays']['show']['tagged_with'] = array( + '#type' => 'textfield', + '#title' => t('tagged with'), + '#autocomplete_path' => 'taxonomy/autocomplete/' . $tag_field_name, + '#size' => 30, + '#maxlength' => 1024, + '#field_name' => $tag_field_name, + '#element_validate' => array('views_ui_taxonomy_autocomplete_validate'), + ); + } + } + } + } + + /** + * Build the part of the form that allows the user to select the view's sort order. + * + * By default, this adds a "sorted by [date]" filter (when it is available). + */ + protected function build_sorts(&$form, &$form_state) { + $sorts = array( + 'none' => t('Unsorted'), + ); + // Check if we are allowed to sort by creation date. + if (!empty($this->plugin['created_column'])) { + $sorts += array( + $this->plugin['created_column'] . ':DESC' => t('Newest first'), + $this->plugin['created_column'] . ':ASC' => t('Oldest first'), + ); + } + if (isset($this->plugin['available_sorts'])) { + $sorts += $this->plugin['available_sorts']; + } + + // If there is no sorts option available continue. + if (!empty($sorts)) { + $form['displays']['show']['sort'] = array( + '#type' => 'select', + '#title' => t('sorted by'), + '#options' => $sorts, + '#default_value' => isset($this->plugin['created_column']) ? $this->plugin['created_column'] . ':DESC' : 'none', + ); + } + } + + protected function instantiate_view($form, &$form_state) { + // Build the basic view properties. + $view = views_new_view(); + $view->name = $form_state['values']['name']; + $view->human_name = $form_state['values']['human_name']; + $view->description = $form_state['values']['description']; + $view->tag = 'default'; + $view->core = VERSION; + $view->base_table = $this->base_table; + + // Build all display options for this view. + $display_options = $this->build_display_options($form, $form_state); + + // Allow the fully built options to be altered. This happens before adding + // the options to the view, so that once they are eventually added we will + // be able to get all the overrides correct. + $this->alter_display_options($display_options, $form, $form_state); + + $this->add_displays($view, $display_options, $form, $form_state); + + return $view; + } + + /** + * Build an array of display options for the view. + * + * @return + * An array whose keys are the names of each display and whose values are + * arrays of options for that display. + */ + protected function build_display_options($form, $form_state) { + // Display: Master + $display_options['default'] = $this->default_display_options($form, $form_state); + $display_options['default'] += array( + 'filters' => array(), + 'sorts' => array(), + ); + $display_options['default']['filters'] += $this->default_display_filters($form, $form_state); + $display_options['default']['sorts'] += $this->default_display_sorts($form, $form_state); + + // Display: Page + if (!empty($form_state['values']['page']['create'])) { + $display_options['page'] = $this->page_display_options($form, $form_state); + + // Display: Feed (attached to the page) + if (!empty($form_state['values']['page']['feed'])) { + $display_options['feed'] = $this->page_feed_display_options($form, $form_state); + } + } + + // Display: Block + if (!empty($form_state['values']['block']['create'])) { + $display_options['block'] = $this->block_display_options($form, $form_state); + } + + return $display_options; + } + + /** + * Alter the full array of display options before they are added to the view. + */ + protected function alter_display_options(&$display_options, $form, $form_state) { + // If any of the displays use jump menus, we want to add fields to the view + // that store the path that will be used in the jump menu. The fields to + // use for this are defined by the plugin. + if (isset($this->plugin['path_field'])) { + $path_field = $this->plugin['path_field']; + $path_fields_added = FALSE; + foreach ($display_options as $display_type => $options) { + if (!empty($options['style_plugin']) && $options['style_plugin'] == 'jump_menu') { + // Regardless of how many displays have jump menus, we only need to + // add a single set of path fields to the view. + if (!$path_fields_added) { + // The plugin might provide supplemental fields that it needs to + // generate the path (for example, node revisions need the node ID + // as well as the revision ID). We need to add these first so they + // are available as replacement patterns in the main path field. + $path_fields = !empty($this->plugin['path_fields_supplemental']) ? $this->plugin['path_fields_supplemental'] : array(); + $path_fields[] = &$path_field; + + // Generate a unique ID for each field so we don't overwrite + // existing ones. + foreach ($path_fields as &$field) { + $field['id'] = view::generate_item_id($field['id'], $display_options['default']['fields']); + $display_options['default']['fields'][$field['id']] = $field; + } + + $path_fields_added = TRUE; + } + + // Configure the style plugin to use the path field to generate the + // jump menu path. + $display_options[$display_type]['style_options']['path'] = $path_field['id']; + } + } + } + + // If any of the displays use the table style, take sure that the fields + // always have a labels by unsetting the override. + foreach ($display_options as &$options) { + if ($options['style_plugin'] == 'table') { + foreach ($display_options['default']['fields'] as &$field) { + unset($field['label']); + } + } + } + } + + /** + * Add the array of display options to the view, with appropriate overrides. + */ + protected function add_displays($view, $display_options, $form, $form_state) { + // Display: Master + $default_display = $view->new_display('default', 'Master', 'default'); + foreach ($display_options['default'] as $option => $value) { + $default_display->set_option($option, $value); + } + + // Display: Page + if (isset($display_options['page'])) { + $display = $view->new_display('page', 'Page', 'page'); + // The page display is usually the main one (from the user's point of + // view). Its options should therefore become the overall view defaults, + // so that new displays which are added later automatically inherit them. + $this->set_default_options($display_options['page'], $display, $default_display); + + // Display: Feed (attached to the page) + if (isset($display_options['feed'])) { + $display = $view->new_display('feed', 'Feed', 'feed'); + $this->set_override_options($display_options['feed'], $display, $default_display); + } + } + + // Display: Block + if (isset($display_options['block'])) { + $display = $view->new_display('block', 'Block', 'block'); + // When there is no page, the block display options should become the + // overall view defaults. + if (!isset($display_options['page'])) { + $this->set_default_options($display_options['block'], $display, $default_display); + } + else { + $this->set_override_options($display_options['block'], $display, $default_display); + } + } + } + + /** + * Most subclasses will need to override this method to provide some fields + * or a different row plugin. + */ + protected function default_display_options($form, $form_state) { + $display_options = array(); + $display_options['access']['type'] = 'none'; + $display_options['cache']['type'] = 'none'; + $display_options['query']['type'] = 'views_query'; + $display_options['exposed_form']['type'] = 'basic'; + $display_options['pager']['type'] = 'full'; + $display_options['style_plugin'] = 'default'; + $display_options['row_plugin'] = 'fields'; + + // Add a least one field so the view validates and the user has already a preview. + // Therefore the basefield could provide 'defaults][field]' in it's base settings. + // If there is nothing like this choose the first field with a field handler. + $data = views_fetch_data($this->base_table); + if (isset($data['table']['base']['defaults']['field'])) { + $field = $data['table']['base']['defaults']['field']; + } + else { + foreach ($data as $field => $field_data) { + if (isset($field_data['field']['handler'])) { + break; + } + } + } + $display_options['fields'][$field] = array( + 'table' => $this->base_table, + 'field' => $field, + 'id' => $field, + ); + + return $display_options; + } + + protected function default_display_filters($form, $form_state) { + $filters = array(); + + // Add any filters provided by the plugin. + if (isset($this->plugin['filters'])) { + foreach ($this->plugin['filters'] as $name => $info) { + $filters[$name] = $info; + } + } + + // Add any filters specified by the user when filling out the wizard. + $filters = array_merge($filters, $this->default_display_filters_user($form, $form_state)); + + return $filters; + } + + protected function default_display_filters_user($form, $form_state) { + $filters = array(); + + if (!empty($form_state['values']['show']['type']) && $form_state['values']['show']['type'] != 'all') { + $bundle_key = $this->entity_info['bundle keys']['bundle']; + // Figure out the table where $bundle_key lives. It may not be the same as + // the base table for the view; the taxonomy vocabulary machine_name, for + // example, is stored in taxonomy_vocabulary, not taxonomy_term_data. + $fields = views_fetch_fields($this->base_table, 'filter'); + if (isset($fields[$this->base_table . '.' . $bundle_key])) { + $table = $this->base_table; + } + else { + foreach ($fields as $field_name => $value) { + if ($pos = strpos($field_name, '.' . $bundle_key)) { + $table = substr($field_name, 0, $pos); + break; + } + } + } + $table_data = views_fetch_data($table); + // Check whether the bundle key filter handler is or an child of it views_handler_filter_in_operator + // If it's not just use a single value instead of an array. + $handler = $table_data[$bundle_key]['filter']['handler']; + if ($handler == 'views_handler_filter_in_operator' || is_subclass_of($handler, 'views_handler_filter_in_operator')) { + $value = drupal_map_assoc(array($form_state['values']['show']['type'])); + } + else { + $value = $form_state['values']['show']['type']; + } + + $filters[$bundle_key] = array( + 'id' => $bundle_key, + 'table' => $table, + 'field' => $bundle_key, + 'value' => $value, + ); + } + + // @todo: Figure out why this isn't part of node_views_wizard. + if (!empty($form_state['values']['show']['tagged_with']['tids'])) { + $filters['tid'] = array( + 'id' => 'tid', + 'table' => 'taxonomy_index', + 'field' => 'tid', + 'value' => $form_state['values']['show']['tagged_with']['tids'], + 'vocabulary' => $form_state['values']['show']['tagged_with']['vocabulary'], + ); + // If the user entered more than one valid term in the autocomplete + // field, they probably intended both of them to be applied. + if (count($form_state['values']['show']['tagged_with']['tids']) > 1) { + $filters['tid']['operator'] = 'and'; + // Sort the terms so the filter will be displayed as it normally would + // on the edit screen. + sort($filters['tid']['value']); + } + } + + return $filters; + } + + protected function default_display_sorts($form, $form_state) { + $sorts = array(); + + // Add any sorts provided by the plugin. + if (isset($this->plugin['sorts'])) { + foreach ($this->plugin['sorts'] as $name => $info) { + $sorts[$name] = $info; + } + } + + // Add any sorts specified by the user when filling out the wizard. + $sorts = array_merge($sorts, $this->default_display_sorts_user($form, $form_state)); + + return $sorts; + } + + protected function default_display_sorts_user($form, $form_state) { + $sorts = array(); + + // Don't add a sort if there is no form value or the user selected none as sort. + if (!empty($form_state['values']['show']['sort']) && $form_state['values']['show']['sort'] != 'none') { + list($column, $sort) = explode(':', $form_state['values']['show']['sort']); + // Column either be a column-name or the table-columnn-ame. + $column = explode('-', $column); + if (count($column) > 1) { + $table = $column[0]; + $column = $column[1]; + } + else { + $table = $this->base_table; + $column = $column[0]; + } + + $sorts[$column] = array( + 'id' => $column, + 'table' => $table, + 'field' => $column, + 'order' => $sort, + ); + } + + return $sorts; + } + + protected function page_display_options($form, $form_state) { + $display_options = array(); + $page = $form_state['values']['page']; + $display_options['title'] = $page['title']; + $display_options['path'] = $page['path']; + $display_options['style_plugin'] = $page['style']['style_plugin']; + // Not every style plugin supports row style plugins. + $display_options['row_plugin'] = isset($page['style']['row_plugin']) ? $page['style']['row_plugin'] : 'fields'; + if (empty($page['items_per_page'])) { + $display_options['pager']['type'] = 'none'; + } + elseif ($page['pager']) { + $display_options['pager']['type'] = 'full'; + } + else { + $display_options['pager']['type'] = 'some'; + } + $display_options['pager']['options']['items_per_page'] = $page['items_per_page']; + if (!empty($page['link'])) { + $display_options['menu']['type'] = 'normal'; + $display_options['menu']['title'] = $page['link_properties']['title']; + $display_options['menu']['name'] = $page['link_properties']['menu_name']; + } + return $display_options; + } + + protected function block_display_options($form, $form_state) { + $display_options = array(); + $block = $form_state['values']['block']; + $display_options['title'] = $block['title']; + $display_options['style_plugin'] = $block['style']['style_plugin']; + $display_options['row_plugin'] = isset($block['style']['row_plugin']) ? $block['style']['row_plugin'] : 'fields'; + $display_options['pager']['type'] = $block['pager'] ? 'full' : (empty($block['items_per_page']) ? 'none' : 'some'); + $display_options['pager']['options']['items_per_page'] = $block['items_per_page']; + return $display_options; + } + + protected function page_feed_display_options($form, $form_state) { + $display_options = array(); + $display_options['pager']['type'] = 'some'; + $display_options['style_plugin'] = 'rss'; + $display_options['row_plugin'] = $form_state['values']['page']['feed_properties']['row_plugin']; + $display_options['path'] = $form_state['values']['page']['feed_properties']['path']; + $display_options['title'] = $form_state['values']['page']['title']; + $display_options['displays'] = array( + 'default' => 'default', + 'page' => 'page', + ); + return $display_options; + } + + /** + * Sets options for a display and makes them the default options if possible. + * + * This function can be used to set options for a display when it is desired + * that the options also become the defaults for the view whenever possible. + * This should be done for the "primary" display created in the view wizard, + * so that new displays which the user adds later will be similar to this + * one. + * + * @param $options + * An array whose keys are the name of each option and whose values are the + * desired values to set. + * @param $display + * The display which the options will be applied to. The default display + * will actually be assigned the options (and this display will inherit + * them) when possible. + * @param $default_display + * The default display, which will store the options when possible. + */ + protected function set_default_options($options, $display, $default_display) { + foreach ($options as $option => $value) { + // If the default display supports this option, set the value there. + // Otherwise, set it on the provided display. + $default_value = $default_display->get_option($option); + if (isset($default_value)) { + $default_display->set_option($option, $value); + } + else { + $display->set_option($option, $value); + } + } + } + + /** + * Sets options for a display, inheriting from the defaults when possible. + * + * This function can be used to set options for a display when it is desired + * that the options inherit from the default display whenever possible. This + * avoids setting too many options as overrides, which will be harder for the + * user to modify later. For example, if $this->set_default_options() was + * previously called on a page display and then this function is called on a + * block display, and if the user entered the same title for both displays in + * the views wizard, then the view will wind up with the title stored as the + * default (with the page and block both inheriting from it). + * + * @param $options + * An array whose keys are the name of each option and whose values are the + * desired values. + * @param $display + * The display which the options will apply to. It will get the options by + * inheritance from the default display when possible. + * @param $default_display + * The default display, from which the options will be inherited when + * possible. + */ + protected function set_override_options($options, $display, $default_display) { + foreach ($options as $option => $value) { + // Only override the default value if it is different from the value that + // was provided. + $default_value = $default_display->get_option($option); + if (!isset($default_value)) { + $display->set_option($option, $value); + } + elseif ($default_value !== $value) { + $display->override_option($option, $value); + } + } + } + + protected function retrieve_validated_view($form, $form_state, $unset = TRUE) { + $key = hash('sha256', serialize($form_state['values'])); + $view = (isset($this->validated_views[$key]) ? $this->validated_views[$key] : NULL); + if ($unset) { + unset($this->validated_views[$key]); + } + return $view; + } + + protected function set_validated_view($form, $form_state, $view) { + $key = hash('sha256', serialize($form_state['values'])); + $this->validated_views[$key] = $view; + } + + /** + * Instantiates a view and validates values. + */ + function validate($form, &$form_state) { + $view = $this->instantiate_view($form, $form_state); + $errors = $view->validate(); + if (!is_array($errors) || empty($errors)) { + $this->set_validated_view($form, $form_state, $view); + return array(); + } + return $errors; + } + + /** + * Create a View from values that have been already submitted to validate(). + * + * @throws ViewsWizardException if the values have not been validated. + */ + function create_view($form, &$form_state) { + $view = $this->retrieve_validated_view($form, $form_state); + if (empty($view)) { + throw new ViewsWizardException(t('Attempted to create_view with values that have not been validated')); + } + return $view; + } + +} diff --git a/plugins/views_wizard/views_ui_comment_views_wizard.class.php b/plugins/views_wizard/views_ui_comment_views_wizard.class.php new file mode 100644 index 00000000..09d4994e --- /dev/null +++ b/plugins/views_wizard/views_ui_comment_views_wizard.class.php @@ -0,0 +1,107 @@ + 'select', + '#title_display' => 'invisible', + '#title' => t('Should links be displayed below each comment'), + '#options' => array( + 1 => t('with links (allow users to reply to the comment, etc.)'), + 0 => t('without links'), + ), + '#default_value' => 1, + ); + break; + } + } + + protected function page_display_options($form, $form_state) { + $display_options = parent::page_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['page']['style']['row_plugin']) ? $form_state['values']['page']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['page']['style']['row_options']) ? $form_state['values']['page']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + protected function block_display_options($form, $form_state) { + $display_options = parent::block_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['block']['style']['row_plugin']) ? $form_state['values']['block']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['block']['style']['row_options']) ? $form_state['values']['block']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + /** + * Set the row style and row style plugins to the display_options. + */ + protected function display_options_row(&$display_options, $row_plugin, $row_options) { + switch ($row_plugin) { + case 'comment': + $display_options['row_plugin'] = 'comment'; + $display_options['row_options']['links'] = !empty($row_options['links']); + break; + } + } + + protected function default_display_options($form, $form_state) { + $display_options = parent::default_display_options($form, $form_state); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + + // Add a relationship to nodes. + $display_options['relationships']['nid']['id'] = 'nid'; + $display_options['relationships']['nid']['table'] = 'comment'; + $display_options['relationships']['nid']['field'] = 'nid'; + $display_options['relationships']['nid']['required'] = 1; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + /* Field: Comment: Title */ + $display_options['fields']['subject']['id'] = 'subject'; + $display_options['fields']['subject']['table'] = 'comment'; + $display_options['fields']['subject']['field'] = 'subject'; + $display_options['fields']['subject']['label'] = ''; + $display_options['fields']['subject']['alter']['alter_text'] = 0; + $display_options['fields']['subject']['alter']['make_link'] = 0; + $display_options['fields']['subject']['alter']['absolute'] = 0; + $display_options['fields']['subject']['alter']['trim'] = 0; + $display_options['fields']['subject']['alter']['word_boundary'] = 0; + $display_options['fields']['subject']['alter']['ellipsis'] = 0; + $display_options['fields']['subject']['alter']['strip_tags'] = 0; + $display_options['fields']['subject']['alter']['html'] = 0; + $display_options['fields']['subject']['hide_empty'] = 0; + $display_options['fields']['subject']['empty_zero'] = 0; + $display_options['fields']['subject']['link_to_comment'] = 1; + + return $display_options; + } +} diff --git a/plugins/views_wizard/views_ui_file_managed_views_wizard.class.php b/plugins/views_wizard/views_ui_file_managed_views_wizard.class.php new file mode 100644 index 00000000..111b6315 --- /dev/null +++ b/plugins/views_wizard/views_ui_file_managed_views_wizard.class.php @@ -0,0 +1,40 @@ + 'select', + '#title_display' => 'invisible', + '#title' => t('Should links be displayed below each node'), + '#options' => array( + 1 => t('with links (allow users to add comments, etc.)'), + 0 => t('without links'), + ), + '#default_value' => 1, + ); + $style_form['row_options']['comments'] = array( + '#type' => 'select', + '#title_display' => 'invisible', + '#title' => t('Should comments be displayed below each node'), + '#options' => array( + 1 => t('with comments'), + 0 => t('without comments'), + ), + '#default_value' => 0, + ); + break; + } + } + + /** + * @override + */ + protected function default_display_options($form, $form_state) { + $display_options = parent::default_display_options($form, $form_state); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + // Add the title field, so that the display has content if the user switches + // to a row style that uses fields. + /* Field: Content: Title */ + $display_options['fields']['title']['id'] = 'title'; + $display_options['fields']['title']['table'] = 'node'; + $display_options['fields']['title']['field'] = 'title'; + $display_options['fields']['title']['label'] = ''; + $display_options['fields']['title']['alter']['alter_text'] = 0; + $display_options['fields']['title']['alter']['make_link'] = 0; + $display_options['fields']['title']['alter']['absolute'] = 0; + $display_options['fields']['title']['alter']['trim'] = 0; + $display_options['fields']['title']['alter']['word_boundary'] = 0; + $display_options['fields']['title']['alter']['ellipsis'] = 0; + $display_options['fields']['title']['alter']['strip_tags'] = 0; + $display_options['fields']['title']['alter']['html'] = 0; + $display_options['fields']['title']['hide_empty'] = 0; + $display_options['fields']['title']['empty_zero'] = 0; + $display_options['fields']['title']['link_to_node'] = 1; + + return $display_options; + } + + protected function page_display_options($form, $form_state) { + $display_options = parent::page_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['page']['style']['row_plugin']) ? $form_state['values']['page']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['page']['style']['row_options']) ? $form_state['values']['page']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + protected function block_display_options($form, $form_state) { + $display_options = parent::block_display_options($form, $form_state); + $row_plugin = isset($form_state['values']['block']['style']['row_plugin']) ? $form_state['values']['block']['style']['row_plugin'] : NULL; + $row_options = isset($form_state['values']['block']['style']['row_options']) ? $form_state['values']['block']['style']['row_options'] : array(); + $this->display_options_row($display_options, $row_plugin, $row_options); + return $display_options; + } + + /** + * Set the row style and row style plugins to the display_options. + */ + protected function display_options_row(&$display_options, $row_plugin, $row_options) { + switch ($row_plugin) { + case 'full_posts': + $display_options['row_plugin'] = 'node'; + $display_options['row_options']['build_mode'] = 'full'; + $display_options['row_options']['links'] = !empty($row_options['links']); + $display_options['row_options']['comments'] = !empty($row_options['comments']); + break; + case 'teasers': + $display_options['row_plugin'] = 'node'; + $display_options['row_options']['build_mode'] = 'teaser'; + $display_options['row_options']['links'] = !empty($row_options['links']); + $display_options['row_options']['comments'] = !empty($row_options['comments']); + break; + case 'titles_linked': + $display_options['row_plugin'] = 'fields'; + $display_options['field']['title']['link_to_node'] = 1; + break; + case 'titles': + $display_options['row_plugin'] = 'fields'; + $display_options['field']['title']['link_to_node'] = 0; + break; + } + } +} diff --git a/plugins/views_wizard/views_ui_taxonomy_term_views_wizard.class.php b/plugins/views_wizard/views_ui_taxonomy_term_views_wizard.class.php new file mode 100644 index 00000000..4f05548b --- /dev/null +++ b/plugins/views_wizard/views_ui_taxonomy_term_views_wizard.class.php @@ -0,0 +1,41 @@ + 'Tests handler argument_comment_user_uid', + 'description' => 'Tests the user posted or commented argument handler', + 'group' => 'Views Modules', + ); + } + + /** + * Post comment. + * + * @param $node + * Node to post comment on. + * @param $comment + * Comment to save + */ + function postComment($node, $comment = array()) { + $comment += array( + 'uid' => $this->loggedInUser->uid, + 'nid' => $node->nid, + 'cid' => '', + 'pid' => '', + ); + return comment_save((object) $comment); + } + + function setUp() { + parent::setUp(); + + // Add two users, create a node with the user1 as author and another node with user2 as author. + // For the second node add a comment from user1. + $this->account = $this->drupalCreateUser(); + $this->account2 = $this->drupalCreateUser(); + $this->drupalLogin($this->account); + $this->node_user_posted = $this->drupalCreateNode(); + $this->node_user_commented = $this->drupalCreateNode(array('uid' => $this->account2->uid)); + $this->postComment($this->node_user_commented); + } + + function testCommentUserUidTest() { + $view = $this->view_comment_user_uid(); + + + $this->executeView($view, array($this->account->uid)); + $resultset = array( + array( + 'nid' => $this->node_user_posted->nid, + ), + array( + 'nid' => $this->node_user_commented->nid, + ), + ); + $this->column_map = array('nid' => 'nid'); + debug($view->result); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function view_comment_user_uid() { + $view = new view; + $view->name = 'test_comment_user_uid'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'test_comment_user_uid'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + /* Field: Content: nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + /* Contextual filter: Content: User posted or commented */ + $handler->display->display_options['arguments']['uid_touch']['id'] = 'uid_touch'; + $handler->display->display_options['arguments']['uid_touch']['table'] = 'node'; + $handler->display->display_options['arguments']['uid_touch']['field'] = 'uid_touch'; + $handler->display->display_options['arguments']['uid_touch']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['uid_touch']['default_argument_skip_url'] = 0; + $handler->display->display_options['arguments']['uid_touch']['summary']['number_of_records'] = '0'; + $handler->display->display_options['arguments']['uid_touch']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['uid_touch']['summary_options']['items_per_page'] = '25'; + + return $view; + } +} diff --git a/tests/comment/views_handler_filter_comment_user_uid.test b/tests/comment/views_handler_filter_comment_user_uid.test new file mode 100644 index 00000000..1f3f75cd --- /dev/null +++ b/tests/comment/views_handler_filter_comment_user_uid.test @@ -0,0 +1,41 @@ + 'Tests handler filter_comment_user_uid', + 'description' => 'Tests the user posted or commented filter handler', + 'group' => 'Views Modules', + ); + } + + /** + * Override the view from the argument test case to remove the argument and + * add filter with the uid as the value. + */ + function view_comment_user_uid() { + $view = parent::view_comment_user_uid(); + // Remove the argument. + $view->set_item('default', 'argument', 'uid_touch', NULL); + + $options = array( + 'id' => 'uid_touch', + 'table' => 'node', + 'field' => 'uid_touch', + 'value' => array($this->loggedInUser->uid), + ); + $view->add_item('default', 'filter', 'node', 'uid_touch', $options); + + return $view; + } +} diff --git a/tests/field/views_fieldapi.test b/tests/field/views_fieldapi.test new file mode 100644 index 00000000..da4c27b3 --- /dev/null +++ b/tests/field/views_fieldapi.test @@ -0,0 +1,494 @@ +drupalCreateRole($permissions))) { + return FALSE; + } + + // Create a user assigned to that role. + $edit = array(); + $edit['name'] = $this->randomName(); + $edit['mail'] = $edit['name'] . '@example.com'; + $edit['roles'] = array($rid => $rid); + $edit['pass'] = user_password(); + $edit['status'] = 1; + $edit += $extra_edit; + + $account = user_save(drupal_anonymous_user(), $edit); + + $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login')); + if (empty($account->uid)) { + return FALSE; + } + + // Add the raw password so that we can log in as this user. + $account->pass_raw = $edit['pass']; + return $account; + } + + function setUpFields($amount = 3) { + // Create three fields. + $field_names = array(); + for ($i = 0; $i < $amount; $i++) { + $field_names[$i] = 'field_name_' . $i; + $field = array('field_name' => $field_names[$i], 'type' => 'text'); + + $this->fields[$i] = $field = field_create_field($field); + } + return $field_names; + } + + function setUpInstances($bundle = 'page') { + foreach ($this->fields as $key => $field) { + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => 'node', + 'bundle' => 'page', + ); + $this->instances[$key] = field_create_instance($instance); + } + } + + /** + * Clear all views caches and static caches which are required for the patch. + */ + function clearViewsCaches() { + // Reset views data cache. + drupal_static_reset('_views_fetch_data_cache'); + drupal_static_reset('_views_fetch_data_recursion_protected'); + drupal_static_reset('_views_fetch_data_fully_loaded'); + } +} + +/** + * Test the produced views_data. + */ +class viewsFieldApiDataTest extends ViewsFieldApiTestHelper { + /** + * Stores the fields for this test case. + */ + var $fields; + + public static function getInfo() { + return array( + 'name' => 'Fieldapi: Views Data', + 'description' => 'Tests the fieldapi views data.', + 'group' => 'Views Modules', + ); + } + + function setUp() { + parent::setUp(); + + $langcode = LANGUAGE_NONE; + + + $field_names = $this->setUpFields(); + + // The first one will be attached to nodes only. + $instance = array( + 'field_name' => $field_names[0], + 'entity_type' => 'node', + 'bundle' => 'page', + ); + field_create_instance($instance); + + // The second one will be attached to users only. + $instance = array( + 'field_name' => $field_names[1], + 'entity_type' => 'user', + 'bundle' => 'user', + ); + field_create_instance($instance); + + // The third will be attached to both nodes and users. + $instance = array( + 'field_name' => $field_names[2], + 'entity_type' => 'node', + 'bundle' => 'page', + ); + field_create_instance($instance); + $instance = array( + 'field_name' => $field_names[2], + 'entity_type' => 'user', + 'bundle' => 'user', + ); + field_create_instance($instance); + + // Now create some example nodes/users for the view result. + for ($i = 0; $i < 5; $i++) { + $edit = array( + // @TODO Write a helper method to create such values. + 'field_name_0' => array($langcode => array((array('value' => $this->randomName())))), + 'field_name_2' => array($langcode => array((array('value' => $this->randomName())))), + ); + $this->nodes[] = $this->drupalCreateNode($edit); + } + + for ($i = 0; $i < 5; $i++) { + $edit = array( + 'field_name_1' => array($langcode => array((array('value' => $this->randomName())))), + 'field_name_2' => array($langcode => array((array('value' => $this->randomName())))), + ); + $this->users[] = $this->CreateUser($edit); + } + + // Reset views data cache. + $this->clearViewsCaches(); + } + + /** + * Unit testing the views data structure. + * + * We check data structure for both node and node revision tables. + */ + function testViewsData() { + $data = views_fetch_data(); + + // Check the table and the joins of the first field. + // Attached to node only. + $field = $this->fields[0]; + $current_table = _field_sql_storage_tablename($field); + $revision_table = _field_sql_storage_revision_tablename($field); + + $this->assertTrue(isset($data[$current_table])); + $this->assertTrue(isset($data[$revision_table])); + // The node field should join against node. + $this->assertTrue(isset($data[$current_table]['table']['join']['node'])); + $this->assertTrue(isset($data[$revision_table]['table']['join']['node_revision'])); + + $expected_join = array( + 'left_field' => 'nid', + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + $this->assertEqual($expected_join, $data[$current_table]['table']['join']['node']); + $expected_join = array( + 'left_field' => 'vid', + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + $this->assertEqual($expected_join, $data[$revision_table]['table']['join']['node_revision']); + + + // Check the table and the joins of the second field. + // Attached to both node and user. + $field_2 = $this->fields[2]; + $current_table_2 = _field_sql_storage_tablename($field_2); + $revision_table_2 = _field_sql_storage_revision_tablename($field_2); + + $this->assertTrue(isset($data[$current_table_2])); + $this->assertTrue(isset($data[$revision_table_2])); + // The second field should join against both node and users. + $this->assertTrue(isset($data[$current_table_2]['table']['join']['node'])); + $this->assertTrue(isset($data[$revision_table_2]['table']['join']['node_revision'])); + $this->assertTrue(isset($data[$current_table_2]['table']['join']['users'])); + + $expected_join = array( + 'left_field' => 'nid', + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ) + ); + $this->assertEqual($expected_join, $data[$current_table_2]['table']['join']['node']); + $expected_join = array( + 'left_field' => 'vid', + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'node'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ) + ); + $this->assertEqual($expected_join, $data[$revision_table_2]['table']['join']['node_revision']); + $expected_join = array( + 'left_field' => 'uid', + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'entity_type', 'value' => 'user'), + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ) + ); + $this->assertEqual($expected_join, $data[$current_table_2]['table']['join']['users']); + + // Check the fields + // @todo + + // Check the arguments + // @todo + + // Check the sort criterias + // @todo + + // Check the relationships + // @todo + + } +} + +/** + * Tests the field_field handler. + * @TODO + * Check a entity-type with bundles + * Check a entity-type without bundles + * Check locale:disabled, locale:enabled and locale:enabled with another language + * Check revisions + */ +class viewsHandlerFieldFieldTest extends ViewsFieldApiTestHelper { + public $nodes; + + public static function getInfo() { + return array( + 'name' => 'Fieldapi: Field handler', + 'description' => 'Tests the field itself of the fieldapi integration', + 'group' => 'Views Modules' + ); + } + + protected function setUp() { + parent::setUp(); + + // Setup basic fields. + $this->setUpFields(3); + + // Setup a field with cardinality > 1. + $this->fields[3] = $field = field_create_field(array('field_name' => 'field_name_3', 'type' => 'text', 'cardinality' => FIELD_CARDINALITY_UNLIMITED)); + // Setup a field that will have no value. + $this->fields[4] = $field = field_create_field(array('field_name' => 'field_name_4', 'type' => 'text', 'cardinality' => FIELD_CARDINALITY_UNLIMITED)); + + $this->setUpInstances(); + + $this->clearViewsCaches(); + + // Create some nodes. + $this->nodes = array(); + for ($i = 0; $i < 3; $i++) { + $edit = array('type' => 'page'); + + for ($key = 0; $key < 3; $key++) { + $field = $this->fields[$key]; + $edit[$field['field_name']][LANGUAGE_NONE][0]['value'] = $this->randomName(8); + } + for ($j = 0; $j < 5; $j++) { + $edit[$this->fields[3]['field_name']][LANGUAGE_NONE][$j]['value'] = $this->randomName(8); + } + // Set this field to be empty. + $edit[$this->fields[4]['field_name']] = array(); + + $this->nodes[$i] = $this->drupalCreateNode($edit); + } + } + + public function testFieldRender() { + $this->_testSimpleFieldRender(); + $this->_testFormatterSimpleFieldRender(); + $this->_testMultipleFieldRender(); + } + + public function _testSimpleFieldRender() { + $view = $this->getFieldView(); + $this->executeView($view); + + // Tests that the rendered fields match the actual value of the fields. + for ($i = 0; $i < 3; $i++) { + for ($key = 0; $key < 2; $key++) { + $field = $this->fields[$key]; + $rendered_field = $view->style_plugin->get_field($i, $field['field_name']); + $expected_field = $this->nodes[$i]->{$field['field_name']}[LANGUAGE_NONE][0]['value']; + $this->assertEqual($rendered_field, $expected_field); + } + } + } + + /** + * Tests that fields with formatters runs as expected. + */ + public function _testFormatterSimpleFieldRender() { + $view = $this->getFieldView(); + $view->display['default']->display_options['fields'][$this->fields[0]['field_name']]['type'] = 'text_trimmed'; + $view->display['default']->display_options['fields'][$this->fields[0]['field_name']]['settings'] = array( + 'trim_length' => 3, + ); + $this->executeView($view); + + // Take sure that the formatter works as expected. + // @TODO: actually there should be a specific formatter. + for ($i = 0; $i < 2; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $this->fields[0]['field_name']); + $this->assertEqual(strlen($rendered_field), 3); + } + } + + public function _testMultipleFieldRender() { + $view = $this->getFieldView(); + + // Test delta limit. + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['group_rows'] = TRUE; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_limit'] = 3; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $this->fields[3]['field_name']); + $items = array(); + $pure_items = $this->nodes[$i]->{$this->fields[3]['field_name']}[LANGUAGE_NONE]; + $pure_items = array_splice($pure_items, 0, 3); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + + // Test that an empty field is rendered without error. + $rendered_field = $view->style_plugin->get_field(4, $this->fields[4]['field_name']); + + $view->destroy(); + + // Test delta limit + offset + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['group_rows'] = TRUE; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_limit'] = 3; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_offset'] = 1; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $this->fields[3]['field_name']); + $items = array(); + $pure_items = $this->nodes[$i]->{$this->fields[3]['field_name']}[LANGUAGE_NONE]; + $pure_items = array_splice($pure_items, 1, 3); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + $view->destroy(); + + // Test delta limit + reverse. + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_offset'] = 0; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['group_rows'] = TRUE; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_limit'] = 3; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_reversed'] = TRUE; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $this->fields[3]['field_name']); + $items = array(); + $pure_items = $this->nodes[$i]->{$this->fields[3]['field_name']}[LANGUAGE_NONE]; + array_splice($pure_items, 0, -3); + $pure_items = array_reverse($pure_items); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + $view->destroy(); + + // Test delta first last. + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['group_rows'] = TRUE; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_limit'] = 0; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_first_last'] = TRUE; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_reversed'] = FALSE; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $this->fields[3]['field_name']); + $items = array(); + $pure_items = $this->nodes[$i]->{$this->fields[3]['field_name']}[LANGUAGE_NONE]; + $items[] = $pure_items[0]['value']; + $items[] = $pure_items[4]['value']; + $this->assertEqual($rendered_field, implode(', ', $items), 'Take sure that the amount of items are limited.'); + } + $view->destroy(); + + // Test delta limit + custom seperator. + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_first_last'] = FALSE; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['delta_limit'] = 3; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['group_rows'] = TRUE; + $view->display['default']->display_options['fields'][$this->fields[3]['field_name']]['separator'] = ':'; + $this->executeView($view); + + for ($i = 0; $i < 3; $i++) { + $rendered_field = $view->style_plugin->get_field($i, $this->fields[3]['field_name']); + $items = array(); + $pure_items = $this->nodes[$i]->{$this->fields[3]['field_name']}[LANGUAGE_NONE]; + $pure_items = array_splice($pure_items, 0, 3); + foreach ($pure_items as $j => $item) { + $items[] = $pure_items[$j]['value']; + } + $this->assertEqual($rendered_field, implode(':', $items), 'Take sure that the amount of items are limited.'); + } + } + + protected function getFieldView() { + $view = new view; + $view->name = 'view_fieldapi'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'view_fieldapi'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + foreach ($this->fields as $key => $field) { + $handler->display->display_options['fields'][$field['field_name']]['id'] = $field['field_name']; + $handler->display->display_options['fields'][$field['field_name']]['table'] = 'field_data_' . $field['field_name']; + $handler->display->display_options['fields'][$field['field_name']]['field'] = $field['field_name']; + } + return $view; + } + +} + diff --git a/tests/handlers/views_handler_area_text.test b/tests/handlers/views_handler_area_text.test new file mode 100644 index 00000000..9f30e0c5 --- /dev/null +++ b/tests/handlers/views_handler_area_text.test @@ -0,0 +1,52 @@ + 'Area: Text', + 'description' => 'Test the core views_handler_area_text handler.', + 'group' => 'Views Handlers', + ); + } + + public function testAreaText() { + $view = $this->getBasicView(); + + // add a text header + $string = $this->randomName(); + $view->display['default']->handler->override_option('header', array( + 'area' => array( + 'id' => 'area', + 'table' => 'views', + 'field' => 'area', + 'content' => $string, + ), + )); + + // Execute the view. + $this->executeView($view); + + $view->display_handler->handlers['header']['area']->options['format'] = $this->randomString(); + $this->assertEqual(NULL, $view->display_handler->handlers['header']['area']->render(), 'Non existant format should return nothing'); + + $view->display_handler->handlers['header']['area']->options['format'] = filter_default_format(); + $this->assertEqual(check_markup($string), $view->display_handler->handlers['header']['area']->render(), 'Existant format should return something'); + + // Empty results, and it shouldn't be displayed . + $this->assertEqual('', $view->display_handler->handlers['header']['area']->render(TRUE), 'No result should lead to no header'); + // Empty results, and it should be displayed. + $view->display_handler->handlers['header']['area']->options['empty'] = TRUE; + $this->assertEqual(check_markup($string), $view->display_handler->handlers['header']['area']->render(TRUE), 'No result, but empty enabled lead to a full header'); + } + +} diff --git a/tests/handlers/views_handler_argument_null.test b/tests/handlers/views_handler_argument_null.test new file mode 100644 index 00000000..e8650a38 --- /dev/null +++ b/tests/handlers/views_handler_argument_null.test @@ -0,0 +1,72 @@ + 'Argument: Null', + 'description' => 'Test the core views_handler_argument_null handler.', + 'group' => 'Views Handlers', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['id']['argument']['handler'] = 'views_handler_argument_null'; + + return $data; + } + + public function testAreaText() { + // Test validation + $view = $this->getBasicView(); + + // Add a null argument. + $string = $this->randomString(); + $view->display['default']->handler->override_option('arguments', array( + 'null' => array( + 'id' => 'null', + 'table' => 'views', + 'field' => 'null', + ), + )); + + $this->executeView($view); + + // Make sure that the argument is not validated yet. + unset($view->argument['null']->argument_validated); + $this->assertTrue($view->argument['null']->validate_arg(26)); + // test must_not_be option. + unset($view->argument['null']->argument_validated); + $view->argument['null']->options['must_not_be'] = TRUE; + $this->assertFalse($view->argument['null']->validate_arg(26), 'must_not_be returns FALSE, if there is an argument'); + unset($view->argument['null']->argument_validated); + $this->assertTrue($view->argument['null']->validate_arg(NULL), 'must_not_be returns TRUE, if there is no argument'); + + // Test execution. + $view = $this->getBasicView(); + + // Add a argument, which has null as handler. + $string = $this->randomString(); + $view->display['default']->handler->override_option('arguments', array( + 'id' => array( + 'id' => 'id', + 'table' => 'views_test', + 'field' => 'id', + ), + )); + + $this->executeView($view, array(26)); + + // The argument should be ignored, so every result should return. + $this->assertEqual(5, count($view->result)); + } + +} diff --git a/tests/handlers/views_handler_argument_string.test b/tests/handlers/views_handler_argument_string.test new file mode 100644 index 00000000..d078abfe --- /dev/null +++ b/tests/handlers/views_handler_argument_string.test @@ -0,0 +1,96 @@ + 'Argument: String', + 'description' => 'Test the core views_handler_argument_string handler.', + 'group' => 'Views Handlers', + ); + } + + /** + * Tests the glossary feature. + */ + function testGlossary() { + // Setup some nodes, one with a, two with b and three with c. + $counter = 1; + foreach (array('a', 'b', 'c') as $char) { + for ($i = 0; $i < $counter; $i++) { + $edit = array( + 'title' => $char . $this->randomName(), + ); + $this->drupalCreateNode($edit); + } + } + + $view = $this->viewGlossary(); + $view->init_display(); + $this->executeView($view); + + $count_field = 'nid'; + foreach ($view->result as &$row) { + if (strpos($row->node_title, 'a') === 0) { + $this->assertEqual(1, $row->{$count_field}); + } + if (strpos($row->node_title, 'b') === 0) { + $this->assertEqual(2, $row->{$count_field}); + } + if (strpos($row->node_title, 'c') === 0) { + $this->assertEqual(3, $row->{$count_field}); + } + } + } + + /** + * Provide a test view for testGlossary. + * + * @see testGlossary + * @return view + */ + function viewGlossary() { + $view = new view(); + $view->name = 'test_glossary'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'test_glossary'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + /* Contextual filter: Content: Title */ + $handler->display->display_options['arguments']['title']['id'] = 'title'; + $handler->display->display_options['arguments']['title']['table'] = 'node'; + $handler->display->display_options['arguments']['title']['field'] = 'title'; + $handler->display->display_options['arguments']['title']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['title']['summary']['number_of_records'] = '0'; + $handler->display->display_options['arguments']['title']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['title']['summary_options']['items_per_page'] = '25'; + $handler->display->display_options['arguments']['title']['glossary'] = TRUE; + $handler->display->display_options['arguments']['title']['limit'] = '1'; + + return $view; + } +} diff --git a/tests/handlers/views_handler_field.test b/tests/handlers/views_handler_field.test new file mode 100644 index 00000000..9e6dfca2 --- /dev/null +++ b/tests/handlers/views_handler_field.test @@ -0,0 +1,314 @@ + 'Field', + 'description' => 'Test the core views_handler_field handler.', + 'group' => 'Views Handlers', + ); + } + + protected function setUp() { + parent::setUp(); + $this->column_map = array( + 'views_test_name' => 'name', + ); + } + + function testEmpty() { + $this->_testHideIfEmpty(); + $this->_testEmptyText(); + } + + /** + * Tests the hide if empty functionality. + * + * This tests alters the result to get easier and less coupled results. + */ + function _testHideIfEmpty() { + $view = $this->getBasicView(); + $view->init_display(); + $this->executeView($view); + + $column_map_reversed = array_flip($this->column_map); + $view->row_index = 0; + $random_name = $this->randomName(); + $random_value = $this->randomName(); + + // Test when results are not rewritten and empty values are not hidden. + $view->field['name']->options['hide_alter_empty'] = FALSE; + $view->field['name']->options['hide_empty'] = FALSE; + $view->field['name']->options['empty_zero'] = FALSE; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'By default, a string should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'By default, "" should not be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, '0', 'By default, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'By default, "0" should not be treated as empty.'); + + // Test when results are not rewritten and non-zero empty values are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If hide_empty is checked, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.'); + + // Test when results are not rewritten and all empty values are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = TRUE; + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, 0 should be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If hide_empty and empty_zero are checked, "0" should be treated as empty.'); + + // Test when results are rewritten to a valid string and non-zero empty + // results are hidden. + $view->field['name']->options['hide_alter_empty'] = FALSE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = $random_name; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_value; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.'); + + // Test when results are rewritten to an empty string and non-zero empty results are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = ""; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is empty, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.'); + + // Test when results are rewritten to zero as a string and non-zero empty + // results are hidden. + $view->field['name']->options['hide_alter_empty'] = FALSE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = "0"; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.'); + + // Test when results are rewritten to a valid string and non-zero empty + // results are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = FALSE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = $random_value; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If either the original or rewritten string is invalid, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.'); + + // Test when results are rewritten to zero as a string and all empty + // original values and results are hidden. + $view->field['name']->options['hide_alter_empty'] = TRUE; + $view->field['name']->options['hide_empty'] = TRUE; + $view->field['name']->options['empty_zero'] = TRUE; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $view->field['name']->options['alter']['text'] = "0"; + + // Test a valid string. + $view->result[0]->{$column_map_reversed['name']} = $random_name; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, it should be treated as empty.'); + + // Test an empty string. + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, "" should be treated as empty.'); + + // Test zero as an integer. + $view->result[0]->{$column_map_reversed['name']} = 0; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, 0 should not be treated as empty.'); + + // Test zero as a string. + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "", 'If the rewritten string is zero, "0" should not be treated as empty.'); + } + + /** + * Tests the usage of the empty text. + */ + function _testEmptyText() { + $view = $this->getBasicView(); + $view->init_display(); + $this->executeView($view); + + $column_map_reversed = array_flip($this->column_map); + $view->row_index = 0; + + $empty_text = $view->field['name']->options['empty'] = $this->randomName(); + $view->result[0]->{$column_map_reversed['name']} = ""; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $empty_text, 'If a field is empty, the empty text should be used for the output.'); + + $view->result[0]->{$column_map_reversed['name']} = "0"; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.'); + + $view->result[0]->{$column_map_reversed['name']} = "0"; + $view->field['name']->options['empty_zero'] = TRUE; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.'); + + $view->result[0]->{$column_map_reversed['name']} = ""; + $view->field['name']->options['alter']['alter_text'] = TRUE; + $alter_text = $view->field['name']->options['alter']['text'] = $this->randomName(); + $view->field['name']->options['hide_alter_empty'] = FALSE; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.'); + + $view->field['name']->options['hide_alter_empty'] = TRUE; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.'); + } + + /** + * Tests views_handler_field::is_value_empty(). + */ + function testIsValueEmpty() { + $view = $this->getBasicView(); + $view->init_display(); + $view->init_handlers(); + $field = $view->field['name']; + + $this->assertFalse($field->is_value_empty("not empty", TRUE), 'A normal string is not empty.'); + $this->assertTrue($field->is_value_empty("not empty", TRUE, FALSE), 'A normal string which skips empty() can be seen as empty.'); + + $this->assertTrue($field->is_value_empty("", TRUE), '"" is considered as empty.'); + + $this->assertTrue($field->is_value_empty('0', TRUE), '"0" is considered as empty if empty_zero is TRUE.'); + $this->assertTrue($field->is_value_empty(0, TRUE), '0 is considered as empty if empty_zero is TRUE.'); + $this->assertFalse($field->is_value_empty('0', FALSE), '"0" is considered not as empty if empty_zero is FALSE.'); + $this->assertFalse($field->is_value_empty(0, FALSE), '0 is considered not as empty if empty_zero is FALSE.'); + + $this->assertTrue($field->is_value_empty(NULL, TRUE, TRUE), 'Null should be always seen as empty, regardless of no_skip_empty.'); + $this->assertTrue($field->is_value_empty(NULL, TRUE, FALSE), 'Null should be always seen as empty, regardless of no_skip_empty.'); + } + +} diff --git a/tests/handlers/views_handler_field_boolean.test b/tests/handlers/views_handler_field_boolean.test new file mode 100644 index 00000000..92ec7a51 --- /dev/null +++ b/tests/handlers/views_handler_field_boolean.test @@ -0,0 +1,76 @@ + 'Field: Boolean', + 'description' => 'Test the core views_handler_field_boolean handler.', + 'group' => 'Views Handlers', + ); + } + + function dataSet() { + // Use default dataset but remove the age from john and paul + $data = parent::dataSet(); + $data[0]['age'] = 0; + $data[3]['age'] = 0; + return $data; + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['age']['field']['handler'] = 'views_handler_field_boolean'; + return $data; + } + + public function testFieldBoolean() { + $view = $this->getBasicView(); + + $view->display['default']->handler->override_option('fields', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + $this->executeView($view); + + // This is john, which has no age, there are no custom formats defined, yet. + $this->assertEqual(t('No'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('Yes'), $view->field['age']->advanced_render($view->result[1])); + + // Reverse the output. + $view->field['age']->options['not'] = TRUE; + $this->assertEqual(t('Yes'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('No'), $view->field['age']->advanced_render($view->result[1])); + + unset($view->field['age']->options['not']); + + // Use another output format. + $view->field['age']->options['type'] = 'true-false'; + $this->assertEqual(t('False'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('True'), $view->field['age']->advanced_render($view->result[1])); + + // test awesome unicode. + $view->field['age']->options['type'] = 'unicode-yes-no'; + $this->assertEqual('✖', $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual('✔', $view->field['age']->advanced_render($view->result[1])); + + // Set a custom output format. + $view->field['age']->formats['test'] = array(t('Test-True'), t('Test-False')); + $view->field['age']->options['type'] = 'test'; + $this->assertEqual(t('Test-False'), $view->field['age']->advanced_render($view->result[0])); + $this->assertEqual(t('Test-True'), $view->field['age']->advanced_render($view->result[1])); + + } +} diff --git a/tests/handlers/views_handler_field_counter.test b/tests/handlers/views_handler_field_counter.test new file mode 100644 index 00000000..2ddcb6f6 --- /dev/null +++ b/tests/handlers/views_handler_field_counter.test @@ -0,0 +1,70 @@ + 'Field: Counter', + 'description' => 'Tests the views_handler_field_counter handler.', + 'group' => 'Views Handlers', + ); + } + + function testSimple() { + $view = $this->getBasicView(); + $view->display['default']->handler->override_option('fields', array( + 'counter' => array( + 'id' => 'counter', + 'table' => 'views', + 'field' => 'counter', + 'relationship' => 'none', + ), + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + $view->preview(); + + $this->assertEqual(1, $view->style_plugin->rendered_fields[0]['counter']); + $this->assertEqual(2, $view->style_plugin->rendered_fields[1]['counter']); + $this->assertEqual(3, $view->style_plugin->rendered_fields[2]['counter']); + $view->destroy(); + + $view = $this->getBasicView(); + $rand_start = rand(5, 10); + $view->display['default']->handler->override_option('fields', array( + 'counter' => array( + 'id' => 'counter', + 'table' => 'views', + 'field' => 'counter', + 'relationship' => 'none', + 'counter_start' => $rand_start + ), + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + $view->preview(); + + $this->assertEqual(0 + $rand_start, $view->style_plugin->rendered_fields[0]['counter']); + $this->assertEqual(1 + $rand_start, $view->style_plugin->rendered_fields[1]['counter']); + $this->assertEqual(2 + $rand_start, $view->style_plugin->rendered_fields[2]['counter']); + } + + // @TODO: Write tests for pager. + function testPager() { + } +} diff --git a/tests/handlers/views_handler_field_custom.test b/tests/handlers/views_handler_field_custom.test new file mode 100644 index 00000000..b45fd177 --- /dev/null +++ b/tests/handlers/views_handler_field_custom.test @@ -0,0 +1,47 @@ + 'Field: Custom', + 'description' => 'Test the core views_handler_field_custom handler.', + 'group' => 'Views Handlers', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['name']['field']['handler'] = 'views_handler_field_custom'; + return $data; + } + + public function testFieldCustom() { + $view = $this->getBasicView(); + + // Alter the text of the field to a random string. + $random = $this->randomName(); + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'alter' => array( + 'text' => $random, + ), + ), + )); + + $this->executeView($view); + + $this->assertEqual($random, $view->style_plugin->get_field(0, 'name')); + } +} diff --git a/tests/handlers/views_handler_field_date.test b/tests/handlers/views_handler_field_date.test new file mode 100644 index 00000000..028b687f --- /dev/null +++ b/tests/handlers/views_handler_field_date.test @@ -0,0 +1,87 @@ + 'Field: Date', + 'description' => 'Test the core views_handler_field_date handler.', + 'group' => 'Views Handlers', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['created']['field']['handler'] = 'views_handler_field_date'; + return $data; + } + + public function testFieldDate() { + $view = $this->getBasicView(); + + $view->display['default']->handler->override_option('fields', array( + 'created' => array( + 'id' => 'created', + 'table' => 'views_test', + 'field' => 'created', + 'relationship' => 'none', + // c is iso 8601 date format @see http://php.net/manual/en/function.date.php + 'custom_date_format' => 'c', + ), + )); + $time = gmmktime(0, 0, 0, 1, 1, 2000); + + $this->executeView($view); + + $timezones = array( + NULL, + 'UTC', + 'America/New_York', + ); + foreach ($timezones as $timezone) { + $dates = array( + 'small' => format_date($time, 'small', '', $timezone), + 'medium' => format_date($time, 'medium', '', $timezone), + 'large' => format_date($time, 'large', '', $timezone), + 'custom' => format_date($time, 'custom', 'c', $timezone), + ); + $this->assertRenderedDatesEqual($view, $dates, $timezone); + } + + $intervals = array( + 'raw time ago' => format_interval(REQUEST_TIME - $time, 2), + 'time ago' => t('%time ago', array('%time' => format_interval(REQUEST_TIME - $time, 2))), + // TODO write tests for them +// 'raw time span' => format_interval(REQUEST_TIME - $time, 2), +// 'time span' => t('%time hence', array('%time' => format_interval(REQUEST_TIME - $time, 2))), + ); + $this->assertRenderedDatesEqual($view, $intervals); + } + + protected function assertRenderedDatesEqual($view, $map, $timezone = NULL) { + foreach ($map as $date_format => $expected_result) { + $view->field['created']->options['date_format'] = $date_format; + $t_args = array( + '%value' => $expected_result, + '%format' => $date_format, + ); + if (isset($timezone)) { + $t_args['%timezone'] = $timezone; + $message = t('Value %value in %format format for timezone %timezone matches.', $t_args); + $view->field['created']->options['timezone'] = $timezone; + } + else { + $message = t('Value %value in %format format matches.', $t_args); + } + $actual_result = $view->field['created']->advanced_render($view->result[0]); + $this->assertEqual($expected_result, $actual_result, $message); + } + } +} diff --git a/tests/handlers/views_handler_field_file_size.test b/tests/handlers/views_handler_field_file_size.test new file mode 100644 index 00000000..8652754f --- /dev/null +++ b/tests/handlers/views_handler_field_file_size.test @@ -0,0 +1,64 @@ + 'Field: file_size', + 'description' => 'Test the core views_handler_field_file_size handler.', + 'group' => 'Views Handlers', + ); + } + + function dataSet() { + $data = parent::dataSet(); + $data[0]['age'] = 0; + $data[1]['age'] = 10; + $data[2]['age'] = 1000; + $data[3]['age'] = 10000; + + return $data; + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['age']['field']['handler'] = 'views_handler_field_file_size'; + + return $data; + } + + public function testFieldFileSize() { + $view = $this->getBasicView(); + + $view->display['default']->handler->override_option('fields', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + ), + )); + + $this->executeView($view); + + // Test with the formatted option. + $this->assertEqual($view->field['age']->advanced_render($view->result[0]), ''); + $this->assertEqual($view->field['age']->advanced_render($view->result[1]), '10 bytes'); + $this->assertEqual($view->field['age']->advanced_render($view->result[2]), '1000 bytes'); + $this->assertEqual($view->field['age']->advanced_render($view->result[3]), '9.77 KB'); + // Test with the bytes option. + $view->field['age']->options['file_size_display'] = 'bytes'; + $this->assertEqual($view->field['age']->advanced_render($view->result[0]), ''); + $this->assertEqual($view->field['age']->advanced_render($view->result[1]), 10); + $this->assertEqual($view->field['age']->advanced_render($view->result[2]), 1000); + $this->assertEqual($view->field['age']->advanced_render($view->result[3]), 10000); + } +} diff --git a/tests/handlers/views_handler_field_math.test b/tests/handlers/views_handler_field_math.test new file mode 100644 index 00000000..ac33ac4a --- /dev/null +++ b/tests/handlers/views_handler_field_math.test @@ -0,0 +1,45 @@ + 'Field: Math', + 'description' => 'Test the core views_handler_field_math handler.', + 'group' => 'Views Handlers', + ); + } + + function viewsData() { + $data = parent::viewsData(); + return $data; + } + + public function testFieldCustom() { + $view = $this->getBasicView(); + + // Alter the text of the field to a random string. + $rand1 = rand(0, 100); + $rand2 = rand(0, 100); + $view->display['default']->handler->override_option('fields', array( + 'expression' => array( + 'id' => 'expression', + 'table' => 'views', + 'field' => 'expression', + 'relationship' => 'none', + 'expression' => $rand1 . ' + ' . $rand2, + ), + )); + + $this->executeView($view); + + $this->assertEqual($rand1 + $rand2, $view->style_plugin->get_field(0, 'expression')); + } +} diff --git a/tests/handlers/views_handler_field_url.test b/tests/handlers/views_handler_field_url.test new file mode 100644 index 00000000..527e94fb --- /dev/null +++ b/tests/handlers/views_handler_field_url.test @@ -0,0 +1,60 @@ + 'Field: Url', + 'description' => 'Test the core views_handler_field_url handler.', + 'group' => 'Views Handlers', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['name']['field']['handler'] = 'views_handler_field_url'; + return $data; + } + + public function testFieldUrl() { + $view = $this->getBasicView(); + + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'display_as_link' => FALSE, + ), + )); + + $this->executeView($view); + + $this->assertEqual('John', $view->field['name']->advanced_render($view->result[0])); + + // Make the url a link. + $view->delete(); + $view = $this->getBasicView(); + + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + + $this->executeView($view); + + $this->assertEqual(l('John', 'John'), $view->field['name']->advanced_render($view->result[0])); + } +} diff --git a/tests/handlers/views_handler_field_xss.test b/tests/handlers/views_handler_field_xss.test new file mode 100644 index 00000000..65a1ce28 --- /dev/null +++ b/tests/handlers/views_handler_field_xss.test @@ -0,0 +1,60 @@ + 'Field: Xss', + 'description' => 'Test the core views_handler_field_css handler.', + 'group' => 'Views Handlers', + ); + } + + function dataHelper() { + $map = array( + 'John' => 'John', + "Foo\xC0barbaz" => '', + 'Fooÿñ' => 'Fooÿñ' + ); + + return $map; + } + + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['name']['field']['handler'] = 'views_handler_field_xss'; + + return $data; + } + + public function testFieldXss() { + $view = $this->getBasicView(); + + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + ), + )); + + $this->executeView($view); + + $counter = 0; + foreach ($this->dataHelper() as $input => $expected_result) { + $view->result[$counter]->views_test_name = $input; + $this->assertEqual($view->field['name']->advanced_render($view->result[$counter]), $expected_result); + $counter++; + } + } +} diff --git a/tests/handlers/views_handler_filter_combine.test b/tests/handlers/views_handler_filter_combine.test new file mode 100644 index 00000000..99bf1eb5 --- /dev/null +++ b/tests/handlers/views_handler_filter_combine.test @@ -0,0 +1,105 @@ + 'Filter: Combine', + 'description' => 'Tests the combine filter handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + $this->column_map = array( + 'views_test_name' => 'name', + 'views_test_job' => 'job', + ); + } + + protected function getBasicView() { + $view = parent::getBasicView(); + $fields = $view->display['default']->handler->options['fields']; + $view->display['default']->display_options['fields']['job'] = array( + 'id' => 'job', + 'table' => 'views_test', + 'field' => 'job', + 'relationship' => 'none', + ); + return $view; + } + + public function testFilterCombineContains() { + $view = $this->getBasicView(); + + // Change the filtering. + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'combine', + 'table' => 'views', + 'field' => 'combine', + 'relationship' => 'none', + 'operator' => 'contains', + 'fields' => array( + 'name', + 'job', + ), + 'value' => 'ing', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'job' => 'Singer', + ), + array( + 'name' => 'George', + 'job' => 'Singer', + ), + array( + 'name' => 'Ringo', + 'job' => 'Drummer', + ), + array( + 'name' => 'Ginger', + 'job' => NULL, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + /** + * Additional data to test the NULL issue. + */ + protected function dataSet() { + $data_set = parent::dataSet(); + $data_set[] = array( + 'name' => 'Ginger', + 'age' => 25, + 'job' => NULL, + 'created' => gmmktime(0, 0, 0, 1, 2, 2000), + ); + return $data_set; + } + + /** + * Allow {views_test}.job to be NULL. + */ + protected function schemaDefinition() { + $schema = parent::schemaDefinition(); + unset($schema['views_test']['fields']['job']['not null']); + return $schema; + } +} diff --git a/tests/handlers/views_handler_filter_date.test b/tests/handlers/views_handler_filter_date.test new file mode 100644 index 00000000..34ccba66 --- /dev/null +++ b/tests/handlers/views_handler_filter_date.test @@ -0,0 +1,190 @@ + 'Filter: Date', + 'description' => 'Test the core views_handler_filter_date handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + // Add some basic test nodes. + $this->nodes = array(); + $this->nodes[] = $this->drupalCreateNode(array('created' => 100000)); + $this->nodes[] = $this->drupalCreateNode(array('created' => 200000)); + $this->nodes[] = $this->drupalCreateNode(array('created' => 300000)); + $this->nodes[] = $this->drupalCreateNode(array('created' => time() + 86400)); + + $this->map = array( + 'nid' => 'nid', + ); + $this->enableViewsUi(); + } + + /** + * Test the general offset functionality. + */ + function testOffset() { + $view = $this->views_test_offset(); + // Test offset for simple operator. + $view->set_display('default'); + $view->init_handlers(); + $view->filter['created']->operator = '>'; + $view->filter['created']->value['type'] = 'offset'; + $view->filter['created']->value['value'] = '+1 hour'; + $view->execute_display('default'); + $expected_result = array( + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test offset for between operator. + $view->set_display('default'); + $view->init_handlers(); + $view->filter['created']->operator = 'between'; + $view->filter['created']->value['type'] = 'offset'; + $view->filter['created']->value['max'] = '+2 days'; + $view->filter['created']->value['min'] = '+1 hour'; + $view->execute_display('default'); + $expected_result = array( + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + } + + + /** + * Tests the filter operator between/not between. + */ + function testBetween() { + // Test between with min and max. + $view = $this->views_test_between(); + $view->set_display('default'); + $view->init_handlers(); + $view->filter['created']->operator = 'between'; + $view->filter['created']->value['min'] = format_date(150000, 'custom', 'Y-m-d H:s'); + $view->filter['created']->value['max'] = format_date(250000, 'custom', 'Y-m-d H:s'); + $view->execute_display('default'); + $expected_result = array( + array('nid' => $this->nodes[1]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test between with just max. + $view = $this->views_test_between(); + $view->set_display('default'); + $view->init_handlers(); + $view->filter['created']->operator = 'between'; + $view->filter['created']->value['max'] = format_date(250000, 'custom', 'Y-m-d H:s'); + $view->execute_display('default'); + $expected_result = array( + array('nid' => $this->nodes[0]->nid), + array('nid' => $this->nodes[1]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test not between with min and max. + $view = $this->views_test_between(); + $view->set_display('default'); + $view->init_handlers(); + $view->filter['created']->operator = 'not between'; + $view->filter['created']->value['min'] = format_date(150000, 'custom', 'Y-m-d H:s'); + $view->filter['created']->value['max'] = format_date(250000, 'custom', 'Y-m-d H:s'); + $view->execute_display('default'); + $expected_result = array( + array('nid' => $this->nodes[0]->nid), + array('nid' => $this->nodes[2]->nid), + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test not between with just max. + $view = $this->views_test_between(); + $view->set_display('default'); + $view->init_handlers(); + $view->filter['created']->operator = 'not between'; + $view->filter['created']->value['max'] = format_date(150000, 'custom', 'Y-m-d H:s'); + $view->execute_display('default'); + $expected_result = array( + array('nid' => $this->nodes[1]->nid), + array('nid' => $this->nodes[2]->nid), + array('nid' => $this->nodes[3]->nid), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + } + + /** + * Make sure the validation callbacks works. + */ + function testUiValidation() { + $view = $this->views_test_between(); + $view->save(); + + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + menu_rebuild(); + $this->drupalGet('admin/structure/views/view/test_filter_date_between/edit'); + $this->drupalGet('admin/structure/views/nojs/config-item/test_filter_date_between/default/filter/created'); + + $edit = array(); + // Generate a definitive wrong value, which should be checked by validation. + $edit['options[value][value]'] = $this->randomString() . '-------'; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertText(t('Invalid date format.'), 'Make sure that validation is runned and the invalidate date format is identified.'); + } + + function views_test_between() { + $view = new view; + $view->name = 'test_filter_date_between'; + $view->description = ''; + $view->tag = ''; + $view->base_table = 'node'; + $view->human_name = ''; + $view->core = 0; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + /* Filter criterion: Content: Post date */ + $handler->display->display_options['filters']['created']['id'] = 'created'; + $handler->display->display_options['filters']['created']['table'] = 'node'; + $handler->display->display_options['filters']['created']['field'] = 'created'; + + return $view; + } + + function views_test_offset() { + $view = $this->views_test_between(); + return $view; + } +} diff --git a/tests/handlers/views_handler_filter_equality.test b/tests/handlers/views_handler_filter_equality.test new file mode 100644 index 00000000..5bb48c8d --- /dev/null +++ b/tests/handlers/views_handler_filter_equality.test @@ -0,0 +1,173 @@ + 'Filter: Equality', + 'description' => 'Test the core views_handler_filter_equality handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + $this->column_map = array( + 'views_test_name' => 'name', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['name']['filter']['handler'] = 'views_handler_filter_equality'; + + return $data; + } + + function testEqual() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '=', + 'value' => array('value' => 'Ringo'), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testEqualGroupedExposed() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: =, Value: Ringo + $filters['name']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testNotEqual() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testEqualGroupedNotExposed() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: !=, Value: Ringo + $filters['name']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + protected function getGroupedExposedFilters() { + $filters = array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'name_op', + 'label' => 'name', + 'identifier' => 'name', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'name', + 'identifier' => 'name', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Name is equal to Ringo', + 'operator' => '=', + 'value' => array('value' => 'Ringo'), + ), + 2 => array( + 'title' => 'Name is not equal to Ringo', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/tests/handlers/views_handler_filter_in_operator.test b/tests/handlers/views_handler_filter_in_operator.test new file mode 100644 index 00000000..3a20f8cc --- /dev/null +++ b/tests/handlers/views_handler_filter_in_operator.test @@ -0,0 +1,196 @@ + 'Filter: in_operator', + 'description' => 'Test the core views_handler_filter_in_operator handler.', + 'group' => 'Views Handlers', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['age']['filter']['handler'] = 'views_handler_filter_in_operator'; + + return $data; + } + + public function testFilterInOperatorSimple() { + $view = $this->getBasicView(); + + // Add a in_operator ordering. + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'age', + 'field' => 'age', + 'table' => 'views_test', + 'value' => array(26, 30), + 'operator' => 'in', + ), + )); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + + $this->assertEqual(2, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + + $view->delete(); + $view = $this->getBasicView(); + + // Add a in_operator ordering. + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'age', + 'field' => 'age', + 'table' => 'views_test', + 'value' => array(26, 30), + 'operator' => 'not in', + ), + )); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + + $this->assertEqual(3, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + public function testFilterInOperatorGroupedExposedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: in, Value: 26, 30 + $filters['age']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + + $this->assertEqual(2, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + public function testFilterNotInOperatorGroupedExposedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: in, Value: 26, 30 + $filters['age']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + + $this->assertEqual(3, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'age_op', + 'label' => 'age', + 'identifier' => 'age', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'age', + 'identifier' => 'age', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Age is one of 26, 30', + 'operator' => 'in', + 'value' => array(26, 30), + ), + 2 => array( + 'title' => 'Age is not one of 26, 30', + 'operator' => 'not in', + 'value' => array(26, 30), + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/tests/handlers/views_handler_filter_numeric.test b/tests/handlers/views_handler_filter_numeric.test new file mode 100644 index 00000000..2ab1aea0 --- /dev/null +++ b/tests/handlers/views_handler_filter_numeric.test @@ -0,0 +1,409 @@ + 'Filter: Numeric', + 'description' => 'Tests the numeric filter handler', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + $this->column_map = array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['age']['filter']['allow empty'] = TRUE; + $data['views_test']['id']['filter']['allow empty'] = FALSE; + + return $data; + } + + public function testFilterNumericSimple() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => '=', + 'value' => array('value' => 28), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: =, Value: 28 + $filters['age']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericBetween() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + + // test not between + $view->delete(); + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'not between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedBetween() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: between, Value: 26 and 29 + $filters['age']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedNotBetween() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: between, Value: 26 and 29 + $filters['age']['group_info']['default_group'] = 3; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + public function testFilterNumericEmpty() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'empty', + ), + )); + + $this->executeView($view); + $resultset = array( + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + + $view->delete(); + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'operator' => 'not empty', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + public function testFilterNumericExposedGroupedEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: empty, Value: + $filters['age']['group_info']['default_group'] = 4; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedNotEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: empty, Value: + $filters['age']['group_info']['default_group'] = 5; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + public function testAllowEmpty() { + $view = $this->getBasicView(); + + $view->display['default']->handler->override_option('filters', array( + 'id' => array( + 'id' => 'id', + 'table' => 'views_test', + 'field' => 'id', + 'relationship' => 'none', + ), + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + $view->set_display('default'); + $view->init_handlers(); + + $id_operators = $view->filter['id']->operators(); + $age_operators = $view->filter['age']->operators(); + + $this->assertFalse(isset($id_operators['empty'])); + $this->assertFalse(isset($id_operators['not empty'])); + $this->assertTrue(isset($age_operators['empty'])); + $this->assertTrue(isset($age_operators['not empty'])); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'age_op', + 'label' => 'age', + 'identifier' => 'age', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'age', + 'identifier' => 'age', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Age is 28', + 'operator' => '=', + 'value' => array('value' => 28), + ), + 2 => array( + 'title' => 'Age is between 26 and 29', + 'operator' => 'between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + 3 => array( + 'title' => 'Age is not between 26 and 29', + 'operator' => 'not between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + 4 => array( + 'title' => 'Age is empty', + 'operator' => 'empty', + ), + 5 => array( + 'title' => 'Age is not empty', + 'operator' => 'not empty', + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/tests/handlers/views_handler_filter_string.test b/tests/handlers/views_handler_filter_string.test new file mode 100644 index 00000000..ee74a282 --- /dev/null +++ b/tests/handlers/views_handler_filter_string.test @@ -0,0 +1,810 @@ + 'Filter: String', + 'description' => 'Tests the core views_handler_filter_string handler.', + 'group' => 'Views Handlers', + ); + } + + function setUp() { + parent::setUp(); + $this->column_map = array( + 'views_test_name' => 'name', + ); + } + + function viewsData() { + $data = parent::viewsData(); + $data['views_test']['name']['filter']['allow empty'] = TRUE; + $data['views_test']['job']['filter']['allow empty'] = FALSE; + $data['views_test']['description'] = $data['views_test']['name']; + + return $data; + } + + protected function schemaDefinition() { + $schema = parent::schemaDefinition(); + $schema['views_test']['fields']['description'] = array( + 'description' => "A person's description", + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + ); + + return $schema; + } + + /** + * An extended test dataset. + */ + protected function dataSet() { + $dataset = parent::dataSet(); + $dataset[0]['description'] = 'John Winston Ono Lennon, MBE (9 October 1940 – 8 December 1980) was an English musician and singer-songwriter who rose to worldwide fame as one of the founding members of The Beatles, one of the most commercially successful and critically acclaimed acts in the history of popular music. Along with fellow Beatle Paul McCartney, he formed one of the most successful songwriting partnerships of the 20th century.'; + $dataset[1]['description'] = 'George Harrison,[1] MBE (25 February 1943 – 29 November 2001)[2] was an English rock guitarist, singer-songwriter, actor and film producer who achieved international fame as lead guitarist of The Beatles.'; + $dataset[2]['description'] = 'Richard Starkey, MBE (born 7 July 1940), better known by his stage name Ringo Starr, is an English musician, singer-songwriter, and actor who gained worldwide fame as the drummer for The Beatles.'; + $dataset[3]['description'] = 'Sir James Paul McCartney, MBE (born 18 June 1942) is an English musician, singer-songwriter and composer. Formerly of The Beatles (1960–1970) and Wings (1971–1981), McCartney is the most commercially successful songwriter in the history of popular music, according to Guinness World Records.[1]'; + $dataset[4]['description'] = NULL; + + return $dataset; + } + + protected function getBasicView() { + $view = parent::getBasicView(); + $view->display['default']->options['fields']['description'] = array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + ); + return $view; + } + + function testFilterStringEqual() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '=', + 'value' => 'Ringo', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedEqual() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: =, Value: Ringo + $filters['name']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNotEqual() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedNotEqual() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: !=, Value: Ringo + $filters['name']['group_info']['default_group'] = '2'; + + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringContains() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => 'contains', + 'value' => 'ing', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringGroupedExposedContains() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: contains, Value: ing + $filters['name']['group_info']['default_group'] = '3'; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringWord() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'word', + 'value' => 'actor', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + $view->destroy(); + + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'allwords', + 'value' => 'Richard Starkey', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringGroupedExposedWord() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: contains, Value: ing + $filters['name']['group_info']['default_group'] = '3'; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + $view->destroy(); + + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: contains, Value: actor + $filters['description']['group_info']['default_group'] = '1'; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringStarts() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'starts', + 'value' => 'George', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedStarts() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: starts, Value: George + $filters['description']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'George', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNotStarts() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'not_starts', + 'value' => 'George', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Ringo', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedNotStarts() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: not_starts, Value: George + $filters['description']['group_info']['default_group'] = 3; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Ringo', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringEnds() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'ends', + 'value' => 'Beatles.', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedEnds() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Descriptino, Operator: ends, Value: Beatles + $filters['description']['group_info']['default_group'] = 4; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNotEnds() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'not_ends', + 'value' => 'Beatles.', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedNotEnds() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: not_ends, Value: Beatles + $filters['description']['group_info']['default_group'] = 5; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNot() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'not', + 'value' => 'Beatles.', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringGroupedExposedNot() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: not (does not contains), Value: Beatles + $filters['description']['group_info']['default_group'] = 6; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + + } + + function testFilterStringShorter() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => 'shorterthan', + 'value' => 5, + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedShorter() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: shorterthan, Value: 5 + $filters['name']['group_info']['default_group'] = 4; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringLonger() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'operator' => 'longerthan', + 'value' => 7, + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedLonger() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: longerthan, Value: 4 + $filters['name']['group_info']['default_group'] = 5; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + function testFilterStringEmpty() { + $view = $this->getBasicView(); + + // Change the filtering + $view->display['default']->handler->override_option('filters', array( + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'operator' => 'empty', + ), + )); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringGroupedExposedEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: empty, Value: + $filters['description']['group_info']['default_group'] = 7; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'name_op', + 'label' => 'name', + 'identifier' => 'name', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'name', + 'identifier' => 'name', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Is Ringo', + 'operator' => '=', + 'value' => 'Ringo', + ), + 2 => array( + 'title' => 'Is not Ringo', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + 3 => array( + 'title' => 'Contains ing', + 'operator' => 'contains', + 'value' => 'ing', + ), + 4 => array( + 'title' => 'Shorter than 5 letters', + 'operator' => 'shorterthan', + 'value' => 5, + ), + 5 => array( + 'title' => 'Longer than 7 letters', + 'operator' => 'longerthan', + 'value' => 7, + ), + ), + ), + ), + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'description_op', + 'label' => 'description', + 'identifier' => 'description', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'description', + 'identifier' => 'description', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Contains the word: Actor', + 'operator' => 'word', + 'value' => 'actor', + ), + 2 => array( + 'title' => 'Starts with George', + 'operator' => 'starts', + 'value' => 'George', + ), + 3 => array( + 'title' => 'Not Starts with: George', + 'operator' => 'not_starts', + 'value' => 'George', + ), + 4 => array( + 'title' => 'Ends with: Beatles', + 'operator' => 'ends', + 'value' => 'Beatles.', + ), + 5 => array( + 'title' => 'Not Ends with: Beatles', + 'operator' => 'not_ends', + 'value' => 'Beatles.', + ), + 6 => array( + 'title' => 'Does not contain: Beatles', + 'operator' => 'not', + 'value' => 'Beatles.', + ), + 7 => array( + 'title' => 'Empty description', + 'operator' => 'empty', + 'value' => '', + ), + ), + ), + ), + ); + return $filters; + } + +} diff --git a/tests/handlers/views_handler_sort.test b/tests/handlers/views_handler_sort.test new file mode 100644 index 00000000..70f2aac4 --- /dev/null +++ b/tests/handlers/views_handler_sort.test @@ -0,0 +1,121 @@ + 'Sort: generic', + 'description' => 'Test the core views_handler_sort handler.', + 'group' => 'Views Handlers', + ); + } + + /** + * Tests numeric ordering of the result set. + */ + public function testNumericOrdering() { + $view = $this->getBasicView(); + + // Change the ordering + $view->display['default']->handler->override_option('sorts', array( + 'age' => array( + 'order' => 'ASC', + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'age'), array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + + $view = $this->getBasicView(); + + // Reverse the ordering + $view->display['default']->handler->override_option('sorts', array( + 'age' => array( + 'order' => 'DESC', + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'age', TRUE), array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + /** + * Tests string ordering of the result set. + */ + public function testStringOrdering() { + $view = $this->getBasicView(); + + // Change the ordering + $view->display['default']->handler->override_option('sorts', array( + 'name' => array( + 'order' => 'ASC', + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'name'), array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + + $view = $this->getBasicView(); + + // Reverse the ordering + $view->display['default']->handler->override_option('sorts', array( + 'name' => array( + 'order' => 'DESC', + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->orderResultSet($this->dataSet(), 'name', TRUE), array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } +} diff --git a/tests/handlers/views_handler_sort_date.test b/tests/handlers/views_handler_sort_date.test new file mode 100644 index 00000000..65a94e81 --- /dev/null +++ b/tests/handlers/views_handler_sort_date.test @@ -0,0 +1,198 @@ + 'Sort: date', + 'description' => 'Test the core views_handler_sort_date handler.', + 'group' => 'Views Handlers', + ); + } + + protected function expectedResultSet($granularity, $reverse = TRUE) { + $expected = array(); + if (!$reverse) { + switch ($granularity) { + case 'second': + $expected = array( + array('name' => 'John'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'Ringo'), + array('name' => 'George'), + ); + break; + case 'minute': + $expected = array( + array('name' => 'John'), + array('name' => 'Paul'), + array('name' => 'Ringo'), + array('name' => 'Meredith'), + array('name' => 'George'), + ); + break; + case 'hour': + $expected = array( + array('name' => 'John'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'George'), + ); + break; + case 'day': + $expected = array( + array('name' => 'John'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'George'), + ); + break; + case 'month': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + case 'year': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + } + } + else { + switch ($granularity) { + case 'second': + $expected = array( + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Meredith'), + array('name' => 'Paul'), + array('name' => 'John'), + ); + break; + case 'minute': + $expected = array( + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Meredith'), + array('name' => 'Paul'), + array('name' => 'John'), + ); + break; + case 'hour': + $expected = array( + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + array('name' => 'John'), + ); + break; + case 'day': + $expected = array( + array('name' => 'George'), + array('name' => 'John'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + case 'month': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + case 'year': + $expected = array( + array('name' => 'John'), + array('name' => 'George'), + array('name' => 'Ringo'), + array('name' => 'Paul'), + array('name' => 'Meredith'), + ); + break; + } + } + + return $expected; + } + + /** + * Tests numeric ordering of the result set. + */ + public function testDateOrdering() { + foreach (array('second', 'minute', 'hour', 'day', 'month', 'year') as $granularity) { + foreach (array(FALSE, TRUE) as $reverse) { + $view = $this->getBasicView(); + + // Change the fields. + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + 'created' => array( + 'id' => 'created', + 'table' => 'views_test', + 'field' => 'created', + 'relationship' => 'none', + ), + )); + + // Change the ordering + $view->display['default']->handler->override_option('sorts', array( + 'created' => array( + 'id' => 'created', + 'table' => 'views_test', + 'field' => 'created', + 'relationship' => 'none', + 'granularity' => $granularity, + 'order' => $reverse ? 'DESC' : 'ASC', + ), + 'id' => array( + 'id' => 'id', + 'table' => 'views_test', + 'field' => 'id', + 'relationship' => 'none', + 'order' => 'ASC', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->expectedResultSet($granularity, $reverse), array( + 'views_test_name' => 'name', + ), t('Result is returned correctly when ordering by granularity @granularity, @reverse.', array('@granularity' => $granularity, '@reverse' => $reverse ? t('reverse') : t('forward')))); + $view->destroy(); + unset($view); + } + } + } +} diff --git a/tests/handlers/views_handler_sort_random.test b/tests/handlers/views_handler_sort_random.test new file mode 100644 index 00000000..19db8a90 --- /dev/null +++ b/tests/handlers/views_handler_sort_random.test @@ -0,0 +1,89 @@ + 'Sort: random', + 'description' => 'Test the core views_handler_sort_random handler.', + 'group' => 'Views Handlers', + ); + } + + /** + * Add more items to the test set, to make the order tests more robust. + */ + protected function dataSet() { + $data = parent::dataSet(); + for ($i = 0; $i < 50; $i++) { + $data[] = array( + 'name' => 'name_' . $i, + 'age' => $i, + 'job' => 'job_' . $i, + 'created' => rand(0, time()), + ); + } + return $data; + } + + /** + * Return a basic view with random ordering. + */ + protected function getBasicRandomView() { + $view = $this->getBasicView(); + + // Add a random ordering. + $view->display['default']->handler->override_option('sorts', array( + 'random' => array( + 'id' => 'random', + 'field' => 'random', + 'table' => 'views', + ), + )); + + return $view; + } + + /** + * Tests random ordering of the result set. + * + * @see DatabaseSelectTestCase::testRandomOrder() + */ + public function testRandomOrdering() { + // Execute a basic view first. + $view = $this->getBasicView(); + $this->executeView($view); + + // Verify the result. + $this->assertEqual(count($this->dataSet()), count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->dataSet(), array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + + // Execute a random view, we expect the result set to be different. + $view_random = $this->getBasicRandomView(); + $this->executeView($view_random); + $this->assertEqual(count($this->dataSet()), count($view_random->result), t('The number of returned rows match.')); + $this->assertNotIdenticalResultset($view_random, $view->result, array( + 'views_test_name' => 'views_test_name', + 'views_test_age' => 'views_test_name', + )); + + // Execute a second random view, we expect the result set to be different again. + $view_random_2 = $this->getBasicRandomView(); + $this->executeView($view_random_2); + $this->assertEqual(count($this->dataSet()), count($view_random_2->result), t('The number of returned rows match.')); + $this->assertNotIdenticalResultset($view_random, $view->result, array( + 'views_test_name' => 'views_test_name', + 'views_test_age' => 'views_test_name', + )); + } +} diff --git a/tests/node/views_node_revision_relations.test b/tests/node/views_node_revision_relations.test new file mode 100644 index 00000000..6b38396f --- /dev/null +++ b/tests/node/views_node_revision_relations.test @@ -0,0 +1,177 @@ + 'Tests basic node_revision integration', + 'description' => 'Tests the integration of node_revision table of node module', + 'group' => 'Views Modules', + ); + } + + /** + * Create a node with revision and rest result count for both views. + */ + public function testNodeRevisionRelationship() { + $node = $this->drupalCreateNode(); + // Create revision of the node. + $node_revision = clone $node; + $node_revision->revision = 1; + node_save($node_revision); + $column_map = array( + 'vid' => 'vid', + 'node_revision_nid' => 'node_revision_nid', + 'node_node_revision_nid' => 'node_node_revision_nid', + ); + + // Here should be two rows. + $view_nid = $this->test_view_node_revision_nid(); + $this->executeView($view_nid, array($node->nid)); + $resultset_nid = array( + array( + 'vid' => '1', + 'node_revision_nid' => '1', + 'node_node_revision_nid' => '1', + ), + array( + 'vid' => '2', + 'node_revision_nid' => '1', + 'node_node_revision_nid' => '1', + ), + ); + $this->assertIdenticalResultset($view_nid, $resultset_nid, $column_map); + + // There should be only one row with active revision 2. + $view_vid = $this->test_view_node_revision_vid(); + $this->executeView($view_vid, array($node->nid)); + $resultset_vid = array( + array( + 'vid' => '2', + 'node_revision_nid' => '1', + 'node_node_revision_nid' => '1', + ), + ); + $this->assertIdenticalResultset($view_vid, $resultset_vid, $column_map); + } + + /** + * Test view with default join on node.nid. + */ + function test_view_node_revision_nid() { + $view = new view(); + $view->name = 'test_node_revision_nid'; + $view->description = ''; + $view->tag = ''; + $view->base_table = 'node_revision'; + $view->human_name = 'Test node revision nid'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['access']['perm'] = 'view revisions'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Relationship: Content revision: Content */ + $handler->display->display_options['relationships']['nid']['id'] = 'nid'; + $handler->display->display_options['relationships']['nid']['table'] = 'node_revision'; + $handler->display->display_options['relationships']['nid']['field'] = 'nid'; + $handler->display->display_options['relationships']['nid']['label'] = 'NID'; + $handler->display->display_options['relationships']['nid']['required'] = TRUE; + /* Field: Content revision: Vid */ + $handler->display->display_options['fields']['vid']['id'] = 'vid'; + $handler->display->display_options['fields']['vid']['table'] = 'node_revision'; + $handler->display->display_options['fields']['vid']['field'] = 'vid'; + /* Field: Content revision: Nid */ + $handler->display->display_options['fields']['nid_1']['id'] = 'nid_1'; + $handler->display->display_options['fields']['nid_1']['table'] = 'node_revision'; + $handler->display->display_options['fields']['nid_1']['field'] = 'nid'; + /* Field: Content: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + $handler->display->display_options['fields']['nid']['relationship'] = 'nid'; + /* Contextual filter: Content revision: Nid */ + $handler->display->display_options['arguments']['nid']['id'] = 'nid'; + $handler->display->display_options['arguments']['nid']['table'] = 'node_revision'; + $handler->display->display_options['arguments']['nid']['field'] = 'nid'; + $handler->display->display_options['arguments']['nid']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['nid']['summary']['number_of_records'] = '0'; + $handler->display->display_options['arguments']['nid']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['nid']['summary_options']['items_per_page'] = '25'; + + return $view; + } + + /** + * Test view with default join on node.vid. + */ + function test_view_node_revision_vid() { + $view = new view(); + $view->name = 'test_node_revision_vid'; + $view->description = ''; + $view->tag = ''; + $view->base_table = 'node_revision'; + $view->human_name = 'Test node revision vid'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['access']['perm'] = 'view revisions'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Relationship: Content revision: Content */ + $handler->display->display_options['relationships']['vid']['id'] = 'vid'; + $handler->display->display_options['relationships']['vid']['table'] = 'node_revision'; + $handler->display->display_options['relationships']['vid']['field'] = 'vid'; + $handler->display->display_options['relationships']['vid']['label'] = 'VID'; + $handler->display->display_options['relationships']['vid']['required'] = TRUE; + /* Field: Content revision: Vid */ + $handler->display->display_options['fields']['vid']['id'] = 'vid'; + $handler->display->display_options['fields']['vid']['table'] = 'node_revision'; + $handler->display->display_options['fields']['vid']['field'] = 'vid'; + /* Field: Content revision: Nid */ + $handler->display->display_options['fields']['nid_1']['id'] = 'nid_1'; + $handler->display->display_options['fields']['nid_1']['table'] = 'node_revision'; + $handler->display->display_options['fields']['nid_1']['field'] = 'nid'; + /* Field: Content: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + $handler->display->display_options['fields']['nid']['relationship'] = 'vid'; + /* Contextual filter: Content revision: Nid */ + $handler->display->display_options['arguments']['nid']['id'] = 'nid'; + $handler->display->display_options['arguments']['nid']['table'] = 'node_revision'; + $handler->display->display_options['arguments']['nid']['field'] = 'nid'; + $handler->display->display_options['arguments']['nid']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['nid']['summary']['number_of_records'] = '0'; + $handler->display->display_options['arguments']['nid']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['nid']['summary_options']['items_per_page'] = '25'; + + return $view; + } +} diff --git a/tests/plugins/views_plugin_display.test b/tests/plugins/views_plugin_display.test new file mode 100644 index 00000000..df33a341 --- /dev/null +++ b/tests/plugins/views_plugin_display.test @@ -0,0 +1,194 @@ + 'Display plugin', + 'description' => 'Tests the basic display plugin.', + 'group' => 'Views Plugins', + ); + } + + /** + * Tests the overriding of filter_groups. + */ + function testFilterGroupsOverriding() { + $view = $this->viewFilterGroupsUpdating(); + $view->init_display(); + + // mark is as overridden, yes FALSE, means overridden. + $view->display['page']->handler->set_override('filter_groups', FALSE); + $this->assertFalse($view->display['page']->handler->is_defaulted('filter_groups'), "Take sure that 'filter_groups' is marked as overridden."); + $this->assertFalse($view->display['page']->handler->is_defaulted('filters'), "Take sure that 'filters'' is marked as overridden."); + } + + /** + * Returns a test view for testFilterGroupsOverriding. + * + * @see testFilterGroupsOverriding + * @return view + */ + function viewFilterGroupsOverriding() { + $view = new view(); + $view->name = 'test_filter_group_override'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'test_filter_group_override'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 1; + $handler->display->display_options['filters']['status']['group'] = 1; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page_1'); + $handler->display->display_options['path'] = 'test'; + + return $view; + } + + /** + * Based on a bug some filter_groups landed in the overridden display, even the filters weren't overridden. + * This caused multiple issues. + * Take sure that the value from the default display are used. + * + * @see http://drupal.org/node/1259608 + * @see views_plugin_display::init() + */ + function testFilterGroupsUpdating() { + $view = $this->viewFilterGroupsUpdating(); + $view->init_display(); + + $this->assertFalse($view->display['page']->handler->options['defaults']['filter_groups']); + $this->assertEqual($view->display['default']->handler->options['filter_groups'], $view->display['page']->handler->options['filter_groups'], 'Take sure the default options are used for the filter_groups'); + } + + /** + * Returns a test view for testFilterGroupUpdating. + * + * @see testFilterGroupUpdating + * + * @return view + */ + function viewFilterGroupsUpdating() { + $view = new view(); + $view->name = 'test_filter_groups'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'test_filter_groups'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'test_filter_groups'; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '10'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + /* Sort criterion: Content: Post date */ + $handler->display->display_options['sorts']['created']['id'] = 'created'; + $handler->display->display_options['sorts']['created']['table'] = 'node'; + $handler->display->display_options['sorts']['created']['field'] = 'created'; + $handler->display->display_options['sorts']['created']['order'] = 'DESC'; + $handler->display->display_options['filter_groups']['groups'] = array( + 1 => 'AND', + 2 => 'AND', + ); + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 1; + $handler->display->display_options['filters']['status']['group'] = 1; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + /* Filter criterion: Content: Nid */ + $handler->display->display_options['filters']['nid']['id'] = 'nid'; + $handler->display->display_options['filters']['nid']['table'] = 'node'; + $handler->display->display_options['filters']['nid']['field'] = 'nid'; + $handler->display->display_options['filters']['nid']['value']['value'] = '1'; + $handler->display->display_options['filters']['nid']['group'] = 2; + /* Filter criterion: Content: Nid */ + $handler->display->display_options['filters']['nid_1']['id'] = 'nid_1'; + $handler->display->display_options['filters']['nid_1']['table'] = 'node'; + $handler->display->display_options['filters']['nid_1']['field'] = 'nid'; + $handler->display->display_options['filters']['nid_1']['value']['value'] = '2'; + $handler->display->display_options['filters']['nid_1']['group'] = 2; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['filter_groups']['operator'] = 'OR'; + $handler->display->display_options['filter_groups']['groups'] = array( + 1 => 'OR', + 2 => 'OR', + ); + $handler->display->display_options['defaults']['filters'] = FALSE; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 1; + $handler->display->display_options['filters']['status']['group'] = 1; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + /* Filter criterion: Content: Nid */ + $handler->display->display_options['filters']['nid']['id'] = 'nid'; + $handler->display->display_options['filters']['nid']['table'] = 'node'; + $handler->display->display_options['filters']['nid']['field'] = 'nid'; + $handler->display->display_options['filters']['nid']['value']['value'] = '1'; + $handler->display->display_options['filters']['nid']['group'] = 2; + /* Filter criterion: Content: Nid */ + $handler->display->display_options['filters']['nid_1']['id'] = 'nid_1'; + $handler->display->display_options['filters']['nid_1']['table'] = 'node'; + $handler->display->display_options['filters']['nid_1']['field'] = 'nid'; + $handler->display->display_options['filters']['nid_1']['value']['value'] = '2'; + $handler->display->display_options['filters']['nid_1']['group'] = 2; + $handler->display->display_options['path'] = 'test-filter-groups'; + + return $view; + } +} diff --git a/tests/styles/views_plugin_style.test b/tests/styles/views_plugin_style.test new file mode 100644 index 00000000..dfc5413a --- /dev/null +++ b/tests/styles/views_plugin_style.test @@ -0,0 +1,264 @@ + 'Styles', + 'description' => 'Test general style functionality.', + 'group' => 'Views Plugins', + ); + } + + /** + * Tests the grouping legacy features of styles. + */ + function testGroupingLegacy() { + $view = $this->getBasicView(); + // Setup grouping by the job. + $view->init_display(); + $view->init_style(); + $view->style_plugin->options['grouping'] = 'job'; + + // Reduce the amount of items to make the test a bit easier. + // Set up the pager. + $view->display['default']->handler->override_option('pager', array( + 'type' => 'some', + 'options' => array('items_per_page' => 3), + )); + + // Add the job field . + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + 'job' => array( + 'id' => 'job', + 'table' => 'views_test', + 'field' => 'job', + 'relationship' => 'none', + ), + )); + + // Now run the query and groupby the result. + $this->executeView($view); + + // This is the old way to call it. + $sets = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping']); + + $expected = array(); + // Use Job: as label, so be sure that the label is used for groupby as well. + $expected['Job: Singer'] = array(); + $expected['Job: Singer'][0] = new StdClass(); + $expected['Job: Singer'][0]->views_test_name = 'John'; + $expected['Job: Singer'][0]->views_test_job = 'Singer'; + $expected['Job: Singer'][0]->views_test_id = '1'; + $expected['Job: Singer'][1] = new StdClass(); + $expected['Job: Singer'][1]->views_test_name = 'George'; + $expected['Job: Singer'][1]->views_test_job = 'Singer'; + $expected['Job: Singer'][1]->views_test_id = '2'; + $expected['Job: Drummer'] = array(); + $expected['Job: Drummer'][2] = new StdClass(); + $expected['Job: Drummer'][2]->views_test_name = 'Ringo'; + $expected['Job: Drummer'][2]->views_test_job = 'Drummer'; + $expected['Job: Drummer'][2]->views_test_id = '3'; + + $this->assertEqual($sets, $expected, t('The style plugin should proper group the results with grouping by the rendered output.')); + + $expected = array(); + $expected['Job: Singer'] = array(); + $expected['Job: Singer']['group'] = 'Job: Singer'; + $expected['Job: Singer']['rows'][0] = new StdClass(); + $expected['Job: Singer']['rows'][0]->views_test_name = 'John'; + $expected['Job: Singer']['rows'][0]->views_test_job = 'Singer'; + $expected['Job: Singer']['rows'][0]->views_test_id = '1'; + $expected['Job: Singer']['rows'][1] = new StdClass(); + $expected['Job: Singer']['rows'][1]->views_test_name = 'George'; + $expected['Job: Singer']['rows'][1]->views_test_job = 'Singer'; + $expected['Job: Singer']['rows'][1]->views_test_id = '2'; + $expected['Job: Drummer'] = array(); + $expected['Job: Drummer']['group'] = 'Job: Drummer'; + $expected['Job: Drummer']['rows'][2] = new StdClass(); + $expected['Job: Drummer']['rows'][2]->views_test_name = 'Ringo'; + $expected['Job: Drummer']['rows'][2]->views_test_job = 'Drummer'; + $expected['Job: Drummer']['rows'][2]->views_test_id = '3'; + + // The newer api passes the value of the grouping as well. + $sets_new_rendered = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], TRUE); + $sets_new_value = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], FALSE); + + $this->assertEqual($sets_new_rendered, $expected, t('The style plugins should proper group the results with grouping by the rendered output.')); + + // Reorder the group structure to group by value. + $expected['Singer'] = $expected['Job: Singer']; + $expected['Drummer'] = $expected['Job: Drummer']; + unset($expected['Job: Singer']); + unset($expected['Job: Drummer']); + + $this->assertEqual($sets_new_value, $expected, t('The style plugins should proper group the results with grouping by the value.')); + } + + function testGrouping() { + $this->_testGrouping(FALSE); + $this->_testGrouping(TRUE); + } + + /** + * Tests the grouping features of styles. + */ + function _testGrouping($stripped = FALSE) { + $view = $this->getBasicView(); + // Setup grouping by the job and the age field. + $view->init_display(); + $view->init_style(); + $view->style_plugin->options['grouping'] = array( + array('field' => 'job'), + array('field' => 'age'), + ); + + // Reduce the amount of items to make the test a bit easier. + // Set up the pager. + $view->display['default']->handler->override_option('pager', array( + 'type' => 'some', + 'options' => array('items_per_page' => 3), + )); + + // Add the job and age field. + $view->display['default']->handler->override_option('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + 'job' => array( + 'id' => 'job', + 'table' => 'views_test', + 'field' => 'job', + 'relationship' => 'none', + ), + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Now run the query and groupby the result. + $this->executeView($view); + + $expected = array(); + $expected['Job: Singer'] = array(); + $expected['Job: Singer']['group'] = 'Job: Singer'; + $expected['Job: Singer']['rows']['Age: 25'] = array(); + $expected['Job: Singer']['rows']['Age: 25']['group'] = 'Age: 25'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0] = new StdClass(); + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_name = 'John'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_job = 'Singer'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_age = '25'; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_id = '1'; + $expected['Job: Singer']['rows']['Age: 27'] = array(); + $expected['Job: Singer']['rows']['Age: 27']['group'] = 'Age: 27'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1] = new StdClass(); + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_name = 'George'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_job = 'Singer'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_age = '27'; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_id = '2'; + $expected['Job: Drummer'] = array(); + $expected['Job: Drummer']['group'] = 'Job: Drummer'; + $expected['Job: Drummer']['rows']['Age: 28'] = array(); + $expected['Job: Drummer']['rows']['Age: 28']['group'] = 'Age: 28'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2] = new StdClass(); + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_name = 'Ringo'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_job = 'Drummer'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_age = '28'; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_id = '3'; + + + // Alter the results to support the stripped case. + if ($stripped) { + + // Add some html to the result and expected value. + $rand = ''; + $view->result[0]->views_test_job .= $rand; + $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_job = 'Singer' . $rand; + $expected['Job: Singer']['group'] = 'Job: Singer'; + $rand = ''; + $view->result[1]->views_test_job .= $rand; + $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_job = 'Singer' . $rand; + $rand = ''; + $view->result[2]->views_test_job .= $rand; + $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_job = 'Drummer' . $rand; + $expected['Job: Drummer']['group'] = 'Job: Drummer'; + + $view->style_plugin->options['grouping'][0] = array('field' => 'job', 'rendered' => TRUE, 'rendered_strip' => TRUE); + $view->style_plugin->options['grouping'][1] = array('field' => 'age', 'rendered' => TRUE, 'rendered_strip' => TRUE); + } + + + // The newer api passes the value of the grouping as well. + $sets_new_rendered = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], TRUE); + + $this->assertEqual($sets_new_rendered, $expected, t('The style plugins should proper group the results with grouping by the rendered output.')); + + // Don't test stripped case, because the actual value is not stripped. + if (!$stripped) { + $sets_new_value = $view->style_plugin->render_grouping($view->result, $view->style_plugin->options['grouping'], FALSE); + + // Reorder the group structure to grouping by value. + $expected['Singer'] = $expected['Job: Singer']; + $expected['Singer']['rows']['25'] = $expected['Job: Singer']['rows']['Age: 25']; + $expected['Singer']['rows']['27'] = $expected['Job: Singer']['rows']['Age: 27']; + $expected['Drummer'] = $expected['Job: Drummer']; + $expected['Drummer']['rows']['28'] = $expected['Job: Drummer']['rows']['Age: 28']; + unset($expected['Job: Singer']); + unset($expected['Singer']['rows']['Age: 25']); + unset($expected['Singer']['rows']['Age: 27']); + unset($expected['Job: Drummer']); + unset($expected['Drummer']['rows']['Age: 28']); + + $this->assertEqual($sets_new_value, $expected, t('The style plugins should proper group the results with grouping by the value.')); + } + } + + /** + * Tests custom css classes. + */ + function testCustomRowClasses() { + $view = $this->getBasicView(); + + // Setup some random css class. + $view->init_display(); + $view->init_style(); + $random_name = $this->randomName(); + $view->style_plugin->options['row_class'] = $random_name . " test-token-[name]"; + + $rendered_output = $view->preview(); + $this->storeViewPreview($rendered_output); + + $rows = $this->elements->body->div->div->div; + $count = 0; + foreach ($rows as $row) { + $attributes = $row->attributes(); + $class = (string) $attributes['class'][0]; + $this->assertTrue(strpos($class, $random_name) !== FALSE, 'Take sure that a custom css class is added to the output.'); + + // Check token replacement. + $name = $view->field['name']->get_value($view->result[$count]); + $this->assertTrue(strpos($class, "test-token-$name") !== FALSE, 'Take sure that a token in custom css class is replaced.'); + + $count++; + } + } +} diff --git a/tests/styles/views_plugin_style_base.test b/tests/styles/views_plugin_style_base.test new file mode 100644 index 00000000..514077db --- /dev/null +++ b/tests/styles/views_plugin_style_base.test @@ -0,0 +1,33 @@ +loadHTML($output); + if ($htmlDom) { + // It's much easier to work with simplexml than DOM, luckily enough + // we can just simply import our DOM tree. + $this->elements = simplexml_import_dom($htmlDom); + } + } + +} diff --git a/tests/styles/views_plugin_style_jump_menu.test b/tests/styles/views_plugin_style_jump_menu.test new file mode 100644 index 00000000..dd4eca0b --- /dev/null +++ b/tests/styles/views_plugin_style_jump_menu.test @@ -0,0 +1,151 @@ + 'Jump menu', + 'description' => 'Test jump menu style functionality.', + 'group' => 'Views Plugins', + ); + } + + + public function setUp() { + parent::setUp(); + $this->nodes = array(); + $this->nodes['page'][] = $this->drupalCreateNode(array('type' => 'page')); + $this->nodes['page'][] = $this->drupalCreateNode(array('type' => 'page')); + $this->nodes['story'][] = $this->drupalCreateNode(array('type' => 'story')); + $this->nodes['story'][] = $this->drupalCreateNode(array('type' => 'story')); + + $this->nodeTitles = array($this->nodes['page'][0]->title, $this->nodes['page'][1]->title, $this->nodes['story'][0]->title, $this->nodes['story'][1]->title); + } + + + /** + * Tests jump menues with more then one same path but maybe differnet titles. + */ + function testDuplicatePaths() { + $view = $this->getJumpMenuView(); + $view->set_display(); + $view->init_handlers(); + + // Setup a [path] which would leed to "duplicate" paths, but still the shouldn't be used for grouping. + $view->field['nothing']->options['alter']['text'] = '[path]'; + $view->preview(); + $form = $view->style_plugin->render($view->result); + + // As there is no grouping setup it should be 4 elements. + $this->assertEqual(count($form['jump']['#options']), 4 + 1); + + // Check that all titles are part of the form as well. + $options = array_values($form['jump']['#options']); + foreach ($options as $key => $title) { + // The first one is the choose label. + if ($key == 0) { + continue; + } + $this->assertTrue($this->nodeTitles[$key - 1] == trim($title), t('Title @title should appear on the jump list, as we do not filter', array('@title' => $title))); + } + } + + function getJumpMenuView() { + $view = new view; + $view->name = 'test_jump_menu'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'test_jump_menu'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'jump_menu'; + $handler->display->display_options['style_options']['hide'] = 0; + $handler->display->display_options['style_options']['path'] = 'nothing'; + $handler->display->display_options['style_options']['default_value'] = 0; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['title']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['title']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 0; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 0; + $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim'] = 0; + $handler->display->display_options['fields']['title']['alter']['html'] = 0; + $handler->display->display_options['fields']['title']['hide_empty'] = 0; + $handler->display->display_options['fields']['title']['empty_zero'] = 0; + $handler->display->display_options['fields']['title']['link_to_node'] = 1; + /* Field: Content: Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + $handler->display->display_options['fields']['type']['exclude'] = 1; + /* Field: Global: Custom text */ + $handler->display->display_options['fields']['nothing']['id'] = 'nothing'; + $handler->display->display_options['fields']['nothing']['table'] = 'views'; + $handler->display->display_options['fields']['nothing']['field'] = 'nothing'; + $handler->display->display_options['fields']['nothing']['alter']['text'] = '[type]'; + $handler->display->display_options['fields']['nothing']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['external'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['nothing']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['nothing']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['trim'] = 0; + $handler->display->display_options['fields']['nothing']['alter']['html'] = 0; + $handler->display->display_options['fields']['nothing']['element_label_colon'] = 1; + $handler->display->display_options['fields']['nothing']['element_default_classes'] = 1; + $handler->display->display_options['fields']['nothing']['hide_empty'] = 0; + $handler->display->display_options['fields']['nothing']['empty_zero'] = 0; + $handler->display->display_options['fields']['nothing']['hide_alter_empty'] = 0; + $handler->display->display_options['fields']['nothing']['exclude'] = 1; + + /* Sort criterion: Content: Post date */ + $handler->display->display_options['sorts']['created']['id'] = 'created'; + $handler->display->display_options['sorts']['created']['table'] = 'node'; + $handler->display->display_options['sorts']['created']['field'] = 'created'; + $handler->display->display_options['sorts']['created']['order'] = 'DESC'; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 1; + $handler->display->display_options['filters']['status']['group'] = 0; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + + return $view; + } +} diff --git a/tests/styles/views_plugin_style_mapping.test b/tests/styles/views_plugin_style_mapping.test new file mode 100644 index 00000000..5785075b --- /dev/null +++ b/tests/styles/views_plugin_style_mapping.test @@ -0,0 +1,144 @@ + 'Style: Mapping', + 'description' => 'Test mapping style functionality.', + 'group' => 'Views Plugins', + ); + } + + public function setUp() { + parent::setUp(); + + // Reset the plugin data. + views_fetch_plugin_data(NULL, NULL, TRUE); + } + + protected function viewsPlugins() { + return array( + 'style' => array( + 'test_mapping' => array( + 'title' => t('Field mapping'), + 'help' => t('Maps specific fields to specific purposes.'), + 'handler' => 'views_test_plugin_style_test_mapping', + 'path' => drupal_get_path('module', 'views_test') . '/test_plugins', + 'theme' => 'views_view_mapping_test', + 'theme path' => drupal_get_path('module', 'views_test'), + 'theme file' => 'views_test.module', + 'uses row plugin' => FALSE, + 'uses fields' => TRUE, + 'uses options' => TRUE, + 'uses grouping' => FALSE, + 'type' => 'normal', + ), + ), + ); + } + + /** + * Overrides ViewsTestCase::getBasicView(). + */ + protected function getBasicView() { + $view = parent::getBasicView(); + $view->display['default']->handler->override_option('style_plugin', 'test_mapping'); + $view->display['default']->handler->override_option('style_options', array( + 'mapping' => array( + 'name_field' => 'name', + 'numeric_field' => array( + 'age', + ), + 'title_field' => 'name', + 'toggle_numeric_field' => TRUE, + 'toggle_title_field' => TRUE, + ), + )); + $view->display['default']->handler->override_option('fields', array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + 'job' => array( + 'id' => 'job', + 'table' => 'views_test', + 'field' => 'job', + 'relationship' => 'none', + ), + )); + return $view; + } + + /** + * Verifies that the fields were mapped correctly. + */ + public function testMappedOutput() { + $view = $this->getBasicView(); + $output = $this->mappedOutputHelper($view); + $this->assertTrue(strpos($output, 'job') === FALSE, 'The job field is added to the view but not in the mapping.'); + + $view = $this->getBasicView(); + $view->display['default']->display_options['style_options']['mapping']['name_field'] = 'job'; + $output = $this->mappedOutputHelper($view); + $this->assertTrue(strpos($output, 'job') !== FALSE, 'The job field is added to the view and is in the mapping.'); + } + + /** + * Tests the mapping of fields. + * + * @param view $view + * The view to test. + * + * @return string + * The view rendered as HTML. + */ + protected function mappedOutputHelper($view) { + $rendered_output = $view->preview(); + $this->storeViewPreview($rendered_output); + $rows = $this->elements->body->div->div->div; + $data_set = $this->dataSet(); + + $count = 0; + foreach ($rows as $row) { + $attributes = $row->attributes(); + $class = (string) $attributes['class'][0]; + $this->assertTrue(strpos($class, 'views-row-mapping-test') !== FALSE, 'Make sure that each row has the correct CSS class.'); + + foreach ($row->div as $field) { + // Split up the field-level class, the first part is the mapping name + // and the second is the field ID. + $field_attributes = $field->attributes(); + $name = strtok((string) $field_attributes['class'][0], '-'); + $field_id = strtok('-'); + + // The expected result is the mapping name and the field value, + // separated by ':'. + $expected_result = $name . ':' . $data_set[$count][$field_id]; + $actual_result = (string) $field; + $this->assertIdentical($expected_result, $actual_result, format_string('The fields were mapped successfully: %name => %field_id', array('%name' => $name, '%field_id' => $field_id))); + } + + $count++; + } + + return $rendered_output; + } + +} diff --git a/tests/styles/views_plugin_style_unformatted.test b/tests/styles/views_plugin_style_unformatted.test new file mode 100644 index 00000000..0c0e8825 --- /dev/null +++ b/tests/styles/views_plugin_style_unformatted.test @@ -0,0 +1,53 @@ + 'Style: unformatted', + 'description' => 'Test unformatted style functionality.', + 'group' => 'Views Plugins', + ); + } + + /** + * Take sure that the default css classes works as expected. + */ + function testDefaultRowClasses() { + $view = $this->getBasicView(); + $rendered_output = $view->preview(); + $this->storeViewPreview($rendered_output); + + $rows = $this->elements->body->div->div->div; + $count = 0; + $count_result = count($view->result); + foreach ($rows as $row) { + $count++; + $attributes = $row->attributes(); + $class = (string) $attributes['class'][0]; + // Take sure that each row has a row css class. + $this->assertTrue(strpos($class, "views-row-$count") !== FALSE, 'Take sure that each row has a row css class.'); + // Take sure that the odd/even classes are set right. + $odd_even = $count % 2 == 0 ? 'even' : 'odd'; + $this->assertTrue(strpos($class, "views-row-$odd_even") !== FALSE, 'Take sure that the odd/even classes are set right.'); + + if ($count == 1) { + $this->assertTrue(strpos($class, "views-row-first") !== FALSE, 'Take sure that the first class is set right.'); + } + else if ($count == $count_result) { + $this->assertTrue(strpos($class, "views-row-last") !== FALSE, 'Take sure that the last class is set right.'); + + } + $this->assertTrue(strpos($class, 'views-row') !== FALSE, 'Take sure that the views row class is set right.'); + } + } + +} diff --git a/tests/taxonomy/views_handler_relationship_node_term_data.test b/tests/taxonomy/views_handler_relationship_node_term_data.test new file mode 100644 index 00000000..37e8aa6c --- /dev/null +++ b/tests/taxonomy/views_handler_relationship_node_term_data.test @@ -0,0 +1,122 @@ + 'Tests handler relationship_node_term_data', + 'description' => 'Tests the taxonomy term on node relationship handler', + 'group' => 'Views Modules', + ); + } + + /** + * Returns a new term with random properties in vocabulary $vid. + */ + function createTerm($vocabulary) { + $term = new stdClass(); + $term->name = $this->randomName(); + $term->description = $this->randomName(); + // Use the first available text format. + $term->format = db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField(); + $term->vid = $vocabulary->vid; + taxonomy_term_save($term); + return $term; + } + + function setUp() { + parent::setUp(); + + //$web_user = $this->drupalCreateUser(array('create article content')); + //$this->drupalLogin($web_user); + + $vocabulary = taxonomy_vocabulary_machine_name_load('tags'); + $this->term_1 = $this->createTerm($vocabulary); + $this->term_2 = $this->createTerm($vocabulary); + + $node = array(); + $node['type'] = 'article'; + $node['field_tags'][LANGUAGE_NONE][]['tid'] = $this->term_1->tid; + $node['field_tags'][LANGUAGE_NONE][]['tid'] = $this->term_2->tid; + $this->node = $this->drupalCreateNode($node); + } + + function testViewsHandlerRelationshipNodeTermData() { + $view = $this->view_taxonomy_node_term_data(); + + $this->executeView($view, array($this->term_1->tid, $this->term_2->tid)); + $resultset = array( + array( + 'nid' => $this->node->nid, + ), + ); + $this->column_map = array('nid' => 'nid'); + debug($view->result); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function view_taxonomy_node_term_data() { + $view = new view(); + $view->name = 'test_taxonomy_node_term_data'; + $view->description = ''; + $view->tag = ''; + $view->base_table = 'node'; + $view->human_name = 'test_taxonomy_node_term_data'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + /* Relationship: Content: Taxonomy terms on node */ + $handler->display->display_options['relationships']['term_node_tid']['id'] = 'term_node_tid'; + $handler->display->display_options['relationships']['term_node_tid']['table'] = 'node'; + $handler->display->display_options['relationships']['term_node_tid']['field'] = 'term_node_tid'; + $handler->display->display_options['relationships']['term_node_tid']['label'] = 'Term #1'; + $handler->display->display_options['relationships']['term_node_tid']['vocabularies'] = array( + 'tags' => 0, + ); + /* Relationship: Content: Taxonomy terms on node */ + $handler->display->display_options['relationships']['term_node_tid_1']['id'] = 'term_node_tid_1'; + $handler->display->display_options['relationships']['term_node_tid_1']['table'] = 'node'; + $handler->display->display_options['relationships']['term_node_tid_1']['field'] = 'term_node_tid'; + $handler->display->display_options['relationships']['term_node_tid_1']['label'] = 'Term #2'; + $handler->display->display_options['relationships']['term_node_tid_1']['vocabularies'] = array( + 'tags' => 0, + ); + /* Contextual filter: Taxonomy term: Term ID */ + $handler->display->display_options['arguments']['tid']['id'] = 'tid'; + $handler->display->display_options['arguments']['tid']['table'] = 'taxonomy_term_data'; + $handler->display->display_options['arguments']['tid']['field'] = 'tid'; + $handler->display->display_options['arguments']['tid']['relationship'] = 'term_node_tid'; + $handler->display->display_options['arguments']['tid']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['tid']['summary']['number_of_records'] = '0'; + $handler->display->display_options['arguments']['tid']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['tid']['summary_options']['items_per_page'] = '25'; + /* Contextual filter: Taxonomy term: Term ID */ + $handler->display->display_options['arguments']['tid_1']['id'] = 'tid_1'; + $handler->display->display_options['arguments']['tid_1']['table'] = 'taxonomy_term_data'; + $handler->display->display_options['arguments']['tid_1']['field'] = 'tid'; + $handler->display->display_options['arguments']['tid_1']['relationship'] = 'term_node_tid_1'; + $handler->display->display_options['arguments']['tid_1']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['tid_1']['summary']['number_of_records'] = '0'; + $handler->display->display_options['arguments']['tid_1']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['tid_1']['summary_options']['items_per_page'] = '25'; + + return $view; + } +} \ No newline at end of file diff --git a/tests/templates/views-view--frontpage.tpl.php b/tests/templates/views-view--frontpage.tpl.php new file mode 100644 index 00000000..eb4f58b7 --- /dev/null +++ b/tests/templates/views-view--frontpage.tpl.php @@ -0,0 +1,85 @@ + +
    + +
    + +
    + + + +
    + +
    + + + +
    + +
    + + + +
    + +
    + +
    + +
    + + + + + + + +
    + +
    + + + + + + + + + + + +
    + +
    + + +
    diff --git a/tests/test_plugins/views_test_plugin_access_test_dynamic.inc b/tests/test_plugins/views_test_plugin_access_test_dynamic.inc new file mode 100644 index 00000000..cecec2f9 --- /dev/null +++ b/tests/test_plugins/views_test_plugin_access_test_dynamic.inc @@ -0,0 +1,26 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + function access($account) { + return !empty($this->options['access']) && isset($this->view->args[0]) && $this->view->args[0] == variable_get('test_dynamic_access_argument1', NULL) && isset($this->view->args[1]) && $this->view->args[1] == variable_get('test_dynamic_access_argument2', NULL); + } + + function get_access_callback() { + return array('views_test_test_dynamic_access_callback', array(!empty($options['access']), 1, 2)); + } +} diff --git a/tests/test_plugins/views_test_plugin_access_test_static.inc b/tests/test_plugins/views_test_plugin_access_test_static.inc new file mode 100644 index 00000000..187d6ea2 --- /dev/null +++ b/tests/test_plugins/views_test_plugin_access_test_static.inc @@ -0,0 +1,26 @@ + FALSE, 'bool' => TRUE); + + return $options; + } + + function access($account) { + return !empty($this->options['access']); + } + + function get_access_callback() { + return array('views_test_test_static_access_callback', array(!empty($options['access']))); + } +} diff --git a/tests/test_plugins/views_test_plugin_style_test_mapping.inc b/tests/test_plugins/views_test_plugin_style_test_mapping.inc new file mode 100644 index 00000000..b9267873 --- /dev/null +++ b/tests/test_plugins/views_test_plugin_style_test_mapping.inc @@ -0,0 +1,52 @@ + array( + '#title' => t('Title field'), + '#description' => t('Choose the field with the custom title.'), + '#toggle' => TRUE, + '#required' => TRUE, + ), + 'name_field' => array( + '#title' => t('Name field'), + '#description' => t('Choose the field with the custom name.'), + ), + 'numeric_field' => array( + '#title' => t('Numeric field'), + '#description' => t('Select one or more numeric fields.'), + '#multiple' => TRUE, + '#toggle' => TRUE, + '#filter' => 'filter_numeric_fields', + '#required' => TRUE, + ), + ); + } + + /** + * Restricts the allowed fields to only numeric fields. + * + * @param array $fields + * An array of field labels, keyed by the field ID. + */ + protected function filter_numeric_fields(&$fields) { + foreach ($this->view->field as $id => $field) { + if (!($field instanceof views_handler_field_numeric)) { + unset($fields[$id]); + } + } + } +} diff --git a/tests/user/views_handler_field_user_name.test b/tests/user/views_handler_field_user_name.test new file mode 100644 index 00000000..6ace4716 --- /dev/null +++ b/tests/user/views_handler_field_user_name.test @@ -0,0 +1,96 @@ + 'Tests user: name field', + 'description' => 'Tests the handler of the user: name field', + 'group' => 'Views Modules', + ); + } + + function testUserName() { + $view = $this->view_user_name(); + $view->init_display(); + $this->executeView($view); + + $view->row_index = 0; + + $view->field['name']->options['link_to_user'] = TRUE; + $username = $view->result[0]->users_name = $this->randomName(); + $view->result[0]->uid = 1; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertTrue(strpos($render, $username) !== FALSE, 'If link to user is checked the username should be part of the output.'); + $this->assertTrue(strpos($render, 'user/1') !== FALSE, 'If link to user is checked the link to the user should appear as well.'); + + $view->field['name']->options['link_to_user'] = FALSE; + $username = $view->result[0]->users_name = $this->randomName(); + $view->result[0]->uid = 1; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $username, 'If the user is not linked the username should be printed out for a normal user.'); + + $view->result[0]->uid = 0; + $anon_name = variable_get('anonymous', t('Anonymous')); + $view->result[0]->users_name = ''; + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $anon_name , 'For user0 it should use the default anonymous name by default.'); + + $view->field['name']->options['overwrite_anonymous'] = TRUE; + $anon_name = $view->field['name']->options['anonymous_text'] = $this->randomName(); + $render = $view->field['name']->advanced_render($view->result[0]); + $this->assertIdentical($render, $anon_name , 'For user0 it should use the configured anonymous text if overwrite_anonymous is checked.'); + + + } + function view_user_name() { + $view = new view; + $view->name = 'test_views_handler_field_user_name'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'users'; + $view->human_name = 'test_views_handler_field_user_name'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['label'] = ''; + $handler->display->display_options['fields']['name']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['name']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['name']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['name']['alter']['word_boundary'] = 0; + $handler->display->display_options['fields']['name']['alter']['ellipsis'] = 0; + $handler->display->display_options['fields']['name']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['name']['alter']['trim'] = 0; + $handler->display->display_options['fields']['name']['alter']['html'] = 0; + $handler->display->display_options['fields']['name']['hide_empty'] = 0; + $handler->display->display_options['fields']['name']['empty_zero'] = 0; + $handler->display->display_options['fields']['name']['link_to_user'] = 1; + $handler->display->display_options['fields']['name']['overwrite_anonymous'] = 0; + + return $view; + } +} diff --git a/tests/user/views_user.test b/tests/user/views_user.test new file mode 100644 index 00000000..52c50267 --- /dev/null +++ b/tests/user/views_user.test @@ -0,0 +1,143 @@ + 'Tests basic user integration', + 'description' => 'Tests the integration of user module', + 'group' => 'Views Modules', + ); + } + + + protected function setUp() { + parent::setUp(); + + $this->users[] = $this->drupalCreateUser(); + $this->users[] = user_load(1); + $this->nodes[] = $this->drupalCreateNode(array('uid' => $this->users[0]->uid)); + $this->nodes[] = $this->drupalCreateNode(array('uid' => 1)); + } + + /** + * Add a view which has no explicit relationship to the author and check the result. + * + * @todo: Remove the following comment once the relationship is required. + * One day a view will require the relationship so it should still work + */ + public function testRelationship() { + $view = $this->test_view_user_relationship(); + + $view->execute_display(); + $expected = array(); + for ($i = 0; $i <= 1; $i++) { + $expected[$i] = array( + 'node_title' => $this->nodes[$i]->title, + 'users_uid' => $this->nodes[$i]->uid, + 'users_name' => $this->users[$i]->name, + ); + } + $this->assertIdenticalResultset($view, $expected); + } + + function test_view_user_relationship() { + $view = new view; + $view->name = 'test_user_relationship'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'test_user_relationship'; + $view->core = 7; + $view->api_version = '3.0-alpha1'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'test_user_relationship'; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '10'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + $handler->display->display_options['row_options']['hide_empty'] = 0; + $handler->display->display_options['row_options']['default_field_elements'] = 1; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['title']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['title']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 0; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 0; + $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim'] = 0; + $handler->display->display_options['fields']['title']['alter']['html'] = 0; + $handler->display->display_options['fields']['title']['hide_empty'] = 0; + $handler->display->display_options['fields']['title']['empty_zero'] = 0; + $handler->display->display_options['fields']['title']['link_to_node'] = 1; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['name']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['name']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['name']['alter']['external'] = 0; + $handler->display->display_options['fields']['name']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['name']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['name']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['name']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['name']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['name']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['name']['alter']['trim'] = 0; + $handler->display->display_options['fields']['name']['alter']['html'] = 0; + $handler->display->display_options['fields']['name']['element_label_colon'] = 1; + $handler->display->display_options['fields']['name']['element_default_classes'] = 1; + $handler->display->display_options['fields']['name']['hide_empty'] = 0; + $handler->display->display_options['fields']['name']['empty_zero'] = 0; + $handler->display->display_options['fields']['name']['hide_alter_empty'] = 0; + $handler->display->display_options['fields']['name']['link_to_user'] = 1; + $handler->display->display_options['fields']['name']['overwrite_anonymous'] = 0; + /* Field: User: Uid */ + $handler->display->display_options['fields']['uid']['id'] = 'uid'; + $handler->display->display_options['fields']['uid']['table'] = 'users'; + $handler->display->display_options['fields']['uid']['field'] = 'uid'; + $handler->display->display_options['fields']['uid']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['uid']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['uid']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['uid']['alter']['external'] = 0; + $handler->display->display_options['fields']['uid']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['uid']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['uid']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['uid']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['uid']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['uid']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['uid']['alter']['trim'] = 0; + $handler->display->display_options['fields']['uid']['alter']['html'] = 0; + $handler->display->display_options['fields']['uid']['element_label_colon'] = 1; + $handler->display->display_options['fields']['uid']['element_default_classes'] = 1; + $handler->display->display_options['fields']['uid']['hide_empty'] = 0; + $handler->display->display_options['fields']['uid']['empty_zero'] = 0; + $handler->display->display_options['fields']['uid']['hide_alter_empty'] = 0; + $handler->display->display_options['fields']['uid']['link_to_user'] = 1; + + return $view; + } +} diff --git a/tests/user/views_user_argument_default.test b/tests/user/views_user_argument_default.test new file mode 100644 index 00000000..afb24d1e --- /dev/null +++ b/tests/user/views_user_argument_default.test @@ -0,0 +1,90 @@ + 'Tests user argument default plugin', + 'description' => 'Tests user argument default plugin', + 'group' => 'Views Plugins', + ); + } + + public function test_plugin_argument_default_current_user() { + // Create a user to test. + $account = $this->drupalCreateUser(); + + // Switch the user, we have to check the global user too, because drupalLogin is only for the simpletest browser. + $this->drupalLogin($account); + global $user; + $admin = $user; + drupal_save_session(FALSE); + $user = $account; + + $view = $this->view_plugin_argument_default_current_user(); + + $view->set_display('default'); + $view->pre_execute(); + $view->init_handlers(); + + $this->assertEqual($view->argument['null']->get_default_argument(), $account->uid, 'Uid of the current user is used.'); + // Switch back. + $user = $admin; + drupal_save_session(TRUE); + } + + function view_plugin_argument_default_current_user() { + $view = new view; + $view->name = 'test_plugin_argument_default_current_user'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = '3.0-alpha1'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '10'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['title']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim'] = 0; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['title']['alter']['html'] = 0; + $handler->display->display_options['fields']['title']['hide_empty'] = 0; + $handler->display->display_options['fields']['title']['empty_zero'] = 0; + $handler->display->display_options['fields']['title']['link_to_node'] = 0; + /* Argument: Global: Null */ + $handler->display->display_options['arguments']['null']['id'] = 'null'; + $handler->display->display_options['arguments']['null']['table'] = 'views'; + $handler->display->display_options['arguments']['null']['field'] = 'null'; + $handler->display->display_options['arguments']['null']['default_action'] = 'default'; + $handler->display->display_options['arguments']['null']['style_plugin'] = 'default_summary'; + $handler->display->display_options['arguments']['null']['default_argument_type'] = 'current_user'; + $handler->display->display_options['arguments']['null']['must_not_be'] = 0; + + return $view; + } +} diff --git a/tests/user/views_user_argument_validate.test b/tests/user/views_user_argument_validate.test new file mode 100644 index 00000000..6e157bf5 --- /dev/null +++ b/tests/user/views_user_argument_validate.test @@ -0,0 +1,115 @@ + 'Tests user argument validator', + 'description' => 'Tests user argument validator', + 'group' => 'Views Plugins', + ); + } + + function setUp() { + parent::setUp('views'); + $this->account = $this->drupalCreateUser(); + } + + function testArgumentValidateUserUid() { + $account = $this->account; + // test 'uid' case + $view = $this->view_argument_validate_user('uid'); + $view->set_display('default'); + $view->pre_execute(); + $view->init_handlers(); + $this->assertTrue($view->argument['null']->validate_arg($account->uid)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a string variable since type is 'uid' + $this->assertFalse($view->argument['null']->validate_arg($account->name)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid numeric, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validate_arg(32)); + } + + function testArgumentValidateUserName() { + $account = $this->account; + // test 'name' case + $view = $this->view_argument_validate_user('name'); + $view->set_display('default'); + $view->pre_execute(); + $view->init_handlers(); + $this->assertTrue($view->argument['null']->validate_arg($account->name)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a uid variable since type is 'name' + $this->assertFalse($view->argument['null']->validate_arg($account->uid)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid string, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validate_arg($this->randomName())); + } + + function testArgumentValidateUserEither() { + $account = $this->account; + // test 'either' case + $view = $this->view_argument_validate_user('either'); + $view->set_display('default'); + $view->pre_execute(); + $view->init_handlers(); + $this->assertTrue($view->argument['null']->validate_arg($account->name)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a uid variable since type is 'name' + $this->assertTrue($view->argument['null']->validate_arg($account->uid)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid string, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validate_arg($this->randomName())); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + // Fail for a valid uid, but for a user that doesn't exist + $this->assertFalse($view->argument['null']->validate_arg(32)); + } + + function view_argument_validate_user($argtype) { + $view = new view; + $view->name = 'view_argument_validate_user'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Argument: Global: Null */ + $handler->display->display_options['arguments']['null']['id'] = 'null'; + $handler->display->display_options['arguments']['null']['table'] = 'views'; + $handler->display->display_options['arguments']['null']['field'] = 'null'; + $handler->display->display_options['arguments']['null']['style_plugin'] = 'default_summary'; + $handler->display->display_options['arguments']['null']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['null']['validate']['type'] = 'user'; + $handler->display->display_options['arguments']['null']['validate_options']['type'] = $argtype; + $handler->display->display_options['arguments']['null']['must_not_be'] = 0; + + return $view; + } + +} diff --git a/tests/views_access.test b/tests/views_access.test new file mode 100644 index 00000000..f02eca99 --- /dev/null +++ b/tests/views_access.test @@ -0,0 +1,285 @@ + 'Access', + 'description' => 'Tests pluggable access for views.', + 'group' => 'Views Plugins' + ); + } + + public function setUp() { + parent::setUp(); + + $this->admin_user = $this->drupalCreateUser(array('access all views')); + $this->web_user = $this->drupalCreateUser(); + $this->web_role = current($this->web_user->roles); + + $this->normal_role = $this->drupalCreateRole(array()); + $this->normal_user = $this->drupalCreateUser(array('views_test test permission')); + $this->normal_user->roles[$this->normal_role] = $this->normal_role; + // Reset the plugin data. + views_fetch_plugin_data(NULL, NULL, TRUE); + } + + function viewsPlugins() { + $plugins = array( + 'access' => array( + 'test_static' => array( + 'title' => t('Static test access plugin'), + 'help' => t('Provides a static test access plugin.'), + 'handler' => 'views_test_plugin_access_test_static', + 'path' => drupal_get_path('module', 'views_test') . '/test_plugins', + ), + 'test_dynamic' => array( + 'title' => t('Dynamic test access plugin'), + 'help' => t('Provides a dynamic test access plugin.'), + 'handler' => 'views_test_plugin_access_test_dynamic', + 'path' => drupal_get_path('module', 'views_test') . '/test_plugins', + ), + ), + ); + + return $plugins; + } + + /** + * Tests none access plugin. + */ + function testAccessNone() { + $view = $this->view_access_none(); + + $view->set_display('default'); + + $this->assertTrue($view->display_handler->access($this->admin_user), t('Admin-Account should be able to access the view everytime')); + $this->assertTrue($view->display_handler->access($this->web_user)); + $this->assertTrue($view->display_handler->access($this->normal_user)); + } + + /** + * Tests perm access plugin. + */ + function testAccessPerm() { + $view = $this->view_access_perm(); + + $view->set_display('default'); + $access_plugin = $view->display_handler->get_plugin('access'); + + $this->assertTrue($view->display_handler->access($this->admin_user), t('Admin-Account should be able to access the view everytime')); + $this->assertFalse($view->display_handler->access($this->web_user)); + $this->assertTrue($view->display_handler->access($this->normal_user)); + } + + /** + * Tests role access plugin. + */ + function testAccessRole() { + $view = $this->view_access_role(); + + $view->set_display('default'); + $access_plugin = $view->display_handler->get_plugin('access'); + + $this->assertTrue($view->display_handler->access($this->admin_user), t('Admin-Account should be able to access the view everytime')); + $this->assertFalse($view->display_handler->access($this->web_user)); + $this->assertTrue($view->display_handler->access($this->normal_user)); + } + + /** + * @todo Test abstract access plugin. + */ + + /** + * Tests static access check. + */ + function testStaticAccessPlugin() { + $view = $this->view_access_static(); + + $view->set_display('default'); + $access_plugin = $view->display_handler->get_plugin('access'); + + $this->assertFalse($access_plugin->access($this->normal_user)); + + $access_plugin->options['access'] = TRUE; + $this->assertTrue($access_plugin->access($this->normal_user)); + + // FALSE comes from hook_menu caching. + $expected_hook_menu = array( + 'views_test_test_static_access_callback', array(FALSE) + ); + $hook_menu = $view->execute_hook_menu('page_1'); + $this->assertEqual($expected_hook_menu, $hook_menu['test_access_static']['access arguments'][0]); + + $expected_hook_menu = array( + 'views_test_test_static_access_callback', array(TRUE) + ); + $this->assertTrue(views_access($expected_hook_menu)); + } + + /** + * Tests dynamic access plugin. + */ + function testDynamicAccessPlugin() { + $view = $this->view_access_dynamic(); + $argument1 = $this->randomName(); + $argument2 = $this->randomName(); + variable_set('test_dynamic_access_argument1', $argument1); + variable_set('test_dynamic_access_argument2', $argument2); + + $view->set_display('default'); + $access_plugin = $view->display_handler->get_plugin('access'); + + $this->assertFalse($access_plugin->access($this->normal_user)); + + $access_plugin->options['access'] = TRUE; + $this->assertFalse($access_plugin->access($this->normal_user)); + + $view->set_arguments(array($argument1, $argument2)); + $this->assertTrue($access_plugin->access($this->normal_user)); + + // FALSE comes from hook_menu caching. + $expected_hook_menu = array( + 'views_test_test_dynamic_access_callback', array(FALSE, 1, 2) + ); + $hook_menu = $view->execute_hook_menu('page_1'); + $this->assertEqual($expected_hook_menu, $hook_menu['test_access_dynamic']['access arguments'][0]); + + $expected_hook_menu = array( + 'views_test_test_dynamic_access_callback', array(TRUE, 1, 2) + ); + $this->assertTrue(views_access($expected_hook_menu, $argument1, $argument2)); + } + + function view_access_none() { + $view = new view; + $view->name = 'test_access_none'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + + return $view; + } + + function view_access_perm() { + $view = new view; + $view->name = 'test_access_perm'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['access']['perm'] = 'views_test test permission'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + + return $view; + } + + function view_access_role() { + $view = new view; + $view->name = 'test_access_role'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'role'; + $handler->display->display_options['access']['role'] = array( + $this->normal_role => $this->normal_role, + ); + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + + return $view; + } + + function view_access_dynamic() { + $view = new view; + $view->name = 'test_access_dynamic'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'test_dynamic'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + + $handler = $view->new_display('page', 'Page', 'page_1'); + $handler->display->display_options['path'] = 'test_access_dynamic'; + + return $view; + } + + function view_access_static() { + $view = new view; + $view->name = 'test_access_static'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'test_static'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + + $handler = $view->new_display('page', 'Page', 'page_1'); + $handler->display->display_options['path'] = 'test_access_static'; + + return $view; + } +} diff --git a/tests/views_analyze.test b/tests/views_analyze.test new file mode 100644 index 00000000..c21a4bb6 --- /dev/null +++ b/tests/views_analyze.test @@ -0,0 +1,51 @@ + 'Views Analyze', + 'description' => 'Test the views analyze system.', + 'group' => 'Views', + ); + } + + public function setUp() { + parent::setUp('views_ui'); + module_enable(array('views_ui')); + // @TODO Figure out why it's required to clear the cache here. + views_module_include('views_default', TRUE); + views_get_all_views(TRUE); + menu_rebuild(); + + // Add an admin user will full rights; + $this->admin = $this->drupalCreateUser(array('administer views')); + } + + /** + * Tests that analyze works in general. + */ + function testAnalyzeBasic() { + $this->drupalLogin($this->admin); + // Enable the frontpage view and click the analyse button. + $view = views_get_view('frontpage'); + $view->save(); + + $this->drupalGet('admin/structure/views/view/frontpage/edit'); + $this->assertLink(t('analyze view')); + + // This redirects the user to the form. + $this->clickLink(t('analyze view')); + $this->assertText(t('View analysis')); + + // This redirects the user back to the main views edit page. + $this->drupalPost(NULL, array(), t('Ok')); + } +} diff --git a/tests/views_argument_default.test b/tests/views_argument_default.test new file mode 100644 index 00000000..9c0a7ebf --- /dev/null +++ b/tests/views_argument_default.test @@ -0,0 +1,137 @@ + 'Argument_default', + 'description' => 'Tests pluggable argument_default for views.', + 'group' => 'Views Plugins' + ); + } + + public function setUp() { + parent::setUp('views'); + + $this->random = $this->randomString(); + } + + /** + * Tests the use of a default argument plugin that provides no options. + */ + function testArgumentDefaultNoOptions() { + module_enable(array('views_ui', 'views_test')); + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + + // The current_user plugin has no options form, and should pass validation. + $argument_type = 'current_user'; + $edit = array( + 'options[default_argument_type]' => $argument_type, + ); + $this->drupalPost('admin/structure/views/nojs/config-item/test_argument_default_current_user/default/argument/uid', $edit, t('Apply')); + + // Note, the undefined index error has two spaces after it. + $error = array( + '%type' => 'Notice', + '!message' => 'Undefined index: ' . $argument_type, + '%function' => 'views_handler_argument->options_validate()', + ); + $message = t('%type: !message in %function', $error); + $this->assertNoRaw($message, t('Did not find error message: !message.', array('!message' => $message))); + } + + /** + * Tests fixed default argument. + */ + function testArgumentDefaultFixed() { + $view = $this->view_argument_default_fixed(); + + $view->set_display('default'); + $view->pre_execute(); + $view->init_handlers(); + + $this->assertEqual($view->argument['null']->get_default_argument(), $this->random, 'Fixed argument should be used by default.'); + + $view->destroy(); + + // Make sure that a normal argument provided is used + $view = $this->view_argument_default_fixed(); + + $view->set_display('default'); + $random_string = $this->randomString(); + $view->execute_display('default', array($random_string)); + + $this->assertEqual($view->args[0], $random_string, 'Provided argument should be used.'); + } + + /** + * @todo Test php default argument. + */ + function testArgumentDefaultPhp() { + + } + + /** + * @todo Test node default argument. + */ + function testArgumentDefaultNode() { + + } + + function view_argument_default_fixed() { + $view = new view; + $view->name = 'test_argument_default_fixed'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '10'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['title']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim'] = 0; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['title']['alter']['html'] = 0; + $handler->display->display_options['fields']['title']['hide_empty'] = 0; + $handler->display->display_options['fields']['title']['empty_zero'] = 0; + $handler->display->display_options['fields']['title']['link_to_node'] = 0; + /* Argument: Global: Null */ + $handler->display->display_options['arguments']['null']['id'] = 'null'; + $handler->display->display_options['arguments']['null']['table'] = 'views'; + $handler->display->display_options['arguments']['null']['field'] = 'null'; + $handler->display->display_options['arguments']['null']['default_action'] = 'default'; + $handler->display->display_options['arguments']['null']['style_plugin'] = 'default_summary'; + $handler->display->display_options['arguments']['null']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['null']['default_argument_options']['argument'] = $this->random; + $handler->display->display_options['arguments']['null']['must_not_be'] = 0; + + return $view; + } +} diff --git a/tests/views_argument_validator.test b/tests/views_argument_validator.test new file mode 100644 index 00000000..fb2e4f2b --- /dev/null +++ b/tests/views_argument_validator.test @@ -0,0 +1,106 @@ + 'Argument validator', + 'group' => 'Views Plugins', + 'description' => 'Test argument validator tests.', + ); + } + + function testArgumentValidatePhp() { + $string = $this->randomName(); + $view = $this->view_test_argument_validate_php($string); + $view->set_display('default'); + $view->pre_execute(); + $view->init_handlers(); + $this->assertTrue($view->argument['null']->validate_arg($string)); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + $this->assertFalse($view->argument['null']->validate_arg($this->randomName())); + } + + function testArgumentValidateNumeric() { + $view = $this->view_argument_validate_numeric(); + $view->set_display('default'); + $view->pre_execute(); + $view->init_handlers(); + $this->assertFalse($view->argument['null']->validate_arg($this->randomString())); + // Reset safed argument validation. + $view->argument['null']->argument_validated = NULL; + $this->assertTrue($view->argument['null']->validate_arg(12)); + } + + function view_test_argument_validate_php($string) { + $code = 'return $argument == \''. $string .'\';'; + $view = new view; + $view->name = 'view_argument_validate_numeric'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Argument: Global: Null */ + $handler->display->display_options['arguments']['null']['id'] = 'null'; + $handler->display->display_options['arguments']['null']['table'] = 'views'; + $handler->display->display_options['arguments']['null']['field'] = 'null'; + $handler->display->display_options['arguments']['null']['style_plugin'] = 'default_summary'; + $handler->display->display_options['arguments']['null']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['null']['validate_type'] = 'php'; + $handler->display->display_options['arguments']['null']['validate_options']['code'] = $code; + $handler->display->display_options['arguments']['null']['must_not_be'] = 0; + + return $view; + } + + function view_argument_validate_numeric() { + $view = new view; + $view->name = 'view_argument_validate_numeric'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Argument: Global: Null */ + $handler->display->display_options['arguments']['null']['id'] = 'null'; + $handler->display->display_options['arguments']['null']['table'] = 'views'; + $handler->display->display_options['arguments']['null']['field'] = 'null'; + $handler->display->display_options['arguments']['null']['style_plugin'] = 'default_summary'; + $handler->display->display_options['arguments']['null']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['null']['validate_type'] = 'numeric'; + $handler->display->display_options['arguments']['null']['must_not_be'] = 0; + + return $view; + } +} diff --git a/tests/views_basic.test b/tests/views_basic.test new file mode 100644 index 00000000..5fc60d8e --- /dev/null +++ b/tests/views_basic.test @@ -0,0 +1,178 @@ + 'Basic query test', + 'description' => 'A basic query test for Views.', + 'group' => 'Views' + ); + } + + /** + * Tests a trivial result set. + */ + public function testSimpleResultSet() { + $view = $this->getBasicView(); + + // Execute the view. + $this->executeView($view); + + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultset($view, $this->dataSet(), array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + /** + * Tests filtering of the result set. + */ + public function testSimpleFiltering() { + $view = $this->getBasicView(); + + // Add a filter. + $view->display['default']->handler->override_option('filters', array( + 'age' => array( + 'operator' => '<', + 'value' => array( + 'value' => '28', + 'min' => '', + 'max' => '', + ), + 'group' => '0', + 'exposed' => FALSE, + 'expose' => array( + 'operator' => FALSE, + 'label' => '', + ), + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Execute the view. + $this->executeView($view); + + // Build the expected result. + $dataset = array( + array( + 'id' => 1, + 'name' => 'John', + 'age' => 25, + ), + array( + 'id' => 2, + 'name' => 'George', + 'age' => 27, + ), + array( + 'id' => 4, + 'name' => 'Paul', + 'age' => 26, + ), + ); + + // Verify the result. + $this->assertEqual(3, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultSet($view, $dataset, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + /** + * Tests simple argument. + */ + public function testSimpleArgument() { + $view = $this->getBasicView(); + + // Add a argument. + $view->display['default']->handler->override_option('arguments', array( + 'age' => array( + 'default_action' => 'ignore', + 'style_plugin' => 'default_summary', + 'style_options' => array(), + 'wildcard' => 'all', + 'wildcard_substitution' => 'All', + 'title' => '', + 'breadcrumb' => '', + 'default_argument_type' => 'fixed', + 'default_argument' => '', + 'validate_type' => 'none', + 'validate_fail' => 'not found', + 'break_phrase' => 0, + 'not' => 0, + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'validate_user_argument_type' => 'uid', + 'validate_user_roles' => array( + '2' => 0, + ), + 'relationship' => 'none', + 'default_options_div_prefix' => '', + 'default_argument_user' => 0, + 'default_argument_fixed' => '', + 'default_argument_php' => '', + 'validate_argument_node_type' => array( + 'page' => 0, + 'story' => 0, + ), + 'validate_argument_node_access' => 0, + 'validate_argument_nid_type' => 'nid', + 'validate_argument_vocabulary' => array(), + 'validate_argument_type' => 'tid', + 'validate_argument_transform' => 0, + 'validate_user_restrict_roles' => 0, + 'validate_argument_php' => '', + ) + )); + + $saved_view = clone $view; + + // Execute with a view + $view->set_arguments(array(27)); + $this->executeView($view); + + // Build the expected result. + $dataset = array( + array( + 'id' => 2, + 'name' => 'George', + 'age' => 27, + ), + ); + + // Verify the result. + $this->assertEqual(1, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultSet($view, $dataset, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + + // Test "show all" if no argument is present. + $view = $saved_view; + $this->executeView($view); + + // Build the expected result. + $dataset = $this->dataSet(); + + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + $this->assertIdenticalResultSet($view, $dataset, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } +} diff --git a/tests/views_cache.test b/tests/views_cache.test new file mode 100644 index 00000000..31037623 --- /dev/null +++ b/tests/views_cache.test @@ -0,0 +1,244 @@ + 'Cache', + 'description' => 'Tests pluggable caching for views.', + 'group' => 'Views Plugins' + ); + } + + /** + * Build and return a basic view of the views_test table. + * + * @return view + */ + protected function getBasicView() { + views_include('view'); + + // Create the basic view. + $view = new view(); + $view->name = 'test_view'; + $view->add_display('default'); + $view->base_table = 'views_test'; + + // Set up the fields we need. + $display = $view->new_display('default', 'Master', 'default'); + $display->override_option('fields', array( + 'id' => array( + 'id' => 'id', + 'table' => 'views_test', + 'field' => 'id', + 'relationship' => 'none', + ), + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + ), + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + ), + )); + + // Set up the sort order. + $display->override_option('sorts', array( + 'id' => array( + 'order' => 'ASC', + 'id' => 'id', + 'table' => 'views_test', + 'field' => 'id', + 'relationship' => 'none', + ), + )); + + return $view; + } + + /** + * Tests time based caching. + * + * @see views_plugin_cache_time + */ + function testTimeCaching() { + // Create a basic result which just 2 results. + $view = $this->getBasicView(); + $view->set_display(); + $view->display_handler->override_option('cache', array( + 'type' => 'time', + 'results_lifespan' => '3600', + 'output_lifespan' => '3600', + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + + // Add another man to the beatles. + $record = array( + 'name' => 'Rod Davis', + 'age' => 29, + 'job' => 'Banjo', + ); + drupal_write_record('views_test', $record); + + // The Result should be the same as before, because of the caching. + $view = $this->getBasicView(); + $view->set_display(); + $view->display_handler->override_option('cache', array( + 'type' => 'time', + 'results_lifespan' => '3600', + 'output_lifespan' => '3600', + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + } + + /** + * Tests no caching. + * + * @see views_plugin_cache_time + */ + function testNoneCaching() { + // Create a basic result which just 2 results. + $view = $this->getBasicView(); + $view->set_display(); + $view->display_handler->override_option('cache', array( + 'type' => 'none', + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(5, count($view->result), t('The number of returned rows match.')); + + // Add another man to the beatles. + $record = array( + 'name' => 'Rod Davis', + 'age' => 29, + 'job' => 'Banjo', + ); + + drupal_write_record('views_test', $record); + + // The Result changes, because the view is not cached. + $view = $this->getBasicView(); + $view->set_display(); + $view->display_handler->override_option('cache', array( + 'type' => 'none', + )); + + $this->executeView($view); + // Verify the result. + $this->assertEqual(6, count($view->result), t('The number of returned rows match.')); + } + + /** + * Tests css/js storage and restoring mechanism. + */ + function testHeaderStorage() { + // Create a view with output caching enabled. + // Some hook_views_pre_render in views_test.module adds the test css/js file. + // so they should be added to the css/js storage. + $view = $this->getBasicView(); + $view->init_display(); + $view->name = 'test_cache_header_storage'; + $view->display_handler->override_option('cache', array( + 'type' => 'time', + 'output_lifespan' => '3600', + )); + + $view->preview(); + $view->destroy(); + unset($view->pre_render_called); + drupal_static_reset('drupal_add_css'); + drupal_static_reset('drupal_add_js'); + + $view->init_display(); + $view->preview(); + $css = drupal_add_css(); + $css_path = drupal_get_path('module', 'views_test') . '/views_cache.test.css'; + $js_path = drupal_get_path('module', 'views_test') . '/views_cache.test.js'; + $js = drupal_add_js(); + + $this->assertTrue(isset($css[$css_path]), 'Make sure the css is added for cached views.'); + $this->assertTrue(isset($js[$js_path]), 'Make sure the js is added for cached views.'); + $this->assertFalse(!empty($view->pre_render_called), 'Make sure hook_views_pre_render is not called for the cached view.'); + $view->destroy(); + + // Now add some css/jss before running the view. + // Make sure that this css is not added when running the cached view. + $view->name = 'test_cache_header_storage_2'; + + $system_css_path = drupal_get_path('module', 'system') . '/system.maintenance.css'; + drupal_add_css($system_css_path); + $system_js_path = drupal_get_path('module', 'system') . '/system.cron.js'; + drupal_add_js($system_js_path); + + $view->init_display(); + $view->preview(); + $view->destroy(); + drupal_static_reset('drupal_add_css'); + drupal_static_reset('drupal_add_js'); + + $view->init_display(); + $view->preview(); + + $css = drupal_add_css(); + $js = drupal_add_js(); + + $this->assertFalse(isset($css[$system_css_path]), 'Make sure that unrelated css is not added.'); + $this->assertFalse(isset($js[$system_js_path]), 'Make sure that unrelated js is not added.'); + + } + + /** + * Check that HTTP headers are cached for views. + */ + function testHttpHeadersCaching() { + // Create a few nodes to appear in RSS feed. + for ($i = 0; $i < 5; $i++) { + $this->drupalCreateNode(); + } + + // Make the first request and check returned content type. + $this->drupalGet('test_feed_http_headers_caching'); + $first_content = $this->drupalGetContent(); + $first_content_type = $this->drupalGetHeader('content-type'); + $expected_type = 'application/rss+xml'; + $this->assertIdentical(0, strpos(trim($first_content_type), $expected_type), t('Expected content type returned.')); + + // Check that we have 5 items in RSS feed returned by the first request. + $xml = simplexml_load_string($first_content); + $items = $xml->xpath('/rss/channel/item'); + $this->assertEqual(5, count($items), t('The number of RSS feed items matched.')); + + // Create another node to be sure we get cached results on the second + // request. + $this->drupalCreateNode(); + + // Make the second request, check content type and content matching. + $this->drupalGet('test_feed_http_headers_caching'); + $second_content = $this->drupalGetContent(); + $this->assertEqual($first_content, $second_content, t('The second result fetched from cache.')); + $second_content_type = $this->drupalGetHeader('content-type'); + $this->assertEqual($first_content_type, $second_content_type, t('Content types of responses are equal.')); + } + +} diff --git a/tests/views_cache.test.css b/tests/views_cache.test.css new file mode 100644 index 00000000..8dd17c14 --- /dev/null +++ b/tests/views_cache.test.css @@ -0,0 +1,5 @@ +/** + * @file + * Just a placeholder file for the test. + * @see ViewsCacheTest::testHeaderStorage + */ diff --git a/tests/views_cache.test.js b/tests/views_cache.test.js new file mode 100644 index 00000000..8dd17c14 --- /dev/null +++ b/tests/views_cache.test.js @@ -0,0 +1,5 @@ +/** + * @file + * Just a placeholder file for the test. + * @see ViewsCacheTest::testHeaderStorage + */ diff --git a/tests/views_exposed_form.test b/tests/views_exposed_form.test new file mode 100644 index 00000000..72baf2cb --- /dev/null +++ b/tests/views_exposed_form.test @@ -0,0 +1,170 @@ + 'Exposed forms', + 'description' => 'Test exposed forms functionality.', + 'group' => 'Views Plugins', + ); + } + + public function setUp() { + parent::setUp('views_ui'); + module_enable(array('views_ui')); + // @TODO Figure out why it's required to clear the cache here. + views_module_include('views_default', TRUE); + views_get_all_views(TRUE); + menu_rebuild(); + } + + /** + * Tests, whether and how the reset button can be renamed. + */ + public function testRenameResetButton() { + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + // Create some random nodes. + for ($i = 0; $i < 5; $i++) { + $this->drupalCreateNode(); + } + // Look at the page and check the label "reset". + $this->drupalGet('test_rename_reset_button'); + // Rename the label of the reset button. + $view = views_get_view('test_rename_reset_button'); + $view->set_display('default'); + + $exposed_form = $view->display_handler->get_option('exposed_form'); + $exposed_form['options']['reset_button_label'] = $expected_label = $this->randomName(); + $exposed_form['options']['reset_button'] = TRUE; + $view->display_handler->set_option('exposed_form', $exposed_form); + $view->save(); + + views_invalidate_cache(); + + // Look whether ther reset button label changed. + $this->drupalGet('test_rename_reset_button'); + + $this->helperButtonHasLabel('edit-reset', $expected_label); + } + + /** + * Tests the admin interface of exposed filter and sort items. + */ + function testExposedAdminUi() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + menu_rebuild(); + $edit = array(); + + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + // Be sure that the button is called exposed. + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose filter')); + + // The first time the filter UI is displayed, the operator and the + // value forms should be shown. + $this->assertFieldById('edit-options-operator-in', '', 'Operator In exists'); + $this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists'); + $this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists'); + $this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists'); + + // Click the Expose filter button. + $this->drupalPost('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type', $edit, t('Expose filter')); + // Check the label of the expose button. + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide filter')); + // Check the label of the grouped exposed button + $this->helperButtonHasLabel('edit-options-group-button-button', t('Grouped filters')); + + // After Expose the filter, Operator and Value should be still here + $this->assertFieldById('edit-options-operator-in', '', 'Operator In exists'); + $this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists'); + $this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists'); + $this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists'); + + // Check the validations of the filter handler. + $edit = array(); + $edit['options[expose][identifier]'] = ''; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertText(t('The identifier is required if the filter is exposed.')); + + $edit = array(); + $edit['options[expose][identifier]'] = 'value'; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertText(t('This identifier is not allowed.')); + + // Now check the sort criteria. + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/sort/created'); + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose sort')); + $this->assertNoFieldById('edit-options-expose-label', '', t('Make sure no label field is shown')); + + // Click the Grouped Filters button. + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $this->drupalPost(NULL, array(), t('Grouped filters')); + + // After click on 'Grouped Filters' standard operator and value should not be displayed + $this->assertNoFieldById('edit-options-operator-in', '', 'Operator In not exists'); + $this->assertNoFieldById('edit-options-operator-not-in', '', 'Operator Not In not exists'); + $this->assertNoFieldById('edit-options-value-page', '', 'Checkbox for Page not exists'); + $this->assertNoFieldById('edit-options-value-article', '', 'Checkbox for Article not exists'); + + + // Check that after click on 'Grouped Filters', a new button is shown to + // add more items to the list. + $this->helperButtonHasLabel('edit-options-group-info-add-group', t('Add another item')); + + // Create a grouped filter + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $edit = array(); + $edit["options[group_info][group_items][1][title]"] = 'Is Article'; + $edit["options[group_info][group_items][1][value][article]"] = 'article'; + + $edit["options[group_info][group_items][2][title]"] = 'Is Page'; + $edit["options[group_info][group_items][2][value][page]"] = TRUE; + + $edit["options[group_info][group_items][3][title]"] = 'Is Page and Article'; + $edit["options[group_info][group_items][3][value][article]"] = TRUE; + $edit["options[group_info][group_items][3][value][page]"] = TRUE; + $this->drupalPost(NULL, $edit, t('Apply')); + + // Validate that all the titles are defined for each group + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $edit = array(); + $edit["options[group_info][group_items][1][title]"] = 'Is Article'; + $edit["options[group_info][group_items][1][value][article]"] = TRUE; + + // This should trigger an error + $edit["options[group_info][group_items][2][title]"] = ''; + $edit["options[group_info][group_items][2][value][page]"] = TRUE; + + $edit["options[group_info][group_items][3][title]"] = 'Is Page and Article'; + $edit["options[group_info][group_items][3][value][article]"] = TRUE; + $edit["options[group_info][group_items][3][value][page]"] = TRUE; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertRaw(t('The title is required if value for this item is defined.'), t('Group items should have a title')); + + // Un-Expose the filter + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $this->drupalPost(NULL, array(), t('Hide filter')); + + // After Un-Expose the filter, Operator and Value should be shown again + $this->assertFieldById('edit-options-operator-in', '', 'Operator In exists after hide filter'); + $this->assertFieldById('edit-options-operator-not-in', '', 'Operator Not In exists after hide filter'); + $this->assertFieldById('edit-options-value-page', '', 'Checkbox for Page exists after hide filter'); + $this->assertFieldById('edit-options-value-article', '', 'Checkbox for Article exists after hide filter'); + + // Click the Expose sort button. + $edit = array(); + $this->drupalPost('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/sort/created', $edit, t('Expose sort')); + // Check the label of the expose button. + $this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide sort')); + $this->assertFieldById('edit-options-expose-label', '', t('Make sure a label field is shown')); + } +} diff --git a/tests/views_glossary.test b/tests/views_glossary.test new file mode 100644 index 00000000..0fe0fbae --- /dev/null +++ b/tests/views_glossary.test @@ -0,0 +1,60 @@ + 'Glossary Test', + 'description' => 'Tests glossary functionality of views.', + 'group' => 'Views', + ); + } + + public function setUp() { + parent::setUp('views'); + } + + /** + * Tests the default glossary view. + */ + public function testGlossaryView() { + // create a contentype and add some nodes, with a non random title. + $type = $this->drupalCreateContentType(); + $nodes_per_char = array( + 'd' => 1, + 'r' => 4, + 'u' => 10, + 'p' => 2, + 'a' => 3, + 'l' => 6, + ); + foreach ($nodes_per_char as $char => $count) { + $setting = array( + 'type' => $type->type + ); + for ($i = 0; $i < $count; $i++) { + $node = $setting; + $node['title'] = $char . $this->randomString(3); + $this->drupalCreateNode($node); + } + } + + // Execute glossary view + $view = views_get_view('glossary'); + $view->set_display('attachment'); + $view->execute_display('attachment'); + + // Check that the amount of nodes per char. + $result_nodes_per_char = array(); + foreach ($view->result as $item) { + $this->assertEqual($nodes_per_char[$item->title_truncated], $item->num_records); + } + } +} diff --git a/tests/views_groupby.test b/tests/views_groupby.test new file mode 100644 index 00000000..718d8dc0 --- /dev/null +++ b/tests/views_groupby.test @@ -0,0 +1,326 @@ + 'Groupby', + 'description' => 'Tests aggregate functionality of views, for example count.', + 'group' => 'Views', + ); + + } + + /** + * Tests aggregate count feature. + */ + public function testAggregateCount() { + // Create 2 nodes of type1 and 3 nodes of type2 + $type1 = $this->drupalCreateContentType(); + $type2 = $this->drupalCreateContentType(); + + $node_1 = array( + 'type' => $type1->type, + ); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + + $node_2 = array( + 'type' => $type2->type, + ); + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + + $view = $this->viewsAggregateCountView(); + $output = $view->execute_display(); + + $this->assertEqual(count($view->result), 2, 'Make sure the count of items is right.'); + + $types = array(); + foreach ($view->result as $item) { + // num_records is a alias for nid. + $types[$item->node_type] = $item->num_records; + } + + $this->assertEqual($types[$type1->type], 4); + $this->assertEqual($types[$type2->type], 3); + } + + //public function testAggregateSum() { + //} + + public function viewsAggregateCountView() { + $view = new view; + $view->name = 'aggregate_count'; + $view->description = ''; + $view->tag = ''; + $view->base_table = 'node'; + $view->human_name = ''; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['group_by'] = TRUE; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'title'; + $handler->display->display_options['fields']['nid']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['nid']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['nid']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['nid']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['nid']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['nid']['alter']['trim'] = 0; + $handler->display->display_options['fields']['nid']['alter']['html'] = 0; + $handler->display->display_options['fields']['nid']['hide_empty'] = 0; + $handler->display->display_options['fields']['nid']['empty_zero'] = 0; + $handler->display->display_options['fields']['nid']['link_to_node'] = 0; + /* Contextual filter: Content: Type */ + $handler->display->display_options['arguments']['type']['id'] = 'type'; + $handler->display->display_options['arguments']['type']['table'] = 'node'; + $handler->display->display_options['arguments']['type']['field'] = 'type'; + $handler->display->display_options['arguments']['type']['default_action'] = 'summary'; + $handler->display->display_options['arguments']['type']['default_argument_type'] = 'fixed'; + $handler->display->display_options['arguments']['type']['summary']['format'] = 'default_summary'; + + + return $view; + } + + /** + * @param $group_by + * Which group_by function should be used, for example sum or count. + */ + function GroupByTestHelper($group_by, $values) { + // Create 2 nodes of type1 and 3 nodes of type2 + $type1 = $this->drupalCreateContentType(); + $type2 = $this->drupalCreateContentType(); + + $node_1 = array( + 'type' => $type1->type, + ); + // Nids from 1 to 4. + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $this->drupalCreateNode($node_1); + $node_2 = array( + 'type' => $type2->type, + ); + // Nids from 5 to 7. + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + $this->drupalCreateNode($node_2); + + $view = $this->viewsGroupByViewHelper($group_by); + $output = $view->execute_display(); + + $this->assertEqual(count($view->result), 2, 'Make sure the count of items is right.'); + // Group by nodetype to identify the right count. + foreach ($view->result as $item) { + $results[$item->node_type] = $item->nid; + } + $this->assertEqual($results[$type1->type], $values[0]); + $this->assertEqual($results[$type2->type], $values[1]); + } + + function viewsGroupByViewHelper($group_by) { + $view = new view; + $view->name = 'group_by_count'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['group_by'] = TRUE; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + $handler->display->display_options['fields']['nid']['group_type'] = $group_by; + $handler->display->display_options['fields']['nid']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['nid']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['nid']['alter']['trim'] = 0; + $handler->display->display_options['fields']['nid']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['nid']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['nid']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['nid']['alter']['html'] = 0; + $handler->display->display_options['fields']['nid']['hide_empty'] = 0; + $handler->display->display_options['fields']['nid']['empty_zero'] = 0; + $handler->display->display_options['fields']['nid']['link_to_node'] = 0; + /* Field: Content: Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + $handler->display->display_options['fields']['type']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['type']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['type']['alter']['trim'] = 0; + $handler->display->display_options['fields']['type']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['type']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['type']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['type']['alter']['html'] = 0; + $handler->display->display_options['fields']['type']['hide_empty'] = 0; + $handler->display->display_options['fields']['type']['empty_zero'] = 0; + $handler->display->display_options['fields']['type']['link_to_node'] = 0; + + return $view; + } + + public function testGroupByCount() { + $this->GroupByTestHelper('count', array(4, 3)); + } + + function testGroupBySum() { + $this->GroupByTestHelper('sum', array(10, 18)); + } + + + function testGroupByAverage() { + $this->GroupByTestHelper('avg', array(2.5, 6)); + } + + function testGroupByMin() { + $this->GroupByTestHelper('min', array(1, 5)); + } + + function testGroupByMax() { + $this->GroupByTestHelper('max', array(4, 7)); + } + + public function testGroupByCountOnlyFilters() { + // Check if GROUP BY and HAVING are included when a view + // Doesn't display SUM, COUNT, MAX... functions in SELECT statment + + $type1 = $this->drupalCreateContentType(); + + $node_1 = array( + 'type' => $type1->type, + ); + for ($x = 0; $x < 10; $x++) { + $this->drupalCreateNode($node_1); + } + + $view = $this->viewsGroupByCountViewOnlyFilters(); + $output = $view->execute_display(); + + $this->assertTrue(strpos($view->build_info['query'], 'GROUP BY'), t('Make sure that GROUP BY is in the query')); + $this->assertTrue(strpos($view->build_info['query'], 'HAVING'), t('Make sure that HAVING is in the query')); + } + + function viewsGroupByCountViewOnlyFilters() { + $view = new view; + $view->name = 'group_by_in_filters'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['group_by'] = TRUE; + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Nodo: Tipo */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + $handler->display->display_options['fields']['type']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['type']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['type']['alter']['trim'] = 0; + $handler->display->display_options['fields']['type']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['type']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['type']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['type']['alter']['html'] = 0; + $handler->display->display_options['fields']['type']['hide_empty'] = 0; + $handler->display->display_options['fields']['type']['empty_zero'] = 0; + $handler->display->display_options['fields']['type']['link_to_node'] = 0; + /* Filtrar: Nodo: Nid */ + $handler->display->display_options['filters']['nid']['id'] = 'nid'; + $handler->display->display_options['filters']['nid']['table'] = 'node'; + $handler->display->display_options['filters']['nid']['field'] = 'nid'; + $handler->display->display_options['filters']['nid']['group_type'] = 'count'; + $handler->display->display_options['filters']['nid']['operator'] = '>'; + $handler->display->display_options['filters']['nid']['value']['value'] = '3'; + + return $view; + } +} + +/** + * Tests UI of aggregate functionality.. + */ +class viewsUiGroupbyTestCase extends DrupalWebTestCase { + function setUp() { + // Enable views_ui. + parent::setUp('views_ui', 'views_test'); + + // Create and log in a user with administer views permission. + $views_admin = $this->drupalCreateUser(array('administer views', 'administer blocks', 'bypass node access', 'access user profiles', 'view revisions')); + $this->drupalLogin($views_admin); + } + + public static function getInfo() { + return array( + 'name' => 'Groupby UI', + 'description' => 'Tests UI of aggregate functionality.', + 'group' => 'Views UI', + ); + } + + /** + * Tests whether basic saving works. + * + * @todo: this should check the change of the settings as well. + */ + function testGroupBySave() { + $this->drupalGet('admin/structure/views/view/test_views_groupby_save/edit'); + + $edit = array( + 'group_by' => TRUE, + ); + $this->drupalPost('admin/structure/views/nojs/display/test_views_groupby_save/default/group_by', $edit, t('Apply')); + + $this->drupalGet('admin/structure/views/view/test_views_groupby_save/edit'); + $this->drupalPost('admin/structure/views/view/test_views_groupby_save/edit', array(), t('Save')); + + $this->drupalGet('admin/structure/views/nojs/display/test_views_groupby_save/default/group_by'); + } +} diff --git a/tests/views_handlers.test b/tests/views_handlers.test new file mode 100644 index 00000000..d54a7dff --- /dev/null +++ b/tests/views_handlers.test @@ -0,0 +1,150 @@ + 'Handlers test', + 'description' => 'test abstract handler definitions', + 'group' => 'Views', + ); + } + + protected function setUp() { + parent::setUp('views', 'views_ui'); + module_enable(array('views_ui')); + } + + function testFilterInOperatorUi() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + menu_rebuild(); + + $path = 'admin/structure/views/nojs/config-item/test_filter_in_operator_ui/default/filter/type'; + $this->drupalGet($path); + $this->assertFieldByName('options[expose][reduce]', FALSE); + + $edit = array( + 'options[expose][reduce]' => TRUE, + ); + $this->drupalPost($path, $edit, t('Apply')); + $this->drupalGet($path); + $this->assertFieldByName('options[expose][reduce]', TRUE); + } + + /** + * Tests views_break_phrase_string function. + */ + function test_views_break_phrase_string() { + $empty_stdclass = new stdClass(); + $empty_stdclass->operator = 'or'; + $empty_stdclass->value = array(); + + $null = NULL; + // check defaults + $this->assertEqual($empty_stdclass, views_break_phrase_string('', $null)); + + $handler = views_get_handler('node', 'title', 'argument'); + $this->assertEqual($handler, views_break_phrase_string('', $handler)); + + // test ors + $handler = views_break_phrase_string('word1 word2+word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = views_break_phrase_string('word1+word2+word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = views_break_phrase_string('word1 word2 word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = views_break_phrase_string('word-1+word-2+word'); + $this->assertEqualValue(array('word-1', 'word-2', 'word'), $handler); + $this->assertEqual('or', $handler->operator); + $handler = views_break_phrase_string('wõrd1+wõrd2+wõrd'); + $this->assertEqualValue(array('wõrd1', 'wõrd2', 'wõrd'), $handler); + $this->assertEqual('or', $handler->operator); + + // test ands. + $handler = views_break_phrase_string('word1,word2,word'); + $this->assertEqualValue(array('word1', 'word2', 'word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = views_break_phrase_string('word1 word2,word'); + $this->assertEqualValue(array('word1 word2', 'word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = views_break_phrase_string('word1,word2 word'); + $this->assertEqualValue(array('word1', 'word2 word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = views_break_phrase_string('word-1,word-2,word'); + $this->assertEqualValue(array('word-1', 'word-2', 'word'), $handler); + $this->assertEqual('and', $handler->operator); + $handler = views_break_phrase_string('wõrd1,wõrd2,wõrd'); + $this->assertEqualValue(array('wõrd1', 'wõrd2', 'wõrd'), $handler); + $this->assertEqual('and', $handler->operator); + + // test a single word + $handler = views_break_phrase_string('word'); + $this->assertEqualValue(array('word'), $handler); + $this->assertEqual('and', $handler->operator); + } + + /** + * Tests views_break_phrase function. + */ + function test_views_break_phrase() { + $empty_stdclass = new stdClass(); + $empty_stdclass->operator = 'or'; + $empty_stdclass->value = array(); + + $null = NULL; + // check defaults + $this->assertEqual($empty_stdclass, views_break_phrase('', $null)); + + $handler = views_get_handler('node', 'title', 'argument'); + $this->assertEqual($handler, views_break_phrase('', $handler)); + + // Generate three random numbers which can be used below; + $n1 = rand(0, 100); + $n2 = rand(0, 100); + $n3 = rand(0, 100); + // test ors + $this->assertEqualValue(array($n1, $n2, $n3), views_break_phrase("$n1 $n2+$n3", $handler)); + $this->assertEqual('or', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), views_break_phrase("$n1+$n2+$n3", $handler)); + $this->assertEqual('or', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), views_break_phrase("$n1 $n2 $n3", $handler)); + $this->assertEqual('or', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), views_break_phrase("$n1 $n2++$n3", $handler)); + $this->assertEqual('or', $handler->operator); + + // test ands. + $this->assertEqualValue(array($n1, $n2, $n3), views_break_phrase("$n1,$n2,$n3", $handler)); + $this->assertEqual('and', $handler->operator); + $this->assertEqualValue(array($n1, $n2, $n3), views_break_phrase("$n1,,$n2,$n3", $handler)); + $this->assertEqual('and', $handler->operator); + } + + /** + * Check to see if two values are equal. + * + * @param $first + * The first value to check. + * @param views_handler $handler + * @param string $message + * The message to display along with the assertion. + * @param string $group + * The type of assertion - examples are "Browser", "PHP". + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertEqualValue($first, $handler, $message = '', $group = 'Other') { + return $this->assert($first == $handler->value, $message ? $message : t('First value is equal to second value'), $group); + } +} diff --git a/tests/views_module.test b/tests/views_module.test new file mode 100644 index 00000000..dc6c1640 --- /dev/null +++ b/tests/views_module.test @@ -0,0 +1,163 @@ + 'Tests views.module', + 'description' => 'Tests some basic functions of views.module', + 'group' => 'Views', + ); + } + + public function setUp() { + parent::setUp(); + drupal_theme_rebuild(); + } + + public function viewsData() { + $data = parent::viewsData(); + $data['views_test_previous'] = array(); + $data['views_test_previous']['id']['field']['moved to'] = array('views_test', 'id'); + $data['views_test_previous']['id']['filter']['moved to'] = array('views_test', 'id'); + $data['views_test']['age_previous']['field']['moved to'] = array('views_test', 'age'); + $data['views_test']['age_previous']['sort']['moved to'] = array('views_test', 'age'); + $data['views_test_previous']['name_previous']['field']['moved to'] = array('views_test', 'name'); + $data['views_test_previous']['name_previous']['argument']['moved to'] = array('views_test', 'name'); + + return $data; + } + + public function test_views_trim_text() { + // Test unicode, @see http://drupal.org/node/513396#comment-2839416 + $text = array( + 'Tuy nhiên, những hi vọng', + 'Giả sử chúng tôi có 3 Apple', + 'siêu nhỏ này là bộ xử lý', + 'Di động của nhà sản xuất Phần Lan', + 'khoảng cách từ đại lí đến', + 'của hãng bao gồm ba dòng', + 'сд асд асд ас', + 'асд асд асд ас' + ); + // Just test maxlength without word boundry. + $alter = array( + 'max_length' => 10, + ); + $expect = array( + 'Tuy nhiên,', + 'Giả sử chú', + 'siêu nhỏ n', + 'Di động củ', + 'khoảng các', + 'của hãng b', + 'сд асд асд', + 'асд асд ас', + ); + + foreach ($text as $key => $line) { + $result_text = views_trim_text($alter, $line); + $this->assertEqual($result_text, $expect[$key]); + } + + // Test also word_boundary + $alter['word_boundary'] = TRUE; + $expect = array( + 'Tuy nhiên', + 'Giả sử', + 'siêu nhỏ', + 'Di động', + 'khoảng', + 'của hãng', + 'сд асд', + 'асд асд', + ); + + foreach ($text as $key => $line) { + $result_text = views_trim_text($alter, $line); + $this->assertEqual($result_text, $expect[$key]); + } + } + + /** + * Tests the dynamic includes of templates via module feature. + */ + function testModuleTemplates() { + $views_status = variable_get('views_defaults', array()); + $views_status['frontpage'] = FALSE; // false is enabled + variable_set('views_defaults', $views_status); + + $existing = array(); + $type = array(); + $theme = array(); + $path = array(); + $registry = views_theme($existing, $type, $theme, $path); + $this->assertTrue(isset($registry['views_view__frontpage'])); + } + + /** + * Tests the views_get_handler method. + */ + function testviews_get_handler() { + $types = array('field', 'area', 'filter'); + foreach ($types as $type) { + $handler = views_get_handler($this->randomName(), $this->randomName(), $type); + $this->assertEqual('views_handler_' . $type . '_broken', get_class($handler), t('Make sure that a broken handler of type: @type are created', array('@type' => $type))); + } + + $views_data = $this->viewsData(); + $test_tables = array('views_test' => array('id', 'name')); + foreach ($test_tables as $table => $fields) { + foreach ($fields as $field) { + $data = $views_data[$table][$field]; + foreach ($data as $id => $field_data) { + if (!in_array($id, array('title', 'help'))) { + $handler = views_get_handler($table, $field, $id); + $this->assertInstanceHandler($handler, $table, $field, $id); + } + } + } + } + + // Test the automatic conversion feature. + + // Test the automatic table renaming. + $handler = views_get_handler('views_test_previous', 'id', 'field'); + $this->assertInstanceHandler($handler, 'views_test', 'id', 'field'); + $handler = views_get_handler('views_test_previous', 'id', 'filter'); + $this->assertInstanceHandler($handler, 'views_test', 'id', 'filter'); + + // Test the automatic field renaming. + $handler = views_get_handler('views_test', 'age_previous', 'field'); + $this->assertInstanceHandler($handler, 'views_test', 'age', 'field'); + $handler = views_get_handler('views_test', 'age_previous', 'sort'); + $this->assertInstanceHandler($handler, 'views_test', 'age', 'sort'); + + // Test the automatic table and field renaming. + $handler = views_get_handler('views_test_previous', 'name_previous', 'field'); + $this->assertInstanceHandler($handler, 'views_test', 'name', 'field'); + $handler = views_get_handler('views_test_previous', 'name_previous', 'argument'); + $this->assertInstanceHandler($handler, 'views_test', 'name', 'argument'); + + // Test the override handler feature. + $handler = views_get_handler('views_test', 'job', 'filter', 'views_handler_filter'); + $this->assertEqual('views_handler_filter', get_class($handler)); + } + + /** + * Ensure that a certain handler is a instance of a certain table/field. + */ + function assertInstanceHandler($handler, $table, $field, $id) { + $table_data = views_fetch_data($table); + $field_data = $table_data[$field][$id]; + + $this->assertEqual($field_data['handler'], get_class($handler)); + } +} diff --git a/tests/views_pager.test b/tests/views_pager.test new file mode 100644 index 00000000..fef49153 --- /dev/null +++ b/tests/views_pager.test @@ -0,0 +1,496 @@ + 'Pager', + 'description' => 'Test the pluggable pager system', + 'group' => 'Views Plugins', + ); + } + + public function setUp() { + parent::setUp('views', 'views_ui', 'views_test'); + } + + /** + * Pagers was sometimes not stored. + * + * @see http://drupal.org/node/652712 + */ + public function testStorePagerSettings() { + $admin_user = $this->drupalCreateUser(array('administer views', 'administer site configuration')); + $this->drupalLogin($admin_user); + // Test behaviour described in http://drupal.org/node/652712#comment-2354918. + + $this->drupalGet('admin/structure/views/view/frontpage/edit'); + + + $edit = array( + 'pager_options[items_per_page]' => 20, + ); + $this->drupalPost('admin/structure/views/nojs/display/frontpage/default/pager_options', $edit, t('Apply')); + $this->assertText('20 items'); + + // Change type and check whether the type is new type is stored. + $edit = array(); + $edit = array( + 'pager[type]' => 'mini', + ); + $this->drupalPost('admin/structure/views/nojs/display/frontpage/default/pager', $edit, t('Apply')); + $this->drupalGet('admin/structure/views/view/frontpage/edit'); + $this->assertText('Mini', 'Changed pager plugin, should change some text'); + + // Test behaviour described in http://drupal.org/node/652712#comment-2354400 + $view = $this->viewsStorePagerSettings(); + // Make it editable in the admin interface. + $view->save(); + + $this->drupalGet('admin/structure/views/view/test_store_pager_settings/edit'); + + $edit = array(); + $edit = array( + 'pager[type]' => 'full', + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager', $edit, t('Apply')); + $this->drupalGet('admin/structure/views/view/test_store_pager_settings/edit'); + $this->assertText('Full'); + + $edit = array( + 'pager_options[items_per_page]' => 20, + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Apply')); + $this->assertText('20 items'); + + // add new display and test the settings again, by override it. + $edit = array( ); + // Add a display and override the pager settings. + $this->drupalPost('admin/structure/views/view/test_store_pager_settings/edit', $edit, t('Add Page')); + $edit = array( + 'override[dropdown]' => 'page_1', + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/page_1/pager', $edit, t('Apply')); + + $edit = array( + 'pager[type]' => 'mini', + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/page_1/pager', $edit, t('Apply')); + $this->drupalGet('admin/structure/views/view/test_store_pager_settings/edit'); + $this->assertText('Mini', 'Changed pager plugin, should change some text'); + + $edit = array( + 'pager_options[items_per_page]' => 10, + ); + $this->drupalPost('admin/structure/views/nojs/display/test_store_pager_settings/default/pager_options', $edit, t('Apply')); + $this->assertText('20 items'); + + } + + public function viewsStorePagerSettings() { + $view = new view; + $view->name = 'test_store_pager_settings'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 3; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'none'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + return $view; + } + + /** + * Tests the none-pager-query. + */ + public function testNoLimit() { + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->viewsPagerNoLimit(); + $view->set_display('default'); + $this->executeView($view); + $this->assertEqual(count($view->result), 11, 'Make sure that every item is returned in the result'); + + $view->destroy(); + + // Setup and test a offset. + $view = $this->viewsPagerNoLimit(); + $view->set_display('default'); + + $pager = array( + 'type' => 'none', + 'options' => array( + 'offset' => 3, + ), + ); + $view->display_handler->set_option('pager', $pager); + $this->executeView($view); + + $this->assertEqual(count($view->result), 8, 'Make sure that every item beside the first three is returned in the result'); + + // Check some public functions. + $this->assertFalse($view->query->pager->use_pager()); + $this->assertFalse($view->query->pager->use_count_query()); + $this->assertEqual($view->query->pager->get_items_per_page(), 0); + } + + public function viewsPagerNoLimit() { + $view = new view; + $view->name = 'test_pager_none'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version =3; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'none'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + return $view; + } + + public function testViewTotalRowsWithoutPager() { + $this->createNodes(23); + + $view = $this->viewsPagerNoLimit(); + $view->get_total_rows = TRUE; + $view->set_display('default'); + $this->executeView($view); + + $this->assertEqual($view->total_rows, 23, "'total_rows' is calculated when pager type is 'none' and 'get_total_rows' is TRUE."); + } + + public function createNodes($count) { + if ($count >= 0) { + for ($i = 0; $i < $count; $i++) { + $this->drupalCreateNode(); + } + } + } + + /** + * Tests the some pager plugin. + */ + public function testLimit() { + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->viewsPagerLimit(); + $view->set_display('default'); + $this->executeView($view); + $this->assertEqual(count($view->result), 5, 'Make sure that only a certain count of items is returned'); + $view->destroy(); + + // Setup and test a offset. + $view = $this->viewsPagerLimit(); + $view->set_display('default'); + + $pager = array( + 'type' => 'none', + 'options' => array( + 'offset' => 8, + 'items_per_page' => 5, + ), + ); + $view->display_handler->set_option('pager', $pager); + $this->executeView($view); + $this->assertEqual(count($view->result), 3, 'Make sure that only a certain count of items is returned'); + + // Check some public functions. + $this->assertFalse($view->query->pager->use_pager()); + $this->assertFalse($view->query->pager->use_count_query()); + } + + public function viewsPagerLimit() { + $view = new view; + $view->name = 'test_pager_some'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 3; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['pager']['options']['offset'] = 0; + $handler->display->display_options['pager']['options']['items_per_page'] = 5; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + return $view; + } + + /** + * Tests the normal pager. + */ + public function testNormalPager() { + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->viewsPagerFull(); + $view->set_display('default'); + $this->executeView($view); + $this->assertEqual(count($view->result), 5, 'Make sure that only a certain count of items is returned'); + $view->destroy(); + + // Setup and test a offset. + $view = $this->viewsPagerFull(); + $view->set_display('default'); + + $pager = array( + 'type' => 'full', + 'options' => array( + 'offset' => 8, + 'items_per_page' => 5, + ), + ); + $view->display_handler->set_option('pager', $pager); + $this->executeView($view); + $this->assertEqual(count($view->result), 3, 'Make sure that only a certain count of items is returned'); + + // Test items per page = 0 + $view = $this->viewPagerFullZeroItemsPerPage(); + $view->set_display('default'); + $this->executeView($view); + + $this->assertEqual(count($view->result), 11, 'All items are return'); + + // TODO test number of pages. + + // Test items per page = 0. + $view->destroy(); + + // Setup and test a offset. + $view = $this->viewsPagerFull(); + $view->set_display('default'); + + $pager = array( + 'type' => 'full', + 'options' => array( + 'offset' => 0, + 'items_per_page' => 0, + ), + ); + + $view->display_handler->set_option('pager', $pager); + $this->executeView($view); + $this->assertEqual($view->query->pager->get_items_per_page(), 0); + $this->assertEqual(count($view->result), 11); + } + + function viewPagerFullZeroItemsPerPage() { + $view = new view; + $view->name = 'view_pager_full_zero_items_per_page'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 3; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '0'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['title']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim'] = 0; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['title']['alter']['html'] = 0; + $handler->display->display_options['fields']['title']['hide_empty'] = 0; + $handler->display->display_options['fields']['title']['empty_zero'] = 0; + $handler->display->display_options['fields']['title']['link_to_node'] = 0; + + return $view; + } + + function viewsPagerFull() { + $view = new view; + $view->name = 'test_pager_full'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 3; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '5'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'node'; + + return $view; + } + + function viewsPagerFullFields() { + $view = new view; + $view->name = 'test_pager_full'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 3; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '5'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['title']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim'] = 0; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['title']['alter']['html'] = 0; + $handler->display->display_options['fields']['title']['hide_empty'] = 0; + $handler->display->display_options['fields']['title']['empty_zero'] = 0; + $handler->display->display_options['fields']['title']['link_to_node'] = 0; + return $view; + } + + /** + * Tests the minipager. + */ + public function testMiniPager() { + // the functionality is the same as the normal pager, so i don't know what to test here. + } + + /** + * Tests rendering with NULL pager. + */ + public function testRenderNullPager() { + // Create 11 nodes and make sure that everyone is returned. + // We create 11 nodes, because the default pager plugin had 10 items per page. + for ($i = 0; $i < 11; $i++) { + $this->drupalCreateNode(); + } + $view = $this->viewsPagerFullFields(); + $view->set_display('default'); + $this->executeView($view); + $view->use_ajax = TRUE; // force the value again here + $view->query->pager = NULL; + $output = $view->render(); + $this->assertEqual(preg_match('/