diff --git a/ABOUT-7.x-3.x.md b/ABOUT-7.x-3.x.md index 01a893562..76579cb9c 100644 --- a/ABOUT-7.x-3.x.md +++ b/ABOUT-7.x-3.x.md @@ -1,98 +1,6 @@ The 7.x-3.x branch of petitions aims to remove the application's dependency on -Mongo DB and run on MySQL. Between now and whenever 7.x-3.0 is released this -branch will be building out and refining a proof-of-concept for what will likely -turn out to be mysql-based petition nodes and signature entities. +Mongo DB and run on MySQL. For a stable Mongo-based petitions application, please use the 7.x-2.x branch. - -Technical notes on phasing out Mongo dependencies -------------------------------------------------- - - Modules to be removed by the code base by 7.x-3.0 (more modules may get added - to this list): - - wh_petitions - - petitions_data - - Any new code added to the 7.x-3.x branch is being written in anticipation of - mongo storage being turned off, and petition nodes and signature_mail entities - based on a mysql back end. Here are some examples of how we're keeping things - organized in anticipation of mongo going away: - - ### Example #1: Keeping things organized in mymodule.mongo2mysql.inc - - Store functions that are intended to be deleted when mongo gets shut off in - mymodule.mongo2mysql.inc. - - Name any new functions created in *.mongo2mysql.inc with mongo2mysql in the - function name. This way, when it's time to shut mongo off, all we have to do - is (1) find any files with mongo2mysql in the name and delete them, then (2) - grep through the code for mongo2mysql, and remove those function calls. - - - ### Example #2: Calling mongo-dependent functions and checking related shunt trips - - While we're in transition phasing mongo out, application functionality - should be decoupled from a specific storage backend as much as possible. - If myslq-based data is available, it's authoritiative. But don't assume it's - available. If mysql-based data is NOT available, try getting mongo-based - data. But don't assume that's available either. Eventually it will get shut - off. For example: - - ```php - - function mymodule_does_something_with_petitions() { - - // Load a petition. - - // First try mysql (petition nodes). - $petition_is_enabled = module_exists('petition'); - $mysql_shunt_is_tripped = shunt_is_enabled('petition_mysql_save'); - if ($petition_is_enabled && !$mysql_shunt_is_tripped) { - $petition = node_load($nid); - } - - // ******************************************************************* - // * Note: This entire block of code can simply be deleted after * - // * mysql is "turned on" and mongo is turned off. * - // ******************************************************************* - // - // If mysql data isn't available or we need to do something with mongo - // data for some reason, get it from mongo like this. - $mongo_shunt_is_tripped = shunt_is_enabled('wh_petitions_petition_create'); - if (!$mongo_shunt_is_tripped) { - $petition_from_mongo = petitions_data_mongo2mysql_get_petition($mongo_petition_id); - } - // Format petition data like a petition node, reconcile the two - // different petition objects, or throw watchdog errors. - $petition = mymodule_mongo2mysql_transitional_reconciling_and_formatting_happens_here($petition, $petition2); - - // Now proceed to do stuff with your $petition node (or node-like - // object)... - - } - - ``` - - For a more complex real-world example of ^^ this, see signatures_queue/includes/process_signatures.inc. - - - ### Example #3: Phasing out mongo-dependent functions. - - To phase out mongo-dependent functions or anything else that's meant to be removed: - - Move functions intended to be removed into example.mongo2myql.inc files - - Rename functions like this: example_get_example() -> example_mongo2mysql_get_example() - - Replace calls to example_get_example() throughout the codebase with example_mongo2mysql_get_example() - - In example.mongo2mysql.inc provide the legacy function, but throw an error explaining what's going on - - See petitions_data_get_petition and petitions_data_mongo2mysql_get_petition - as an example here: - https://github.com/bryanhirsch/_petitions/commit/453e2f96793e9ce8a95edbc325497bfebd298942 - - Note: Lines like this [here](https://github.com/bryanhirsch/_petitions/commit/453e2f96793e9ce8a95edbc325497bfebd298942#diff-325f9f5d1e9e226e082ddc1c205b536eR47) - are NOT safe and future proofed as described in example #2. - Developers/Contributors, wherever possible, please code defensively and - write things like #2 along the way so there's less housekeeping to do when it's time to grep for things like - [this](https://github.com/bryanhirsch/_petitions/commit/453e2f96793e9ce8a95edbc325497bfebd298942#diff-325f9f5d1e9e226e082ddc1c205b536eR47) - and replace it with its mysql equivalents. - +This document has been moved to an internal doc. Please contact us through the github issue queue to get involved: github.com/Whitehouse/petitions . diff --git a/INSTALL.md b/INSTALL.md index 37b3a0ea3..b8b50f2ed 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -4,10 +4,8 @@ INSTALL.md **Contents** * "Alpha" software status -* Install MongoDB for local development * Installing and configuring Petitions * Manual Rules import -* MongoDB configuration in settings.php "Alpha" software status @@ -15,29 +13,13 @@ INSTALL.md "Alpha" means we cannot promise to provide an upgrade path to users who build sites on the current code base. -Later releases will remove this application's dependence on MongoDB. Our intention is to evolve this code base into an install profile that others can easily reuse, extend and contribute to. This is not the state of the current application, which was made specifically for the White House's particular use cases and hosting environment. +Our intention is to evolve this code base into an install profile that others can easily reuse, extend and contribute to. This is not the state of the current application, which was made specifically for the White House's particular use cases and hosting environment. -These instructions will help you install Drupal, get Drupal talking to MySQL and MongoDB, and let you try out the existing code base. +These instructions will help you install Drupal, get Drupal talking to MySQL, and let you try out the existing code base. Where the application still has dependencies on configuration stored in the site's database, these are areas where the install profile remains a work in progress. We will release improvements as we make them on GitHub. In the meanwhile, patches are welcome too. - -Install MongoDB for local development -------------------------------------- - -For local development on Mac OSX with MAMP (similar with XAMPP), install Homebrew, then do this: - -``` -$ brew versions mongo -$ cd /usr/local/Cellar -$ git checkout dae14ec /usr/local/Library/Formula/mongodb.rb -$ brew install mongo - -$ /Applications/MAMP/bin/php/php5.3.6/bin/pecl install mongo -$ mkdir /data/db -$ mongod # This starts mongo. -$ mongo # This starts the mongo client. -``` +At times the application may encounter issues due to expecting a MongoDB connection. These issues are artifacts of the previous architecture of the application, and the plan is to remove them in future releases. Installing and configuring Petitions @@ -57,34 +39,6 @@ drush -y make --no-core --contrib-destination=. drupal-org.make 5) Follow the normal Drupal installation process. When prompted to select a profile, select "Petitions." Drupal will rewrite your settings.php file. - After it does, you will be prompted to add a snippet like this to the end - of settings.php. Do this before you visit your site, otherwise Drupal will - be unhappy: - -```php - // Set mongo configuration - $mongo_host = '127.0.0.1'; - $mongo_db_name = 'petition'; - $conf['mongodb_connections'] = array( - 'default' => array('host' => $mongo_host, 'db' => $mongo_db_name), - 'petition_tool' => array('host' => $mongo_host, 'db' => $mongo_db_name), - 'petition_tool_archive' => array('host' => $mongo_host, 'db' => $mongo_db_name), - 'petition_tool_response' => array('host' => $mongo_host, 'db' => $mongo_db_name), - 'petition_tool_signatures' => array('host' => $mongo_host, 'db' => $mongo_db_name), - ); - $conf['mongodb_collections'] = array( - 'petitions' => 'petition_tool', - 'archive_petitions' => 'petition_tool_archive', - 'petition_response' => 'petition_tool_response', - 'petition_signatures' => 'petition_tool_signatures', - ); - - # (Optional): - # $conf['mongodb_options'] = array( - # 'replicaSet' => 'petitions', - # 'timeout' => 1, - # ); -``` 6) **IMPORTANT!** Configure second database for signature processing: @@ -297,3 +251,11 @@ Import the user_submit rule here (check "Overwrite"): // "white screen of death" (WSOD) pages. ini_set('display_startup_errors', TRUE); ``` + +13) The simplified signing functionality requires 4 cron jobs to be configured: +* initiate_signature_validation +* preprocess_signatures +* process_signatures +* archive_signatures + +These jobs can be executed via drush by using the `signatures-queue-invoke-workflow` command. diff --git a/README.md b/README.md index 4694d1964..151b000fd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Drupal 7 code base used to build an application that lets users create and sign petitions. -This application is under active development and will continue to be modified and improved over time. The current release is an "alpha" pending changes that will remove its dependency on MongoDB (see “Roadmap” section below). +This application is under active development and will continue to be modified and improved over time. The current release is an "alpha". ## Goals @@ -18,13 +18,12 @@ Releasing the source code for this application is meant to empower other governm * Drupal 7.x * MySQL 5.x -* MongoDB 2.2.4 * PHP 5.2 or 5.3 *Recommended:* * RAM +512 M -* Dedicated MongoDB server (this dependency will be removed soon, see “Roadmap”) +* Dedicated Solr server ## Usage @@ -36,6 +35,8 @@ For installation instructions, see INSTALL.md. NOTE: Setting up the application and configuring it for use in your organization’s context requires Drupal development experience. The application ships with a similar design (theme) to what is used on petitions.whitehouse.gov and needs to be customized for use by others using standard Drupal 7 themeing conventions. The application also ships with various user interface elements, user account settings, and other configurations that users should expect to customize using standard Drupal 7 techniques and conventions. +Keys for the write API are issued, validated, and rate limited by a third-party service. + ## Roadmap Have an idea or question about future features for We the People? Let us know by opening a ticket on GitHub, emailing us directly at wethepeople@who.eop.gov, or tweeting @WHWeb. @@ -44,12 +45,6 @@ We the People is a work in progress and currently exists at a very basic level o The following descriptions are for informational purposes only and should not be interpreted as commitments or guarantees of future code releases in any way. -*Move from MongoDB to MySQL* - -The current release depends on MongoDB. When we first created the application, we wanted to make sure we had a highly scalable application and database to meet our anticipated performance needs under high loads. We have been running MongoDB in production for over a year, but we have decided that the performance benefits it provides are outweighed by the complexity of trying to extend Drupal features backed by MongoDB. - -We are currently moving to a fully MySQL-backed application to increase the speed the development of new features and other aspects of maintaining the Drupal application. Our next release will be a dev branch that will be fully backed by MySQL, and once there is a tag for that branch, we will no longer maintain the MongoDB branch. - *Install Profile* The codebase is released as-is and currently supports a specific, standalone website. In the future we would like to provide an install profile that supports a wider range of applications. diff --git a/build-petitions.make b/build-petitions.make index 04d5974e9..4bff98565 100644 --- a/build-petitions.make +++ b/build-petitions.make @@ -8,14 +8,9 @@ core = 7.x ; @see http://drupal.org/node/972536 ; ; -------------------------------------- -projects[drupal][version] = 7.32 +projects[drupal][version] = 7.35 projects[drupal][patch][] = http://drupal.org/files/drupal-menu-int-972536-83-D7.patch -; Patch correcting database switching within a Simpletest run. -; @see https://drupal.org/node/2155023 -; ------------------------------- -projects[drupal][patch][] = https://drupal.org/files/issues/drupal-insertassert_exception-2155023-3.patch - ; Petitions installation profile ; ------------------------------- projects[petitions][type] = profile diff --git a/drupal-org.make b/drupal-org.make index 644332219..f1e670e51 100644 --- a/drupal-org.make +++ b/drupal-org.make @@ -8,11 +8,17 @@ defaults[projects][subdir] = contrib projects[advanced_help][version] = 1.0 +projects[apachesolr][version] = 1.7 +projects[apachesolr][patch][1764352-2] = https://www.drupal.org/files/issues/decouple_cron-1764352-2.patch +projects[apachesolr][patch][2457953] = https://www.drupal.org/files/issues/apachesolr-slow_queries_reindex-10.patch +projects[apachesolr][patch][2459461] = https://www.drupal.org/files/issues/apache_solr_profiling-2459461-5.patch +projects[apachesolr][patch][2476229] = https://www.drupal.org/files/issues/apachesolr-solr_clear_batch-2476229-1.patch + projects[captcha][version] = 1.0 projects[conditional_styles][version] = 2.1 -projects[context][version] = 3.1 +projects[context][version] = 3.6 projects[ctools][version] = 1.4 @@ -20,6 +26,11 @@ projects[date][version] = 2.6 projects[diff][version] = 3.2 +projects[efq_extra_field][download][type] = git +projects[efq_extra_field][download][url] = http://git.drupal.org/project/efq_extra_field.git +projects[efq_extra_field][download][revision] = c81036076d3818afb8fd16041b00bf6dabf0b6b1 +projects[efq_extra_field][patch][2399063-1] = https://www.drupal.org/files/issues/efq_extra_field-move_class_to_include-2399063-1.patch + projects[email_confirm][version] = 1.1 projects[entity][version] = 1.2 @@ -111,14 +122,22 @@ projects[textcaptcha][patch][2279207-1] = https://drupal.org/files/issues/textca projects[token][version] = 1.5 -projects[views][version] = 3.7 +projects[transliteration][version] = 3.2 + +projects[views][version] = 3.11 projects[views_infinite_scroll][version] = 1.1 +; Apply patch from https://www.drupal.org/node/1199794 to eliminate count query from infinite scroller for performance +projects[views_infinite_scroll][patch][1199794] = https://www.drupal.org/files/issues/infinite_scroll_no_count.patch projects[views_bulk_operations][version] = 3.1 +projects[views_data_export][version] = 3.0-beta8 + projects[wysiwyg][version] = 2.2 +projects[usfedgov_google_analytics][version] = 1.0-rc1 + ; Contrib Themes ; ============================================================================== @@ -159,3 +178,6 @@ libraries[petitions-php-sdk][download][revision] = fe03d49e39e88e87cff2295172d02 libraries[spyc][download][type] = file libraries[spyc][download][url] = https://raw.github.com/mustangostang/spyc/79f61969f63ee77e0d9460bc254a27a671b445f3/spyc.php libraries[spyc][download][filename] = spyc.php + +libraries[fed_analytics][download][type] = get +libraries[fed_analytics][download][url] = https://github.com/GSA/DAP-Gov-wide-GA-Code/archive/1785a8c79cb991ef4efd1a8ee6b7c3d66647119f.zip diff --git a/modules/custom/generate_mongo2mysql_petsig/PetitionContentGenerator.php b/modules/custom/generate_mongo2mysql_petsig/PetitionContentGenerator.php index 79521d7de..0fc120dd8 100644 --- a/modules/custom/generate_mongo2mysql_petsig/PetitionContentGenerator.php +++ b/modules/custom/generate_mongo2mysql_petsig/PetitionContentGenerator.php @@ -155,7 +155,7 @@ private function getPetitionDummyData() { $data['field_petition_featured'] = $faker->randomNumber(0, 1); $data['field_petition_hidden'] = 0; $data['field_petition_status'] = WH_PETITION_STATUS_PUBLIC; - $data['field_petition_response_status'] = WH_PETITION_RESPONSE_STATUS_UNANSWERED; + $data['field_response_status'] = WH_PETITION_RESPONSE_STATUS_UNANSWERED; $data['field_petition_review_timeframe'] = $review_timeframe; return $data; @@ -286,7 +286,7 @@ private function saveMongoPetition($data) { 'private_tags' => array(), 'related_petitions' => array(), 'petition_status' => $data['field_petition_status'], - 'response_status' => $data['field_petition_response_status'], + 'response_status' => $data['field_response_status'], 'published' => $data['field_timestamp_published'], 'reached_public' => $data['field_timestamp_reached_public'], 'reached_ready' => $data['field_timestamp_reached_ready'], diff --git a/modules/custom/migrate_mongo2mysql_petsig/link_responses_to_mysql_petitions.drush b/modules/custom/migrate_mongo2mysql_petsig/link_responses_to_mysql_petitions.drush new file mode 100644 index 000000000..3aacf8b7f --- /dev/null +++ b/modules/custom/migrate_mongo2mysql_petsig/link_responses_to_mysql_petitions.drush @@ -0,0 +1,52 @@ +entity_id])) { + $deltas[$result->entity_id] = 0; + } + else { + $deltas[$result->entity_id]++; + } + db_query("INSERT IGNORE INTO field_data_field_petition_id (entity_type, bundle, deleted, entity_id, revision_id, language, delta, field_petition_id_target_id) VALUES (:entity_type, :bundle, :deleted, :entity_id, :revision_id, :language, :delta, :field_petition_id_target_id)", + array( + ':entity_type' => $result->entity_type, + ':bundle' => $result->bundle, + ':deleted' => $result->deleted, + ':entity_id' => $result->entity_id, + ':revision_id' => $result->revision_id, + ':language' => $result->language, + ':delta' => $deltas[$result->entity_id], + ':field_petition_id_target_id' => $result->field_petition_id_target_id, + )); + db_query("INSERT IGNORE INTO field_revision_field_petition_id (entity_type, bundle, deleted, entity_id, revision_id, language, delta, field_petition_id_target_id) VALUES (:entity_type, :bundle, :deleted, :entity_id, :revision_id, :language, :delta, :field_petition_id_target_id)", + array( + ':entity_type' => $result->entity_type, + ':bundle' => $result->bundle, + ':deleted' => $result->deleted, + ':entity_id' => $result->entity_id, + ':revision_id' => $result->revision_id, + ':language' => $result->language, + ':delta' => $deltas[$result->entity_id], + ':field_petition_id_target_id' => $result->field_petition_id_target_id, + )); +} diff --git a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.admin.inc b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.admin.inc new file mode 100644 index 000000000..c3545e25b --- /dev/null +++ b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.admin.inc @@ -0,0 +1,74 @@ + '
' . t('Change migration settings for petition and signature migrations.') . '
', + ); + + // Petition Configuration Settings. + $form['petition_config'] = array( + '#type' => 'fieldset', + '#title' => t('Petition Migration Settings'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + $form['petition_config']['migrate_mongo2mysql_pet_unpublish'] = array( + '#type' => 'checkbox', + '#title' => t('Migrate unpublished petitions'), + '#description' => t('By default, only published petitions are migrated. Check this box to include unpublished migrations during the final delta migration.'), + '#default_value' => (bool) variable_get('migrate_mongo2mysql_pet_unpublish', 0), + ); + + $form['signature_config'] = array( + '#type' => 'fieldset', + '#title' => t('Signature Migration Settings'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + + $date_format = 'Y-m-d'; + + $default_start_date = '2005-01-01 00:00:00'; + $form['signature_config']['migrate_mongo2mysql_sig_date_start'] = array( + '#type' => 'date_select', + '#date_format' => $date_format, + '#title' => t('Signatures created after'), + '#description' => t('By default, all signatures are migrated. Use this field to restrict signature migrations to signatures that were created after a specified date.'), + '#default_value' => variable_get('migrate_mongo2mysql_sig_date_start', $default_start_date), + '#date_year_range' => '-10:+1', + ); + + $default_end_date = '2014-12-08 00:00:00'; + $form['signature_config']['migrate_mongo2mysql_sig_date_end'] = array( + '#type' => 'date_select', + '#date_format' => $date_format, + '#title' => t('Signatures created before'), + '#description' => t('By default, all signatures are migrated. Use this field to restrict signature migrations to signatures that were created before a specified date.'), + '#default_value' => variable_get('migrate_mongo2mysql_sig_date_end', $default_end_date), + '#date_year_range' => '-10:+1', + ); + + $form['signature_config']['migrate_mongo2mysql_sig_depends_on_pet'] = array( + '#type' => 'checkbox', + '#title' => t('Signature migrations depend on petitions'), + '#description' => t('By default, signatures cannot be imported while petitions are available to be imported. Uncheck this box to remove this dependency. *Unchecking this box can lead to data corruption - only use this box if you understand the implications.* + The drush command: {drush mreg} must be run for this change to take effect.'), + '#default_value' => (bool) variable_get('migrate_mongo2mysql_sig_depends_on_pet', 1), + ); + + $form['signature_config']['migrate_mongo2mysql_sig_all'] = array( + '#type' => 'checkbox', + '#title' => t('Migrate ALL signatures'), + '#description' => t('Ignore all date restrictions on signatures, including the settings directly above this one. This can have performance implications, including importing signatures throwing exceptions.'), + '#default_value' => (bool) variable_get('migrate_mongo2mysql_sig_all', 0), + ); + + return system_settings_form($form, FALSE); +} diff --git a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.info b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.info index 625d9b04b..d896a35b1 100644 --- a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.info +++ b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.info @@ -8,4 +8,4 @@ dependencies[] = signature_mail files[] = petsig.inc files[] = plugins/destinations/signature.inc -files[] = plugins/destinations/node_modified.inc \ No newline at end of file +files[] = plugins/destinations/node_modified.inc diff --git a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.migrate.inc b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.migrate.inc index 75277383f..93e430a68 100644 --- a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.migrate.inc +++ b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.migrate.inc @@ -29,5 +29,11 @@ function migrate_mongo2mysql_petsig_migrate_api() { ), ), ); + + $sig_depends_on_pet = variable_get('migrate_mongo2mysql_sig_depends_on_pet', 1); + if (empty($sig_depends_on_pet)) { + unset($api['migrations']['Signature']['dependencies']); + } + return $api; } diff --git a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.module b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.module index d376e0590..18570c357 100644 --- a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.module +++ b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig.module @@ -24,6 +24,18 @@ function migrate_mongo2mysql_petsig_menu() { 'file' => 'migrate_mongo2mysql_petsig_compare.inc', ); + // Admin - Petition Tool Settings. + $items['admin/config/system/mongo-mysql'] = array( + 'title' => 'Petition Migration Settings', + 'description' => 'Manage mongo2mysql migration settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('_migrate_mongo2mysql_settings'), + 'access arguments' => array('administer petition settings'), + 'weight' => -10, + 'file' => 'migrate_mongo2mysql_petsig.admin.inc', + 'type' => MENU_NORMAL_ITEM, + ); + return $items; } @@ -110,4 +122,4 @@ function migrate_mongo2mysql_sanitize_output($string) { $string = filter_xss($string); return $string; -} \ No newline at end of file +} diff --git a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig_compare.inc b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig_compare.inc index 9eeb9f045..45bdd2eb5 100644 --- a/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig_compare.inc +++ b/modules/custom/migrate_mongo2mysql_petsig/migrate_mongo2mysql_petsig_compare.inc @@ -141,6 +141,11 @@ function _migrate_mongo2mysql_petsig_page($petition_id = 0) { 'mongo' => 'short_url', 'mysql' => "field_short_url", ); + $a_compare[] = array( + 'name' => 'Legacy_URL', + 'mongo' => 'nice_url', + 'mysql' => "field_legacy_path", + ); $a_compare[] = array( 'name' => 'Timestamp_Created', 'mongo' => 'created', @@ -153,7 +158,7 @@ function _migrate_mongo2mysql_petsig_page($petition_id = 0) { 'field_petition_response_sign', 'field_petition_review_timeframe', 'field_abuse_count', 'field_petition_signature_count', 'field_timestamp_responded', 'field_legacy_id', 'body', 'field_response_id', 'field_petition_status', - 'field_petition_response_status', 'field_timestamp_published', + 'field_response_status', 'field_timestamp_published', 'field_timestamp_reached_public', 'field_timestamp_reached_ready', "field_response_status"); diff --git a/modules/custom/migrate_mongo2mysql_petsig/petsig.inc b/modules/custom/migrate_mongo2mysql_petsig/petsig.inc index f99339cc8..95d074c05 100644 --- a/modules/custom/migrate_mongo2mysql_petsig/petsig.inc +++ b/modules/custom/migrate_mongo2mysql_petsig/petsig.inc @@ -34,16 +34,18 @@ class PetitionMigration extends PetSigMigration { $collection = wh_petitions_mongo_petition_connection(); - $query = array( - 'petition_status' => array( - '$nin' => array((int) WH_PETITION_STATUS_DRAFT), - ), - ); - - if(_menu_site_is_offline(TRUE)) { - $query = array(); + $query = array(); + + $include_unpublished_petitions = variable_get('migrate_mongo2mysql_pet_unpublish', 0); + if (empty($include_unpublished_petitions)) { + // Exclude unpublished petitions. + $query = array( + 'petition_status' => array( + '$nin' => array((int) WH_PETITION_STATUS_DRAFT), + ), + ); } - + $options = array('track_changes' => 1); $this->source = new MigrateSourceMongoDB($collection, $query, array( '_id' => "MongoDB identifier", 'uid' => "User ID", 'title' => "Title", 'body' => "Body", @@ -57,9 +59,10 @@ class PetitionMigration extends PetSigMigration { "review_timeframe" => "Review Timeframe", "response_signatures" => "Response Signature", "public_signatures" => "Public Signature", "short_url" => "Short URL", - "created" => "Created", "closed" => "Closed", + "created" => "Created", "closed" => "Closed", "nice_url" => "Legacy Path", + "review_threshold_mail_sent" => "Review threshold email sent", - )); + ), array('_id' => 1), $options); $this->destination = new MigrateDestinationNodeModified('petition'); @@ -80,15 +83,16 @@ class PetitionMigration extends PetSigMigration { $this->addFieldMapping("field_petition_status", 'petition_status'); $this->addFieldMapping("field_response_status", 'response_status'); $this->addFieldMapping("field_short_url", "short_url"); + $this->addFieldMapping("field_legacy_path", "nice_url"); $this->addFieldMapping("field_timestamp_published", 'published'); $this->addFieldMapping("field_timestamp_reached_public", 'reached_public'); $this->addFieldMapping("field_timestamp_reached_ready", 'reached_ready'); $this->addFieldMapping("field_timestamp_responded", "closed"); + $this->addFieldMapping("field_review_threshold_mail_sent", "review_threshold_mail_sent"); $this->addUnmigratedSources(array("field_response_id")); $this->addUnmigratedDestinations(array( - 'status', 'changed', 'sticky', 'revision', @@ -119,6 +123,7 @@ class PetitionMigration extends PetSigMigration { 'field_petition_issues:create_term', 'field_petition_issues:ignore_case', "field_short_url:language", + "field_legacy_path:language", )); $this->map = new MigrateSQLMap($this->machineName, @@ -149,7 +154,25 @@ class SignatureMigration extends PetSigMigration { $this->description = t('Migrate mongodb signature objects, to a signature entity'); $collection = wh_petitions_mongo_petition_signatures_connection(); - $this->source = new MigrateSourceMongoDB($collection, array(), array( + + $query = array(); + + $include_all_signatures = variable_get('migrate_mongo2mysql_sig_all', 0); + if (empty($include_all_signatures)) { + // Exclude signatures created after specified date. + $signatures_created_after = variable_get('migrate_mongo2mysql_sig_date_start', 0); + if (!empty($signatures_created_after)) { + $signatures_created_after = strtotime($signatures_created_after); + $query['timestamp']['$gt'] = (int) $signatures_created_after; + } + // Exclude signatures created before specified date. + $signatures_created_before = variable_get('migrate_mongo2mysql_sig_date_end', 0); + if (!empty($signatures_created_before)) { + $signatures_created_before = strtotime($signatures_created_before); + $query['timestamp']['$lt'] = (int) $signatures_created_before; + } + } + $this->source = new MigrateSourceMongoDB($collection, $query, array( '_id' => "MongoDB identifier", 'petition_id' => "Petition ID", "uid" => "User ID", "timestamp" => "Created Date", 'user_agent' => "User Agent", diff --git a/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/node_modified.inc b/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/node_modified.inc index 5d4066d39..bf09246a8 100644 --- a/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/node_modified.inc +++ b/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/node_modified.inc @@ -20,5 +20,10 @@ class MigrateDestinationNodeModified extends MigrateDestinationNode { $entity->body[LANGUAGE_NONE][0]['value'] = replace4byte( $entity->body[LANGUAGE_NONE][0]['value']); + // Let's check the legacy path for unsupported chars. + $entity->field_legacy_path[LANGUAGE_NONE][0]['value'] = replace4byte( + $entity->field_legacy_path[LANGUAGE_NONE][0]['value']); + + $entity->status = (empty($entity->field_petition_status[LANGUAGE_NONE][0]['value']) ? 0 : 1); } } diff --git a/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/signature.inc b/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/signature.inc index 7850f95d9..ba50b08c4 100644 --- a/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/signature.inc +++ b/modules/custom/migrate_mongo2mysql_petsig/plugins/destinations/signature.inc @@ -106,7 +106,7 @@ class MigrateDestinationSignatureEntity extends MigrateDestinationEntity { } if (isset($entity->user_country)) { - $entity->user_country = substr( migrate_mongo2mysql_sanitize_output(replace4byte($entity->user_country)), 0, 255); + $entity->user_country = substr(migrate_mongo2mysql_sanitize_output(replace4byte($entity->user_country)), 0, 255); } } diff --git a/modules/custom/petition/includes/petition.field.inc b/modules/custom/petition/includes/petition.field.inc index da1cd1493..03ce1b8b7 100644 --- a/modules/custom/petition/includes/petition.field.inc +++ b/modules/custom/petition/includes/petition.field.inc @@ -257,6 +257,9 @@ function petition_fields() { 'format' => array( 0 => 'format', ), + 'value' => array( + 0 => 'value', + ), ), 'locked' => '0', 'module' => 'text', @@ -713,12 +716,13 @@ function petition_fields() { 'module' => 'list', 'settings' => array( 'allowed_values' => array( + 0 => 'Draft', 1 => 'Private', 2 => 'Public', 3 => 'Closed', 4 => 'Under Review', 5 => 'Reviewed', - 6 => 'Flagged', + 6 => 'Hidden', ), 'allowed_values_function' => '', 'profile2_private' => FALSE, @@ -979,6 +983,75 @@ function petition_fields() { ), ); + // Exported field: 'node-petition-field_legacy_path'. + $fields['node-petition-field_legacy_path'] = array( + 'field_config' => array( + 'active' => '1', + 'cardinality' => '1', + 'deleted' => '0', + 'entity_types' => array(), + 'field_name' => 'field_legacy_path', + 'foreign keys' => array( + 'format' => array( + 'columns' => array( + 'format' => 'format', + ), + 'table' => 'filter_format', + ), + ), + 'indexes' => array( + 'format' => array( + 0 => 'format', + ), + ), + 'locked' => '0', + 'module' => 'text', + 'settings' => array( + 'max_length' => '255', + 'profile2_private' => FALSE, + ), + 'translatable' => '0', + 'type' => 'text', + ), + 'field_instance' => array( + 'bundle' => 'petition', + 'default_value' => NULL, + 'deleted' => '0', + 'description' => 'The absolute path of the legacy, Mongo-based petition, without a leading forward slash (/).', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => '6', + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_legacy_path', + 'label' => 'Legacy Path', + 'required' => 0, + 'settings' => array( + 'text_processing' => '0', + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'text', + 'settings' => array( + 'size' => '80', + ), + 'type' => 'text_textfield', + 'weight' => '6', + ), + ), + ); + // Exported field: 'node-petition-field_timestamp_published'. $fields['node-petition-field_timestamp_published'] = array( 'field_config' => array( @@ -1057,7 +1130,7 @@ function petition_fields() { 'text_parts' => array(), 'year_range' => '-3:+3', ), - 'type' => 'date_text', + 'type' => 'date_popup', 'weight' => '7', ), ), @@ -1134,7 +1207,7 @@ function petition_fields() { 'text_parts' => array(), 'year_range' => '-3:+3', ), - 'type' => 'date_text', + 'type' => 'date_popup', 'weight' => '12', ), ), @@ -1211,7 +1284,7 @@ function petition_fields() { 'text_parts' => array(), 'year_range' => '-3:+3', ), - 'type' => 'date_text', + 'type' => 'date_popup', 'weight' => '13', ), ), @@ -1288,12 +1361,89 @@ function petition_fields() { 'text_parts' => array(), 'year_range' => '-3:+3', ), - 'type' => 'date_text', + 'type' => 'date_popup', 'weight' => '17', ), ), ); + // Exported field: 'node-petition-field_review_threshold_mail_sent'. + $fields['node-petition-field_review_threshold_mail_sent'] = array( + 'field_config' => array( + 'active' => '1', + 'cardinality' => '1', + 'deleted' => '0', + 'entity_types' => array(), + 'field_name' => 'field_review_threshold_mail_sent', + 'foreign keys' => array(), + 'indexes' => array(), + 'locked' => '0', + 'module' => 'date', + 'settings' => array( + 'cache_count' => '4', + 'cache_enabled' => 0, + 'granularity' => array( + 'day' => 'day', + 'hour' => 'hour', + 'minute' => 'minute', + 'month' => 'month', + 'second' => 'second', + 'year' => 'year', + ), + 'profile2_private' => FALSE, + 'timezone_db' => 'UTC', + 'todate' => '', + 'tz_handling' => 'site', + ), + 'translatable' => '0', + 'type' => 'datestamp', + ), + 'field_instance' => array( + 'bundle' => 'petition', + 'deleted' => '0', + 'description' => 'Timestamp for when a review threshold e-mail was sent for this Petition.', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => '15', + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_review_threshold_mail_sent', + 'label' => 'Review Threshold Mail Sent', + 'required' => 0, + 'settings' => array( + 'default_value' => '', + 'default_value2' => 'same', + 'default_value_code' => '', + 'default_value_code2' => '', + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'date', + 'settings' => array( + 'increment' => 15, + 'input_format' => 'm/d/Y - H:i:s', + 'input_format_custom' => '', + 'label_position' => 'above', + 'text_parts' => array(), + 'year_range' => '-3:+3', + ), + 'type' => 'date_text', + 'weight' => '13', + ), + ), + ); + // Translatables included for use with string extractors like potx. t('Abuse Count'); t('Abuse Flags'); @@ -1331,6 +1481,7 @@ function petition_fields() { t('Timestamp Responded'); t('Timestamp for when this petition reached the required signatures to receive a review.'); t('Timestamp for when this petition was published.'); + t('Timestamp for when a review threshold e-mail was sent for this Petition.'); return $fields; } diff --git a/modules/custom/petition/includes/petition.mongo2mysql.inc b/modules/custom/petition/includes/petition.mongo2mysql.inc index 8a75a4800..a234cbcab 100644 --- a/modules/custom/petition/includes/petition.mongo2mysql.inc +++ b/modules/custom/petition/includes/petition.mongo2mysql.inc @@ -1,4 +1,5 @@ __toString(); - - // If petition already exists, load it. - if ($nid = petition_get_nid($mongodb_petition['id'])) { - $node = node_load($nid); - } - // Otherwise, create a new one. - else { - $node_array = array(); - // The type is always the same. - $node_array['type'] = "petition"; - $node = entity_create('node', $node_array); - } - - $node->status = (petition_mongo2mysql_is_mongo_published($mongodb_petition)) ? 1 : 0; - - // The node properties we are interested in are: type, title, uid, status, - // created, and changed. - $properties = array('title', 'uid', 'created'); - - // Changed is always whatever time it is right now. - $node->changed = time(); - - // The rest of the properties must be set, so let's check. - foreach ($properties as $property) { - if (array_key_exists($property, $mongodb_petition)) { - $node->{$property} = $mongodb_petition[$property]; - } - } - - // The rest of the data is organized in fields: - // body:Long text and summary. - $info = array('body' => "body"); - $key = 'value'; - _petition_set_field($node, $mongodb_petition, $info, $key); - - // field_petition_signature_count:Integer. - // field_petition_public_signatures:Integer. - // field_petition_review_timeframe:Integer. - // field_abuse_count:Integer. @todo What do we do with this? - // field_petition_response_sign:Integer. - // field_response_status:List (integer). - // field_petition_status:List (integer). - $info = array( - 'field_petition_signature_count' => "signature_count", - 'field_petition_public_signatures' => "public_signatures", - 'field_petition_review_timeframe' => "review_timeframe", - 'field_petition_response_sign' => 'response_signatures', - 'field_response_status' => "response_status", - 'field_petition_status' => "petition_status"); - $key = 'value'; - _petition_set_field($node, $mongodb_petition, $info, $key); - - // field_legacy_id:Text. - // field_short_url:Text. - $info = array('field_legacy_id' => 'id', 'field_short_url' => 'short_url'); - $key = 'value'; - _petition_set_field($node, $mongodb_petition, $info, $key); - - // field_petition_issues:Term reference. - $info = array('field_petition_issues' => 'issues'); - $key = 'tid'; - _petition_set_field($node, $mongodb_petition, $info, $key); - - // field_petition_related_petitions:Entity Reference. - // field_abuse_flags:Entity Reference. - // field_response_id:Entity Reference. @todo This does not exist in mongo. - $info = array( - 'field_petition_related_petitions' => 'related_petitions', - 'field_abuse_flags' => 'abuse_flags', - // 'field_response_id' => 'related_petitions', - ); - $key = 'target_id'; - _petition_set_field($node, $mongodb_petition, $info, $key); - - // field_timestamp_reached_public:Date. - // field_timestamp_reached_ready:Date. - // field_timestamp_published:Date. - // field_timestamp_responded:Date. - $info = array( - 'field_timestamp_reached_public' => 'reached_public', - 'field_timestamp_reached_ready' => 'reached_ready', - 'field_timestamp_published' => 'published', - 'field_timestamp_responded' => 'closed', - ); - $key = 'value'; - _petition_set_field($node, $mongodb_petition, $info, $key); - - node_save($node); -} - -/** - * Set a field in a node. - * - * @param object $node - * A node object. - * @param array $raw_data - * An array with data as it is loaded from MongoDB. - * @param array $info - * A simple map in which the key is the name of a field on the node, and - * the value is the key of a piece of data from the $raw_data array. - * @param string $key - * We use the key to get around the ambiguity of multiple field types and how - * they store the data, ex. if we are mapping to numeric or text fields, the - * key param should be "value", if we are mapping entityreference fields it - * should be target_id. - */ -function _petition_set_field($node, $raw_data, $info, $key) { - foreach ($info as $field => $raw) { - if (array_key_exists($raw, $raw_data)) { - $value = $raw_data[$raw]; - if (is_array($value)) { - $counter = 0; - foreach ($value as $v) { - $node->{$field}[LANGUAGE_NONE][$counter][$key] = $v; - $counter++; - } - } - else { - $node->{$field}[LANGUAGE_NONE][0][$key] = $value; - } - } - } -} - -/** - * Implements hook_wh_petitions_petition_save(). - */ -function petition_wh_petitions_petition_save($petition) { - - if (shunt_is_enabled('petition_mysql_save')) { - return; - } - - petition_mongo2mysql_save($petition); -} - -/** - * Implements hook_wh_petitions_petition_close(). - */ -function petition_wh_petitions_petition_close($petition) { - - if (shunt_is_enabled('petition_mysql_save')) { - return; - } - - $node = petition_load_from_legacy_id($petition['_id']); - if ($node) { - $node->field_petition_status[LANGUAGE_NONE][0]['value'] = WH_PETITION_STATUS_CLOSED; - // We are not saving the time of closing. - node_save($node); - } -} - -/** - * Implements hook_wh_petitions_petition_body_update(). - */ -function petition_wh_petitions_petition_body_update($petition_id, $body) { - - if (shunt_is_enabled('petition_mysql_save')) { - return; - } - - $node = petition_load_from_legacy_id($petition_id); - if ($node) { - $node->body[LANGUAGE_NONE][0]['value'] = $body; - node_save($node); - } -} - -/** - * Implements hook_wh_petitions_petition_inappropriate(). - */ -function petition_wh_petitions_petition_inappropriate($petition_id) { - - if (shunt_is_enabled('petition_mysql_save')) { - return; - } - - $node = petition_load_from_legacy_id($petition_id); - - if ($node) { - $items = field_get_items("node", $node, "field_abuse_count"); - if ($items) { - $node->field_abuse_count[LANGUAGE_NONE][0]['value']++; - } - else { - $node->field_abuse_count[LANGUAGE_NONE][0]['value'] = 0; - } - - $items = field_get_items("node", $node, "field_abuse_flags"); - - if ($items) { - $node->field_abuse_flags[LANGUAGE_NONE][]['target_id'] = $user->uid; - } - else { - $node->field_abuse_flags[LANGUAGE_NONE][0]['target_id'] = $user->uid; - } - - node_save($node); - } -} - -/** - * Implements hook_shunt(). - */ -function petition_shunt() { - return array( - 'petition_mysql_save' => t('Disable petition writes to MySQL.'), - ); -} - -/** - * Checks to see if a mongo_petition is published. - * - * Determining if the published/unpublished state of a mongo petition is - * surprisingly complex. - * - * @param array $mongo_petition - * A Mongo petition--NOT a MySQL petition. - * - * @return bool - * Returns TRUE if the mongo petition published. FALSE otherwise. - */ -function petition_mongo2mysql_is_mongo_published(array $mongo_petition) { - $was_published = isset($mongo_petition['published']); - $is_published = !in_array($mongo_petition['petition_status'], wh_petitions_unpublished_statuses()); - - if ($was_published && !$is_published) { - return FALSE; - } - elseif ($was_published && $is_published) { - return TRUE; - } - else { - // $was_published is FALSE, $is_published is TRUE. This shouldn’t be - // possible. - watchdog("Mongo2Mysql", "Encountered a petition with an indeterminate published state."); - } -} - - /** * Function to load an individual mongo petition from a URL. * diff --git a/modules/custom/petition/petition.info b/modules/custom/petition/petition.info index 601adfb75..58df842ee 100644 --- a/modules/custom/petition/petition.info +++ b/modules/custom/petition/petition.info @@ -1,5 +1,5 @@ name = Petition -description = Creates the content type: Petition +description = Provides a Petition content type. core = 7.x package = Petitions php = 5.2.4 @@ -9,9 +9,11 @@ project = petition files[] = petition.mongo2mysql.inc dependencies[] = date +dependencies[] = date_popup dependencies[] = entityreference dependencies[] = list dependencies[] = number dependencies[] = options dependencies[] = taxonomy dependencies[] = wh_response_feature +dependencies[] = shunt \ No newline at end of file diff --git a/modules/custom/petition/petition.install b/modules/custom/petition/petition.install index 3d5feed27..092a32c81 100644 --- a/modules/custom/petition/petition.install +++ b/modules/custom/petition/petition.install @@ -11,11 +11,7 @@ function petition_enable() { $t = get_t(); drupal_set_message($message = $t('The Petition module was successfully enabled.'), $type = 'status'); - // Start MySQL writes off disabled. - // Note: shunt_enable_shunt() isn't available yet when installing the distro - // fresh, so set the variable directly. - // @todo: remove this once MongoDB is removed. - variable_set('shunt_petition_mysql_save', TRUE); + variable_set('pathauto_node_petition_pattern', 'petition/[node:title]'); } /** @@ -37,13 +33,6 @@ function petition_install() { petition_build_fields(petition_fields()); $t = get_t(); drupal_set_message($message = $t('The Petition module has been installed and a Petition content type has been created.'), $type = 'status'); - - // Set the initial state of MySQL shunt to disable mysql writes. - // Note: shunt_enable_shunt() isn't available yet when installing the distro - // fresh, so set the variable directly. - // @todo This default setting should be removed when mongo is fully removed. - variable_set('shunt_petition_mysql_save', TRUE); - } /** @@ -70,6 +59,20 @@ function petition_update_last_removed() { return 7000; } +/** + * Revert petition_pages feature. + */ +function petition_update_7303(&$sandbox) { + _petition_pt_1613(); +} + +/** + * PT-1613: Revert petition_pages feature. + */ +function _petition_pt_1613() { + features_revert(array('petition_pages' => array('views_view'))); +} + /** * Updates fields from fields array if it changed. */ @@ -78,6 +81,47 @@ function petition_update_7300(&$sandbox) { // Update the fields and add their instances to the petition content type. petition_build_fields(petition_fields()); } +/** + * Updates fields from fields array if it changed. + */ +function petition_update_7301(&$sandbox) { + require_once 'includes/petition.field.inc'; + // Update the fields and add their instances to the petition content type. + petition_build_fields(petition_fields()); +} + +/** + * Set pathauto pattern for petitions. + * + * Update fields from fields array if it changed. + */ +function petition_update_7302(&$sandbox) { + variable_set('pathauto_node_petition_pattern', 'petition/[node:title]'); + + require_once 'includes/petition.field.inc'; + // Update the fields and add their instances to the petition content type. + petition_build_fields(petition_fields()); +} + +/** + * Updates fields from fields array if it changed. + */ +function petition_update_7305(&$sandbox) { + require_once 'includes/petition.field.inc'; + // Update the fields and add their instances to the petition content type. + petition_build_fields(petition_fields()); +} + +/** + * Add index to field_data_field_legacy_id.field_legacy_id_value + * + * Update fields from fields array if changed. + */ +function petition_update_7306(&$sandbox) { + require_once 'includes/petition.field.inc'; + // Update the fields and add their instances to the petition content type. + petition_build_fields(petition_fields()); +} /** * Creates fields and adds field instances to petition content type. @@ -96,7 +140,7 @@ function petition_build_fields($fields = array()) { // Load all the existing fields and instances up-front so that we don't // have to rebuild the cache all the time. $existing_fields = field_info_fields(); - $existing_instances = field_info_instances('node', 'petition'); + $existing_instances = field_info_instances(); // Loop through and process each field and field instance. foreach ($fields as $field) { diff --git a/modules/custom/petition/petition.module b/modules/custom/petition/petition.module index 6af2350f9..a0d414847 100644 --- a/modules/custom/petition/petition.module +++ b/modules/custom/petition/petition.module @@ -1,17 +1,55 @@ $data) { + if (!isset($conf['apachesolr_environments'][$env]['index_bundles']['node']) || !in_array('petition', $conf['apachesolr_environments'][$env]['index_bundles']['node'])) { + $conf['apachesolr_environments'][$env]['index_bundles']['node'][] = 'petition'; + } + } + } + } +} /** * Implements hook_entity_view(). @@ -75,6 +113,71 @@ function petition_node_view($node, $view_mode, $langcode) { } } +/** + * Implements hook_apachesolr_index_document_build_ENTITY_TYPE(). + */ +function petition_apachesolr_index_document_build_node(ApacheSolrDocument $document, $entity, $env_id) { + if ($entity->type == 'petition') { + $document->addField(PETITION_SOLR_FIELD_BODY, $entity->body[LANGUAGE_NONE][0]['safe_value']); + + $issue_tids = array(); + foreach ($entity->field_petition_issues[LANGUAGE_NONE] as $value) { + $issue_tids[] = $value['tid']; + } + $issues = taxonomy_term_load_multiple($issue_tids); + foreach ($issues as $term) { + $document->addField(PETITION_SOLR_FIELD_ISSUE_TIDS, (int) $term->tid); + $document->addField(PETITION_SOLR_FIELD_ISSUE_NAMES, $term->name); + } + + $legacy_id = ''; + if (!empty($entity->field_legacy_id[LANGUAGE_NONE][0]['value'])) { + $legacy_id = $entity->field_legacy_id[LANGUAGE_NONE][0]['value']; + } + $document->addField(PETITION_SOLR_FIELD_LEGACY_ID, $legacy_id); + + $document->addField(PETITION_SOLR_FIELD_PATH, drupal_get_path_alias("node/{$entity->nid}")); + + $legacy_path = ''; + if (!empty($entity->field_legacy_path[LANGUAGE_NONE][0]['value'])) { + $legacy_path = $entity->field_legacy_path[LANGUAGE_NONE][0]['value']; + } + $document->addField(PETITION_SOLR_FIELD_LEGACY_PATH, $legacy_path); + + $document->addField(PETITION_SOLR_FIELD_PETITION_STATUS, (int) $entity->field_petition_status[LANGUAGE_NONE][0]['value']); + + if (!empty($entity->field_response_id[LANGUAGE_NONE][0]['target_id'])) { + $response_id = $entity->field_response_id[LANGUAGE_NONE][0]['target_id']; + $response = node_load($response_id); + $document->addField(PETITION_SOLR_FIELD_RESPONSE_ID, (int) $response_id); + $document->addField(PETITION_SOLR_FIELD_TIMESTAMP_RESPONSE_ASSOCIATED, (int) $response->created); + } + + $document->addField(PETITION_SOLR_FIELD_RESPONSE_STATUS, (int) $entity->field_response_status[LANGUAGE_NONE][0]['value']); + + $document->addField(PETITION_SOLR_FIELD_REVIEW_TIMEFRAME, (int) $entity->field_petition_review_timeframe[LANGUAGE_NONE][0]['value']); + + $signature_count = 0; + if (!empty($entity->field_petition_signature_count[LANGUAGE_NONE][0]['value'])) { + $signature_count = $entity->field_petition_signature_count[LANGUAGE_NONE][0]['value']; + } + $document->addField(PETITION_SOLR_FIELD_SIGNATURE_COUNT, $signature_count); + + $document->addField(PETITION_SOLR_FIELD_SIGNATURE_PUBLIC_THRESHOLD, (int) $entity->field_petition_public_signatures[LANGUAGE_NONE][0]['value']); + + $document->addField(PETITION_SOLR_FIELD_SIGNATURE_RESPONSE_THRESHOLD, (int) $entity->field_petition_response_sign[LANGUAGE_NONE][0]['value']); + + if (!empty($entity->field_timestamp_published[LANGUAGE_NONE][0]['value'])) { + $timestamp_published = $entity->field_timestamp_published[LANGUAGE_NONE][0]['value']; + $document->addField(PETITION_SOLR_FIELD_TIMESTAMP_PUBLISHED, (int) $timestamp_published); + } + + if (!empty($entity->field_timestamp_reached_public[LANGUAGE_NONE][0]['value'])) { + $timestamp_reached_public = $entity->field_timestamp_reached_public[LANGUAGE_NONE][0]['value']; + $document->addField(PETITION_SOLR_FIELD_TIMESTAMP_REACHED_PUBLIC, $timestamp_reached_public); + } + } +} /** * Implements hook_entity_view_alter(). @@ -90,6 +193,44 @@ function petition_entity_view_alter(&$build, $type) { } } +/** + * Extracts the legacy IDs from a given set of petition IDs. + * + * @param array $petition_ids + * An array of petition IDs. + * + * @return array + * Returns an array of legacy IDs extracted from the given petition IDs. + */ +function petition_extract_legacy_ids_from_petition_ids(array $petition_ids) { + $legacy_ids = array(); + foreach ($petition_ids as $id) { + if (petition_is_legacy_id($id)) { + $legacy_ids[] = $id; + } + } + return $legacy_ids; +} + +/** + * Extracts the node IDs (nids) from a given set of petition IDs. + * + * @param array $petition_ids + * An array of petition IDs. + * + * @return array + * Returns an array of node IDs (nids) extracted from the given petition + * IDs. + */ +function petition_extract_nids_from_petition_ids(array $petition_ids) { + $nids = array(); + foreach ($petition_ids as $id) { + if (!petition_is_legacy_id($id)) { + $nids[] = $id; + } + } + return $nids; +} /** * Implements hook_node_info(). @@ -114,14 +255,14 @@ function petition_node_info() { * @param array $node * Actual node to update. */ -function petition_unpublish($node) { - $node->status = NODE_NOT_PUBLISHED; +function petition_hide($node) { + $node->field_petition_status[LANGUAGE_NONE][0]['value'] = WH_PETITION_STATUS_FLAGGED; node_save($node); - $legacy_id = $node->field_legacy_id['und'][0]['value']; - $mongo2mysql_function = 'wh_petitions_mongo2mysql_unpublish'; - if (function_exists($mongo2mysql_function) && !shunt_is_enabled('petition_mysql_save')) { - $mongo2mysql_function($legacy_id); + $legacy_id = isset($node->field_legacy_id) ? $node->field_legacy_id['und'][0]['value'] : NULL; + + if (isset($legacy_id) && petitions_data_mongo_writes_are_enabled()) { + wh_petitions_mongo2mysql_hide($legacy_id); } } @@ -131,14 +272,35 @@ function petition_unpublish($node) { * @param array $node * Actual node to update. */ -function petition_publish($node) { +function petition_show($node) { $node->status = NODE_PUBLISHED; + + // @todo replace this unfortunate field_petition_status logic with a proper domain model. + $timestamp_review = strtotime('- ' . $node->field_petition_review_timeframe[LANGUAGE_NONE][0]['value'] . ' days'); + $timestamp_published = $node->field_timestamp_published[LANGUAGE_NONE][0]['value']; + $signature_count = $node->field_petition_signature_count[LANGUAGE_NONE][0]['value']; + $public_signatures = $node->field_petition_public_signatures[LANGUAGE_NONE][0]['value']; + $response_signatures = $node->field_petition_response_sign[LANGUAGE_NONE][0]['value']; + + if ($timestamp_published < $timestamp_review) { + $node->field_petition_status[LANGUAGE_NONE][0]['value'] = WH_PETITION_STATUS_CLOSED; + } + elseif ($signature_count < $public_signatures) { + $node->field_petition_status[LANGUAGE_NONE][0]['value'] = WH_PETITION_STATUS_PRIVATE; + } + elseif ($signature_count < $response_signatures) { + $node->field_petition_status[LANGUAGE_NONE][0]['value'] = WH_PETITION_STATUS_PUBLIC; + } + elseif ($signature_count >= $response_signatures) { + $node->field_petition_status[LANGUAGE_NONE][0]['value'] = WH_PETITION_STATUS_UNDER_REVIEW; + $node->field_response_status[LANGUAGE_NONE][0]['value'] = WH_PETITION_RESPONSE_STATUS_PENDING; + } + node_save($node); - $legacy_id = $node->field_legacy_id['und'][0]['value']; + $legacy_id = isset($node->field_legacy_id) ? $node->field_legacy_id['und'][0]['value'] : NULL; - $mongo2mysql_function = 'wh_petitions_mongo2mysql_publish'; - if (function_exists($mongo2mysql_function) && !shunt_is_enabled('petition_mysql_save')) { - $mongo2mysql_function($legacy_id); + if (isset($legacy_id) && petitions_data_mongo_writes_are_enabled()) { + wh_petitions_mongo2mysql_show($legacy_id); } } @@ -160,9 +322,32 @@ function petition_increment_count($petition_id, $increment = 1, $debug = FALSE) // Load the node, increment the signature count, save. $node = node_load($nid); - $signature_count_before = $node->field_petition_signature_count['und'][0]['value']; + $lang = $node->language; + $signature_count_before = $node->field_petition_signature_count[$lang][0]['value']; $signature_count_after = $signature_count_before + $increment; - $node->field_petition_signature_count['und'][0]['value'] = $signature_count_after; + $node->field_petition_signature_count[$lang][0]['value'] = $signature_count_after; + + // Update to public petition. + $reached_response = FALSE; + $petition_status = $node->field_petition_status[$lang][0]['value']; + $public_signatures = $node->field_petition_public_signatures[$lang][0]['value']; + $response_signature_count = $node->field_petition_response_sign[$lang][0]['value']; + if ($petition_status == WH_PETITION_STATUS_PRIVATE && $signature_count_after >= $public_signatures) { + $node->field_petition_status[$lang][0]['value'] = (int) WH_PETITION_STATUS_PUBLIC; + $node->field_timestamp_reached_public[$lang][0]['value'] = (int) time(); + } + // Record the time the petition became eligible for a response. + elseif ($petition_status == WH_PETITION_STATUS_PUBLIC && $signature_count_after >= $response_signature_count) { + $node->field_petition_status[$lang][0]['value'] = (int) WH_PETITION_STATUS_UNDER_REVIEW; + $node->field_response_status[$lang][0]['value'] = (int) WH_PETITION_RESPONSE_STATUS_PENDING; + $node->field_timestamp_reached_ready[$lang][0]['value'] = (int) time(); + $reached_response = TRUE; + } + + // Email the creator if the petition is ready for a response. + if ($reached_response && !petitions_data_mongo_reads_are_enabled()) { + wh_petitions_email_ready_response($petition_id, $node->uid); + } node_save($node); // Debug. Log everything. @@ -207,6 +392,36 @@ function petition_is_legacy_id($id) { return $is_legacy; } +/** + * Determines whether a given path is a legacy path or not. + * + * @param string $path + * The path to test, without a leading forward slash (/). + * + * @return bool + * TRUE if the given path is a legacy path or FALSE if not. + */ +function petition_is_legacy_path($path) { + $path_parts = explode('/', $path); + + // A legacy path has exactly three components. + if (count($path_parts) !== 3) { + return FALSE; + } + + // The first component is the string "petition". + if ($path_parts[0] !== 'petition') { + return FALSE; + } + + // The final component is a hash generated by wh_petitions_generate_hash(). + if (preg_match('/^[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ0123456789]{8}$/', $path_parts[2]) !== 1) { + return FALSE; + } + + return TRUE; +} + /** * Wrapper around _petition_get_nid() for central exception handling. * @@ -223,7 +438,7 @@ function petition_get_nid($petition_id) { $nid = _petition_get_nid($petition_id); } catch (Exception $e) { - watchdog('petition', "Error in workflow.\n\n@exception", array( + watchdog('petition', "Error getting petition NID.\n\n@exception", array( '@exception' => print_r($e, TRUE), ), WATCHDOG_ERROR); } @@ -272,3 +487,58 @@ function _petition_get_nid($petition_id) { return $nid; } + +/** + * Wrapper around _petition_get_legacy_id() for central exception handling. + * + * @param array $node + * Petition node. + * + * @return string|false + * Returns mongo id for petition. 0 for not found. FALSE in case of error. + */ +function petition_get_legacy_id($node) { + $legacy_id = FALSE; + + try { + $legacy_id = _petition_get_legacy_id($node); + } + catch (Exception $e) { + watchdog('petition', "Error getting petition legacy ID.\n\n@exception", array( + '@exception' => print_r($e, TRUE), + ), WATCHDOG_ERROR); + } + + return $legacy_id; +} + +/** + * Look up legacy ID for a petition node. + * + * @param array $node + * Petition node. + * + * @return string|false + * Returns mongo id for petition. 0 for not found. FALSE in case of error. + * + * @throws Exception + * Throws an exception when the petition is neither a node or a valid nid. + */ +function _petition_get_legacy_id($node) { + $legacy_id = FALSE; + + if (is_numeric($node) || is_object($node)) { + // Load the node if NID was provided. + $node = (is_numeric($node) ? node_load($node) : $node); + } + else { + throw new Exception(t('This petition ID is neither a valid node ID nor a node object: !petition', + array('!petition' => $node) + )); + } + $legacy_id = $node->field_legacy_id[$node->language][0]['value']; + // Return '0' if legacy ID is null. + $legacy_id = (!empty($legacy_id) ? $legacy_id : 0); + + return $legacy_id; +} diff --git a/modules/custom/petition_install/petition_install1/petition_install1.info b/modules/custom/petition_install/petition_install1/petition_install1.info index 9e5a42d1a..8d9379ccf 100644 --- a/modules/custom/petition_install/petition_install1/petition_install1.info +++ b/modules/custom/petition_install/petition_install1/petition_install1.info @@ -1,12 +1,11 @@ name = Whitehouse Petition Install 1 (no contrib or custom dependencies) -description = Install modules with no dependencies +description = Install modules with no dependencies core = 7.x ; contrib ; ----------------- -; dependencies[] = acquia_connector dependencies[] = advanced_help dependencies[] = captcha ; dependencies[] = coder @@ -40,8 +39,6 @@ dependencies[] = govdelivery dependencies[] = wh_feedback -; Information added by drush on 2013-08-08 version = "" project = "petitions" -datestamp = "1375992615" diff --git a/modules/custom/petition_install/petition_install2/petition_install2.info b/modules/custom/petition_install/petition_install2/petition_install2.info index 26f5224c0..0c591349e 100644 --- a/modules/custom/petition_install/petition_install2/petition_install2.info +++ b/modules/custom/petition_install/petition_install2/petition_install2.info @@ -14,8 +14,6 @@ dependencies[] = wh_response_feature dependencies[] = wh_user_ss_data -; Information added by drush on 2013-08-08 version = "" project = "petitions" -datestamp = "1375992615" diff --git a/modules/custom/petition_install/petition_install3/petition_install3.info b/modules/custom/petition_install/petition_install3/petition_install3.info index e54d6f932..221bd6871 100644 --- a/modules/custom/petition_install/petition_install3/petition_install3.info +++ b/modules/custom/petition_install/petition_install3/petition_install3.info @@ -23,8 +23,6 @@ dependencies[] = wh_zipcodelookup dependencies[] = wh_core -; Information added by drush on 2013-08-08 version = "" project = "petitions" -datestamp = "1375992615" diff --git a/modules/custom/petition_install/petition_install4/petition_install4.info b/modules/custom/petition_install/petition_install4/petition_install4.info index 7262d5551..5682b6a42 100644 --- a/modules/custom/petition_install/petition_install4/petition_install4.info +++ b/modules/custom/petition_install/petition_install4/petition_install4.info @@ -62,8 +62,6 @@ dependencies[] = wh_contexts dependencies[] = wh_misc -; Information added by drush on 2013-08-08 version = "" project = "petitions" -datestamp = "1375992615" diff --git a/modules/custom/petitionadmin/petitionadmin.drush.inc b/modules/custom/petitionadmin/petitionadmin.drush.inc new file mode 100644 index 000000000..ed2274f20 --- /dev/null +++ b/modules/custom/petitionadmin/petitionadmin.drush.inc @@ -0,0 +1,128 @@ + 'Provide csv export of signatures by petition.', + 'aliases' => array('pse'), + 'callback' => 'petitionadmin_drush_callback', + 'arguments' => array( + 'petition_id' => 'Unique ID of the petition', + ), + 'options' => array( + 'filepath' => 'The path to write the output to.', + 'name' => 'Name for the report.', + ), + 'examples' => array( + 'drush pse 54bfde14d063554d04d63af1' => 'Export petition signatures to current directory.', + 'drush pse 54bfde14d063554d04d63af1 --filepath=~/directory' => 'export petition signatures specifying path.', + ), + ); + + return $items; +} + +/** + * Drush callback function. + * + * @param mixed $petition_id + * Either a string or int of the petition Id. + * + * @return mixed + * Returns a string of data to a file. + */ +function petitionadmin_drush_callback($petition_id = 'ALL') { + $filepath = drush_get_option('filepath'); + $name = drush_get_option('name'); + + if (!$filepath) { + $filepath = getcwd(); + } + + if (!$name) { + $name = 'signature-export'; + } + + $args = drush_get_arguments(); + if (count($args) > 0) { + $petition_id = $args[1]; + } + + $results = petitionadmin_drush_export_signatures($petition_id); + + $output = ""; + while ($values = $results->fetchAssoc()) { + $output .= $values['uid'] . ',' . $values['email'] . "\n"; + } + + $run_time = date('m-d-Y--H-i-s', time()); + $file_name = $filepath . '/petition-export-' . $run_time . '.' . $petition_id . '.' . $name . '.csv'; + + if (count($results)) { + drush_print('Writing results to file: ' . $file_name); + file_put_contents($file_name, $output); + } + else { + drush_print('No signatures where found -- no file was written.'); + } + +} + +/** + * Wrapper function to trigger callback. + * + * @param mixed $petition_id + * Either a string or int petition ID. + * + * @return mixed + * either a string output to a file or notices. + */ +function drush_petitionadmin_drush_callback($petition_id) { + petitionadmin_drush_callback($petition_id); +} + +/** + * Returns a query result set. + * + * @param mixed $petition_id + * Either a string or int petition id. + * + * @return DatabaseStatementInterface|null + * Either a database result set or nothing. + */ +function petitionadmin_drush_export_signatures($petition_id) { + + try { + $query = db_select('signature_mail', 'sm'); + $query->distinct(); + $query->addJoin('INNER', 'users', 'us', 'us.uid = sm.uid'); + $query->addField('sm', 'uid'); + $query->addField('us', 'mail', 'email'); + + if (is_numeric($petition_id)) { + $query->condition('sm.petition_id', $petition_id); + } + elseif (is_string($petition_id) && !is_int($petition_id) && $petition_id != 'ALL') { + $query->condition('sm.legacy_petition_id', $petition_id); + } + + return $query->execute(); + } + catch (Exception $e) { + watchdog('petitionadmin', "Error.\n\n@exception", array( + '@exception' => print_r($e, TRUE), + ), WATCHDOG_ERROR); + } +} diff --git a/modules/custom/petitionadmin/petitionadmin.features.inc b/modules/custom/petitionadmin/petitionadmin.features.inc new file mode 100644 index 000000000..551c67615 --- /dev/null +++ b/modules/custom/petitionadmin/petitionadmin.features.inc @@ -0,0 +1,12 @@ + "3.0"); +} diff --git a/modules/custom/petitionadmin/petitionadmin.info b/modules/custom/petitionadmin/petitionadmin.info index c2ca84880..e451b096e 100644 --- a/modules/custom/petitionadmin/petitionadmin.info +++ b/modules/custom/petitionadmin/petitionadmin.info @@ -1,9 +1,20 @@ name = Petition Admin -description = Contains administrative tools for petitions. +description = Petitions: Administrative interface core = 7.x package = Petitions - +php = 5.2.4 +version = 7.x-1.0 +project = petitionadmin dependencies[] = ctools -dependencies[] = views +dependencies[] = date_views dependencies[] = petition dependencies[] = petitionevents +dependencies[] = signature_mail +dependencies[] = views +features[ctools][] = views:views_default:3.0 +features[features_api][] = api:1 +features[views_view][] = petitionadmin +features[views_view][] = petitionadmin_signatures +features[views_view][] = petitions +files[] = petitionadmin_views_access_plugin.inc +mtime = 1430350353 diff --git a/modules/custom/petitionadmin/petitionadmin.install b/modules/custom/petitionadmin/petitionadmin.install index 078d9ed6c..15311779a 100644 --- a/modules/custom/petitionadmin/petitionadmin.install +++ b/modules/custom/petitionadmin/petitionadmin.install @@ -16,3 +16,27 @@ function petitionadmin_install() { ->condition('name', 'petitionadmin', '=') ->execute(); } + +/** + * PT-1686: Optimize signature downloads. + * + * 1) Reverts this feature module. + * 2-3) Disables and uninstalls views_data_export, even though it is no longer + * present in the system. + */ +function petitionadmin_update_7001() { + // 1) Revert the feature. + features_revert(array('petitionadmin' => array('views_view'))); + + // 2) Disable views_data_export in the database. + db_query("UPDATE system SET status = '0' WHERE name = 'views_data_export';"); + db_query("DELETE FROM cache_bootstrap WHERE cid = 'system_list';"); + + // 3) Uninstall views_data_export: clear tables from schema. + $tables = array('views_data_export', 'views_data_export_object_cache'); + foreach ($tables as $table_name) { + if (db_table_exists($table_name)) { + db_drop_table($table_name); + } + } +} diff --git a/modules/custom/petitionadmin/petitionadmin.module b/modules/custom/petitionadmin/petitionadmin.module index 923e77183..620b5238a 100644 --- a/modules/custom/petitionadmin/petitionadmin.module +++ b/modules/custom/petitionadmin/petitionadmin.module @@ -1,26 +1,41 @@ "3.0"); -} +include_once 'petitionadmin.features.inc'; /** * Implements hook_menu_alter(). */ function petitionadmin_menu_alter(&$items) { - $deny_access = shunt_is_enabled('petition_mysql_save'); - if ($deny_access) { + // Hide MySQL-based moderations tools if MySQL functionality isn't enabled. + // e.g Hide Mongo2MySql phase 1 and phase 4. + if (!(petitions_data_mysql_writes_are_enabled() && petitions_data_mongo_writes_are_enabled())) { unset($items['admin/moderation-tools']); } + + // Show mysql admin tool only when we are in Mongo2MySql phase 4. + if (petitions_data_mysql_writes_are_enabled() && !petitions_data_mongo_writes_are_enabled()) { + + unset($items['admin/petitions']); + + // Update the current petition menu item to point to the new mysql admin + // page. + $items['admin/petitions'] = array( + 'title' => 'Petitions', + 'page callback' => 'drupal_goto', + 'page arguments' => array('admin/content/petitions'), + 'access arguments' => array('administer petitions'), + 'type' => MENU_NORMAL_ITEM, + 'weight' => -10, + ); + } } /** @@ -29,3 +44,271 @@ function petitionadmin_menu_alter(&$items) { function petitionadmin_form_views_form_petitions_moderation_tools_alter(&$form, &$form_state, $form_id) { $form['select']['submit']['#value'] = t('Next'); } + + +/** + * Implements hook_permission(). + */ +function petitionadmin_permission() { + $permissions = array(); + + $permissions['petition access signatures'] = array( + 'title' => t('View petition signatures'), + 'description' => t('Have the right to view petition signatures.'), + ); + + $permissions['petition access administrator'] = array( + 'title' => t('Access petition administration'), + 'description' => t('Have the right to access petition administration.'), + ); + + return $permissions; +} + +/** + * Implements hook_menu(). + */ +function petitionadmin_menu() { + $items = array(); + + $items['node/%/signatures'] = array( + 'type' => MENU_LOCAL_TASK, + 'title' => 'Signatures', + 'page callback' => 'views_embed_view', + 'page arguments' => array('petitionadmin_signatures', 'page_1', 1), + 'access callback' => '_petitionadmin_access_callback', + 'access arguments' => array(1), + 'weight' => 50, + ); + + $items['node/%/signatures/export/%'] = array( + 'type' => MENU_CALLBACK, + 'page callback' => 'petitionadmin_export_signatures', + 'page arguments' => array(1, 4, 'download'), + 'access callback' => '_petitionadmin_access_callback', + 'access arguments' => array(1), + ); + + $items['node/%/signatures/export/%/%'] = array( + 'type' => MENU_CALLBACK, + 'page callback' => 'petitionadmin_export_signatures', + 'page arguments' => array(1, 4, 5), + 'access callback' => '_petitionadmin_access_callback', + 'access arguments' => array(1), + ); + + $items['admin/content/petitions/content'] = array( + 'title' => 'Listing', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + + $items['admin/content/petitions/api-keys'] = array( + 'title' => 'API keys', + 'page callback' => 'drupal_goto', + 'page arguments' => array('admin/petitions/api-keys'), + 'access arguments' => array('administer api keys'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + ); + + $items['admin/content/petitions/settings'] = array( + 'title' => 'Settings', + 'page callback' => 'drupal_goto', + 'page arguments' => array('admin/petitions/settings'), + 'access arguments' => array('administer petitions'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 3, + ); + + return $items; +} + +/** + * Export signatures for a petition. + * + * @param int $node_id + * Petition node ID to export signatures of. + * + * @param string $export_fields + * base = all signatures CSV data without emails + * email = only email signatures CSV data + * + * @param string $export_type + * download = Download as a file attachment + * stdout = Output to stdout + */ +function petitionadmin_export_signatures($node_id, $export_fields, $export_type = 'stdout') { + $users = array(); + + // Number of rows to load into memory on one loop, adjust for performance. + $loop_size = variable_get('signatures_export_loop_size', 5000); + $loop_index = 0; + + if ($export_type == 'download') { + $filename = 'petition_' . $node_id . '_signatures_' . date('c') . '.csv'; + petitionadmin_download_send_headers($filename); + } + + do { + // Force MySQL. + $signatures = SignaturesSelectQueryFactory::create(TRUE); + $signatures->setPetitionId($node_id); + $signatures->setMaxReturnLimit($loop_size); + $signatures->setOffset($loop_index++ * $loop_size); + $signatures_data = $signatures->execute()->getResultObjects(); + + if ($export_fields == 'email') { + $uids = array(); + + foreach ($signatures_data as $signature) { + $uids[] = $signature->getUid(); + } + // Use db_select to improve performance instead of user_load_multiple. + $query = db_select('users', 'u'); + $query->fields('u', array('uid', 'mail')) + ->condition('u.uid', $uids, 'IN'); + $users = $query->execute()->fetchAllAssoc('uid'); + } + + $df = fopen("php://output", 'w'); + foreach ($signatures_data as $signature) { + if ($export_fields == 'base') { + $fields = array( + $signature->getEntityId(), + $signature->getFirstName(), + $signature->getLastName(), + $signature->getCity(), + $signature->getState(), + $signature->getZip(), + $signature->getCreated(), + ); + } + elseif ($export_fields == 'email') { + $fields = array($users[$signature->getUid()]->mail); + } + fputcsv($df, $fields); + } + fclose($df); + $signatures_count = count($signatures_data); + + unset($signatures_data); + unset($signatures); + unset($query); + unset($users); + } while ($signatures_count == $loop_size); + + if ($export_type == 'download') { + // Exit out to prevent any other data from entering download stream. + die; + } +} + +/** + * Generate headers for downloading a file via stream. + * + * @param string $filename + * Filename to use for attachment download + */ +function petitionadmin_download_send_headers($filename) { + // Disable caching. + $now = gmdate("D, d M Y H:i:s"); + header("Expires: Tue, 03 Jul 2001 06:00:00 GMT"); + header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate"); + header("Last-Modified: {$now} GMT"); + + // Force download. + header("Content-Type: application/force-download"); + header("Content-Type: application/octet-stream"); + header("Content-Type: application/download"); + + // Disposition / encoding on response body. + header("Content-Disposition: attachment;filename={$filename}"); + header("Content-Transfer-Encoding: binary"); +} + +/** + * Renders the signatures entities for a petition. + * + * @param null $node + * Target petition ID. + * + * @return bool|void + * Rendered view or nothing. + */ +function petitionadmin_display_signatures($node = NULL) { + if (!empty($node) && is_object($node)) { + return views_embed_view('petitions_signatures', 'page_1', $node->nid); + } +} + +/** + * Security access callback for signatures tab and view. + * + * @param null $node + * Target petition ID. + * + * @return bool + * TRUE if access is allowed. FALSE otherwise + */ +function _petitionadmin_access_callback($node = NULL) { + if (empty($node) || is_object($node)) { + return FALSE; + } + + $node = node_load($node); + $is_petition_node = $node->type == 'petition'; + $is_user_authorized = user_access('petition access signatures'); + + // This check represents an abundance of caution to prevent this page from + // being accessed in Phase 1, Phase 2, and Phase 3 of the mongo2mysql + // transition. + $is_mongo_off = !petitions_data_mongo_writes_are_enabled(); + return $is_petition_node && $is_user_authorized && $is_mongo_off; +} + +/** + * Implements hook_views_plugins(). + */ +function petitionadmin_views_plugins() { + $plugins = array( + 'access' => array( + 'petition_access' => array( + 'title' => t('Petition Admin Index Access'), + 'help' => t('this is a custom access plugin'), + 'handler' => 'PetitionUiViewsAccessPlugin', + 'path' => drupal_get_path('module', 'petitionadmin'), + ), + ), + ); + + return $plugins; +} + +/** + * Petition views access callback. + * + * @param null $account + * Current user account or null. + * + * @return bool + * boolean if user should have access. + */ +function petitionadmin_views_access_callback($account = NULL) { + global $user; + $access = FALSE; + + if (is_null($account)) { + $account = $user; + } + + $is_user_authorized = views_check_perm('petition access administrator', $account); + + // This line has a lot of meaning. This code will only execute when + // in Phase 4 of the mongo2 mysql transition: reading from mysql and writing + // to mysql. No mongo! + $is_mongo_off = !petitions_data_mongo_writes_are_enabled(); + $access = $is_user_authorized && $is_mongo_off; + + return $access; +} diff --git a/modules/custom/petitionadmin/petitionadmin.views_default.inc b/modules/custom/petitionadmin/petitionadmin.views_default.inc index 8266e8ebb..8339c39b2 100644 --- a/modules/custom/petitionadmin/petitionadmin.views_default.inc +++ b/modules/custom/petitionadmin/petitionadmin.views_default.inc @@ -8,6 +8,1397 @@ * Implements hook_views_default_views(). */ function petitionadmin_views_default_views() { + $export = array(); + + $view = new view(); + $view->name = 'petitionadmin'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Petition Administration'; + $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'] = 'Petitions'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['access']['perm'] = 'petition access administrator'; + $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'] = '50'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'views_bulk_operations' => 'views_bulk_operations', + 'nid' => 'nid', + 'field_legacy_id' => 'field_legacy_id', + 'field_response_status' => 'field_response_status', + 'field_petition_review_timeframe' => 'field_petition_review_timeframe', + 'field_timestamp_published' => 'field_timestamp_published', + 'field_timestamp_reached_public' => 'field_timestamp_reached_public', + 'field_timestamp_reached_ready' => 'field_timestamp_reached_ready', + 'field_timestamp_responded' => 'field_timestamp_responded', + 'field_short_url' => 'field_short_url', + 'title' => 'title', + 'nothing_1' => 'nothing_1', + 'field_petition_public_signatures' => 'field_petition_public_signatures', + 'field_petition_signature_count' => 'field_petition_signature_count', + 'promote' => 'promote', + 'field_petition_status' => 'field_petition_status', + 'field_petition_issues' => 'field_petition_issues', + 'status' => 'status', + 'created' => 'created', + 'changed_1' => 'changed_1', + 'edit_node' => 'edit_node', + 'nothing' => 'nothing', + ); + $handler->display->display_options['style_options']['default'] = '-1'; + $handler->display->display_options['style_options']['info'] = array( + 'views_bulk_operations' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'nid' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_legacy_id' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_response_status' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_petition_review_timeframe' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_timestamp_published' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_timestamp_reached_public' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_timestamp_reached_ready' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_timestamp_responded' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_short_url' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'title' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'nothing_1' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_petition_public_signatures' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_petition_signature_count' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'promote' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_petition_status' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_petition_issues' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'status' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'created' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'changed_1' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'edit_node' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'nothing' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + ); + $handler->display->display_options['style_options']['sticky'] = TRUE; + /* Header: Global: Unfiltered text */ + $handler->display->display_options['header']['area_text_custom']['id'] = 'area_text_custom'; + $handler->display->display_options['header']['area_text_custom']['table'] = 'views'; + $handler->display->display_options['header']['area_text_custom']['field'] = 'area_text_custom'; + $handler->display->display_options['header']['area_text_custom']['empty'] = TRUE; + $handler->display->display_options['header']['area_text_custom']['content'] = '' . $petition_summary_manager->renderSummaryTable() . ' | '; + $output .= '' . $petition_summary_manager->renderIssueTable() . ' |
' . print_r($petition_summary_manager->loadSummaryData(), true) . ''; + + $output .= 'Issues Data:
' . print_r($petition_summary_manager->loadIssueData(), true) . ''; + */ + + return $output; +} diff --git a/modules/custom/petitionevents/petitionevents.module b/modules/custom/petitionevents/petitionevents.module index 134e2392d..b6944538a 100644 --- a/modules/custom/petitionevents/petitionevents.module +++ b/modules/custom/petitionevents/petitionevents.module @@ -82,10 +82,9 @@ function petitionevents_mail_additions(&$message, $context) { // process mongo info here. // $is_mongo_petition = !empty($context['legacy_id']); $is_mysql_petition = !empty($context['nid']); - $is_mysql_enabled = !shunt_is_enabled('petition_mysql_save'); $title = $context['petition_title']; - if ($is_mysql_petition && $is_mysql_enabled) { + if ($is_mysql_petition && petitions_data_mysql_writes_are_enabled()) { $message = str_replace('!petition_nid', $context['nid'], $message); $nid = $context['nid']; diff --git a/modules/custom/petitionevents/petitionevents_vboactions.inc b/modules/custom/petitionevents/petitionevents_vboactions.inc index 23bc827a5..91862b50d 100644 --- a/modules/custom/petitionevents/petitionevents_vboactions.inc +++ b/modules/custom/petitionevents/petitionevents_vboactions.inc @@ -9,15 +9,15 @@ */ function petitionevents_action_info() { return array( - 'petitionevents_unpublish_petition_action' => array( + 'petitionevents_hide_petition_action' => array( 'type' => 'node', - 'label' => t('Remove petitions'), + 'label' => t('Petitions Hide (Remove)'), 'configurable' => TRUE, 'vbo_configurable' => TRUE, ), - 'petitionevents_publish_petition_action' => array( + 'petitionevents_show_petition_action' => array( 'type' => 'node', - 'label' => t('Restore petitions'), + 'label' => t('Petitions Restore'), ), ); } @@ -32,9 +32,9 @@ function petitionevents_action_info() { * Contextual information passed into the action. Values set in a VBO form * would be present here. */ -function petitionevents_unpublish_petition_action(&$node, $context) { +function petitionevents_hide_petition_action(&$node, $context) { try{ - petition_unpublish($node); + petition_hide($node); } catch (Exception $e) { @@ -48,7 +48,7 @@ function petitionevents_unpublish_petition_action(&$node, $context) { if (!$context['skip_notification']) { $context['petition_title'] = $node->title; $context['petition_uid'] = $node->uid; - petitionevents_send_remove_email($context); + petitionevents_send_hidden_email($context); } watchdog('petition_removal', 'Unpublishing petition: @title nid: @nid', array( @@ -67,9 +67,9 @@ function petitionevents_unpublish_petition_action(&$node, $context) { * Contextual information passed into the action. Values set in a VBO form * would be present here. */ -function petitionevents_publish_petition_action(&$node, $context) { +function petitionevents_show_petition_action(&$node, $context) { try{ - petition_publish($node); + petition_show($node); } catch (Exception $e) { @@ -80,7 +80,7 @@ function petitionevents_publish_petition_action(&$node, $context) { return; } - watchdog('petition_publish', 'Publishing petition: @title nid: @nid', array( + watchdog('petition_show', 'Publishing petition: @title nid: @nid', array( '@title' => $node->title, '@nid' => $node->nid, )); @@ -94,7 +94,7 @@ function petitionevents_publish_petition_action(&$node, $context) { * Contextual information passed into the action. Values set in a VBO form * would be present here. */ -function petitionevents_send_remove_email($context) { +function petitionevents_send_hidden_email($context) { // If an address was given, send a test email message. $user = user_load($context['petition_uid']); @@ -115,7 +115,7 @@ function petitionevents_send_remove_email($context) { $params['body'] = $message_body; $params['bcc'] = check_plain($context['bcc']); $params['from'] = $from_full_email; - $result = drupal_mail('petitionevents', 'send_remove_email', $petition_owner_full_email, $language, $params); + $result = drupal_mail('petitionevents', 'send_hidden_email', $petition_owner_full_email, $language, $params); } /** @@ -123,9 +123,9 @@ function petitionevents_send_remove_email($context) { */ function petitionevents_mail($key, &$message, $params) { - if ($key == 'send_remove_email') { + if ($key == 'send_hidden_email') { $message['subject'] = $params['subject']; - $message['body'] = $params['body']; + $message['body'][] = $params['body']; $message['bcc'] = $params['bcc']; $message['from'] = $params['from']; } @@ -140,7 +140,7 @@ function petitionevents_mail($key, &$message, $params) { * @return array * Form of the array. */ -function petitionevents_unpublish_petition_action_form($settings) { +function petitionevents_hide_petition_action_form($settings) { $form = array(); $form['petitionevents_remove_email_skip_notification'] = array( '#type' => 'checkbox', @@ -170,7 +170,7 @@ function petitionevents_unpublish_petition_action_form($settings) { /** * Implements hook_action_validate(). */ -function petitionevents_unpublish_petition_action_validate($form, &$form_state) { +function petitionevents_hide_petition_action_validate($form, &$form_state) { $skip_notification = (boolean) $form_state['values']['petitionevents_remove_email_skip_notification']; if ($skip_notification) { return; @@ -201,7 +201,7 @@ function petitionevents_unpublish_petition_action_validate($form, &$form_state) /** * Submit callback for actions form. */ -function petitionevents_unpublish_petition_action_submit($form, $form_state) { +function petitionevents_hide_petition_action_submit($form, $form_state) { $action_context = array(); $action_context['skip_notification'] = $form_state['values']['petitionevents_remove_email_skip_notification']; $action_context['from_name'] = $form_state['values']['petitionevents_remove_email_from_name']; diff --git a/modules/custom/petitions_api/api_petitions/api_petitions.info b/modules/custom/petitions_api/api_petitions/api_petitions.info index f170f918f..3bccd72a4 100644 --- a/modules/custom/petitions_api/api_petitions/api_petitions.info +++ b/modules/custom/petitions_api/api_petitions/api_petitions.info @@ -1,5 +1,5 @@ name = Petitions Resource -description = Provides petitions resource for Services according to Whitehouse API Standards +description = Provides a petitions Services resource. core = 7.x package = Petitions API dependencies[] = petitions_api diff --git a/modules/custom/petitions_api/api_petitions/api_petitions.module b/modules/custom/petitions_api/api_petitions/api_petitions.module index e8c34ae0d..282531c6d 100644 --- a/modules/custom/petitions_api/api_petitions/api_petitions.module +++ b/modules/custom/petitions_api/api_petitions/api_petitions.module @@ -2,10 +2,7 @@ /** * @file - * api_petitions.module - * - * Provides petitions and signatures for Services according to Whitehouse API - * Standards. + * Provides a petitions Services resource. */ /** @@ -363,7 +360,7 @@ function _api_petitions_resource_retrieve($petition_id, $mock) { // Attempt to load response. try { - $results = PetitionsController::load($petition_id); + $petition = PetitionsController::load($petition_id); } // Catch any errors that may cause $resource->load() to fail. E.g., mongodb. // errors. Throw our own, custom error instead. @@ -378,7 +375,7 @@ function _api_petitions_resource_retrieve($petition_id, $mock) { } // Throw error if petition was not found. - if (empty($results)) { + if (empty($petition)) { $status_code = 404; $developer_message = t("Petition @petition_id not found.", array('@petition_id' => $petition_id)); $user_message = t("The petition that you requested does not exist.", array('@petition_id' => $petition_id)); @@ -388,7 +385,7 @@ function _api_petitions_resource_retrieve($petition_id, $mock) { } $response_params = array( - 'results' => $results, + 'results' => array($petition), 'limit' => 1, ); @@ -414,18 +411,54 @@ function _api_petitions_resource_index($is_public, $is_signable, $created_before return $response; } + // Err if request is for non-public petitions. This is not allowed. + if (!$is_public) { + $developer_message = t('You need a petition ID or URL to access non-public petitions.'); + api_errors_throw_error(403, $developer_message); + } + + // Err if request includes an invalid responseID argument. + if ($response_id && !is_numeric($response_id)) { + $developer_message = t('Invalid responseID argument.'); + api_errors_throw_error(400, $developer_message); + } + // The "url" argument is, for all practical purposes, a unique identifier. So // if one is provided, just redirect the request to the retrieve method so // that private petitions are accessible, just like they would be if the // petition ID were provided directly. (Obviously the user knows about the // existence of this petition if they have its URL.) if (!empty($url)) { - $nice_url = petitions_data_get_nice_url_from_full_url($url); - $conn = wh_petitions_mongo_petition_connection(); - $petition = $conn->findOne(array('nice_url' => $nice_url), array('title')); - $petition_id = (string) $petition['_id']; - return _api_petitions_resource_retrieve($petition_id, $mock); + // If the URL doesn't include a path, it's not a valid petition URL. Note: + // the path component returned by parse_url() includes the leading forward + // slash (/), so the minimum required length is two. + if (strlen(parse_url($url, PHP_URL_PATH)) < 2) { + $developer_message = t('Invalid url argument.'); + api_errors_throw_error(400, $developer_message); + } + + $results = PetitionsSelectQueryFactory::create(FALSE) + ->setURL($url) + ->execute() + ->getResult(); + + // Return a 404 if no results are found. + if (empty($results[0]['id'])) { + $status_code = 404; + $developer_message = t('No petition found at @url.', array( + '@url' => $url, + )); + $user_message = t('The petition that you requested does not exist.'); + $error_code = 84; + $more_info = t('See the documentation: !url/developers#petitions-retrieve-error-404', array( + '!url' => $website_url, + )); + return api_errors_throw_error($status_code, $developer_message, $user_message, $more_info, $error_code); + } + + return _api_petitions_resource_retrieve($results[0]['id'], $mock); } + // Makes the assumption that all query parameters are present at this instant. // This brittle, the values here can be altered by setters when petitions_data // module builds the query to correspond with this request. @@ -460,7 +493,7 @@ function _api_petitions_resource_index($is_public, $is_signable, $created_before ksort($query_parameters); try { - $query = PetitionsSelectQueryFactory::create(); + $query = PetitionsSelectQueryFactory::create(FALSE); // Scaffolding is in place for inactive parameters but logic is required. $query ->setBaseURL($website_url) @@ -490,8 +523,6 @@ function _api_petitions_resource_index($is_public, $is_signable, $created_before 'resource' => 'petitions', 'query' => $query_parameters, ); - // $petition_ids = explode(',' check_plain($petition_id)); - // setPetitionIds($petition_ids); $response_params = array( 'request_info' => $request_info, @@ -537,10 +568,21 @@ function _api_petitions_load_signatures($petition_id, $city, $state, $zipcode, $ return $response; } + // Ensure the petition ID is valid. + $petition = PetitionsController::load($petition_id); + if (empty($petition)) { + $status_code = 404; + $developer_message = t("Petition @petition_id not found.", array('@petition_id' => $petition_id)); + $user_message = t("The petition that you requested does not exist.", array('@petition_id' => $petition_id)); + $error_code = 84; + $more_info = t('See the documentation: !url/developers#petitions-signatures-error-404', array('!url' => $website_url)); + return api_errors_throw_error($status_code, $developer_message, $user_message, $more_info, $error_code); + } + $signatures_default_limit = variable_get('api_petitions_signatures_index_max_limit', 1000); try { - $resource = SignaturesSelectQueryFactory::create(); + $resource = SignaturesSelectQueryFactory::create(FALSE); $resource->setCity($city) ->setState($state) ->setZipCode($zipcode) @@ -614,7 +656,6 @@ function _api_petitions_retrieve_doc_example_implementations() { 'thermometer' => array( '#name' => t('Thermometer'), '#description' => t('Display the status of a petition as a thermometer.'), - // '#location' => _api_petitions_example_implementation('retrieve', 'javascript', 'thermometer'), '#download_link' => drupal_get_path('module', 'api_petitions') . '/example_implementations/javascript/thermometer.zip', '#uses_sdk' => FALSE, ), @@ -699,6 +740,11 @@ function _api_petitions_load_signatures_doc() { */ function _api_petitions_load_signatures_doc_errors() { $errors = array( + '404' => array( + '#question' => t('What happens if an invalid petition ID is requested?'), + '#description' => t('If the request specifies an invalid petition ID, what will the response look like?'), + '#response' => _api_petitions_mock_response('load_signatures404'), + ), '599' => array( '#question' => t('What happens in case of server error?'), '#description' => t('If the request fails due to a server problem, what will the response look like?'), @@ -711,6 +757,13 @@ function _api_petitions_load_signatures_doc_errors() { /** * Returns a mock response for petitions methods. + * + * @param string $method + * A method identifier. + * + * @return string + * The JSON mock response for the given identifier, or an empty string in case + * of an invalid identifier. */ function _api_petitions_mock_response($method) { switch ($method) { @@ -738,10 +791,16 @@ function _api_petitions_mock_response($method) { $file = 'api_petitions_load_signatures.response.json'; break; + case 'load_signatures404': + $file = 'api_petitions_load_signatures.response404.json'; + break; + case 'load_signatures599': $file = 'api_petitions_load_signatures.response599.json'; break; + default: + return ''; } $response = file_get_contents(drupal_get_path('module', 'api_petitions') . '/example_responses/' . $file); return $response; @@ -790,13 +849,15 @@ function _api_petitions_api_update_message(array $api_change_parameters) { return ''; } if (!empty($api_change_parameters['msg'])) { - $message = t($api_change_parameters['msg'] . ' '); + $message = $api_change_parameters['msg'] . ' '; } else { $message = ''; } if (!empty($api_change_parameters['website_url'])) { - $website_url = t('See the documentation: ') . $api_change_parameters['website_url']; + $website_url = t('See the documentation: @url', array( + '@url' => $api_change_parameters['website_url'], + )); } else { $website_url = ''; diff --git a/modules/custom/petitions_api/api_petitions/example_responses/api_petitions_load_signatures.response.json b/modules/custom/petitions_api/api_petitions/example_responses/api_petitions_load_signatures.response.json index ee0877a85..2c76e7ef6 100644 --- a/modules/custom/petitions_api/api_petitions/example_responses/api_petitions_load_signatures.response.json +++ b/modules/custom/petitions_api/api_petitions/example_responses/api_petitions_load_signatures.response.json @@ -8,155 +8,171 @@ "moreInfo": "" }, "resultset": { - "count": 34435, + "count": 24533539, "limit": 16, "offset": 0 } }, "results": [ { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b218a4bd504c17a000001", "type": "signature", - "name": "E. Z.", - "city": "Madison", - "state": "NJ", - "zip": "07940", - "created": 1357952611 - }, - { - "id": "50a3fd762f2c88cd65000015", - "type": "signature", - "name": "J. Y.", + "petitionId": "4e7b21632ee8d04577000000", + "name": "K. S.", "city": "", "state": "", "zip": "", - "created": 1357952366 + "created": 1316692362 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b2269709f03747a000001", "type": "signature", - "name": "C. C.", - "city": "Rehoboth", - "state": "MA", - "zip": "02769", - "created": 1357952340 + "petitionId": "4e7b21632ee8d04577000000", + "name": "A. J.", + "city": "Washington", + "state": "DC", + "zip": "20502", + "created": 1316692585 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b229f709f036f7a000002", "type": "signature", - "name": "R. S.", - "city": "", - "state": "", - "zip": " 20181", - "created": 1357952047 + "petitionId": "4e7b21632ee8d04577000000", + "name": "C. B.", + "city": "Alexandria", + "state": "VA", + "zip": "22193", + "created": 1316692639 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b22b58d8c37d875000001", "type": "signature", - "name": "D. G.", - "city": "Edgewood", - "state": "MD", - "zip": "21040", - "created": 1357951907 + "petitionId": "4e7b21632ee8d04577000000", + "name": "B. B.", + "city": "Washington", + "state": "DC", + "zip": "20502", + "created": 1316692661 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b23768d8c37d775000000", "type": "signature", - "name": "J. J.", - "city": "Divide", - "state": "CO", - "zip": "80814", - "created": 1357950875 + "petitionId": "4e7b21632ee8d04577000000", + "name": ". .", + "city": "", + "state": "", + "zip": "", + "created": 1316692854 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b23d011fb9c3e7a000001", "type": "signature", - "name": "K. B.", - "city": "", - "state": "", - "zip": "95624", - "created": 1357950310 + "petitionId": "4e7b21632ee8d04577000000", + "name": "E. M.", + "city": "Washington", + "state": "DC", + "zip": "20502", + "created": 1316692944 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b23efcf436a2274000002", "type": "signature", - "name": "M. J.", - "city": "", - "state": "", - "zip": "30518", - "created": 1357949893 + "petitionId": "4e7b21632ee8d04577000000", + "name": "A. J.", + "city": "Arlington", + "state": "MA", + "zip": "02476", + "created": 1316692975 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b23f8ec309e5f76000001", "type": "signature", - "name": "F. B.", - "city": "", - "state": "", - "zip": "", - "created": 1357949551 + "petitionId": "4e7b21632ee8d04577000000", + "name": "B. B.", + "city": "Augusta", + "state": "GA", + "zip": "30907", + "created": 1316692984 + }, + { + "id": "4e7b23fc4bd5043606000000", + "type": "signature", + "petitionId": "4e7b21632ee8d04577000000", + "name": "S. P.", + "city": "Montgomery Village", + "state": "MD", + "zip": "20886", + "created": 1316692988 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b2406cf436a2874000001", "type": "signature", - "name": "R. H.", - "city": "Holbrook", - "state": "NY", - "zip": "11741", - "created": 1357947886 + "petitionId": "4e7b23c2ec309e5e76000001", + "name": "T. C.", + "city": "Washington", + "state": "DC", + "zip": "20010", + "created": 1316692998 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b241d709f036d7a000001", "type": "signature", - "name": "L. L.", - "city": "", - "state": "", - "zip": "97317", - "created": 1357947819 + "petitionId": "4e7b21632ee8d04577000000", + "name": "C. B.", + "city": "Woodbridge", + "state": "VA", + "zip": "22193", + "created": 1316693021 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b25fd11fb9c3c7a000001", "type": "signature", - "name": "C. G.", - "city": "Norwalk", - "state": "OH", - "zip": "44857", - "created": 1357947415 + "petitionId": "4e7b21632ee8d04577000000", + "name": "A. T.", + "city": "Arlington", + "state": "MA", + "zip": "02476", + "created": 1316693501 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b268c4bd504c37a000001", "type": "signature", - "name": "M. G.", - "city": "Santa Fe", - "state": "NM", - "zip": "87505", - "created": 1357947020 + "petitionId": "4e7b21632ee8d04577000000", + "name": "K. S.", + "city": "Washington", + "state": "DC", + "zip": "20009", + "created": 1316693644 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b2730cf436a2574000001", "type": "signature", - "name": "O. W.", - "city": "Clemmons", - "state": "NC", - "zip": "27012", - "created": 1357946950 + "petitionId": "4e7b21632ee8d04577000000", + "name": "E. L.", + "city": "Washington", + "state": "DC", + "zip": "20008", + "created": 1316693808 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b273a8d8c37dc75000000", "type": "signature", - "name": "R. H.", - "city": "Westville", - "state": "IN", - "zip": "46391", - "created": 1357946582 + "petitionId": "4e7b2719b25636dd79000001", + "name": "T. C.", + "city": "Washington", + "state": "DC", + "zip": "20010", + "created": 1316693818 }, { - "id": "50a3fd762f2c88cd65000015", + "id": "4e7b27ee709f03727a000000", "type": "signature", - "name": "W. B.", - "city": "La Canada Flintridge", - "state": "CA", - "zip": "91011", - "created": 1357945636 + "petitionId": "4e7b21632ee8d04577000000", + "name": "E. L.", + "city": "Washington", + "state": "DC", + "zip": "20001", + "created": 1316693998 } ] } diff --git a/modules/custom/petitions_api/api_petitions/example_responses/api_petitions_load_signatures.response404.json b/modules/custom/petitions_api/api_petitions/example_responses/api_petitions_load_signatures.response404.json new file mode 100644 index 000000000..92b5cd805 --- /dev/null +++ b/modules/custom/petitions_api/api_petitions/example_responses/api_petitions_load_signatures.response404.json @@ -0,0 +1,11 @@ +{ + "metadata": { + "responseInfo": { + "status": "404", + "developerMessage:": "Petition {petition_id} not found.", + "userMessage": "The petition that you requested does not exist.", + "errorCode": "84", + "moreInfo": "See the documentation: https://petitions.whitehouse.gov/developers#petitions-signatures-error-404" + } + } +} diff --git a/modules/custom/petitions_api/api_signatures/api_signatures.inc b/modules/custom/petitions_api/api_signatures/api_signatures.inc index 4087ddc0a..5e71331f1 100644 --- a/modules/custom/petitions_api/api_signatures/api_signatures.inc +++ b/modules/custom/petitions_api/api_signatures/api_signatures.inc @@ -22,7 +22,7 @@ function _api_signatures_resource_create($api_key, array $signature) { $website_url = variable_get('petitions_data_petitions_website_url', $base_url); // Verbose logging, if debugging is enabled. - if ($debug = variable_get('signatures_queue_enable_debugging', FALSE)) { + if ($debug = variable_get('signatures_queue_debug', FALSE)) { watchdog('api_signatures', 'DEBUG ENABLED: _api_signatures_resource_create() param $api_key: @api_key', array( '@api_key' => $api_key, )); diff --git a/modules/custom/petitions_api/api_validations/tests/ApiValidationsTestCase.test b/modules/custom/petitions_api/api_validations/tests/ApiValidationsTestCase.test deleted file mode 100644 index 513977c75..000000000 --- a/modules/custom/petitions_api/api_validations/tests/ApiValidationsTestCase.test +++ /dev/null @@ -1,107 +0,0 @@ -assertTrue(module_exists('api_validations'), - 'api_validations module enabled.'); - - // Unknown why these lines are needed but they are on certain environments. - drupal_load('module', 'api_validations'); - module_load_include('inc', 'api_validations'); - } - - /** - * {@inheritdoc} - */ - public static function getInfo() { - return array( - 'name' => 'Validations API', - 'description' => 'Ensure signature validation are properly readable by the api.', - 'group' => 'Petitions API', - ); - } - - /** - * Check the Validations API mock results. - */ - public function testMockValidation() { - // Set the third parameter to TRUE to request a Mock response. - $result = _api_validations_resource_index('dummy_key', 'dummy_id', TRUE, 0, 5); - $this->assertTrue(isset($result) && is_array($result), - 'Validations API returns an array'); - $this->assertTrue(isset($result['metadata']), 'Validations API result has metadata'); - $this->assertTrue(isset($result['results']), 'Mock Validations API request has results'); - $this->assertTrue(($result['metadata']['resultset']['count'] > 0), 'One or more result found for Validations API mock request'); - } - - /** - * Check the results of a generated validation. - */ - public function testGeneratedValidation() { - $this->createDummyIssues(); - $petition = $this->createDummyPetition(); - $this->assertNotNull($petition->getId(), - 'Petition created with id ' . $petition->getId()); - - // Create a signature. - $signature = $this->createDummySignature($petition->getId()); - $this->assertNotNull($signature, 'Signature created with id ' . $signature->getId()); - - // Create an API Key. - $key = $this->createDummyAPIKey(); - - // Create a validation. - $id = $this->createDummyValidation( - $signature->getId(), $petition->getId(), $key); - $this->assertNotNull($id, 'Validation created with id ' . $id); - - // Assert with the correct petition but an invalid key. - // We should not get results with a key that isn't in the system. Instead - // it should throw an exception containing the appropriate error code - // and message. - try { - $result = _api_validations_resource_index('Invalid key', - $petition->getId(), FALSE, 0, 5); - } - catch (Exception $exception) { - $data = $exception->getData(); - } - $this->assertTrue((!empty($data['errorCode']) && $data['errorCode'] == 85), - 'Correct error code with an invalid key.'); - - // Assert results with correct parameters and valid key. - $result = _api_validations_resource_index($key, $petition->getId(), FALSE, 0, 5); - $this->assertTrue((!empty($result['metadata']['resultset']) && - $result['metadata']['resultset']['count']), - 'One or more result found for Validations API request'); - - // Get the first result item. - $validation = isset($result['results'][0]) ? $result['results'][0] : NULL; - $is_object = $this->assertTrue(is_object($validation), - 'Results are objects as expected.'); - - // Check for the correct fields. - $this->assertTrue(($is_object && isset($validation->vid)), - 'Found vid field in validation object.'); - $this->assertTrue(($is_object && isset($validation->email)), - 'Found email field in validation object.'); - $this->assertTrue(($is_object && isset($validation->signature_id)), - 'Found signature_id field in validation object.'); - $this->assertTrue(($is_object && isset($validation->petition_id)), - 'Found petition_id field in validation object.'); - } - -} diff --git a/modules/custom/petitions_api/signatures_queue/includes/archive_signatures.inc b/modules/custom/petitions_api/signatures_queue/includes/archive_signatures.inc index e73f96dd6..e47a5a7b1 100644 --- a/modules/custom/petitions_api/signatures_queue/includes/archive_signatures.inc +++ b/modules/custom/petitions_api/signatures_queue/includes/archive_signatures.inc @@ -129,10 +129,20 @@ function _signatures_queue_archive_invalid_signatures($queues_last_emptied, $wat // Insert the invalid signatures into the signatures_not_validated_archive // table. while ($row = $query->fetchAssoc()) { - db_insert('signatures_not_validated_archive') - ->fields($row) - ->execute(); - $archived_count++; + try { + db_insert('signatures_not_validated_archive') + ->fields($row) + ->execute(); + $archived_count++; + } + catch (PDOException $e) { + petitionslog_event('exceptions.signatures_queue.4b4729c'); + watchdog('signatures_queue', 'Failed to archive item to signatures_not_validated_archive due to a database error: item: !item, exception: !exception. @suffix', array( + '!item' => petitionslog_format_for_watchdog($row), + '!exception' => petitionslog_format_for_watchdog($e), + '@suffix' => $watchdog_suffix, + ), WATCHDOG_CRITICAL); + } } $count_signatures_not_validated_archive = db_select('signatures_not_validated_archive')->countQuery()->execute()->fetchField(); _signatures_queue_data_store_size_event('signatures_not_validated_archive', $count_signatures_not_validated_archive); @@ -226,10 +236,20 @@ function _signatures_queue_archive_orphaned_validations($queues_last_emptied, $w // Insert the orphaned validations into validations_orphaned_archive table. while ($row = $results->fetchAssoc()) { $columns[] = $row['secret_validation_key']; - db_insert('validations_orphaned_archive') - ->fields($row) - ->execute(); - $orphaned_count++; + try { + db_insert('validations_orphaned_archive') + ->fields($row) + ->execute(); + $orphaned_count++; + } + catch (PDOException $e) { + petitionslog_event('exceptions.signatures_queue.fa29d17'); + watchdog('signatures_queue', 'Failed to archive item to validations_orphaned_archive due to a database error: item: !item, exception: !exception. @suffix', array( + '!item' => petitionslog_format_for_watchdog($row), + '!exception' => petitionslog_format_for_watchdog($e), + '@suffix' => $watchdog_suffix, + ), WATCHDOG_CRITICAL); + } } // Log the number of successfully archived orphaned signatures. @@ -282,6 +302,9 @@ function _signatures_queue_delete_orphaned_validations($queues_last_emptied, $wa $columns = $query->execute()->fetchCol(1); } + //Initialize variable. + $deleted_signatures = 0; + // Delete signatures that are no longer valid from the pending table. if (count($columns)) { $deleted_signatures = db_delete('validations') @@ -376,10 +399,20 @@ function _signatures_queue_archive_processed_signatures($queues_last_emptied, $w // Insert the processed signatures into the signatures_processed_archive // table. while ($row = $results->fetchAssoc()) { - db_insert('signatures_processed_archive') - ->fields($row) - ->execute(); - $archived_count++; + try { + db_insert('signatures_processed_archive') + ->fields($row) + ->execute(); + $archived_count++; + } + catch (PDOException $e) { + petitionslog_event('exceptions.signatures_queue.9491ced'); + watchdog('signatures_queue', 'Failed to archive an item to signatures_processed_archive due to a database error: item: !item, exception: !exception. @suffix', array( + '!item' => petitionslog_format_for_watchdog($row), + '!exception' => petitionslog_format_for_watchdog($e), + '@suffix' => $watchdog_suffix, + ), WATCHDOG_CRITICAL); + } } $count_signatures_processed_archive = db_select('signatures_processed_archive')->countQuery()->execute()->fetchField(); @@ -456,10 +489,20 @@ function _signatures_queue_archive_processed_validations($queues_last_emptied, $ // Insert the processed validations into the validations_processed_archive // table. while ($row = $results->fetchAssoc()) { - db_insert('validations_processed_archive') - ->fields($row) - ->execute(); - $archived_count++; + try { + db_insert('validations_processed_archive') + ->fields($row) + ->execute(); + $archived_count++; + } + catch (PDOException $e) { + petitionslog_event('exceptions.signatures_queue.807e94e'); + watchdog('signatures_queue', 'Failed to archive item to validations_processed_archive due to a database error: item: !item, exception: !exception. @suffix', array( + '!item' => petitionslog_format_for_watchdog($row), + '!exception' => petitionslog_format_for_watchdog($e), + '@suffix' => $watchdog_suffix, + ), WATCHDOG_CRITICAL); + } } $count = db_select('validations_processed_archive')->countQuery()->execute()->fetchField(); diff --git a/modules/custom/petitions_api/signatures_queue/includes/initiate_signature_validation.inc b/modules/custom/petitions_api/signatures_queue/includes/initiate_signature_validation.inc index 3170f9716..777666dfa 100644 --- a/modules/custom/petitions_api/signatures_queue/includes/initiate_signature_validation.inc +++ b/modules/custom/petitions_api/signatures_queue/includes/initiate_signature_validation.inc @@ -39,7 +39,7 @@ function _signatures_queue_initiate_signature_validation($job_id, $server_name, $worker_name, array $options = array()) { $watchdog_suffix = _signatures_queue_watchdog_suffix('initiate_signature_validation', $job_id, $server_name, $worker_name); - $debug = variable_get('signatures_queue_enable_debugging', FALSE); + $debug = variable_get('signatures_queue_debug', FALSE); // Retrieve and delete signatures from the signatures_submitted_queue. $signatures = _signatures_queue_retrieve_submitted_signatures($watchdog_suffix, 0, $debug); @@ -73,7 +73,6 @@ function _signatures_queue_initiate_signature_validation($job_id, $server_name, * * @param int $batch_size * The number of signatures to process in this batch. - * * @param bool $debug * Enable verbose logging for debugging. * @@ -217,21 +216,33 @@ function _signatures_queue_send_validation_emails(array $signatures, $watchdog_s $elapsed = signatures_queue_get_microtime_elapsed_since($signature->data['timestamp_received_new_signature']); petitionslog_event('signatures_queue.data_flow.time_elapsed.received_new_signature__to__initiated_signature_validation', 'time', $elapsed); - // Send validation email. $to = $signature->data['email']; + + // Make sure email address is valid before attempting to send to it. + if (!valid_email_address($to)) { + continue; + } + + // Send validation email. global $language; $params = array('signature info' => $signature->data); try { + $event = 'signatures_queue.workflow.initiate_signature_validation.time_elapsed.drupal_mail'; + petitionslog_reusable_timer_start($event); $message = drupal_mail('signatures_queue', 'initiate_signature_validation', $to, $language, $params); + $elapsed = petitionslog_reusable_timer_read_and_destroy($event); + petitionslog_event($event, 'time', $elapsed); if ($message['result']) { $signatures_mailed['sent'][] = $signature; } else { + petitionslog_event('exceptions.signatures_queue.ef6b8ea'); // Mailing errors should be logged by active mail system. $signatures_mailed['not sent'][] = $signature; } } catch (Exception $e) { + petitionslog_event('exceptions.signatures_queue.286e111'); // Mailing errors should be logged by active mail system. $signatures_mailed['not sent'][] = $signature; } diff --git a/modules/custom/petitions_api/signatures_queue/includes/preprocess_signatures.inc b/modules/custom/petitions_api/signatures_queue/includes/preprocess_signatures.inc index 1b2c1eb27..fed9778d9 100644 --- a/modules/custom/petitions_api/signatures_queue/includes/preprocess_signatures.inc +++ b/modules/custom/petitions_api/signatures_queue/includes/preprocess_signatures.inc @@ -41,7 +41,7 @@ */ function _signatures_queue_preprocess_signatures($job_id, $server_name, $worker_name, array $options = array()) { $watchdog_suffix = _signatures_queue_watchdog_suffix('preprocess_signatures', $job_id, $server_name, $worker_name); - $debug = variable_get('signatures_queue_enable_debugging', FALSE); + $debug = variable_get('signatures_queue_debug', FALSE); // Preprocess signatures pending validation. _signatures_queue_preprocess_signatures_preprocess_signatures($watchdog_suffix, $debug); @@ -275,24 +275,28 @@ function _signatures_queue_preprocess_signatures_safe_insert_batch_into_table($b $query->fields(array_values($fields)); // Add values. + $items_added = 0; foreach ($sanitized_signatures as $item) { // Restrict values to those that correspond to database fields. $values = array_intersect_key((array) $item->data, $fields); // Add the resulting values to the query. $query->values($values); - petitionslog_event("signatures_queue.data_store.{$table_name}.item_added"); + $items_added++; } // Execute the query. try { $query->execute(); + petitionslog_event("signatures_queue.data_store.{$table_name}.item_added", 'count', $items_added); } catch (Exception $e) { + petitionslog_event('exceptions.signatures_queue.6e13628'); db_set_active(); - watchdog('signatures_queue', "Error while attempting db insert. @suffix.\n%error", array( - '%error' => $e->getMessage(), + watchdog('signatures_queue', 'Error inserting signatures into @table. @suffix. Exception: !e', array( + '@table' => $table_name, '@suffix' => $watchdog_suffix, + '!e' => petitionslog_format_for_watchdog($e), ), WATCHDOG_ERROR); return FALSE; diff --git a/modules/custom/petitions_api/signatures_queue/includes/process_signatures.inc b/modules/custom/petitions_api/signatures_queue/includes/process_signatures.inc index 78a4d2101..e7db0b673 100644 --- a/modules/custom/petitions_api/signatures_queue/includes/process_signatures.inc +++ b/modules/custom/petitions_api/signatures_queue/includes/process_signatures.inc @@ -39,10 +39,10 @@ * * @see signatures_queue_invoke_workflow() */ -function _signatures_queue_process_signatures($job_id, $server_name, $worker_name, $options) { +function _signatures_queue_process_signatures($job_id, $server_name, $worker_name, array $options) { $watchdog_suffix = _signatures_queue_watchdog_suffix('process_signatures', $job_id, $server_name, $worker_name); $limit = signatures_queue_get_queue_batch_size('process_signatures'); - $debug = variable_get('signatures_queue_enable_debugging', FALSE); + $debug = variable_get('signatures_queue_debug', FALSE); // Set the active database to the signatures_processing db. signatures_queue_set_db(); @@ -105,11 +105,12 @@ function _signatures_queue_process_signatures($job_id, $server_name, $worker_nam // Set ID. $item['signature_id'] = $signature_id; // Increment signature count. - _signatures_queue_process_signatures_increment_count($item['petition_id']); + PetitionsController::incrementSignatureCount($item['petition_id']); } // If it's a new signature, but no new signature was created... elseif ($new_signature && empty($signature_id)) { // Log failure. + petitionslog_event('exceptions.signatures_queue.f79c9a5'); watchdog('signatures_queue', 'New signature could not be created. secret_validation_key: @secret_validation_key, vid: @vid, petition_id: @petition_id, timestamp_received_signature_validation: @timestamp_received_signature_validation. @suffix', array( '@secret_validation_key' => $item['secret_validation_key'], '@vid' => $item['vid'], @@ -265,60 +266,21 @@ function _signatures_queue_process_signatures_assert_legitimate($item, $debug = */ function _signatures_queue_process_signatures_valid_petition($petition_id, $debug = FALSE) { // Make sure petition exists and is signable. - // @todo This should really use the logic in Petition::isSignable(). And - // Petition::isSignable should really be updated to honor the signatures + // @todo This should really use the logic in PetitionItem::isSignable(). And + // PetitionItem::isSignable should really be updated to honor the signatures // queue grace period if passed in as a param. But getting that to work // will take some refactoring. For now, just try to load the petition. If // you can't load it, you shouldn't be able to sign it. It was likely // been removed/unpublished by an administrator because it violates terms of // service. - if (!shunt_is_enabled('signature_mail_mysql_save')) { - // MySQL is the authoritative data store. Check petition node to confirm - // validity. - $nid = petition_get_nid($petition_id); - $node = node_load($nid); - // Edge case from PT-1423 hit here. Mongo to mysql migration has happened, - // but we have somehow managed to create a mongo based petition without a - // node counterpart (maybe by switching shunts after the initial migration). - // A good way to address this is by trying to create the node on the fly, - // instead of just erroring out and dropping the signature. - if (!$node) { - // Debug? - if ($debug) { - watchdog('signatures_queue', 'DEBUG ENABLED: Petition id @petition_id does not seem to have a petition node.', array( - '@petition_id' => $petition_id, - ), WATCHDOG_DEBUG); - } - // Node doesn't exist. - return FALSE; - } - if (!$node->status) { - // Debug? - if ($debug) { - watchdog('signatures_queue', 'DEBUG ENABLED: Invalid petition, not published: node = !node', array( - '!node' => petitionslog_format_for_watchdog($node), - ), WATCHDOG_DEBUG); - } - // Node is unpublished. - return FALSE; - } - } - else { - // If mongo2mysql switch is not complete or the mysql shunt is tripped, use - // the legacy petitions_data module to try and load the petition being - // signed. If it can't be loaded, it can't be signed either. - if (module_exists('petitions_data')) { - $petition = petitions_data_mongo2mysql_get_petition($petition_id); - if (!$petition) { - // Debug? - if ($debug) { - watchdog('signatures_queue', 'Invalid petition, cannot load petition_id from mongo: id = @id', array( - '@id' => $petition_id, - ), WATCHDOG_DEBUG); - } - return FALSE; - } + $petition = petitions_data_get_petition($petition_id); + if (!$petition) { + if ($debug) { + watchdog('signatures_queue', 'Invalid petition: id = @id', array( + '@id' => $petition_id, + ), WATCHDOG_DEBUG); } + return FALSE; } // If not found to be invalid, it's valid. @@ -415,6 +377,7 @@ function _signatures_queue_process_signatures_add_to_signatures_validations($ite } catch (PDOException $exception) { // @todo Abuse detection. + petitionslog_event('exceptions.signatures_queue.a49d9ef'); watchdog('signatures_queue', 'An item could not be added due to a database error: item: !item, exception: !exception. @suffix', array( '!item' => petitionslog_format_for_watchdog($item), '!exception' => petitionslog_format_for_watchdog($exception), @@ -435,9 +398,6 @@ function _signatures_queue_process_signatures_add_to_signatures_validations($ite * The item from the database query. * @param string $watchdog_suffix * A string of job details as created by _signatures_queue_watchdog_suffix(). - * - * @return bool - * Returns TRUE on success. */ function _signatures_queue_process_signatures_move_to_processed($item, $watchdog_suffix) { // Set the active database to the signatures_processing db. @@ -477,6 +437,7 @@ function _signatures_queue_process_signatures_move_to_processed($item, $watchdog petitionslog_event('signatures_queue.data_store.signatures_processed.item_added'); } catch (PDOException $exception) { + petitionslog_event('exceptions.signatures_queue.e5f8a95'); $erred = TRUE; } try { @@ -494,12 +455,13 @@ function _signatures_queue_process_signatures_move_to_processed($item, $watchdog petitionslog_event('signatures_queue.data_store.validations_processed.item_added'); } catch (PDOException $exception) { + petitionslog_event('exceptions.signatures_queue.48d42b3'); $erred = TRUE; } // Log errors. if ($erred) { - // @todo Abuse detection. + petitionslog_event('exceptions.signatures_queue.388ac8b'); watchdog('signatures_queue', 'An item could not be moved to processed due to a database error: item: !item, exception: !exception. @suffix', array( '!item' => petitionslog_format_for_watchdog($item), '!exception' => petitionslog_format_for_watchdog($exception), @@ -551,65 +513,24 @@ function _signatures_queue_process_signatures_get_unique_username($email) { } /** - * Try to look up signature id based on uid and petition id. - * - * @todo This is hardcoded to signature_mail use cases. After the mongo2mysql - * refactor is done, consider whether this should be genericized to support more - * types of signature entities besides just signature_mail (e.g. Facebook, - * Twitter, other). + * Gets a signature ID based on a petition ID and user ID. * - * @param string $petition_id - * Petition id. + * @param string|int $petition_id + * A petition ID. * @param int $uid - * User id + * A user ID. * - * @return string - * Legacy ids are mongo IDs. New mysql based IDs are serial IDs assigned by - * mysql. + * @return string|int|false + * The ID of the signature if found or FALSE if not. IDs may be + * strings (legacy MongoDB IDs) or integers (entity IDs). */ function _signatures_queue_process_signatures_get_signature_id($petition_id, $uid) { - $signature_id = ''; - - // Use deprecated mongo2mysql lookup? - // @todo Remove this for 7.x-3.0 release. - if (function_exists('wh_petitions_mongo2mysql_get_signature_id') - && !shunt_is_enabled('wh_petitions_signature_create')) { - // Get signature id from mongo petition_signatures collection. - $signature_id = wh_petitions_mongo2mysql_get_signature_id($petition_id, $uid); - } - - // @todo We should start using this when mysql is enabled and test to confirm - // the ids match when both are enabled. It's a good, low risk opportunity to - // validate things working as expected before finally flipping the switcht. - else { - - // Get signature id from signature_mail table in mysql. - $query = db_select('signature_mail', 'sm') - ->fields('sm', array('id')) - ->condition('uid', $uid, '=') - ->condition('petition_id', $petition_id, '='); - - // Figure out if the id passed in was a legacy mongo id or a node id. - $is_node_id = is_numeric($petition_id); - if ($is_node_id) { - $query->condition('petition_id', $petition_id, '='); - } - else { - $query->condition('legacy_petition_id', $petition_id, '='); - } - $result = $query->execute(); - - // Get the signature ID. - while ($row = $result->fetchAssoc()) { - $signature_id = $row['id']; - } - - } - - // @todo Note: If ID was found with _process_signatures_mongo2mysql_get_id(), - // this returns a mongo signature_id. If found in mysql, it returns a - // serial/numeric signature_mail id. Will this cause problems? - return $signature_id; + $signatures = SignaturesSelectQueryFactory::create() + ->setPetitionId($petition_id) + ->setUid($uid) + ->execute() + ->getResult(); + return (!empty($signatures[0]['id'])) ? $signatures[0]['id'] : FALSE; } /** @@ -618,130 +539,45 @@ function _signatures_queue_process_signatures_get_signature_id($petition_id, $ui * @param array $item * Signature data that has worked its way down the pipeline through the * signature queue. - * @param obj $user + * @param object $user * Drupal user, owner of this signature. * @param bool $debug * Enable debugging. * - * @return string - * $signature_id The ID of the signature created + * @return string|false + * The ID of the signature created, or FALSE if none could be created. */ -function _signatures_queue_process_signatures_save_signature($item, $user, $debug) { - // Save to mongo? - if (function_exists('wh_petitions_mongo2mysql_create_new_signature') && !shunt_is_enabled('wh_petitions_signature_create')) { - $client_ip = 'not available'; - - // Debug? Print to log. - if ($debug) { - watchdog('signatures_queue', 'DEBUG ENABLED: Saving signature to mongo: !item', array( - '!item' => petitionslog_format_for_watchdog($item), - ), WATCHDOG_DEBUG); - } - - // Save. Get legacy mongo petition ID. - $legacy_signature_id = wh_petitions_mongo2mysql_create_new_signature($item['petition_id'], $user, $client_ip, $item['timestamp_received_new_signature']); +function _signatures_queue_process_signatures_save_signature(array $item, $user, $debug) { + $petition = PetitionsController::loadObject($item['petition_id']); - // If there's no ID here, saving the signature to mongo failed. Log failure. - if (!$legacy_signature_id) { - watchdog('signatures_queue', 'Error: Attempt to save signature to mongo failed. Queue item = !item', array( - '!item' => petitionslog_format_for_watchdog($item), - ), WATCHDOG_ERROR); - } - - // Debugging? Print success result to log. - if ($debug && $legacy_signature_id) { - // Signature was successfully saved to mongo. - watchdog('signatures_queue', 'DEBUG ENABLED: Signature successfully saved to mongo. Legacy mongo signature id: @id', array( - '@id' => $legacy_signature_id, - ), WATCHDOG_DEBUG); - } + // No such petition could be found. + if (!$petition) { + return FALSE; } - // Save to mysql? - if (!shunt_is_enabled('signature_mail_mysql_save')) { - // Build up $signature array to save via Entity::save(). - $signature = array(); - - // If there's a corresponding mongo signature ID, save it as legacy_id. - $signature['legacy_id'] = (!empty($legacy_signature_id)) ? $legacy_signature_id : ''; - - // Is petition_id from the item in the queue a mongo id? If so, store as - // legacy_petition_id. - if (petition_is_legacy_id($item['petition_id'])) { - $signature['legacy_petition_id'] = $item['petition_id']; - } - - // Get the node id of the petition. Store this as main petition_id. - $nid = petition_get_nid($item['petition_id']); - $signature['petition_id'] = $nid; - - // Get user id of signer. - $signature['uid'] = $user->uid; - - // Map properties of $item pulled from the queue to signature entity fields. - $property_map = array( - "first_name" => "user_first_name", - "last_name" => "user_last_name", - "zip" => "user_zip", - 'timestamp_received_new_signature' => 'timestamp', - "client_ip" => "ip_address", - ); - foreach ($property_map as $item_key => $signature_key) { - $signature[$signature_key] = $item[$item_key]; - } - - // Double check we have default values for all properties. - // @todo This is currently hard coded to signature_mail. If/When we create - // more types of signatures, that come through this pipeline, refactor to - // accommodate broader use cases. - $schema = drupal_get_schema("signature_mail"); - foreach ($schema['fields'] as $property => $info) { - if (empty($signature[$property])) { - if ($property != 'id' && ($info['type'] == 'text' || $info['type'] == "varchar")) { - $signature[$property] = ""; - } - elseif ($property != 'id' && $info['type'] == 'int') { - $signature[$property] = 0; - } - } - } - - // Convert $signature array to a signature entity (object). - $signature['bundle'] = array(); - $signature = entity_create("signature_mail", $signature); - - // Save it! - $signature->save(); - - // Debugging? Print result to log. - if ($debug) { - watchdog('signatures_queue', 'DEBUG ENABLED: Signature has been saved to mysql. Signature entity: !signature', array( - '!signature' => petitionslog_format_for_watchdog($signature), + $signature = new SignatureItem(); + // Set the legacy petition ID first else we end up with no petition ID at all. + // We have to set it in the case when mongo reads are disabled but mongo + // writes are still enabled. + $signature + ->setLegacyPetitionId($petition->getLegacyId()) + ->setPetitionId($petition->getEntityId()) + ->setFirstName($item['first_name']) + ->setLastName($item['last_name']) + ->setCreated($item['timestamp_received_new_signature']) + ->setUid($user->uid) + ->setUser($user) + ->setZip($item['zip']); + $signature = SignaturesController::save($signature); + + if ($debug) { + if ($signature->getId() !== NULL) { + watchdog('signatures_queue', 'DEBUG ENABLED: Signature has been saved. Signature entity: !signature', array( + '!signature' => petitionslog_format_for_watchdog($signature->toEntity()), ), WATCHDOG_DEBUG); } } - // @TODO Refactor. Return mysql-based id if available. - return $legacy_signature_id; -} - -/** - * Increase the cached signature count for a petition. - * - * @param string $petition_id - * Unique ID for a petition. - * @param bool $debug - * Enable debugging. - */ -function _signatures_queue_process_signatures_increment_count($petition_id, $debug = FALSE) { - // Increment count in petition stored in mongo? - $mongo2mysql_function = 'wh_petitions_mongo2mysql_increment_count'; - if (function_exists($mongo2mysql_function) && !shunt_is_enabled('wh_petitions_signature_create')) { - $mongo2mysql_function($petition_id); - } + return $signature->getId(); - // Increment count in petition node? - if (!shunt_is_enabled('signature_mail_mysql_save')) { - petition_increment_count($petition_id, 1, $debug); - } } diff --git a/modules/custom/petitions_api/signatures_queue/includes/receive_new_signatures.inc b/modules/custom/petitions_api/signatures_queue/includes/receive_new_signatures.inc index be6785a2e..111c1cb8c 100644 --- a/modules/custom/petitions_api/signatures_queue/includes/receive_new_signatures.inc +++ b/modules/custom/petitions_api/signatures_queue/includes/receive_new_signatures.inc @@ -41,14 +41,14 @@ function _signatures_queue_receive_new_signatures($job_id, $server_name, $worker_name, array $options = array()) { $signature = (array) $options['signature']; $api_key = $signature['signature_source_api_key']; - $petition_id = $signature['petition_id']; + $petition_id = !empty($signature['petition_id']) ? $signature['petition_id'] : 'NONE'; try { petitionslog_event("signatures_queue.workflow.receive_new_signatures.api_key.{$api_key}.invoked"); petitionslog_event("signatures_queue.workflow.receive_new_signatures.petition_id.{$petition_id}.invoked"); // Debugging. Log signature data received. - if ($debug = variable_get('signatures_queue_enable_debugging', FALSE)) { + if ($debug = variable_get('signatures_queue_debug', FALSE)) { watchdog('signatures_queue', 'DEBUG ENABLED: New signature received: !signature', array( '!signature' => petitionslog_format_for_watchdog($signature), ), WATCHDOG_DEBUG); @@ -62,20 +62,22 @@ function _signatures_queue_receive_new_signatures($job_id, $server_name, $worker return SIGNATURES_QUEUE_STATUS_BAD_REQUEST; } - $petition = petitions_data_mongo2mysql_get_petition($petition_id); - - // Make sure the petition exists. - if (empty($petition)) { - petitionslog_event("signatures_queue.workflow.receive_new_signatures.api_key.{$api_key}.not_found"); - petitionslog_event("signatures_queue.workflow.receive_new_signatures.petition_id.{$petition_id}.not_found"); - return SIGNATURES_QUEUE_STATUS_NOT_FOUND; - } - - // Make sure the petition is "signable". - if (!$petition['isSignable']) { - petitionslog_event("signatures_queue.workflow.receive_new_signatures.api_key.{$api_key}.forbidden"); - petitionslog_event("signatures_queue.workflow.receive_new_signatures.petition_id.{$petition_id}.forbidden"); - return SIGNATURES_QUEUE_STATUS_FORBIDDEN; + if (!shunt_is_enabled('signatures_queue_receive_new_signatures_validate_petition')) { + $petition = petitions_data_get_petition($petition_id); + + // Make sure the petition exists. + if (empty($petition)) { + petitionslog_event("signatures_queue.workflow.receive_new_signatures.api_key.{$api_key}.not_found"); + petitionslog_event("signatures_queue.workflow.receive_new_signatures.petition_id.{$petition_id}.not_found"); + return SIGNATURES_QUEUE_STATUS_NOT_FOUND; + } + + // Make sure the petition is "signable". + if (!$petition['isSignable']) { + petitionslog_event("signatures_queue.workflow.receive_new_signatures.api_key.{$api_key}.forbidden"); + petitionslog_event("signatures_queue.workflow.receive_new_signatures.petition_id.{$petition_id}.forbidden"); + return SIGNATURES_QUEUE_STATUS_FORBIDDEN; + } } // Input is all valid. Build the queue item. @@ -89,6 +91,7 @@ function _signatures_queue_receive_new_signatures($job_id, $server_name, $worker return SIGNATURES_QUEUE_STATUS_OK; } catch (Exception $e) { + petitionslog_event('exceptions.signatures_queue.3a67b6f'); throw $e; } } @@ -151,7 +154,7 @@ function _signatures_queue_build_new_queue_item(array $signature) { $item = array_merge($expected_fields, $signature_trimmed); // Add application data to the item. - $petition = petitions_data_mongo2mysql_get_petition($signature['petition_id']); + $petition = petitions_data_get_petition($signature['petition_id']); $validation_grace_period = (int) variable_get('signatures_queue_validation_grace_period', 2); $item['timestamp_received_new_signature'] = time(); $item['timestamp_petition_close'] = $petition['deadline']; @@ -195,7 +198,6 @@ function _signatures_queue_enqueue_item(array $item) { * _signatures_queue_build_new_queue_item(). */ function _signatures_queue_log_signatory_details($api_key, $petition_id, array $signature) { - $domain = petitionslog_filter_metric(signatures_queue_get_domain_from_email($signature['email'])); $is_disposable = signatures_queue_is_disposable_email($signature['email']) ? 'true' : 'false'; $is_subaddressed = signatures_queue_is_subaddressed_email($signature['email']) ? 'true' : 'false'; @@ -206,7 +208,6 @@ function _signatures_queue_log_signatory_details($api_key, $petition_id, array $ "signatures_queue.workflow.receive_new_signatures.petition_id.{$petition_id}", ); foreach ($metric_prefixes as $prefix) { - petitionslog_event("{$prefix}.email_address.domain.{$domain}"); petitionslog_event("{$prefix}.email_address.is_disposable.{$is_disposable}"); petitionslog_event("{$prefix}.email_address.is_subaddressed.{$is_subaddressed}"); } diff --git a/modules/custom/petitions_api/signatures_queue/includes/receive_signature_validation.inc b/modules/custom/petitions_api/signatures_queue/includes/receive_signature_validation.inc index 259fd5f58..350b22153 100644 --- a/modules/custom/petitions_api/signatures_queue/includes/receive_signature_validation.inc +++ b/modules/custom/petitions_api/signatures_queue/includes/receive_signature_validation.inc @@ -69,10 +69,6 @@ function _signatures_queue_receive_signature_validation($job_id, $server_name, $ // Construct the validated signature data array for validations_queue. $signature_data = _signatures_queue_validated_signature_data($options['secret_validation_key'], $options['petition_id']); - // Log time elapsed since passing through previous workflow. - $elapsed_since_initiating_signature_validation = signatures_queue_get_microtime_elapsed_since($signature_data['timestamp_initiated_signature_validation']); - petitionslog_event('signatures_queue.data_flow.time_elapsed.initiated_signature_validation__to__received_signature_validation', 'time', $elapsed_since_initiating_signature_validation); - // Queue validated signatures for next step. $queue = SignaturesQueue::get('validations_queue'); $queue->createQueue(); @@ -86,7 +82,7 @@ function _signatures_queue_receive_signature_validation($job_id, $server_name, $ } // Debugging. Log options and signature data. - if ($debug = variable_get('signatures_queue_enable_debugging', FALSE)) { + if ($debug = variable_get('signatures_queue_debug', FALSE)) { watchdog('signatures_queue', 'DEBUG ENABLED: Options passed to receive_signature_validation workflow: !options', array( '!options' => petitionslog_format_for_watchdog($options), ), WATCHDOG_DEBUG); diff --git a/modules/custom/petitions_api/signatures_queue/signatures_queue.admin.inc b/modules/custom/petitions_api/signatures_queue/signatures_queue.admin.inc index 2433b7547..4763c282c 100644 --- a/modules/custom/petitions_api/signatures_queue/signatures_queue.admin.inc +++ b/modules/custom/petitions_api/signatures_queue/signatures_queue.admin.inc @@ -20,11 +20,11 @@ function signatures_queue_configure() { '#collapsible' => TRUE, '#collapsed' => FALSE, ); - $form['all']['signatures_queue_enable_debugging'] = array( + $form['all']['signatures_queue_debug'] = array( '#type' => 'checkbox', '#title' => 'Enable debugging', '#description' => t('Enable debugging mode. This does things like increase the amount of details logged via watchdog throughout the signature queue pipeline.'), - '#default_value' => variable_get('signatures_queue_enable_debugging', FALSE), + '#default_value' => variable_get('signatures_queue_debug', FALSE), ); $form['all']['signatures_queue_notify_email'] = array( '#type' => 'textfield', diff --git a/modules/custom/petitions_api/signatures_queue/signatures_queue.install b/modules/custom/petitions_api/signatures_queue/signatures_queue.install index 6fe89c8e5..81a4b4e00 100644 --- a/modules/custom/petitions_api/signatures_queue/signatures_queue.install +++ b/modules/custom/petitions_api/signatures_queue/signatures_queue.install @@ -41,6 +41,11 @@ function signatures_queue_install() { drupal_set_message($t('IMPORTANT! Signature archive tables created in default database. This is not recommended for production installations. See INSTALL.md for more information.'), 'warning'); } + // Set some default permissions. + $perms = array('validate petition signatures'); + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, $perms); + user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, $perms); + // Warn users about two week limit on archiving. drupal_set_message($t('IMPORTANT! The archiving workflow assumes you are processing signatures regularly. The default assumption is that all queues will be cleared within any given two week period. If you intend to NOT process signatures regularly, make the necessary adjustments to $queues_last_emptied in includes/archive_signatures.inc.'), 'warning'); } @@ -503,3 +508,12 @@ function signatures_queue_update_7300() { db_change_field($table, $old_name, $new_name, $signatures_queue_schema['signature_validations']['fields'][$new_name]); } } + +/** + * Set default for new permission. + */ +function signatures_queue_update_7301() { + $perms = array('validate petition signatures'); + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, $perms); + user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, $perms); +} diff --git a/modules/custom/petitions_api/signatures_queue/signatures_queue.module b/modules/custom/petitions_api/signatures_queue/signatures_queue.module index 47a56bd47..444e20435 100644 --- a/modules/custom/petitions_api/signatures_queue/signatures_queue.module +++ b/modules/custom/petitions_api/signatures_queue/signatures_queue.module @@ -83,14 +83,14 @@ function signatures_queue_menu() { $items['thank-you'] = array( 'title' => 'Thank You', 'page callback' => '_signatures_queue_validation_page_callback', - 'access arguments' => array('access content'), + 'access arguments' => array('validate petition signatures'), 'file' => 'signatures_queue.pages.inc', ); $items['thank-you/%'] = array( 'title' => 'Thank You', 'page callback' => '_signatures_queue_validated_page_callback', 'page arguments' => array(1), - 'access arguments' => array('access content'), + 'access arguments' => array('validate petition signatures'), 'file' => 'signatures_queue.pages.inc', ); $items['validation-error'] = array( @@ -120,6 +120,10 @@ function signatures_queue_permission() { 'monitor queues' => array( 'title' => t('Monitor signatures queue'), ), + 'validate petition signatures' => array( + 'title' => t('Validate petition signatures'), + 'description' => t('Validate petition signatures by visiting their email verification links.'), + ), ); } @@ -246,12 +250,12 @@ function signatures_queue_validation_email_tokens(array &$replacements, array $d $replacements['[signature:validation-url]'] = _signatures_queue_validation_link($website_url, $data['signature info']); // Load the petition via the API retrieve method. $petition = PetitionsController::load($petition_id); - if (!empty($petition[0])) { + if (!empty($petition)) { // Tokens for values associated to the petition. - $replacements['[petition:title]'] = $petition[0]['title']; - $replacements['[petition:body]'] = $petition[0]['body']; - $replacements['[petition:url]'] = $petition[0]['url']; - $replacements['[petition:deadline]'] = date('l, F j, Y', $petition[0]['deadline']); + $replacements['[petition:title]'] = $petition['title']; + $replacements['[petition:body]'] = $petition['body']; + $replacements['[petition:url]'] = $petition['url']; + $replacements['[petition:deadline]'] = date('l, F j, Y', $petition['deadline']); } } } @@ -271,6 +275,9 @@ function signatures_queue_shunt() { )); $shunts[$name] = $description; } + + $shunts['signatures_queue_receive_new_signatures_validate_petition'] = t('Prevents the "receive new signatures" workflow from validating the requested petition ID, so that the workflow performs no database queries.'); + return $shunts; } @@ -545,6 +552,10 @@ function signatures_queue_set_db($conf_name = 'signatures_processing') { // Set database so that we create the tables there. db_set_active($conf_name); } + // If the database is not defined, set back to the default db. + elseif (empty($databases[$conf_name])) { + db_set_active(); + } } /** diff --git a/modules/custom/petitions_api/signatures_queue/signatures_queue.pages.inc b/modules/custom/petitions_api/signatures_queue/signatures_queue.pages.inc index 35257ebf3..d2d85230e 100644 --- a/modules/custom/petitions_api/signatures_queue/signatures_queue.pages.inc +++ b/modules/custom/petitions_api/signatures_queue/signatures_queue.pages.inc @@ -91,25 +91,27 @@ function _signatures_queue_validation_page_callback() { * Callback for thank-you/%. */ function _signatures_queue_validated_page_callback($petition_id) { - $conn = wh_petitions_mongo_petition_connection(); - $petition = wh_petitions_load_petition($conn, $petition_id); + + $petition = PetitionsController::loadObject($petition_id); if (!empty($petition)) { - $url = (!empty($petition['short_url'])) ? $petition['short_url'] : $petition['nice_url']; + $short_url = $petition->getShortUrl(); + $nice_url = $petition->getNiceUrl(); + $url = (!empty($short_url)) ? $short_url : $nice_url; $plain_petition_id = check_plain($petition_id); - $plain_petition_title = check_plain($petition['title']); + $plain_petition_title = check_plain($petition->getTitle()); return theme('signatures_queue_validation_thank_you', array( 'petition' => $petition, 'page_title' => t('Thank you for signing this petition!'), 'message' => t('
You\'ve successfully signed the petition "!title". Your signature has been verified and counted.
Take the next step and gather support for it by sharing this petition on Twitter and Facebook, or by sharing a link to this petition: @petition_link
', array( '@petition_link' => url($url, array('absolute' => TRUE)), - '!title' => drupal_ucfirst(check_plain($petition['title'])), + '!title' => drupal_ucfirst($plain_petition_title), )), 'email_link' => wh_petition_tool_email_link(t('E-mail'), t('A We the People Petition'), t('I wanted to share this We the People petition with you: @petition_link', array( '@petition_link' => url($url, array('absolute' => TRUE)), ))), - 'twitter_link' => wh_petition_tool_twitter_link($plain_petition_id, $plain_petition_title, 'petition', '', $petition['short_url'], $petition['nice_url']), - 'facebook_link' => wh_petition_tool_facebook_link($plain_petition_id, $plain_petition_title, 'petition', '', $petition['nice_url']), + 'twitter_link' => wh_petition_tool_twitter_link($plain_petition_id, $plain_petition_title, 'petition', '', $short_url, $nice_url), + 'facebook_link' => wh_petition_tool_facebook_link($plain_petition_id, $plain_petition_title, 'petition', '', $nice_url), )); } else { diff --git a/modules/custom/petitions_data/classes/Petition.inc b/modules/custom/petitions_data/classes/Petition.inc deleted file mode 100644 index 0d44da716..000000000 --- a/modules/custom/petitions_data/classes/Petition.inc +++ /dev/null @@ -1,699 +0,0 @@ -body. - * - * @param string $body - * The petition body. - * - * @return Petition - * Returns current instance of object. - */ - public function setBody($body) { - $this->body = $body; - - return $this; - } - - /** - * Get $this->body. - * - * @return string - * The petition body. - */ - public function getBody() { - return petitions_data_sanitize_output($this->body); - } - - /** - * Sets $this->created. - * - * @param int $created - * The UNIX timestamp when this petition was created. - * - * @return Petition - * Returns current instance of object. - */ - public function setCreated($created) { - $this->created = $created; - - return $this; - } - - /** - * Gets $this->created. - * - * @return int - * The UNIX timestamp when this petition was created. - */ - public function getCreated() { - return $this->created; - } - - /** - * Sets $this->deadline. - * - * @return Petition - * Returns current instance of object. - */ - protected function setDeadline() { - $this->deadline = $this->calcDeadline($this->getPublished(), $this->getReviewTimeframe()); - - return $this; - } - - /** - * Gets $this->deadline. - * - * @return int - * The UNIX timestamp when this petition no longer accepts signatures. - */ - public function getDeadline() { - if (!$this->deadline) { - $this->setDeadline(); - } - - return $this->deadline; - } - - /** - * Sets $this->id. - * - * @param string $id - * The petition id. - * - * @return Petition - * Returns current instance of object. - */ - public function setId($id) { - $this->id = $id; - - return $this; - } - - /** - * Gets $this->id. - * - * @return string - * The petition id. - */ - public function getId() { - return $this->id; - } - - /** - * Sets $this->issues. - * - * @param array $issues - * An array of issues, containing the following keys: - * - id: The issue id. - * - name: The issue name. - * - * @return Petition - * Returns current instance of object. - */ - public function setIssues(array $issues) { - $this->issues = $issues; - - return $this; - } - - /** - * Gets $this->issues. - * - * @return array - * An associative array of issues. - */ - public function getIssues() { - return $this->issues; - } - - /** - * Sets $this->published. - * - * @param int $published - * The UNIX timestamp when this petition was published. - * - * @return Petition - * Returns current instance of object. - */ - public function setPublished($published) { - $this->published = $published; - - return $this; - } - - /** - * Gets $this->published. - * - * @return int - * The UNIX timestamp when this petition was published. - */ - public function getPublished() { - return $this->published; - } - - /** - * Sets $this->reviewTimeframe. - * - * @param int $review_timeframe - * The length of time in seconds between when a petition reaches threshold - * and when a response must be posted (business rule). - * - * @return Petition - * Returns current instance of object. - */ - public function setReviewTimeframe($review_timeframe) { - $this->reviewTimeframe = $review_timeframe; - - return $this; - } - - /** - * Gets $this->reviewTimeframe. - * - * @return int - * The petition review timeframe in seconds. - */ - public function getReviewTimeframe() { - return $this->reviewTimeframe; - } - - /** - * Sets $this->signaturePublicThreshold. - * - * @param int $signature_public_threshold - * The number of signatures required for this petition to become public. - * - * @return Petition - * Returns current instance of object. - */ - public function setSignaturePublicThreshold($signature_public_threshold) { - $this->signaturePublicThreshold = $signature_public_threshold; - - return $this; - } - - /** - * Gets $this->signaturePublicThreshold. - * - * @return int - * The number of signatures required for this petition to become public. - */ - public function getSignaturePublicThreshold() { - return $this->signaturePublicThreshold; - } - - /** - * Sets $this->signatureThreshold. - * - * @param int $signature_threshold - * The number of signatures required for this petition to get a response. - * - * @return Petition - * Returns current instance of object. - */ - public function setSignatureThreshold($signature_threshold) { - $this->signatureThreshold = $signature_threshold; - - return $this; - } - - /** - * Gets $this->signatureThreshold. - * - * @return int - * The number of signatures required for this petition to get a response. - */ - public function getSignatureThreshold() { - return $this->signatureThreshold; - } - - /** - * Sets $this->signatureCount. - * - * @param int $signature_count - * The nubmer of signatures submitted to this petition. - * - * @return Petition - * Returns current instance of object. - */ - public function setSignatureCount($signature_count) { - $this->signatureCount = $signature_count; - - return $this; - } - - /** - * Gets $this->signatureCount. - * - * @return int - * The nubmer of signatures submitted to this petition. - */ - public function getSignatureCount() { - return $this->signatureCount; - } - - /** - * Sets $this->signaturesNeeded. - * - * @return Petition - * Returns current instance of object. - */ - protected function setSignaturesNeeded() { - $this->signaturesNeeded = $this->calcSignaturesNeeded($this->getSignatureThreshold(), $this->getSignatureCount()); - - return $this; - } - - /** - * Gets $this->signaturesNeeded. - * - * @return int - * The number of signatures needed for this petition to get a response. - */ - public function getSignaturesNeeded() { - if (!isset($this->signaturesNeeded)) { - $this->setSignaturesNeeded(); - } - return $this->signaturesNeeded; - } - - /** - * Sets $this->status. - * - * @param int $status - * The integer representing petition status. Should use constants defined - * in wh_petition module. - * - * @return Petition - * Returns current instance of object. - */ - public function setStatus($status) { - $this->status = $status; - - return $this; - } - - /** - * Gets $this->status. - * - * @return int - * The petition status. - */ - public function getStatus() { - return $this->status; - } - - /** - * Helper function to translate status codes into readable string formats. - * - * @param int $status_code - * Status code as stored. - * - * @return string - * String representing status code. - */ - static public function formatStatus($status_code) { - switch ($status_code) { - // Published, but does not have the required number of signatures to - // become public, only visible by direct URL. - case WH_PETITION_STATUS_PRIVATE: - // Has the required number of signatures to become public, - // visible everywhere on site. - case WH_PETITION_STATUS_PUBLIC: - return "open"; - - // Has received the required number of signatures, visible on site. - case WH_PETITION_STATUS_UNDER_REVIEW: - return "pending response"; - - // Has received a response. - case WH_PETITION_STATUS_REVIEWED: - return "responded"; - - // Has been removed by administrative action. - case WH_PETITION_STATUS_FLAGGED: - return "removed"; - - default: - return "closed"; - } - } - - /** - * Sets $this->title. - * - * @param string $title - * The petition title. - * - * @return Petition - * Returns current instance of object. - */ - public function setTitle($title) { - $this->title = $title; - - return $this; - } - - /** - * Gets $this->title. - * - * @return string - * The petition title. - */ - public function getTitle() { - return petitions_data_sanitize_output($this->title); - } - - /** - * Sets $this->niceUrl. - * - * @param string $url - * The petition URL. - * - * @return Petition - * Returns current instance of object. - */ - public function setNiceUrl($url) { - $this->niceUrl = $url; - - return $this; - } - - /** - * Gets $this->niceUrl. - * - * @return string - * The petition URL. - */ - public function getNiceUrl() { - global $base_url; - $website_url = rtrim(variable_get('petitions_data_petitions_website_url', $base_url), '/'); - - return url(ltrim($this->niceUrl, '/'), array('absolute' => TRUE, 'base_url' => $website_url)); - } - - /** - * Sets $this->shortUrl. - * - * @param string $url - * The petition URL. - * - * @return Petition - * Returns current instance of object. - */ - public function setShortUrl($url) { - $this->shortUrl = $url; - - return $this; - } - - /** - * Gets $this->shortUrl. - * - * @return string - * The petition URL. - */ - public function shortUrl() { - return $this->shortUrl; - } - - /** - * Sets $this->reachedPublic. - * - * @param int $reached_public - * A UNIX timestamp representing when this petition gained public status. - * - * @return Petition - * Returns current instance of object. - */ - public function setReachedPublic($reached_public) { - $this->reachedPublic = $reached_public; - - return $this; - } - - /** - * Gets $this->reachedPublic. - * - * @return int - * A UNIX timestamp representing when this petition gained public status. - */ - public function getReachedPublic() { - return $this->reachedPublic; - } - - /** - * Sets $this->response. - * - * @param object $response - * The response to the petition. - * - * @return Petition - * Returns current instance of object. - */ - public function setResponse($response) { - $this->response = $response; - - return $this; - } - - /** - * Gets $this->response. - * - * @return object - * The response to the petition. - */ - public function getResponse() { - return $this->response; - } - - /** - * Sets $this->responseStatus. - * - * @param object $response_status - * Indicates whether this petition has a response. - * - * @return Petition - * Returns current instance of object. - */ - public function setResponseStatus($response_status) { - $this->responseStatus = $response_status; - - return $this; - } - - /** - * Gets $this->responseStatus. - * - * @return bool - * The whether the petition has a response. - */ - public function getResponseStatus() { - return $this->responseStatus; - } - - /** - * Sets $this->uid. - * - * @param int $uid - * The user id of the signatory. - * - * @return Petition - * Returns current instance of object. - */ - public function setUid($uid) { - $this->uid = $uid; - - return $this; - } - - /** - * Gets $this->uid. - * - * @return int - * The user id of the signatory. - */ - public function getUid() { - return $this->uid; - } - - /** - * Sets $this->userTags. - * - * @param array $user_tags - * An array of tags for this petition, created by petition creator. - * - * @return Petition - * Returns current instance of object. - */ - public function setUserTags(array $user_tags) { - $this->userTags = $user_tags; - - return $this; - } - - /** - * Gets $this->userTags. - * - * @return array - * An array of tags for this petition. - */ - public function getUserTags() { - return $this->userTags; - } - - - /** - * Helper function to calculate needed signatures. - * - * @param int $threshold - * Signatures required to solicit a response. - * - * @param int $current - * Current number of signatures. - * - * @return int - * Signatures remaining to required to reach response threshold. - */ - public static function calcSignaturesNeeded($threshold, $current) { - return ($current >= $threshold) ? 0 : $threshold - $current; - } - - /** - * Helper function to calculate deadline. - * - * @param int $published - * Epoch (UNIX style) time stamp. - * - * @param int $days - * Number of days from publication that the Petition will remain open. - * - * @return int - * Epoch (UNIX style) time stamp. - */ - public static function calcDeadline($published, $days) { - return strtotime("+{$days} days", $published); - } - - /** - * Determine if petition is signable. - * - * Confirm petition has not passed dealine for accepting signatures. If it - * hasn't, petitions considered signable will be of one of these statuses: - * - WH_PETITION_STATUS_PRIVATE: Is signable but only visible by direct link. - * - WH_PETITION_STATUS_PUBLIC: Is public and signable. - * - WH_PETITION_STATUS_UNDER_REVIEW: Has reached enough signatures for a - * response, but has not been responded to, so can get more signatures. - * - * @return bool - * TRUE if petition is signable, FALSE if not. - */ - public function isSignable() { - if (!isset($this->status)) { - throw new Exception("Can't determine if petition is signable if petition status is not set."); - } - - // If the signature threshold has been passed and the petition has not - // received a response. - $signatures_passed_threshold = $this->getSignaturesNeeded() == 0; - $pending_response = $this->getStatus() == WH_PETITION_STATUS_UNDER_REVIEW; - if ($signatures_passed_threshold && $pending_response) { - return TRUE; - } - - // Has the petition passed its deadline for accepting signatures? - if ($this->deadline < time()) { - return FALSE; - } - - // A petition that has not passed its deadline is considered signable if its - // status can be found in the array returned by - // wh_petitions_signable_statuses(). - $array_signable_statuses = wh_petitions_signable_statuses(); - return (in_array($this->status, $array_signable_statuses)) ? TRUE : FALSE; - } - - - /** - * Determine if petition is public by looking at its numeric status. - * - * Petitions considered public will be of one of these statuses: - * - WH_PETITION_STATUS_PUBLIC: By definition this is public. - * - WH_PETITION_STATUS_UNDER_REVIEW: No action has been taken on this. - * - WH_PETITION_STATUS_REVIEWED: Has been reviewed and responded to. - * - WH_PETITION_STATUS_CLOSED: Is closed but should still be visible. - * - * @return bool - * TRUE if petition is public, FALSE if not. - */ - public function isPublic() { - if (!isset($this->status)) { - throw new Exception("Can't determine if petition is public if petition status is not set."); - } - // A petition is considered public if its status can be found in the array - // returned by wh_petitions_public_statuses(). - return (in_array($this->status, wh_petitions_public_statuses())) ? TRUE : FALSE; - } - - - /** - * Converts into a publicly consumable array. - * - * @return array - * An array to be used for public display. - */ - public function toArray() { - $output = array( - 'id' => $this->getId(), - 'type' => 'petition', - 'title' => $this->getTitle(), - 'body' => $this->getBody(), - 'issues' => $this->getIssues(), - 'signatureThreshold' => $this->getSignatureThreshold(), - 'signatureCount' => $this->getSignatureCount(), - 'signaturesNeeded' => $this->getSignaturesNeeded(), - 'url' => $this->getNiceUrl(), - 'deadline' => $this->getDeadline(), - 'status' => $this->formatStatus($this->getStatus()), - 'response' => $this->getResponse(), - 'created' => $this->getPublished(), - 'isSignable' => $this->isSignable(), - 'isPublic' => $this->isPublic(), - ); - - return $output; - } -} diff --git a/modules/custom/petitions_data/classes/PetitionItem.inc b/modules/custom/petitions_data/classes/PetitionItem.inc new file mode 100644 index 000000000..73ee80b8c --- /dev/null +++ b/modules/custom/petitions_data/classes/PetitionItem.inc @@ -0,0 +1,1226 @@ +abuseFlags = $uids; + + return $this; + } + + /** + * Get abuse flags for the petition. + * + * @return int[] + * An array of UIDs of users who have flagged the petition as abusive. + */ + public function getAbuseFlags() { + return (array) $this->abuseFlags; + } + + /** + * Sets the petition body. + * + * @param string $body + * The petition body. + * + * @return $this + */ + public function setBody($body) { + $this->body = $body; + + return $this; + } + + /** + * Gets the petition body. + * + * @return string + * The petition body. + */ + public function getBody() { + return $this->body; + } + + /** + * Gets an array of keywords from the petition body. + * + * @return string[] + * An indexed array of keywords. + */ + public function getBodyKeywords() { + return wh_petitions_generate_keywords($this->getBody()); + } + + /** + * Sets the users who have bookmarked the petition. + * + * @param int[] $uids + * An array of UIDs of users who have bookmarked the petition. + * + * @return $this + */ + public function setBookmarked($uids) { + $this->bookmarked = $uids; + + return $this; + } + + /** + * Gets the users who have bookmarked the petition. + * + * @return int[] + * An array of UIDs of users who have bookmarked the petition. + */ + public function getBookmarked() { + return (array) $this->bookmarked; + } + + /** + * Sets the time the petition was closed. + * + * @param int $timestamp + * The UNIX timestamp when this petition was closed. + * + * @return $this + */ + public function setClosed($timestamp) { + $this->closed = $timestamp; + + return $this; + } + + /** + * Gets the time the petition was closed. + * + * @return int + * The UNIX timestamp when this petition was closed. + */ + public function getClosed() { + return $this->closed; + } + + /** + * Sets the time the petition was created. + * + * See PetitionItem::getCreated() for a note on the "created" property. + * + * @param int $timestamp + * The UNIX timestamp when this petition was created. + * + * @return $this + * + * @see PetitionItem::getCreated() + */ + public function setCreated($timestamp) { + $this->created = $timestamp; + + return $this; + } + + /** + * Gets the time the petition was created. + * + * Note: PetitionItem::toRestResponseItemArray() does NOT use this method to + * get the "created" property--it overwrites it with the "published" property + * for reasons explained in that method. The value set with this method, + * therefore, will never surface in parts of the application that rely on the + * array transformation--only those that individually invoke this method + * directly. + * + * @todo On account of the above note, this method should be deprecated or + * just become a wrapper around PetitionItem::getPublished() as soon as the + * implications of such a change are better understood. + * + * @return int + * The UNIX timestamp when this petition was created. + * + * @see PetitionItem::toRestResponseItemArray() + */ + public function getCreated() { + return (int) $this->created; + } + + /** + * Gets the deadline for accepting signatures for the petition. + * + * @return int + * The UNIX timestamp when this petition no longer accepts signatures. + */ + public function getDeadline() { + return $this->calcDeadline($this->getPublished(), $this->getReviewTimeframe()); + } + + /** + * Sets the featured flag on the petition. + * + * @param int $flag + * The "featured" flag value for the petition--1 for featured or 0 for not. + * + * @return $this + */ + public function setFeatured($flag) { + $this->featured = (int) $flag; + + return $this; + } + + /** + * Gets the featured flag on the petition. + * + * @return int + * The "featured" flag value for the petition--1 for featured or 0 for not. + */ + public function getFeatured() { + return (int) $this->featured; + } + + /** + * Gets a unique identifier for the petition. + * + * Gets one of the unique identifiers for the petition--either the legacy + * (Mongo) ID or the entity (node) ID--according to preference and + * availability. + * + * @param bool $prefer_legacy_id + * TRUE to prefer the legacy (Mongo) ID or FALSE to prefer the entity (node) + * ID. Defaults to TRUE for backward compatibility. + * + * @return string|int|null + * Returns a legacy (Mongo) petition ID or an entity (node) ID, or NULL if + * no ID has been set at all. + */ + public function getId($prefer_legacy_id = TRUE) { + $preferred_id = ($prefer_legacy_id) ? $this->getLegacyId() : $this->getEntityId(); + $fallback_id = ($prefer_legacy_id) ? $this->getEntityId() : $this->getLegacyId(); + return ($preferred_id) ? $preferred_id : $fallback_id; + } + + /** + * Sets the legacy ID of the petition. + * + * @param string $legacy_id + * The legacy (MongoDB) ID. + * + * @return $this + */ + public function setLegacyId($legacy_id) { + if (!empty($legacy_id)) { + $this->legacyId = $legacy_id; + } + + return $this; + } + + /** + * Gets the legacy ID of the petition. + * + * @return string|null + * Returns a legacy (MongoDB) ID if set or NULL if not. + */ + public function getLegacyId() { + return $this->legacyId; + } + + /** + * Sets the legacy path of the petition. + * + * @param string $path + * The legacy (MongoDB) path, without a leading forward slash (/). + * + * @return $this + */ + public function setLegacyPath($path) { + $this->legacyPath = $path; + + return $this; + } + + /** + * Gets the legacy path of the petition. + * + * @return string|null + * The legacy (MongoDB) path, without a leading forward slash (/), if + * available, of NULL if not. + */ + public function getLegacyPath() { + return $this->legacyPath; + } + + /** + * Sets the entity (node) ID. + * + * @param int $id + * The entity ID. + * + * @return $this + */ + public function setEntityId($id) { + $this->entityId = $id; + + return $this; + } + + /** + * Gets the petition's entity ID. + * + * @return int + * Returns the petition's entity (node) ID. + */ + public function getEntityId() { + return $this->entityId; + } + + /** + * Sets the issues the petition is tagged with. + * + * @param array[] $issues + * An array of issue arrays, containing the following key/value pairs: + * - id: The issue id. + * - name: The issue name. + * + * @return $this + */ + public function setIssues($issues) { + $this->issues = $issues; + + return $this; + } + + /** + * Sets the issues the petition is tagged with. + * + * @return array[] + * An array of issue arrays, containing the following key/value pairs: + * - id: The issue id. + * - name: The issue name. + */ + public function getIssues() { + return $this->issues; + } + + /** + * Sets the time the petition was published. + * + * @param int $timestamp + * The UNIX timestamp when this petition was published. + * + * @return $this + */ + public function setPublished($timestamp) { + $this->published = $timestamp; + + return $this; + } + + /** + * Sets the private tags for the petition. + * + * @param string[] $tags + * An indexed array of tags for the petition, set by an administrator. + * + * @return $this + */ + public function setPrivateTags($tags) { + $this->privateTags = $tags; + + return $this; + } + + /** + * Gets the private tags for the petition. + * + * @return string[] + * An indexed array of tags for the petition, set by an administrator. + */ + public function getPrivateTags() { + return (array) $this->privateTags; + } + + /** + * Gets the published date for the petition. + * + * @return int + * The UNIX timestamp when this petition was published. + */ + public function getPublished() { + return $this->published; + } + + /** + * Sets the time the review threshold email for the petition was sent. + * + * @param int $timestamp + * The UNIX timestamp when the review threshold email for this petition was + * sent. + * + * @return $this + */ + public function setReviewThresholdMailSent($timestamp) { + $this->reviewThresholdMailSent = $timestamp; + + return $this; + } + + /** + * Gets the time the review threshold email for the petition was sent. + * + * @return int + * The UNIX timestamp when the review threshold email for this petition was + * sent. + */ + public function getReviewThresholdMailSent() { + return $this->reviewThresholdMailSent; + } + + /** + * Sets the review timeframe for the petition. + * + * @param int $days + * The length of time in days between when a petition reaches threshold + * and when a response must be posted (business rule). + * + * @return $this + */ + public function setReviewTimeframe($days) { + $this->reviewTimeframe = $days; + + return $this; + } + + /** + * Gets the review timeframe for the petition. + * + * @return int + * The petition review timeframe in days. + */ + public function getReviewTimeframe() { + return $this->reviewTimeframe; + } + + /** + * Sets the signature threshold for making the petition public. + * + * @param int $number + * The number of signatures required for this petition to become public. + * + * @return $this + */ + public function setSignaturePublicThreshold($number) { + $this->signaturePublicThreshold = $number; + + return $this; + } + + /** + * Gets the signature threshold for making the petition public. + * + * @return int + * The number of signatures required for this petition to become public. + */ + public function getSignaturePublicThreshold() { + return $this->signaturePublicThreshold; + } + + /** + * Sets the signature threshold for getting a response. + * + * @param int $number + * The number of signatures required for this petition to get a response. + * + * @return $this + */ + public function setSignatureThreshold($number) { + $this->signatureThreshold = $number; + + return $this; + } + + /** + * Gets the signature threshold for getting a response. + * + * @return int + * The number of signatures required for this petition to get a response. + */ + public function getSignatureThreshold() { + return $this->signatureThreshold; + } + + /** + * Sets the current signature count on the petition. + * + * @param int $number + * The number of signatures submitted to this petition. + * + * @return $this + */ + public function setSignatureCount($number) { + $this->signatureCount = $number; + + return $this; + } + + /** + * Gets the signature count on the petition. + * + * @return int + * The number of signatures submitted to this petition. + */ + public function getSignatureCount() { + return $this->signatureCount; + } + + /** + * Gets the number of signatures needed to reach the response threshold. + * + * @return int + * The number of signatures needed for this petition to get a response. + */ + public function getSignaturesNeeded() { + return $this->calcSignaturesNeeded($this->getSignatureThreshold(), $this->getSignatureCount()); + } + + /** + * Sets the petition status. + * + * @param int $status + * An integer representing the petition status. See + * PetitionItem::formatStatus() for possible values. + * + * @return $this + * + * @see PetitionItem::formatStatus() + */ + public function setStatus($status) { + $this->status = $status; + + return $this; + } + + /** + * Gets the petition status. + * + * @return int + * An integer representing the petition status. See + * PetitionItem::formatStatus() for possible values. + * + * @see PetitionItem::formatStatus() + */ + public function getStatus() { + return (int) $this->status; + } + + /** + * Translates petition status codes into readable string formats. + * + * @param int $status_code + * Status code as stored. + * + * @return string + * String representing status code. + */ + static public function formatStatus($status_code) { + switch ($status_code) { + // Published, but does not have the required number of signatures to + // become public, only visible by direct URL. + case WH_PETITION_STATUS_PRIVATE: + // Has the required number of signatures to become public, + // visible everywhere on site. + case WH_PETITION_STATUS_PUBLIC: + return "open"; + + // Has received the required number of signatures, visible on site. + case WH_PETITION_STATUS_UNDER_REVIEW: + return "pending response"; + + // Has received a response. + case WH_PETITION_STATUS_REVIEWED: + return "responded"; + + // Has been removed by administrative action. + case WH_PETITION_STATUS_FLAGGED: + return "removed"; + + default: + return "closed"; + } + } + + /** + * Sets the petition title. + * + * @param string $title + * The petition title. + * + * @return $this + */ + public function setTitle($title) { + $this->title = $title; + + return $this; + } + + /** + * Gets the petition title. + * + * @return string + * The petition title. + */ + public function getTitle() { + return $this->title; + } + + /** + * Gets an array of keywords from the petition title. + * + * @return string[] + * An indexed array of keywords. + */ + public function getTitleKeywords() { + return wh_petitions_generate_keywords($this->getTitle()); + } + + /** + * Sets the "nice URL" of the petition. + * + * The "nice URL" is a legacy concept from the Mongo implementation that + * basically corresponds to a path alias in Drupal terms. It is the path + * component of the petition URL, without the leading forward slash (/). + * + * @param string $path + * The petition's nice URL path. + * + * @return $this + */ + public function setNiceUrl($path) { + $this->niceUrl = $path; + + return $this; + } + + /** + * Gets the "nice URL" of the petition. + * + * See PetitionItem::setNiceUrl() for a description of the "nice URL" concept. + * + * @param bool $absolute + * Whether to return an absolute link (beginning with http:). Useful for + * links that will be displayed outside the site, such as in an API response + * or email. If FALSE, returns a path fragment without the leading forward + * slash (/). Defaults to TRUE. + * + * @return string + * The petition's URL if available, or an empty string if not. + * + * @see PetitionItem::setNiceUrl() + */ + public function getNiceUrl($absolute = TRUE) { + if (!$this->niceUrl) { + return ''; + } + + return petitions_data_url($this->niceUrl, $absolute); + } + + /** + * Sets the short URL for the petition. + * + * @param string $url + * A full URL from a URL shortener service, pointing to the petition. + * + * @return $this + */ + public function setShortUrl($url) { + $this->shortUrl = $url; + + return $this; + } + + /** + * Sets the short URL for the petition. + * + * @return string + * A full URL from a URL shortener service, pointing to the petition. + */ + public function getShortUrl() { + return $this->shortUrl; + } + + /** + * Sets the time the petition reached public status. + * + * @param int $timestamp + * A UNIX timestamp representing when this petition gained public status. + * + * @return $this + */ + public function setReachedPublic($timestamp) { + $this->reachedPublic = $timestamp; + + return $this; + } + + /** + * Gets the time the petition reached public status. + * + * @return int + * A UNIX timestamp representing when this petition gained public status. + */ + public function getReachedPublic() { + return $this->reachedPublic; + } + + /** + * Sets the response details. + * + * @param array $response + * An array of response details as returned by + * PetitionsSelectQuery::formatReturnResponse(), with the follow key/value + * pairs: + * - id: The response node ID (nid). + * - url: The full URL of the response. + * - associationTime: The time the response was associated with the + * petition, in the form of a UNIX timestamp. + * + * @return $this + * + * @see PetitionsSelectQuery::formatReturnResponse() + */ + public function setResponse($response) { + $this->response = $response; + + return $this; + } + + /** + * Sets the time when the petition reached response ready status. + * + * @param int $timestamp + * A UNIX timestamp representing when this petition crossed the signature + * response threshold. + * + * @return $this + */ + public function setReachedReady($timestamp) { + $this->reachedReady = $timestamp; + + return $this; + } + + /** + * Gets the time when the petition reached response ready status. + * + * @return int + * A UNIX timestamp representing when this petition crossed the signature + * response threshold. + */ + public function getReachedReady() { + return $this->reachedReady; + } + + /** + * Gets the response details. + * + * @return array + * An array of response details as returned by + * PetitionsSelectQuery::formatReturnResponse(), with the follow key/value + * pairs: + * - id: The response node ID (nid). + * - url: The full URL of the response. + * - associationTime: The time the response was associated with the + * petition, in the form of a UNIX timestamp. + * + * @see PetitionsSelectQuery::formatReturnResponse() + */ + public function getResponse() { + return $this->response; + } + + /** + * Gets the response ID for the petition. + * + * @return int|null + * The node ID (nid) of the response to the petition, if available, or NULL + * if not. + */ + public function getResponseId() { + $response = $this->getResponse(); + + $response_id = NULL; + if (!empty($response['id'])) { + $response_id = $response['id']; + } + + return $response_id; + } + + /** + * Sets the petition's response status. + * + * @param int $response_status + * The petition's response status. One of these values: + * - WH_PETITION_RESPONSE_STATUS_UNANSWERED: Unanswered. + * - WH_PETITION_RESPONSE_STATUS_PENDING: Awaiting action. + * - WH_PETITION_RESPONSE_STATUS_ANSWERED: Responded. + * + * @return $this + */ + public function setResponseStatus($response_status) { + $this->responseStatus = $response_status; + + return $this; + } + + /** + * Gets the petition's response status. + * + * @return int + * The petition's response status. One of these values: + * - WH_PETITION_RESPONSE_STATUS_UNANSWERED: Unanswered. + * - WH_PETITION_RESPONSE_STATUS_PENDING: Awaiting action. + * - WH_PETITION_RESPONSE_STATUS_ANSWERED: Responded. + */ + public function getResponseStatus() { + return $this->responseStatus; + } + + /** + * Sets user UID of the petition creator. + * + * @param int $uid + * The user UID of the petition creator. + * + * @return $this + */ + public function setUid($uid) { + $this->uid = $uid; + + return $this; + } + + /** + * Sets user UID of the petition creator. + * + * @return int + * The user UID of the petition creator. + */ + public function getUid() { + return $this->uid; + } + + /** + * Sets the user tags on the petition. + * + * @param string[] $tags + * An array of tags for this petition, created by petition creator. + * + * @return $this + */ + public function setUserTags($tags) { + $this->userTags = $tags; + + return $this; + } + + /** + * Gets the user tags on the petition. + * + * @return string[] + * An array of tags for this petition. + */ + public function getUserTags() { + return (array) $this->userTags; + } + + /** + * Calculates the number of needed signatures. + * + * @param int $threshold + * Signatures required to solicit a response. + * @param int $current + * Current number of signatures. + * + * @return int + * Signatures remaining to required to reach response threshold. + */ + public static function calcSignaturesNeeded($threshold, $current) { + return ($current >= $threshold) ? 0 : $threshold - $current; + } + + /** + * Calculate the deadline. + * + * @param int $published + * Epoch (UNIX style) time stamp. + * @param int $days + * Number of days from publication that the Petition will remain open. + * + * @return int + * Epoch (UNIX style) time stamp. + */ + public static function calcDeadline($published, $days) { + return strtotime("+{$days} days", $published); + } + + /** + * Determine if petition is signable. + * + * Confirm petition has not passed deadline for accepting signatures. If it + * hasn't, petitions considered signable will be of one of these statuses: + * - WH_PETITION_STATUS_PRIVATE: Is signable but only visible by direct link. + * - WH_PETITION_STATUS_PUBLIC: Is public and signable. + * - WH_PETITION_STATUS_UNDER_REVIEW: Has reached enough signatures for a + * response, but has not been responded to, so can get more signatures. + * + * @return bool + * TRUE if petition is signable, FALSE if not. + * + * @throws Exception + * Throws an exception if the petition status can't be determined. + */ + public function isSignable() { + if (!isset($this->status)) { + throw new Exception("Can't determine if petition is signable if petition status is not set."); + } + + // If the signature threshold has been passed and the petition has not + // received a response. + $signatures_passed_threshold = $this->getSignaturesNeeded() == 0; + $pending_response = $this->getStatus() == WH_PETITION_STATUS_UNDER_REVIEW; + if ($signatures_passed_threshold && $pending_response) { + return TRUE; + } + + // Has the petition passed its deadline for accepting signatures? + if ($this->getDeadline() < time()) { + return FALSE; + } + + // A petition that has not passed its deadline is considered signable if its + // status can be found in the array returned by + // wh_petitions_signable_statuses(). + $array_signable_statuses = wh_petitions_signable_statuses(); + return (in_array($this->status, $array_signable_statuses)) ? TRUE : FALSE; + } + + + /** + * Determine if petition is public by looking at its numeric status. + * + * Petitions considered public will be of one of these statuses: + * - WH_PETITION_STATUS_PUBLIC: By definition this is public. + * - WH_PETITION_STATUS_UNDER_REVIEW: No action has been taken on this. + * - WH_PETITION_STATUS_REVIEWED: Has been reviewed and responded to. + * - WH_PETITION_STATUS_CLOSED: Is closed but should still be visible. + * + * @return bool + * TRUE if petition is public, FALSE if not. + * + * @throws Exception + * Throws an exception if the petition status can't be determined. + */ + public function isPublic() { + if (!isset($this->status)) { + throw new Exception("Can't determine if petition is public if petition status is not set."); + } + // A petition is considered public if its status can be found in the array + // returned by wh_petitions_public_statuses(). + return (in_array($this->status, wh_petitions_public_statuses())) ? TRUE : FALSE; + } + + + /** + * Gets the petition in the form of a REST response item array. + * + * @return array + * An array as used by the REST API. + */ + public function toRestResponseItemArray() { + $output = array( + 'id' => $this->getId(), + 'type' => 'petition', + 'title' => petitions_data_sanitize_output($this->getTitle()), + 'body' => petitions_data_sanitize_output($this->getBody()), + 'issues' => $this->getIssues(), + 'signatureThreshold' => $this->getSignatureThreshold(), + 'signatureCount' => $this->getSignatureCount(), + 'signaturesNeeded' => $this->getSignaturesNeeded(), + 'url' => $this->getNiceUrl(), + 'deadline' => $this->getDeadline(), + 'status' => $this->formatStatus($this->getStatus()), + 'response' => $this->getResponse(), + + // The "created" timestamp was never meant to be publicly exposed. + // Stakeholders intended to expose when a petition was PUBLISHED, not + // when it was created. Simply return the published timestamp instead. + // @todo An array transformation is not the right place to implement this + // kind of business logic. Find a better place for it. + 'created' => $this->getPublished(), + + 'isSignable' => $this->isSignable(), + 'isPublic' => $this->isPublic(), + ); + + return $output; + } + + /** + * Gets the petition as an entity (node) object. + * + * It does not automatically save the result. + * + * @return object + * A node object created from the petition. + */ + public function toEntity() { + $abuse_flags = array(); + foreach ((array) $this->getAbuseFlags() as $uid) { + $abuse_flags[]['target_id'] = $uid; + } + + $issues = array(); + foreach ((array) $this->getIssues() as $issue) { + $issues[] = array('tid' => $issue['id']); + } + + $node_status = !in_array($this->getStatus(), wh_petitions_unpublished_statuses()); + + $values = array( + 'type' => 'petition', + 'nid' => $this->getEntityId(), + 'uid' => $this->getUid(), + 'title' => $this->getTitle(), + 'body' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getBody()))), + 'created' => $this->getCreated(), + 'status' => $node_status, + 'field_abuse_count' => array(LANGUAGE_NONE => array(0 => array('value' => count($abuse_flags)))), + 'field_abuse_flags' => array(LANGUAGE_NONE => $abuse_flags), + 'field_legacy_id' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getLegacyId()))), + 'field_petition_issues' => array(LANGUAGE_NONE => $issues), + 'field_petition_public_signatures' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getSignaturePublicThreshold()))), + // 'field_petition_related_petitions' corresponds to an unused Mongo field + // and is therefore unimplemented in MySQL. + 'field_petition_response_sign' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getSignatureThreshold()))), + 'field_petition_review_timeframe' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getReviewTimeframe()))), + 'field_petition_signature_count' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getSignatureCount()))), + 'field_petition_status' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getStatus()))), + 'field_response_status' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getResponseStatus()))), + 'field_short_url' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getShortUrl()))), + 'field_timestamp_published' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getPublished()))), + 'field_timestamp_reached_public' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getReachedPublic()))), + 'field_timestamp_reached_ready' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getReachedReady()))), + 'field_timestamp_responded' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getClosed()))), + 'field_legacy_path' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getLegacyPath()))), + 'field_review_threshold_mail_sent' => array(LANGUAGE_NONE => array(0 => array('value' => $this->getReviewThresholdMailSent()))), + // The following Mongo fields have no equivalent in MySQL: + // - bookmarked + // - deadline + // - featured + // - hidden + // - nice_url has been replaced with core path aliasing. + // - private_tags + // - user_tags + ); + + if ($response = $this->getResponse()) { + $response_id = $response['id']; + $values['field_response_id'] = array(LANGUAGE_NONE => array(0 => array('target_id' => $response_id))); + } + + // If it already has an entity ID, it's an UPDATE not an INSERT. + if ($this->getEntityId()) { + $values['is_new'] = FALSE; + $nid = $this->getEntityId(); + + + // Ensure node has vid property set if it previously existed in a node. + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'node') + ->propertyCondition('nid', $nid); + $result = $query->execute(); + if ($result['node'][$nid]->vid) { + $values['vid'] = $result['node'][$nid]->vid; + } + } + else { + $this->setCreated($created = time()); + } + + return entity_create('node', $values); + } + + /** + * Gets the petition as an array in the form expected by legacy Mongo code. + * + * @return array + * An array as formerly returned by wh_petitions_load_petition(). + * + * @see wh_petitions_load_petition() + * + * @mongo2mysql + */ + public function toLegacyArray() { + $issues = array(); + foreach ((array) $this->getIssues() as $issue) { + $issues[] = $issue['id']; + } + + $values = array( + 'uid' => $this->getUid(), + 'title' => $this->getTitle(), + 'title_keywords' => $this->getTitleKeywords(), + 'body' => $this->getBody(), + 'body_keywords' => $this->getBodyKeywords(), + 'issues' => $issues, + 'user_tags' => $this->getUserTags(), + 'private_tags' => $this->getPrivateTags(), + 'petition_status' => $this->getStatus(), + 'response_id' => $this->getResponseId(), + 'response_status' => $this->getResponseStatus(), + 'published' => $this->getPublished(), + 'reached_public' => $this->getReachedPublic(), + 'reached_ready' => $this->getReachedReady(), + 'closed' => (int) $this->getClosed(), + 'signature_count' => $this->getSignatureCount(), + 'abuse_flags' => $this->getAbuseFlags(), + 'review_timeframe' => $this->getReviewTimeframe(), + 'response_signatures' => $this->getSignatureThreshold(), + 'public_signatures' => $this->getSignaturePublicThreshold(), + 'bookmarked' => $this->getBookmarked(), + 'featured' => $this->getFeatured(), + // 'hidden' is unused. + 'nice_url' => $this->getNiceUrl(FALSE), + 'short_url' => $this->getShortUrl(), + 'created' => $this->getCreated(), + 'review_threshold_mail_sent' => $this->getReviewThresholdMailSent(), + 'abuse_count' => count($this->getAbuseFlags()), + ); + + if ($this->getLegacyId()) { + $values['_id'] = new MongoId($this->getLegacyId()); + } + + // Merge with default values and return. + return array_merge(wh_petitions_default_petition(), $values); + } + + /** + * Creates an object from an array in the form expected by legacy Mongo code. + * + * @param array $legacy_array + * An array as formerly returned by wh_petitions_load_petition(). + * + * @return PetitionItem + * A PetitionItem created from the given array. + * + * @see wh_petitions_load_petition() + * + * @mongo2mysql + */ + public static function fromLegacyArray($legacy_array) { + // Merge with default petition to fill in any missing fields. + $legacy_array = array_merge(wh_petitions_default_petition(), $legacy_array); + + $issue_tids = $legacy_array['issues']; + $issues = taxonomy_term_load_multiple($issue_tids); + $issues_array = array(); + if ($issues) { + foreach ($issues as $tid => $term) { + $issues_array[] = array( + 'id' => $tid, + 'name' => petitions_data_sanitize_output($term->name), + ); + } + } + + // Create the object. A few notes on omitted fields: + // - A legacy array doesn't have an entity ID for setId(). + // - Body and title keywords are generated dynamically on output rather than + // being set on object creation. + // - The "hidden" Mongo field was apparently never used (and therefore is + // not handled here). + $petition = new self(); + if (!empty($legacy_array['_id'])) { + $petition->setLegacyId($legacy_array['_id']->__toString()); + } + $petition + ->setAbuseFlags($legacy_array['abuse_flags']) + ->setBody($legacy_array['body']) + ->setBookmarked($legacy_array['bookmarked']) + ->setClosed($legacy_array['closed']) + ->setCreated($legacy_array['created']) + ->setFeatured($legacy_array['featured']) + ->setIssues($issues_array) + + // In a legacy array, the nice URL and legacy path are the same thing. + ->setLegacyPath($legacy_array['nice_url']) + ->setNiceUrl($legacy_array['nice_url']) + + ->setStatus($legacy_array['petition_status']) + ->setSignaturePublicThreshold($legacy_array['public_signatures']) + ->setPrivateTags($legacy_array['private_tags']) + ->setPublished($legacy_array['published']) + ->setReachedPublic($legacy_array['reached_public']) + ->setSignatureThreshold($legacy_array['response_signatures']) + ->setReachedReady($legacy_array['reached_ready']) + ->setReviewTimeframe($legacy_array['review_timeframe']) + ->setShortUrl($legacy_array['short_url']) + ->setSignatureCount($legacy_array['signature_count']) + ->setTitle($legacy_array['title']) + ->setUid($legacy_array['uid']) + ->setUserTags($legacy_array['user_tags']); + if (!empty($legacy_array['response_id'])) { + $petition->setResponse(PetitionsSelectQuery::formatReturnResponse($legacy_array['response_id'])); + } + if (!empty($legacy_array['review_threshold_mail_sent'])) { + $petition->getReviewThresholdMailSent($legacy_array['review_threshold_mail_sent']); + } + + return $petition; + } + +} diff --git a/modules/custom/petitions_data/classes/PetitionNotFoundException.inc b/modules/custom/petitions_data/classes/PetitionNotFoundException.inc new file mode 100644 index 000000000..dbaa7801d --- /dev/null +++ b/modules/custom/petitions_data/classes/PetitionNotFoundException.inc @@ -0,0 +1,11 @@ +getLegacyId()) { + try { + $conn = wh_petitions_mongo_petition_connection(); + $conn->setSlaveOkay(FALSE); + wh_petitions_delete_petition($conn, $petition->getLegacyId()); + } + catch (Exception $e) { + watchdog('petitions_data', 'Failed to delete petition @petition_id from MongoDB in PetitionsController::delete().', array( + '@petition_id' => $petition_id, + ), WATCHDOG_WARNING); + } + } + + if (petitions_data_mysql_writes_are_enabled()) { + // If MySQL writes and Mongo reads are simultaneously enabled (phase 2), + // the above loadObject() call was issued against Mongo and therefore had + // no access to the entity ID. Manually re-issue against MySQL. + if (petitions_data_mongo_reads_are_enabled()) { + $petitions_query = new PetitionsSelectQueryMysql(); + $petitions = $petitions_query + ->setPetitionId($petition_id) + ->execute() + ->getResultObjects(); + $petition = $petitions[0]; + } + + if ($petition->getEntityId()) { + node_delete($petition->getEntityId()); + } + } + } + /** * Saves a petition. * - * Upon save, this should update $petition->id, $petition->niceUrl, and - * $petition->shortUrl. + * Saves a petition to the active database backend(s) and updates the entity + * ID, legacy ID, nice URL, and short URL properties as appropriate. * - * @param Petition $petition + * @param PetitionItem $petition * A petition object to save. + */ + public static function save(PetitionItem $petition) { + // The Mongo save must come first because it adds a legacy ID which the + // MySQL save will use if present. + if (petitions_data_mongo_writes_are_enabled()) { + self::saveToMongo($petition); + } + + if (petitions_data_mysql_writes_are_enabled()) { + self::saveToMySql($petition); + } + } + + /** + * Saves a petition to MongoDB. * - * @return Petition - * Returns the Petition with id property set if save is successful. + * @param PetitionItem $petition + * The petition to save. */ - public static function save(Petition $petition) { - - $petition_form['uid'] = $petition->getUid(); - $petition_form['created'] = $petition->getCreated(); - $petition_form['title'] = $petition->getTitle(); - $petition_form['body'] = $petition->getBody(); - $petition_form['body_keywords'] = wh_petitions_generate_keywords($petition->getBody()); - $petition_form['title_keywords'] = wh_petitions_generate_keywords($petition->getTitle()); - $petition_form['petition_status'] = $petition->getStatus(); - $petition_form['response_status'] = $petition->getResponseStatus(); - $petition_form['user_tags'] = $petition->getUserTags(); - $petition_form['issues'] = $petition->getIssues(); - $petition_form['signature_count'] = $petition->getSignatureCount(); - $petition_form['public_signatures'] = $petition->getSignaturePublicThreshold(); - $petition_form['response_signatures'] = $petition->getSignatureThreshold(); - $petition_form['review_timeframe'] = $petition->getReviewTimeframe(); - $petition_form['reached_public'] = $petition->getReachedPublic(); - $petition_form['published'] = $petition->getPublished(); + protected static function saveToMongo(PetitionItem $petition) { + $petition_form = $petition->toLegacyArray(); // Save the petition to MongoDB. $conn = wh_petitions_mongo_petition_connection(); @@ -50,18 +115,44 @@ class PetitionsController { // Update petition with new MongoDB id. $petition_form['_id'] = new MongoId($petition_id); - $petition->setId($petition_id); + $petition->setLegacyId($petition_id); // Generate a Friendly & Short URL. These require MongoDB ID to be set. wh_petitions_generate_nice_url($petition_form); - wh_petitions_generate_short_url($petition_form); $petition->setNiceUrl($petition_form['nice_url']); - $petition->setShortUrl($petition_form['short_url']); + + if ($petition->getStatus() != WH_PETITION_STATUS_DRAFT) { + wh_petitions_generate_short_url($petition_form); + $petition->setShortUrl($petition_form['short_url']); + } + + $petition->setLegacyPath($petition->getNiceUrl()); // Re-save with updated values. wh_petitions_save_petition($conn, $petition_form); + } + + /** + * Saves a petition to MySQL. + * + * @param PetitionItem $petition + * The petition to save. + */ + protected static function saveToMysql(PetitionItem $petition) { + $node = $petition->toEntity(); + node_save($node); + $petition->setEntityId($node->nid); + + $nice_url = url("node/{$node->nid}"); + $petition->setNiceUrl($nice_url); - return $petition; + if ($petition->getStatus() != WH_PETITION_STATUS_DRAFT) { + $short_url = wh_petition_tool_shortenurl($petition->getNiceUrl(FALSE)); + $petition->setShortUrl($short_url); + + $node = $petition->toEntity(); + node_save($node); + } } /** @@ -69,12 +160,21 @@ class PetitionsController { * * @param string $pid * A petition id. + * @param bool $realtime + * Whether or not realtime accuracy is required. See + * PetitionsSelectQueryFactory::create() for details. Defaults to TRUE. * - * @return Petition - * A single petition object, or FALSE if none is found. + * @return array + * An array of petition data, or an empty array if no matches are found. */ - public static function load($pid) { - return PetitionsController::loadMultiple(array($pid)); + public static function load($pid, $realtime = TRUE) { + $petitions = PetitionsController::loadMultiple(array($pid), $realtime); + + if (empty($petitions)) { + return array(); + } + + return $petitions[0]; } /** @@ -82,15 +182,61 @@ class PetitionsController { * * @param array $pids * An array of petition ids. + * @param bool $realtime + * Whether or not realtime accuracy is required. See + * PetitionsSelectQueryFactory::create() for details. Defaults to TRUE. * * @return array - * An array of Petition objects. + * An array of petition data arrays. */ - public static function loadMultiple(array $pids) { - $petitions_query = PetitionsSelectQueryFactory::create(); + public static function loadMultiple(array $pids, $realtime = TRUE) { + $petitions_query = PetitionsSelectQueryFactory::create($realtime); $petitions = $petitions_query->setPetitionIds($pids) ->execute()->getResult(); return $petitions; } + + /** + * Loads a single petition as an object. + * + * @param string $pid + * A petition ID. + * @param bool $realtime + * Whether or not realtime accuracy is required. See + * PetitionsSelectQueryFactory::create() for details. Defaults to TRUE. + * + * @return PetitionItem|false + * A PetitionItem object if found or FALSE if not. + */ + public static function loadObject($pid, $realtime = TRUE) { + $petitions = PetitionsController::loadObjectMultiple(array($pid), $realtime); + + if (empty($petitions)) { + return FALSE; + } + + return $petitions[0]; + } + + /** + * Loads multiple petitions as objects. + * + * @param array $pids + * An array of petition IDs. + * @param bool $realtime + * Whether or not realtime accuracy is required. See + * PetitionsSelectQueryFactory::create() for details. Defaults to TRUE. + * + * @return PetitionItem[] + * An array of PetitionItem objects. + */ + public static function loadObjectMultiple(array $pids, $realtime = TRUE) { + $petitions_query = PetitionsSelectQueryFactory::create($realtime); + $petitions = $petitions_query->setPetitionIds($pids) + ->execute()->getResultObjects(); + + return $petitions; + } + } diff --git a/modules/custom/petitions_data/classes/PetitionsSelectQuery.inc b/modules/custom/petitions_data/classes/PetitionsSelectQuery.inc index 81ef1294d..c956ab12d 100644 --- a/modules/custom/petitions_data/classes/PetitionsSelectQuery.inc +++ b/modules/custom/petitions_data/classes/PetitionsSelectQuery.inc @@ -2,7 +2,7 @@ /** * @file - * Defines PetitionsSelectQuery class. + * Contains PetitionsSelectQuery. */ /** @@ -15,8 +15,7 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { protected $title; protected $body; - protected $issueID; - protected $issue; + protected $issueIDs; protected $petitionIds; protected $baseURL; protected $responseID; @@ -40,12 +39,90 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { protected $status; protected $isSignable; protected $isPublic; + protected $isOpen; + protected $orderByFieldsMap; + protected $orderByDirMap; + protected $uid; + + /** + * These constants are used by the setOrderBy function. + * Mapping is done in PetitionsSelectQueryMongo.inc and + * PetitionsSelectQueryMysql.inc to determine the column names + * for each one of these constants. + */ + const SELECT_QUERY_ORDER_BY_FIELD_PUBLISHED = 'PUBLISHED'; + const SELECT_QUERY_ORDER_BY_FIELD_SIGNATURE_COUNT = 'SIGNATURE_COUNT'; + const SELECT_QUERY_ORDER_BY_FIELD_DATE_REACHED_PUBLIC = 'DATE_REACHED_PUBLIC'; + + /** + * Valid petition fields for order by + * + * NOTE: Adding any additional fields requires adding of indexes to database. + * Use of a new field without indexes will cause ADVERSE performance issues. + * + * @var array + */ + protected $validPetitionsOrderByFields = array( + self::SELECT_QUERY_ORDER_BY_FIELD_PUBLISHED, + self::SELECT_QUERY_ORDER_BY_FIELD_SIGNATURE_COUNT, + self::SELECT_QUERY_ORDER_BY_FIELD_DATE_REACHED_PUBLIC, + ); /** * Constructor. This must be explicitly called by subclasses. */ public function __construct() { parent::__construct(); + // Merge petition specific order by fields with common order by fields from SelectQueryBase. + $this->setValidOrderByFields(array_merge($this->getValidOrderByFields(), $this->getValidPetitionsOrderByFields())); + } + + /** + * Getter accessor for $this->validPetitionsOrderByFields. + * + * @return array + * The array of validPetitionsOrderByFields + */ + public function getValidPetitionsOrderByFields() { + return $this->validPetitionsOrderByFields; + } + + /** + * Get accessor for $this->orderBy. + * + * @return array + * The array of orderBy stdClass objects. + * orderBy objects contain properties field and direction + */ + public function getOrderBy() { + // If a single, specific petition has been requested, there is no need to + // add an ORDER BY clause to the query. + $petition_ids = (array) $this->getPetitionIds(); + if (count($petition_ids) == 1) { + return NULL; + } + + return $this->orderBy; + } + + /** + * Get accessor for $this->orderByFieldsMap. + * + * @return array + * Array of orderByFieldsMap + */ + public function getOrderByFieldsMap() { + return $this->orderByFieldsMap; + } + + /** + * Get accessor for $this->orderByDirMap. + * + * @return array + * Array of orderByDirMap + */ + public function getOrderByDirMap() { + return $this->orderByDirMap; } /** @@ -243,6 +320,16 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { return $this->isPublic; } + /** + * Get accessor for $this->isOpen. + * + * @return bool|null + * Returns the value of the isOpen query filter if set or NULL if not. + */ + protected function getIsOpen() { + return $this->isOpen; + } + /** * Set accessor for $this->isPublic. * @@ -279,37 +366,61 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { } /** - * Get accessor for $this->issueID. + * Set accessor for $this->isOpen. * - * @return int - * The issue ID used to filter results. + * @param string $is_open + * The open status of the petition. + * + * @return PetitionsSelectQuery + * Returns current instance of object. */ - protected function getIssueID() { - return $this->issueID; + public function setIsOpen($is_open) { + // Case race: First to evaluate TRUE wins. + switch (TRUE) { + // These are all the possible true cases. + case intval($is_open) == 1: + case strtolower($is_open) == 'true': + case $is_open === TRUE: + $this->isOpen = TRUE; + break; + + // These are all the possible false cases. + case $is_open === '0': + case $is_open === 0: + case strtolower($is_open) == 'false': + case $is_open === FALSE: + $this->isOpen = FALSE; + break; + + case $is_open === NULL: + default: + break; + } + return $this; } /** - * Set accessor for $this->issueID. - * - * @param int $issue_id - * The issue ID used to filter results. + * Get accessor for $this->issueIDs. * - * @return PetitionsSelectQuery - * Returns current instance of object. + * @return array + * Array of the issue IDs used to filter results. */ - public function setIssueID($issue_id) { - $this->issueID = $issue_id; - return $this; + protected function getIssueIDs() { + return $this->issueIDs; } /** - * Get accessor for $this->issue. + * Set accessor for $this->issueIDs. * - * @return string - * The issue used to filter results. + * @param array $issue_ids + * Array of the issue IDs used to filter results. + * + * @return PetitionsSelectQuery + * Returns current instance of object. */ - protected function getIssue() { - return $this->issue; + public function setIssueIDs($issue_ids) { + $this->issueIDs = $issue_ids; + return $this; } /** @@ -352,20 +463,6 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { return $this; } - /** - * Set accessor for $this->issue. - * - * @param string $issue - * The issue used to filter results. - * - * @return PetitionsSelectQuery - * Returns current instance of object. - */ - public function setIssue($issue) { - $this->issue = $issue; - return $this; - } - /** * Get accessor for $this->signatureThresholdCeiling. * @@ -769,14 +866,14 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { $issues = array($issues); } foreach ($issues as $issue_id) { - $term = taxonomy_term_load($issue_id); + $term = taxonomy_term_load((int) $issue_id); if (empty($term)) { continue; } $row = array( - 'id' => $term->tid, + 'id' => (int) $term->tid, 'name' => petitions_data_sanitize_output($term->name), ); $terms[] = $row; @@ -821,6 +918,99 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { return $this; } + /** + * Get accessor for $this->uid. + * + * @return int + * The user ID used to filter results against. + */ + protected function getUid() { + return $this->uid; + } + + /** + * Set accessor for $this->uid. + * + * @param int $uid + * The User ID to filter results against. + * + * @return PetitionsSelectQuery + * Returns current instance of object. + */ + public function setUid($uid) { + $this->uid = (int) $uid; + return $this; + } + + /** + * Parses the various status-related filters to get resulting status codes. + * + * @return array + * Returns an array of status codes allowed by the active filters. + * + * @see PetitionsSelectQuery::setStatus() + * @see PetitionsSelectQuery::setIsOpen() + * @see PetitionsSelectQuery::setIsPublic() + * @see PetitionsSelectQuery::setIsSignable() + */ + protected function parseStatus() { + // Start with all status codes and remove them as filters are applied. + $included_statuses = wh_petitions_all_statuses(); + + $not_public_statuses = $this->removeStatusesFromArray(wh_petitions_public_statuses(), wh_petitions_all_statuses()); + $not_open_statuses = $this->removeStatusesFromArray(wh_petitions_open_statuses(), wh_petitions_all_statuses()); + $not_signable_statuses = $this->removeStatusesFromArray(wh_petitions_signable_statuses(), wh_petitions_all_statuses()); + + // Is public. + if ($this->getIsPublic() === TRUE) { + $included_statuses = $this->removeStatusesFromArray($not_public_statuses, $included_statuses); + } + elseif ($this->getIsPublic() === FALSE) { + $included_statuses = $this->removeStatusesFromArray(wh_petitions_public_statuses(), $included_statuses); + } + + // Is open. + if ($this->getIsOpen() === TRUE) { + $included_statuses = $this->removeStatusesFromArray($not_open_statuses, $included_statuses); + } + elseif ($this->getIsOpen() === FALSE) { + $included_statuses = $this->removeStatusesFromArray(wh_petitions_open_statuses(), $included_statuses); + } + + // Is signable. + if ($this->getIsSignable() === TRUE) { + $included_statuses = $this->removeStatusesFromArray($not_signable_statuses, $included_statuses); + } + elseif ($this->getIsSignable() === FALSE) { + $included_statuses = $this->removeStatusesFromArray(wh_petitions_signable_statuses(), $included_statuses); + } + + // Status. + if ($this->getStatus() == 'open') { + $included_statuses = $this->removeStatusesFromArray($not_open_statuses, $included_statuses); + } + elseif ($this->getStatus() == 'closed') { + $included_statuses = $this->removeStatusesFromArray(wh_petitions_open_statuses(), $included_statuses); + } + + return $included_statuses; + } + + /** + * Filters unwanted statuses from a status array. + * + * @param array $statuses_to_remove + * The list of statuses to remove from $status_array + * @param array $statuses + * The current list of statuses from which statuses need to be removed. + * + * @return array + * Status array based on $statuses with all $statuses_to_remove removed. + */ + public static function removeStatusesFromArray($statuses_to_remove, $statuses) { + return array_diff($statuses, $statuses_to_remove); + } + /** * Helper function to calculate and load info for responses. * @@ -830,23 +1020,21 @@ abstract class PetitionsSelectQuery extends SelectQueryBase { * @return array * Basic information and a link to the associated petition response. */ - protected function formatReturnResponse($response_id = 0) { + public static function formatReturnResponse($response_id = 0) { if (!$response_id) { return array(); } - $uri = drupal_lookup_path('alias', 'node/' . $response_id); $created_time = db_select('node', 'n') ->fields('n', array('created')) ->condition('nid', $response_id) - ->orderBy('created', 'DESC') - ->range(0, 1) ->execute() ->fetchField(); $response = array( - 'id' => $response_id, - 'url' => $this->getBaseURL() . '/' . $uri, - 'associationTime' => $created_time, + 'id' => (int) $response_id, + 'url' => petitions_data_url("node/" . $response_id), + 'associationTime' => (int) $created_time, ); return $response; } + } diff --git a/modules/custom/petitions_data/classes/PetitionsSelectQueryFactory.inc b/modules/custom/petitions_data/classes/PetitionsSelectQueryFactory.inc index 9e75865bb..7063de2a9 100644 --- a/modules/custom/petitions_data/classes/PetitionsSelectQueryFactory.inc +++ b/modules/custom/petitions_data/classes/PetitionsSelectQueryFactory.inc @@ -1,18 +1,40 @@ 1, + self::SELECT_QUERY_ORDER_BY_DESC => -1, + ); /** - * Constructor. + * OrderBy Field mapping + * + * Maps Order By constants to appropriate database column name. + * + * @var array + * An array containing 'field' element with field name. + */ + protected $orderByFieldsMap = array( + self::SELECT_QUERY_ORDER_BY_FIELD_ID => array('field' => '_id'), + self::SELECT_QUERY_ORDER_BY_FIELD_TITLE => array('field' => 'title'), + self::SELECT_QUERY_ORDER_BY_FIELD_DATE_CREATED => array('field' => 'created'), + self::SELECT_QUERY_ORDER_BY_FIELD_PUBLISHED => array('field' => 'published'), + self::SELECT_QUERY_ORDER_BY_FIELD_SIGNATURE_COUNT => array('field' => 'signature_count'), + self::SELECT_QUERY_ORDER_BY_FIELD_DATE_REACHED_PUBLIC => array('field' => 'reached_public'), + ); + + /** + * {@inheritdoc} */ public function __construct() { parent::__construct(); - $collection = mongodb_collection('petitions'); + try { + $collection = mongodb_collection('petitions'); + } + catch (Exception $e) { + petitionslog_event('exception.petitions_data.e54cea3'); + } $this->setCollection($collection); + $this->setOrderBy(self::SELECT_QUERY_ORDER_BY_FIELD_DATE_CREATED, self::SELECT_QUERY_ORDER_BY_DESC); } - /** * Get accessor for $this->collection. * @@ -42,8 +73,7 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { * @param MongoCollection $collection * MongoCollection resource for querying against a collection. * - * @return PetitionsMongoRaw - * Returns current instance of object. + * @return $this */ protected function setCollection($collection) { $this->collection = $collection; @@ -66,8 +96,7 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { * @param array $query * Array of query parameters to get passed to mongodb. * - * @return PetitionsMongoRaw - * Returns current instance of object. + * @return $this */ protected function setQuery(array $query) { $this->query = $query; @@ -77,12 +106,9 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { /** * Primary processing function of class. Makes query to MongoDB. * - * @return PetitionsQueryMongo - * Returns current instance of object. + * @return $this */ public function execute() { - $results = array(); - if (($this->getIsSignable() === TRUE) || ($this->getIsSignable() === FALSE)) { $this->addIsSignableToQuery(); } @@ -91,6 +117,10 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { $this->addIsPublicToQuery(); } + if (($this->getIsOpen() === TRUE) || ($this->getIsOpen() === FALSE)) { + $this->addIsOpenToQuery(); + } + if ($this->getCreatedDate()) { $this->addCreatedDateToQuery(); } @@ -116,11 +146,13 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { } $this->addTitleToQuery(); + $this->addIssuesToQuery(); $this->addBodyToQuery(); $this->addURLToQuery(); $this->addStatusToQuery(); $this->addResponseIDToQuery(); $this->addPetitionIdsToQuery(); + $this->addUidToQuery(); $fields = array( 'title', @@ -136,6 +168,17 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { 'petition_status', 'review_timeframe', 'response_id', + 'response_status', + 'reached_public', + 'reached_ready', + 'public_signatures', + 'uid', + 'bookmarked', + 'featured', + 'private_tags', + 'user_tags', + 'abuse_flags', + 'review_threshold_mail_sent', ); // WARNING: All query additions must happen prior to calling formatStatus(). @@ -146,34 +189,62 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { ->find($query, $fields) ->limit($this->getLimit()) ->skip($this->getOffset()) - ->sort(array('created' => -1)); - - if (!$mongo_results->hasNext()) { - $this->setResult(array()); - return $this; - } + ->sort($this->getSort()); + $result_objects = array(); + $result_arrays = array(); foreach ($mongo_results as $doc) { - $petition = new Petition(); - $petition->setId($this->formatReturnId($doc['_id'])) + $petition = new PetitionItem(); + $petition->setLegacyId($this->formatReturnId($doc['_id'])) ->setTitle($doc['title']) ->setBody($doc['body']) ->setIssues($this->formatReturnIssues($doc['issues'])) ->setSignatureThreshold($doc['response_signatures']) ->setSignatureCount($doc['signature_count']) ->setNiceUrl($this->formatReturnUrl($doc['nice_url'])) + ->setShortUrl($doc['short_url']) ->setReviewTimeframe($doc['review_timeframe']) ->setStatus($doc['petition_status']) - ->setResponse(isset($doc['response_id']) ? $this->formatReturnResponse($doc['response_id']) : NULL) ->setCreated($doc['created']) - ->setPublished($doc['published']); + ->setPublished($doc['published']) + ->setUid($doc['uid']) + ->setResponseStatus($doc['response_status']) + ->setReachedPublic($doc['reached_public']) + ->setSignaturePublicThreshold($doc['public_signatures']) + ->setUserTags($doc['user_tags']); + if (!empty($doc['abuse_flags'])) { + $petition->setAbuseFlags($doc['abuse_flags']); + } + if (!empty($doc['bookmarked'])) { + $petition->setBookmarked($doc['bookmarked']); + } + if (!empty($doc['closed'])) { + $petition->setClosed($doc['closed']); + } + if (!empty($doc['featured'])) { + $petition->setFeatured($doc['featured']); + } + if (!empty($doc['private_tags'])) { + $petition->setPrivateTags($doc['private_tags']); + } + if (!empty($doc['reached_ready'])) { + $petition->setReachedReady($doc['reached_ready']); + } + if (!empty($doc['response_id'])) { + $petition->setResponse(PetitionsSelectQuery::formatReturnResponse($doc['response_id'])); + } + if (!empty($doc['review_threshold_mail_sent'])) { + $petition->setReviewThresholdMailSent($doc['review_threshold_mail_sent']); + } - $results[] = $petition->toArray(); + $result_objects[] = $petition; + $result_arrays[] = $petition->toRestResponseItemArray(); } + $this->setResultObjects($result_objects); + $this->setResult($result_arrays); $this->setCount($mongo_results->count()); - $this->setResult($results); return $this; } @@ -320,6 +391,19 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { } } + /** + * Adds the issues argument to the query. + */ + protected function addIssuesToQuery() { + if ($this->getIssueIDs()) { + $query = $this->getQuery(); + $query += array( + 'issues' => array('$in' => $this->getIssueIDs()), + ); + $this->setQuery($query); + } + } + /** * Helper function to add $this->body as a query parameter. */ @@ -359,7 +443,7 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { protected function addURLToQuery() { // Filter by URL. if ($this->getURL()) { - $nice_url = petitions_data_get_nice_url_from_full_url($this->getURL()); + $nice_url = petitions_data_get_path_from_full_url($this->getURL()); $query = $this->getQuery(); $query += array( 'nice_url' => $nice_url, @@ -441,11 +525,28 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { // Request is for petitions that are NOT public. // Non-public petitions are not available through the index api. // Return access denied with a message. - $developer_message = t('You need a petition id or url to access non-public petitions.'); + $developer_message = t('You need a petition ID or URL to access non-public petitions.'); api_errors_throw_error(403, $developer_message); } } + /** + * Helper function to add $this->isOpen as a query parameter. + */ + protected function addIsOpenToQuery() { + // Petitions with any of the following status, are considered open. + $array_open_statuses = wh_petitions_open_statuses(); + + if ($this->getIsOpen()) { + // Request is for petitions that are open. + $this->setStatusInclude($array_open_statuses); + } + elseif ($this->getIsOpen() === FALSE) { + // Request is for petitions that are NOT open. + $this->setStatusExclude($array_open_statuses); + } + } + /** * Helper function to add $this->isSignable as a query parameter. */ @@ -464,6 +565,45 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { } } + /** + * Helper function to add $this->uid as a query parameter. + */ + protected function addUidToQuery() { + if ($this->getUid()) { + $query = $this->getQuery(); + $query += array( + 'uid' => (int) $this->getUid(), + ); + $this->setQuery($query); + } + } + + /** + * Adds the orderBy arguments to the query. + */ + protected function getSort() { + $sort_array = array(); + if (!$this->getOrderBy()) { + return $sort_array; + } + + foreach ($this->getOrderBy() as $order_by) { + $field = $order_by->field; + $order_by_dir = $order_by->direction; + + if (!$this->isValidOrderByField($field)) { + throw new Exception('getSort - Not a valid field: ' . $field); + } + if (!$this->isValidOrderByDirection($order_by_dir)) { + throw new Exception('getSort - Not a valid direction: ' . $order_by_dir); + } + $mapped_field = $this->orderByFieldsMap[$field]['field']; + $mapped_dir = $this->orderByDirMap[$order_by_dir]; + $sort_array[$mapped_field] = $mapped_dir; + } + return $sort_array; + } + /** * Helper function to format return response ID field. * @@ -477,7 +617,6 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { return $id->{'$id'}; } - /** * Helper function to remove petition statuses from another array. * @@ -568,7 +707,6 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { $this->setQuery($query); } - /** * Get accessor for $query->statusExclude. * @@ -585,7 +723,6 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { } } - /** * Set accessor for $query->status exclude. * @@ -644,14 +781,32 @@ class PetitionsSelectQueryMongo extends PetitionsSelectQuery { // Remove non-mongofied statuses in the query array. unset($query['petition_status_in']); unset($query['petition_status_exclude']); + if (!empty($status_to_query)) { $query['petition_status'] = array('$in' => (Array) $status_to_query); } - else { + elseif (!count($query)) { // Some combination of status filters have resulted in no filters, // which is impossible to return. Set this to nin for the entire status. $query['petition_status'] = array('$nin' => wh_petitions_all_statuses()); } $this->setQuery($query); } + + /** + * Gets a petition ID from a given petition URL. + * + * @param $url + * The URL to get a petition ID from. + * + * @return mixed|false + * A petition ID corresponding to the given URL, or FALSE if there is no + * corresponding petition. + */ + public static function getPetitionIdFromUrl($url) { + $nice_url = petitions_data_get_path_from_full_url($url); + $conn = wh_petitions_mongo_petition_connection(); + $petition = $conn->findOne(array('nice_url' => $nice_url), array('title')); + return (string) $petition['_id']; + } } diff --git a/modules/custom/petitions_data/classes/PetitionsSelectQueryMysql.inc b/modules/custom/petitions_data/classes/PetitionsSelectQueryMysql.inc new file mode 100644 index 000000000..f6c0668f5 --- /dev/null +++ b/modules/custom/petitions_data/classes/PetitionsSelectQueryMysql.inc @@ -0,0 +1,544 @@ + 'ASC', + self::SELECT_QUERY_ORDER_BY_DESC => 'DESC', + ); + + /** + * OrderBy MySQL Field mapping + * + * Maps Order By constants to appropriate database column name. + * + * @var array + * An array containing 'field' and 'column' elements. For node columns, only + * 'field' is required. For petition fields, 'field' and 'column' name + * are required. + */ + protected $orderByFieldsMap = array( + self::SELECT_QUERY_ORDER_BY_FIELD_ID => array('field' => 'nid'), + self::SELECT_QUERY_ORDER_BY_FIELD_TITLE => array('field' => 'title'), + self::SELECT_QUERY_ORDER_BY_FIELD_DATE_CREATED => array('field' => 'created'), + self::SELECT_QUERY_ORDER_BY_FIELD_PUBLISHED => array('field' => 'field_timestamp_published', 'column' => 'value'), + self::SELECT_QUERY_ORDER_BY_FIELD_SIGNATURE_COUNT => array('field' => 'field_petition_signature_count', 'column' => 'value'), + self::SELECT_QUERY_ORDER_BY_FIELD_DATE_REACHED_PUBLIC => array('field' => 'field_timestamp_reached_public', 'column' => 'value'), + ); + + + /** + * Constructor. + */ + public function __construct() { + parent::__construct(); + $this->setOrderBy(self::SELECT_QUERY_ORDER_BY_FIELD_DATE_CREATED, self::SELECT_QUERY_ORDER_BY_DESC); + } + + /** + * {@inheritdoc} + */ + public function execute() { + try { + $this->buildQuery(); + $nodes = $this->executeQuery(); + $this->buildResults($nodes); + } + catch (PetitionNotFoundException $e) { + $this->setResult(array()); + } + catch (Exception $e) { + watchdog('petitions_data', 'Exception in PetitionsSelectQueryMySQL::execute(): !e', array( + '!e' => petitionslog_format_for_watchdog($e), + )); + } + return $this; + } + + /** + * Builds the query. + */ + protected function buildQuery() { + $this->initializeQuery(); + $this->addArgumentsToQuery(); + } + + /** + * Initializes the basic query. + */ + protected function initializeQuery() { + $this->query = new EntityFieldQueryExtraFields(); + $this->query + ->entityCondition('entity_type', 'node') + ->propertyCondition('type', array('petition')) + ->addExtraField('', 'title', 'title', 'node') + ->addExtraField('', 'created', 'created', 'node') + ->addExtraField('', 'uid', 'uid', 'node'); + } + + /** + * Adds the orderBy arguments to the query. + */ + protected function addOrderByToQuery() { + if (!$this->getOrderBy()) { + return; + } + foreach ($this->getOrderBy() as $order_by) { + $field = $order_by->field; + $order_by_dir = $order_by->direction; + if (!$this->isValidOrderByField($field)) { + throw new Exception('addOrderByToQuery - Not a valid field: ' . $field); + } + if (!$this->isValidOrderByDirection($order_by_dir)) { + throw new Exception('addOrderByToQuery - Not a valid direction: ' . $order_by_dir); + } + + if (in_array($field, $this->getValidPetitionsOrderByFields())) { + $mapped_field = $this->orderByFieldsMap[$field]['field']; + $mapped_column = $this->orderByFieldsMap[$field]['column']; + $mapped_dir = $this->orderByDirMap[$order_by_dir]; + + $this->query->fieldCondition($mapped_field); + $this->query->fieldOrderBy($mapped_field, $mapped_column, $mapped_dir); + + // Adding propertyOrderBy is necessary to ensure node table is joined. + $this->query->propertyOrderBy('created', 'DESC'); + } + else { + $mapped_field = $this->orderByFieldsMap[$field]['field']; + $mapped_dir = $this->orderByDirMap[$order_by_dir]; + $this->query->propertyOrderBy($mapped_field, $mapped_dir); + } + } + } + + /** + * Adds the supplied arguments to the query. + */ + protected function addArgumentsToQuery() { + // Start with the arguments that fail cheapest in case of bad input. + $this->addUrlToQuery(); + + $this->addBodyToQuery(); + $this->addCreatedArgumentsToQuery(); + $this->addStatusToQuery(); + $this->addPetitionIdsToQuery(); + $this->addResponseIdToQuery(); + $this->addSignatureCountArgumentsToQuery(); + $this->addSignatureThresholdArgumentsToQuery(); + $this->addTitleToQuery(); + $this->addIssuesToQuery(); + $this->addUidToQuery(); + + // Clone the query before limiting its range. + $this->countQuery = clone $this->query; + + // Add order by after clone, since order isn't necessary on count. + $this->addOrderByToQuery(); + + // This should be the only addition to the query after cloning it. + $this->addOffsetAndLimitToQuery(); + } + + /** + * Adds the url argument to the query. + */ + protected function addUrlToQuery() { + $url = $this->getURL(); + if ($url) { + $nid = static::getPetitionIdFromUrl($url); + if ($nid) { + $this->query->propertyCondition('nid', $nid); + } + else { + $path = petitions_data_get_path_from_full_url($url); + $this->query->fieldCondition('field_legacy_path', 'value', $path); + } + } + } + + /** + * Adds the body argument to the query. + */ + protected function addBodyToQuery() { + if ($this->getBody()) { + $this->query->fieldCondition('body', 'value', '%' . $this->getBody() . '%', 'LIKE'); + } + } + + /** + * Adds the created time-related arguments to the query. + */ + protected function addCreatedArgumentsToQuery() { + if ($this->getCreatedDate()) { + $this->addCreatedAtToQuery(); + } + else { + $this->addCreatedAfterToQuery(); + $this->addCreatedBeforeToQuery(); + } + } + + /** + * Adds the createdAt argument to the query. + */ + protected function addCreatedAtToQuery() { + $this->query->fieldCondition('field_timestamp_published', 'value', $this->getCreatedDate()); + } + + /** + * Adds the createdAfter argument to the query. + */ + protected function addCreatedAfterToQuery() { + if ($this->getStartDate()) { + $this->query->fieldCondition('field_timestamp_published', 'value', $this->getStartDate(), '>'); + } + } + + /** + * Adds the createdBefore argument to the query. + */ + protected function addCreatedBeforeToQuery() { + if ($this->getEndDate()) { + $this->query->fieldCondition('field_timestamp_published', 'value', $this->getEndDate(), '<'); + } + } + + /** + * Adds the offset and limit arguments to the query. + */ + protected function addOffsetAndLimitToQuery() { + $this->query->range($this->getOffset(), $this->getLimit()); + } + + /** + * Adds the responseId argument to the query. + */ + protected function addResponseIdToQuery() { + if ($this->getResponseID()) { + $this->query->fieldCondition('field_response_id', 'target_id', $this->getResponseID()); + } + } + + /** + * Adds the signature count-related arguments to the query. + */ + protected function addSignatureCountArgumentsToQuery() { + if ($this->getSignatureCount()) { + $this->addSignatureCountToQuery(); + } + else { + $this->addSignatureCountCeilingToQuery(); + $this->addSignatureCountFloorToQuery(); + } + } + + /** + * Adds the signatureCount argument to the Query. + */ + protected function addSignatureCountToQuery() { + $this->query->fieldCondition('field_petition_signature_count', 'value', $this->getSignatureCount()); + } + + /** + * Adds the signatureCountCeiling argument to the query. + */ + protected function addSignatureCountCeilingToQuery() { + if ($this->getSignatureCountCeiling()) { + $this->query->fieldCondition('field_petition_signature_count', 'value', $this->getSignatureCountCeiling(), '<='); + } + } + + /** + * Adds the signatureCountFloor argument to the query. + */ + protected function addSignatureCountFloorToQuery() { + if ($this->getSignatureCountFloor()) { + $this->query->fieldCondition('field_petition_signature_count', 'value', $this->getSignatureCountFloor(), '>='); + } + } + + /** + * Adds the signature threshold-related arguments to the query. + */ + protected function addSignatureThresholdArgumentsToQuery() { + if ($this->getSignatureThreshold()) { + $this->addSignatureThresholdToQuery(); + } + else { + $this->addSignatureThresholdCeilingToQuery(); + $this->addSignatureThresholdFloorToQuery(); + } + } + + /** + * Adds the signatureThreshold argument to the Query. + */ + protected function addSignatureThresholdToQuery() { + $this->query->fieldCondition('field_petition_response_sign', 'value', $this->getSignatureThreshold()); + } + + /** + * Adds the signatureThresholdCeiling argument to the query. + */ + protected function addSignatureThresholdCeilingToQuery() { + if ($this->getSignatureThresholdCeiling()) { + $this->query->fieldCondition('field_petition_response_sign', 'value', $this->getSignatureThresholdCeiling(), '<='); + } + } + + /** + * Adds the signatureThresholdFloor argument to the query. + */ + protected function addSignatureThresholdFloorToQuery() { + if ($this->getSignatureThresholdFloor()) { + $this->query->fieldCondition('field_petition_response_sign', 'value', $this->getSignatureThresholdFloor(), '>='); + } + } + + /** + * Adds the title argument to the query. + */ + protected function addTitleToQuery() { + if ($this->getTitle()) { + $this->query->propertyCondition('title', '%' . $this->getTitle() . '%', 'LIKE'); + } + } + + /** + * Adds the issues argument to the query. + */ + protected function addIssuesToQuery() { + if ($this->getIssueIDs()) { + $this->query->fieldCondition('field_petition_issues', 'tid', $this->getIssueIDs(), 'IN'); + } + } + + /** + * Adds petition IDs to the query. + */ + protected function addPetitionIdsToQuery() { + $petition_ids = $this->getPetitionIds(); + if (!empty($petition_ids)) { + $nids = petition_extract_nids_from_petition_ids($petition_ids); + if (!empty($nids)) { + $this->query->propertyCondition('nid', $nids, 'IN'); + } + + $legacy_ids = petition_extract_legacy_ids_from_petition_ids($petition_ids); + if (!empty($legacy_ids)) { + $this->query->fieldCondition('field_legacy_id', 'value', $legacy_ids, 'IN'); + } + + // None of the given petition IDs were valid. + if (empty($nids) && empty($legacy_ids)) { + throw new PetitionNotFoundException(); + } + } + } + + /** + * Adds User ID to the query. + */ + protected function addUidToQuery() { + $uid = $this->getUid(); + if (!empty($uid)) { + $this->query->propertyCondition('uid', $uid); + } + } + + /** + * Adds the status argument to the query. + */ + protected function addStatusToQuery() { + $include_statuses = static::parseStatus(); + + if ($include_statuses) { + $this->query->fieldCondition('field_petition_status', 'value', $include_statuses, 'IN'); + } + else { + // There are no statuses so don't bother executing any query because there + // aren't any results. + throw new PetitionNotFoundException(); + } + } + + /** + * Gets a petition ID from a given petition URL. + * + * @param $url + * The URL to get a petition ID from. + * + * @return mixed|false + * A petition ID corresponding to the given URL, or FALSE if there is no + * corresponding petition. + */ + public static function getPetitionIdFromUrl($url) { + $url_parts = parse_url($url); + $path = substr($url_parts['path'], 1); + $internal_path = drupal_get_normal_path($path); + + // Find a node ID, if present. + $matches = array(); + preg_match('@^node/([1-9][0-9]*)$@', $internal_path, $matches); + if (!isset($matches[1])) { + return FALSE; + } + $nid = (int) $matches[1]; + + return $nid; + } + + /** + * Executes the query. + * + * @return array + * An array of nodes with field data, or an empty array if there are no + * results. + */ + protected function executeQuery() { + $this->executeCountQuery(); + return $this->executeFullQuery(); + } + + /** + * Executes the COUNT query to get the unlimited resultset size. + */ + protected function executeCountQuery() { + $this->resultsetCount = $this->countQuery->addTag('petitions_data_debug')->count()->execute(); + } + + /** + * Executes the full query to get the resultset data. + * + * @return array + * An array of node data. + */ + protected function executeFullQuery() { + // If the COUNT query found no results, there's no reason to execute the + // full query. + if ($this->resultsetCount === 0) { + return array(); + } + + $result = $this->query->addTag('petitions_data_debug')->execute(); + + // Return early if no results. + if (empty($result['node'])) { + return array(); + } + + $nodes = $result['node']; + field_attach_load('node', $nodes); + return $nodes; + } + + /** + * Builds the results arrays from the query results. + * + * @param array $nodes + * An array of node data, as returned by + * PetitionsSelectQueryMysql::executeQuery. + */ + protected function buildResults(array $nodes) { + $result_objects = array(); + $result_arrays = array(); + foreach ($nodes as $node) { + $response = isset($node->field_response_id[LANGUAGE_NONE][0]['target_id']) ? static::formatReturnResponse($node->field_response_id[LANGUAGE_NONE][0]['target_id']) : array(); + $response_status = isset($node->field_response_status[LANGUAGE_NONE][0]['value']) ? (int) $node->field_response_status[LANGUAGE_NONE][0]['value'] : NULL; + + // This is the pre-PHP 5.5.0 equivalent of array_column(). + // @see http://php.net/manual/en/function.array-column.php + $issue_tids = array(); + if (!empty($node->field_petition_issues[LANGUAGE_NONE])) { + $issue_tids = array_map(function ($element) { + return $element['tid']; + }, $node->field_petition_issues[LANGUAGE_NONE]); + } + + $petition = new PetitionItem(); + // Make sure we have a legacy ID before we assign it. + $legacy_id = (is_array($node->field_legacy_id) && isset($node->field_legacy_id[LANGUAGE_NONE])) ? (string) $node->field_legacy_id[LANGUAGE_NONE][0]['value'] : NULL; + $timestamp_published = isset($node->field_timestamp_published[LANGUAGE_NONE][0]['value']) ? $node->field_timestamp_published[LANGUAGE_NONE][0]['value'] : NULL; + $timestamp_reached_public = isset($node->field_timestamp_reached_public[LANGUAGE_NONE][0]['value']) ? $node->field_timestamp_reached_public[LANGUAGE_NONE][0]['value'] : NULL; + $timestamp_reached_ready = isset($node->field_timestamp_reached_ready[LANGUAGE_NONE][0]['value']) ? $node->field_timestamp_reached_ready[LANGUAGE_NONE][0]['value'] : NULL; + $timestamp_review_threshold_mail_sent = isset($node->field_review_threshold_mail_sent[LANGUAGE_NONE][0]['value']) ? $node->field_review_threshold_mail_sent[LANGUAGE_NONE][0]['value'] : NULL; + $timestamp_responded = isset($node->field_timestamp_responded[LANGUAGE_NONE][0]['value']) ? $node->field_timestamp_responded[LANGUAGE_NONE][0]['value'] : NULL; + $short_url = isset($node->field_short_url[LANGUAGE_NONE][0]['value']) ? $node->field_short_url[LANGUAGE_NONE][0]['value'] : NULL; + $body = isset($node->body[LANGUAGE_NONE][0]['value']) ? $node->body[LANGUAGE_NONE][0]['value'] : ''; + + $petition + ->setEntityId($node->nid) + ->setLegacyId($legacy_id) + ->setTitle($node->extraFields->title) + ->setBody($body) + ->setIssues($this->formatReturnIssues($issue_tids)) + + // Since path aliases are cached, using url() for this value here will + // ultimately be much more performant (not to mention less complex) than + // adding a JOIN to the database query for it. + ->setNiceUrl(drupal_get_path_alias("node/{$node->nid}")) + + ->setShortUrl($short_url) + ->setReviewTimeframe($node->field_petition_review_timeframe[LANGUAGE_NONE][0]['value']) + ->setSignatureCount((int) $node->field_petition_signature_count[LANGUAGE_NONE][0]['value']) + ->setSignatureThreshold((int) $node->field_petition_response_sign[LANGUAGE_NONE][0]['value']) + ->setStatus($node->field_petition_status[LANGUAGE_NONE][0]['value']) + ->setResponse($response) + ->setCreated($node->extraFields->created) + ->setPublished((int) $timestamp_published) + ->setUid($node->extraFields->uid) + ->setReachedPublic((int) $timestamp_reached_public) + ->setReachedReady((int) $timestamp_reached_ready) + ->setSignaturePublicThreshold((int) $node->field_petition_public_signatures[LANGUAGE_NONE][0]['value']) + ->setResponseStatus($response_status) + ->setReviewThresholdMailSent($timestamp_review_threshold_mail_sent) + ->setClosed($timestamp_responded); + if (!empty($node->field_legacy_path[LANGUAGE_NONE][0]['value'])) { + $petition->setLegacyPath($node->field_legacy_path[LANGUAGE_NONE][0]['value']); + } + + $result_objects[] = $petition; + $result_arrays[] = $petition->toRestResponseItemArray(); + } + + $this->setResultObjects($result_objects); + $this->setResult($result_arrays); + $this->setCount($this->resultsetCount); + } + +} diff --git a/modules/custom/petitions_data/classes/PetitionsSelectQuerySolr.inc b/modules/custom/petitions_data/classes/PetitionsSelectQuerySolr.inc new file mode 100644 index 000000000..c1262e728 --- /dev/null +++ b/modules/custom/petitions_data/classes/PetitionsSelectQuerySolr.inc @@ -0,0 +1,442 @@ +buildQuery(); + $response = $this->executeQuery(); + $this->buildResults($response); + } + catch (Exception $e) { + watchdog('petitions_data', 'Exception in PetitionsSelectQuerySolr::execute(): !e', array( + '!e' => petitionslog_format_for_watchdog($e), + )); + } + return $this; + } + + /** + * Builds the query. + */ + protected function buildQuery() { + $this->initializeQuery(); + $this->addArgumentsToQuery(); + } + + /** + * Adds the supplied arguments to the query. + */ + protected function addArgumentsToQuery() { + $this->addBodyToQuery(); + $this->addCreatedArgumentsToQuery(); + $this->addOffsetAndLimitToQuery(); + $this->addPetitionIdsToQuery(); + $this->addResponseIdToQuery(); + $this->addSignatureCountArgumentsToQuery(); + $this->addSignatureThresholdArgumentsToQuery(); + $this->addSortToQuery(); + $this->addStatusToQuery(); + $this->addTitleToQuery(); + $this->addIssuesToQuery(); + $this->addUidToQuery(); + $this->addUrlToQuery(); + } + + /** + * Initializes the basic query. + */ + protected function initializeQuery() { + $this->addQueryParam('bundle', 'petition'); + } + + /** + * Adds the body argument to the query. + */ + protected function addBodyToQuery() { + if ($this->getBody()) { + $this->addQueryParam(PETITION_SOLR_FIELD_BODY, $this->getBody(), FALSE); + } + } + + /** + * Adds the created time-related arguments to the query. + */ + protected function addCreatedArgumentsToQuery() { + if ($this->getCreatedDate()) { + $this->addCreatedAtToQuery(); + } + else { + $this->addCreatedAfterToQuery(); + $this->addCreatedBeforeToQuery(); + } + } + + /** + * Adds the createdAt argument to the query. + */ + protected function addCreatedAtToQuery() { + $this->addQueryParam(PETITION_SOLR_FIELD_TIMESTAMP_PUBLISHED, $this->getCreatedDate()); + } + + /** + * Adds the createdAfter argument to the query. + */ + protected function addCreatedAfterToQuery() { + if ($this->getStartDate()) { + $time = $this->getStartDate() + 1; + $this->addQueryParam(PETITION_SOLR_FIELD_TIMESTAMP_PUBLISHED, "[{$time} TO *]"); + } + } + + /** + * Adds the createdBefore argument to the query. + */ + protected function addCreatedBeforeToQuery() { + if ($this->getEndDate()) { + $time = $this->getEndDate() - 1; + $this->addQueryParam(PETITION_SOLR_FIELD_TIMESTAMP_PUBLISHED, "[* TO {$time}]"); + } + } + + /** + * Adds the offset and limit arguments to the query. + */ + protected function addOffsetAndLimitToQuery() { + $this->params['start'] = $this->getOffset(); + $this->params['rows'] = $this->getLimit(); + } + + /** + * Adds petition IDs to the query. + */ + protected function addPetitionIdsToQuery() { + $petition_ids = $this->getPetitionIds(); + if (!empty($petition_ids)) { + $expressions = array(); + + $nids = petition_extract_nids_from_petition_ids($petition_ids); + if (!empty($nids)) { + $nid_expression = '(' . implode(' OR ', $nids) . ')'; + $expressions[] = PETITION_SOLR_FIELD_NODE_ID . ':' . $nid_expression; + } + + $legacy_ids = petition_extract_legacy_ids_from_petition_ids($petition_ids); + if (!empty($legacy_ids)) { + $legacy_id_expression = '(' . implode(' OR ', $legacy_ids) . ')'; + $expressions[] = PETITION_SOLR_FIELD_LEGACY_ID . ':' . $legacy_id_expression; + } + + if ($expressions) { + $this->params['fq'][] = '(' . implode(' OR ', $expressions) . ')'; + } + // If no expressions could be generated, none of the given petition IDs + // were valid. + else { + throw new PetitionNotFoundException(); + } + } + } + + /** + * Adds the responseId argument to the query. + */ + protected function addResponseIdToQuery() { + if ($this->getResponseID()) { + $this->addQueryParam(PETITION_SOLR_FIELD_RESPONSE_ID, $this->getResponseID()); + } + } + + /** + * Adds the signature count-related arguments to the query. + */ + protected function addSignatureCountArgumentsToQuery() { + if ($this->getSignatureCount()) { + $this->addSignatureCountToQuery(); + } + else { + $this->addSignatureCountCeilingToQuery(); + $this->addSignatureCountFloorToQuery(); + } + } + + /** + * Adds the signatureCount argument to the Query. + */ + protected function addSignatureCountToQuery() { + $this->addQueryParam(PETITION_SOLR_FIELD_SIGNATURE_COUNT, $this->getSignatureCount()); + } + + /** + * Adds the signatureCountCeiling argument to the query. + */ + protected function addSignatureCountCeilingToQuery() { + if ($this->getSignatureCountCeiling()) { + $count = $this->getSignatureCountCeiling(); + $this->addQueryParam(PETITION_SOLR_FIELD_SIGNATURE_COUNT, "[* TO {$count}]"); + } + } + + /** + * Adds the signatureCountFloor argument to the query. + */ + protected function addSignatureCountFloorToQuery() { + if ($this->getSignatureCountFloor()) { + $count = $this->getSignatureCountFloor(); + $this->addQueryParam(PETITION_SOLR_FIELD_SIGNATURE_COUNT, "[{$count} TO *]"); + } + } + + /** + * Adds the signature threshold-related arguments to the query. + */ + protected function addSignatureThresholdArgumentsToQuery() { + if ($this->getSignatureThreshold()) { + $this->addSignatureThresholdToQuery(); + } + else { + $this->addSignatureThresholdCeilingToQuery(); + $this->addSignatureThresholdFloorToQuery(); + } + } + + /** + * Adds the signatureThreshold argument to the Query. + */ + protected function addSignatureThresholdToQuery() { + $this->addQueryParam(PETITION_SOLR_FIELD_SIGNATURE_RESPONSE_THRESHOLD, $this->getSignatureThreshold()); + } + + /** + * Adds the signatureThresholdCeiling argument to the query. + */ + protected function addSignatureThresholdCeilingToQuery() { + if ($this->getSignatureThresholdCeiling()) { + $threshold = $this->getSignatureThresholdCeiling(); + $this->addQueryParam(PETITION_SOLR_FIELD_SIGNATURE_RESPONSE_THRESHOLD, "[* TO {$threshold}]"); + } + } + + /** + * Adds the signatureThresholdFloor argument to the query. + */ + protected function addSignatureThresholdFloorToQuery() { + if ($this->getSignatureThresholdFloor()) { + $threshold = $this->getSignatureThresholdFloor(); + $this->addQueryParam(PETITION_SOLR_FIELD_SIGNATURE_RESPONSE_THRESHOLD, "[{$threshold} TO *]"); + } + } + + /** + * Adds the sort argument to the query. + */ + protected function addSortToQuery() { + $this->sortString = PETITION_SOLR_FIELD_TIMESTAMP_CREATED . ' desc'; + } + + /** + * Adds the status argument to the query. + */ + protected function addStatusToQuery() { + $include_statuses = static::parseStatus(); + + if ($include_statuses) { + $status_expression = implode(' OR ', $include_statuses); + $this->addQueryParam(PETITION_SOLR_FIELD_PETITION_STATUS, "({$status_expression})"); + } + // All possible results are excluded. + else { + throw new PetitionNotFoundException(); + } + } + + /** + * Adds the title argument to the query. + */ + protected function addTitleToQuery() { + if ($this->getTitle()) { + $this->addQueryParam(PETITION_SOLR_FIELD_TITLE, $this->getTitle(), FALSE); + } + } + + /** + * Adds the issues argument to the query. + */ + protected function addIssuesToQuery() { + if ($this->getIssueIDs()) { + $tid_expression = implode(' OR ', $this->getIssueIDs()); + $this->params['fq'][] = PETITION_SOLR_FIELD_ISSUE_TIDS . ":({$tid_expression})"; + } + } + + /** + * Adds User ID to the query. + */ + protected function addUidToQuery() { + $uid = $this->getUid(); + if (!empty($uid)) { + $this->addQueryParam(PETITION_SOLR_FIELD_UID, $uid); + } + } + + /** + * Adds the url argument to the query. + */ + protected function addUrlToQuery() { + $url = $this->getURL(); + if ($url) { + $path = ltrim(parse_url($url, PHP_URL_PATH), '/'); + + // Invalid URL or no path. + if ($path === FALSE) { + throw new PetitionNotFoundException(); + } + + if (petition_is_legacy_path($path)) { + $this->addQueryParam(PETITION_SOLR_FIELD_LEGACY_PATH, $path);; + } + else { + $this->addQueryParam(PETITION_SOLR_FIELD_PATH, $path);; + } + } + } + + /** + * Adds a Solr search parameter. + * + * @param string $field + * The field to query. + * @param string $value + * The value to query for. + * @param bool $exact + * Whether to require an exact match (a filter query) or not (an ordinary + * query). A filter query can be cached, so an exact search is more + * performant. Defaults to TRUE. + */ + private function addQueryParam($field, $value, $exact = TRUE) { + $query_type = $exact ? 'fq' : 'q'; + $this->params[$query_type][] = "{$field}:{$value}"; + } + + /** + * Executes the query. + * + * @return object + * A search result response object. + */ + protected function executeQuery() { + $query = new SolrBaseQuery('petitions_data', apachesolr_get_solr(), $this->params, $this->sortString); + $result = $query->search(); + // @todo Handle a non-200 response code. + return $result->response; + } + + /** + * Builds the results arrays from the query results. + * + * @param object $response + * A search result response object, from the return value from + * DrupalSolrQueryInterface::search(). + */ + protected function buildResults($response) { + $result_objects = array(); + $result_arrays = array(); + foreach ($response->docs as $doc) { + $issues = array(); + foreach ($doc->{PETITION_SOLR_FIELD_ISSUE_TIDS} as $delta => $tid) { + $issues[] = array( + 'id' => $tid, + 'name' => petitions_data_sanitize_output($doc->{PETITION_SOLR_FIELD_ISSUE_NAMES}[$delta]), + ); + } + + $petition_response = array(); + if (!empty($doc->{PETITION_SOLR_FIELD_RESPONSE_ID})) { + $petition_response = array( + 'id' => $doc->{PETITION_SOLR_FIELD_RESPONSE_ID}, + 'url' => petitions_data_url("node/{$doc->{PETITION_SOLR_FIELD_RESPONSE_ID}}"), + 'associationTime' => $doc->{PETITION_SOLR_FIELD_TIMESTAMP_RESPONSE_ASSOCIATED}, + ); + } + + $timestamp_published = NULL; + if (!empty($doc->{PETITION_SOLR_FIELD_TIMESTAMP_PUBLISHED})) { + $timestamp_published = $doc->{PETITION_SOLR_FIELD_TIMESTAMP_PUBLISHED}; + } + + $timestamp_reached_public = NULL; + if (!empty($doc->{PETITION_SOLR_FIELD_TIMESTAMP_REACHED_PUBLIC})) { + $timestamp_reached_public = $doc->{PETITION_SOLR_FIELD_TIMESTAMP_REACHED_PUBLIC}; + } + + $petition = new PetitionItem(); + $petition + ->setEntityId($doc->{PETITION_SOLR_FIELD_NODE_ID}) + ->setLegacyId($doc->{PETITION_SOLR_FIELD_LEGACY_ID}) + ->setTitle(static::sanitizeSolrString($doc->{PETITION_SOLR_FIELD_TITLE})) + ->setBody(static::sanitizeSolrString($doc->{PETITION_SOLR_FIELD_BODY})) + ->setIssues($issues) + ->setNiceUrl($doc->{PETITION_SOLR_FIELD_PATH}) + ->setReviewTimeframe($doc->{PETITION_SOLR_FIELD_REVIEW_TIMEFRAME}) + ->setSignatureCount($doc->{PETITION_SOLR_FIELD_SIGNATURE_COUNT}) + ->setSignatureThreshold($doc->{PETITION_SOLR_FIELD_SIGNATURE_RESPONSE_THRESHOLD}) + ->setStatus($doc->{PETITION_SOLR_FIELD_PETITION_STATUS}) + ->setResponse($petition_response) + ->setCreated(strtotime($doc->{PETITION_SOLR_FIELD_TIMESTAMP_CREATED})) + ->setPublished($timestamp_published) + ->setUid($doc->{PETITION_SOLR_FIELD_UID}) + ->setReachedPublic($timestamp_reached_public) + ->setSignaturePublicThreshold($doc->{PETITION_SOLR_FIELD_SIGNATURE_PUBLIC_THRESHOLD}) + ->setResponseStatus($doc->{PETITION_SOLR_FIELD_RESPONSE_STATUS}); + + $result_objects[] = $petition; + $result_arrays[] = $petition->toRestResponseItemArray(); + } + + $this->setResultObjects($result_objects); + $this->setResult($result_arrays); + $this->setCount($response->numFound); + } + + /** + * Sanitizes string values retrieved from the Solr index. + * + * @param string $value + * The value to sanitize. + * + * @return string + * The sanitized string. + */ + public static function sanitizeSolrString($value) { + // @todo Don't store HTML entities encoded in the first place. + $decoded_value = html_entity_decode($value, ENT_QUOTES); + + return petitions_data_sanitize_output($decoded_value); + } + +} diff --git a/modules/custom/petitions_data/classes/SelectQueryBase.inc b/modules/custom/petitions_data/classes/SelectQueryBase.inc index f7895bbe9..3d4e369b5 100644 --- a/modules/custom/petitions_data/classes/SelectQueryBase.inc +++ b/modules/custom/petitions_data/classes/SelectQueryBase.inc @@ -2,7 +2,7 @@ /** * @file - * Defines SelectQueryBase. + * Contains SelectQueryBase. */ /** @@ -17,7 +17,43 @@ abstract class SelectQueryBase { protected $maxReturnLimit; protected $offset; protected $result; + protected $resultObjects; protected $count; + protected $orderBy = array(); + + const SELECT_QUERY_ORDER_BY_ASC = 'ASC'; + const SELECT_QUERY_ORDER_BY_DESC = 'DESC'; + + /** + * These constants are used by the setOrderBy function. + * Mapping is done in *SelectQueryMongo.inc and + * *SelectQueryMysql.inc classes to determine the column names + * for each one of these constants. + */ + + const SELECT_QUERY_ORDER_BY_FIELD_ID = 'ID'; + const SELECT_QUERY_ORDER_BY_FIELD_TITLE = 'TITLE'; + const SELECT_QUERY_ORDER_BY_FIELD_DATE_CREATED = 'DATE_CREATED'; + + protected $validOrderByDirections = array( + self::SELECT_QUERY_ORDER_BY_ASC, + self::SELECT_QUERY_ORDER_BY_DESC, + ); + + + /** + * Valid common fields for order by + * + * NOTE: Adding any additional fields requires adding of indexes to database. + * Use of a new field without indexes will cause ADVERSE performance issues. + * + * @var array + */ + protected $validOrderByFields = array( + self::SELECT_QUERY_ORDER_BY_FIELD_ID, + self::SELECT_QUERY_ORDER_BY_FIELD_TITLE, + self::SELECT_QUERY_ORDER_BY_FIELD_DATE_CREATED, + ); /** * Constructor. This must be explicitly called by subclasses. @@ -161,6 +197,143 @@ abstract class SelectQueryBase { return $this; } + /** + * Gets the result of the query as an array of objects. + * + * @return PetitionItem[] | SignatureItem[] + * An array of PetitionItem or SignatureItem objects. + */ + public function getResultObjects() { + return $this->resultObjects; + } + + /** + * Sets the result of the query as an array of objects. + * + * @param PetitionItem[] $result_objects + * An array of PetitionItem objects. + * + * @return $this + */ + protected function setResultObjects(array $result_objects) { + $this->resultObjects = $result_objects; + return $this; + } + + /** + * Get accessor for $this->orderBy. + * + * @return array + * The array of orderBy stdClass objects. + * orderBy objects contain properties field and direction + */ + public function getOrderBy() { + return $this->orderBy; + } + + /** + * Set accessor for $this->orderBy. + * + * @param string $order_by_field + * Field to order by, such as SelectQueryBase::SELECT_QUERY_ORDER_BY_FIELD_TITLE + * + * @param string $order_by_dir + * Order by direction, such as SelectQueryBase::SELECT_QUERY_ORDER_BY_DESC + * + * @param bool $append + * If TRUE, will append to orderBy array, otherwise will replace + * + * @return SelectQueryBase + * Returns current instance of object. + */ + public function setOrderBy($order_by_field, $order_by_dir, $append = FALSE) { + $order_by_obj = new stdClass(); + $order_by_obj->field = $order_by_field; + $order_by_obj->direction = $order_by_dir; + + if ($append) { + $this->orderBy = array_merge($this->orderBy, array($order_by_obj)); + } + else { + $this->orderBy = array($order_by_obj); + } + return $this; + } + + /** + * Getter accessor for $this->validOrderByFields. + * + * @return array + * The array of validOrderByFields + */ + public function getValidOrderByFields() { + return $this->validOrderByFields; + } + + /** + * Set accessor for $this->validOrderByFields. + * + * @param array $order_by_fields + * Array of valid fields. + * + * @return SelectQueryBase + * Returns current instance of object. + */ + protected function setValidOrderByFields($order_by_fields) { + $this->validOrderByFields = $order_by_fields; + return $this; + } + + /** + * Getter accessor for $this->validOrderByDirections. + * + * @return array + * The array of validOrderByFields + */ + public function getValidOrderByDirections() { + return $this->validOrderByDirections; + } + + /** + * Set accessor for $this->validOrderByDirections. + * + * @param array $order_by_dirs + * Array of valid directions. + * + * @return SelectQueryBase + * Returns current instance of object. + */ + protected function setValidOrderByDirections($order_by_dirs) { + $this->validOrderByDirections = $order_by_dirs; + return $this; + } + + /** + * Checks if field is a valid order by field. + * + * @param string $field + * Field to validate + * + * @return bool + * TRUE if field is valid + */ + protected function isValidOrderByField($field) { + return in_array($field, $this->validOrderByFields); + } + + /** + * Checks if direction is a valid order by ASC/DESC. + * + * @param string $direction + * Direction to validate + * + * @return bool + * TRUE if direction is valid + */ + protected function isValidOrderByDirection($direction) { + return in_array($direction, $this->validOrderByDirections); + } + /** * Executes the query and sets $this->result and $this->count. * diff --git a/modules/custom/petitions_data/classes/Signature.inc b/modules/custom/petitions_data/classes/Signature.inc deleted file mode 100644 index 9c36ab2cc..000000000 --- a/modules/custom/petitions_data/classes/Signature.inc +++ /dev/null @@ -1,354 +0,0 @@ -setPetitionId($petition_id); - } - - /** - * Sets $this->city. - * - * @param string $city - * The city of the signatory. - * - * @return Signature - * Returns current instance of object. - */ - public function setCity($city) { - $this->city = $city; - - return $this; - } - - /** - * Gets $this->city. - * - * @return string - * The city of the signatory. - */ - public function getCity() { - return petitions_data_sanitize_output($this->city); - } - - /** - * Sets $this->created. - * - * @param int $created - * The UNIX timestamp when this signature was created. - * - * @return Signature - * Returns current instance of object. - */ - public function setCreated($created) { - $this->created = $created; - - return $this; - } - - /** - * Gets $this->created. - * - * @return int - * The UNIX timestamp when this signature was created. - */ - public function getCreated() { - return $this->created; - } - - /** - * Sets $this->firstName. - * - * @param string $first_name - * The first name of thet signatory. - * - * @return Signature - * Returns current instance of object. - */ - public function setFirstName($first_name) { - $this->firstName = $first_name; - - return $this; - } - - /** - * Gets $this->firstName. - * - * @return string - * The first name of thet signatory. - */ - protected function getFirstName() { - return $this->firstName; - } - - /** - * Sets $this->id. - * - * @param string $id - * The signature id. - * - * @return Signature - * Returns current instance of object. - */ - public function setId($id) { - $this->id = $id; - - return $this; - } - - /** - * Gets $this->id. - * - * @return string - * The signature id. - */ - public function getId() { - return $this->id; - } - - /** - * Sets $this->lastName. - * - * @param string $last_name - * The last name of the signatory. - * - * @return Signature - * Returns current instance of object. - */ - public function setLastName($last_name) { - $this->lastName = $last_name; - - return $this; - } - - /** - * Get $this->lastName. - * - * @return string - * The last name of the signatory. - */ - protected function getLastName() { - return $this->lastName; - } - - /** - * Gets $this->name. - * - * @return string - * The name of the signatory. - * - * @throws InvalidArgumentException. - */ - public function getName() { - return $this->formatName($this->getFirstName(), $this->getLastName()); - } - - /** - * Helper function to format first and last name. - * - * @param string $first_name - * First name of petition signer. - * @param string $last_name - * Last name of petition signer. - * - * @return string - * Formatted name. - */ - protected function formatName($first_name, $last_name) { - if (module_exists('petitions_signatures_display')) { - if (variable_get('petitions_data_signatures_display_names', 0)) { - $name = petitions_data_sanitize_output(petitions_signatures_display_style($first_name, $last_name)); - } - } - else { - $first_name = petitions_data_sanitize_output($first_name); - $last_name = petitions_data_sanitize_output($last_name); - - // Grab first letter of first and last name. - $name = substr($first_name, 0, 1) . substr($last_name, 0, 1); - } - - return (string) $name; - } - - /** - * Sets $this->state. - * - * @param string $state - * The state of the signatory. - * - * @return Signature - * Returns current instance of object. - */ - public function setState($state) { - $this->state = $state; - - return $this; - } - - /** - * Gets $this->state. - * - * @return string - * The state of the signatory. - */ - public function getState() { - return petitions_data_sanitize_output($this->state); - } - - /** - * Sets $this->uid. - * - * @param int $uid - * The signatory user's uid. - * - * @return Signature - * Returns current instance of object. - */ - public function setUid($uid) { - $this->uid = $uid; - - return $this; - } - - /** - * Gets $this->uid. - * - * @return int - * The signatory user's uid. - */ - public function getUid() { - return $this->uid; - } - - /** - * Sets $this->user. - * - * @param object $user - * The signatory user. Either $user->name or $user->uid must be set. - * - * @return Signature - * Returns current instance of object. - * - * @throws InvalidArgumentException() - */ - public function setUser($user) { - if (!isset($user->name) && !isset($user->uid)) { - throw new InvalidArgumentException('$user->name and $user->uid must be set.'); - } - $this->user = $user; - - return $this; - } - - /** - * Gets $this->user. - * - * @return obj - * The signatory user. - * - * @throws InvalidArgumentException() - */ - public function getUser() { - if (!$this->user) { - if (isset($this->uid)) { - $this->setUser(user_load($this->uid)); - } - else { - throw new InvalidArgumentException('$this->user is not set.'); - } - } - - return $this->user; - } - - /** - * Sets $this->zip. - * - * @param string $zip - * The zip code of the signatory. - * - * @return Signature - * Returns current instance of object. - */ - public function setZip($zip) { - $this->zip = $zip; - - return $this; - } - - /** - * Get $this->zip. - * - * @return string - * The zip code of the signatory. - */ - public function getZip() { - return petitions_data_sanitize_output($this->zip); - } - - /** - * Sets $this->petitionId. - * - * @param string $petition_id - * The petition id to which this signature belongs. - */ - public function setPetitionId($petition_id) { - $this->petitionId = $petition_id; - } - - /** - * Gets $this->petitionId. - * - * @return string - * The petition id to which this signature belongs. - */ - public function getPetitionId() { - return $this->petitionId; - } - - /** - * Converts into a publicly consumable array. - * - * @return array - * An array to be used for public display. - */ - public function toArray() { - - $output = array( - 'id' => $this->getPetitionId(), - 'type' => 'signature', - 'name' => $this->getName(), - 'city' => $this->getCity(), - 'state' => $this->getState(), - 'zip' => $this->getZip(), - 'created' => $this->getCreated(), - ); - - return $output; - } - -} diff --git a/modules/custom/petitions_data/classes/SignatureItem.inc b/modules/custom/petitions_data/classes/SignatureItem.inc new file mode 100644 index 000000000..06f29d4e5 --- /dev/null +++ b/modules/custom/petitions_data/classes/SignatureItem.inc @@ -0,0 +1,573 @@ +city. + * + * @param string $city + * The city of the signatory. + * + * @return $this + */ + public function setCity($city) { + $this->city = $city; + + return $this; + } + + /** + * Gets $this->city. + * + * @return string + * The city of the signatory. + */ + public function getCity() { + return petitions_data_sanitize_output($this->city); + } + + /** + * Sets $this->created. + * + * @param int $created + * The UNIX timestamp when this signature was created. + * + * @return $this + */ + public function setCreated($created) { + $this->created = $created; + + return $this; + } + + /** + * Gets $this->created. + * + * @return int + * The UNIX timestamp when this signature was created. + */ + public function getCreated() { + return $this->created; + } + + /** + * Sets $this->firstName. + * + * @param string $first_name + * The first name of the signatory. + * + * @return $this + */ + public function setFirstName($first_name) { + $this->firstName = $first_name; + + return $this; + } + + /** + * Gets $this->firstName. + * + * @return string + * The first name of the signatory. + */ + public function getFirstName() { + return $this->firstName; + } + + /** + * Sets $this->id. + * + * @param string $id + * The signature id. + * + * @return $this + */ + public function setId($id) { + if (!empty($id)) { + $this->id = $id; + } + + return $this; + } + + /** + * Sets the IP from which the request to create the signature originated. + * + * @param string $ip_address + * An IP address. + * + * @throws InvalidArgumentException + * Throws an exception in case of an invalid argument. + * + * @return $this + */ + public function setIpAddress($ip_address) { + if (!filter_var($ip_address, FILTER_VALIDATE_IP)) { + throw new InvalidArgumentException('Invalid IP address.'); + } + $this->ipAddress = $ip_address; + return $this; + } + + /** + * Gets the IP from which the request to create the signature originated. + * + * @return string|null + * Returns the IP address the request to create the signature originated + * from if set or NULL if not. + */ + public function getIpAddress() { + return $this->ipAddress; + } + + /** + * Sets $this->legacyId. + * + * @param string $legacy_id + * The signature id. + * + * @return $this + */ + public function setLegacyId($legacy_id) { + $this->legacyId = $legacy_id; + + return $this; + } + + /** + * Gets the canonical ID. + * + * If there is a legacy ID, that is used, for API backward compatibility. + * + * @return string + * The signature id. + */ + public function getId() { + if (!is_null($this->legacyId)) { + return $this->legacyId; + } + else { + return $this->id; + } + } + + /** + * Gets the legacy ID. + * + * @return string|null + * Returns the signature's legacy ID if set or NULL if not. + */ + protected function getLegacyId() { + return (!empty($this->legacyId)) ? $this->legacyId : NULL; + } + + /** + * Gets $this->legacyId. + * + * @return string + * Returns the signature's legacy ID if set or an empty string if not. + */ + public function getEntityId() { + return (!empty($this->id)) ? $this->id : ''; + } + + /** + * Sets $this->lastName. + * + * @param string $last_name + * The last name of the signatory. + * + * @return $this + */ + public function setLastName($last_name) { + $this->lastName = $last_name; + + return $this; + } + + /** + * Get $this->lastName. + * + * @return string + * The last name of the signatory. + */ + public function getLastName() { + return $this->lastName; + } + + /** + * Gets $this->name. + * + * @return string + * The name of the signatory. + */ + public function getName() { + return $this->formatName($this->getFirstName(), $this->getLastName()); + } + + /** + * Formats a name. + * + * @param string $first_name + * The first name. + * @param string $last_name + * The first name. + * + * @return string + * Returns the formatted name. + */ + protected function formatName($first_name, $last_name) { + // Use a display style, if available. + if (variable_get('petitions_data_signatures_display_names', 0)) { + $name = petitions_data_sanitize_output(petitions_signatures_display_style($first_name, $last_name)); + } + // Otherwise use first and last initials (e.g., "JD" for "John Doe"). + else { + $first_initial = strtoupper(substr(petitions_data_sanitize_output($first_name), 0, 1)); + $last_initial = strtoupper(substr(petitions_data_sanitize_output($last_name), 0, 1)); + $name = $first_initial . $last_initial; + } + + return (string) $name; + } + + /** + * Sets $this->state. + * + * @param string $state + * The state of the signatory. + * + * @return $this + */ + public function setState($state) { + $this->state = $state; + + return $this; + } + + /** + * Gets $this->state. + * + * @return string + * The state of the signatory. + */ + public function getState() { + return petitions_data_sanitize_output($this->state); + } + + /** + * Sets $this->uid. + * + * @param int $uid + * The signatory user's uid. + * + * @return $this + */ + public function setUid($uid) { + $this->uid = $uid; + + return $this; + } + + /** + * Gets $this->uid. + * + * @return int + * The signatory user's uid. + */ + public function getUid() { + return $this->uid; + } + + /** + * Sets the user agent used to create the signature. + * + * @param string $user_agent + * A string corresponding to the User-Agent header sent with the request to + * create the signature. + * + * @throws InvalidArgumentException + * Throws an exception in case of an invalid argument. + * + * @return $this + */ + public function setUserAgent($user_agent) { + if (!is_string($user_agent)) { + throw new InvalidArgumentException('Invalid user agent.'); + } + $this->userAgent = $user_agent; + return $this; + } + + /** + * Gets the user agent used to create the signature. + * + * @return string|null + * Returns a string corresponding to the User-Agent header sent with the + * request to create the signature if set or NULL if not. + */ + public function getUserAgent() { + return $this->userAgent; + } + + /** + * Sets the country of residence of the signatory. + * + * @param string $user_country + * The signatory's country of residence. + * + * @throws InvalidArgumentException + * Throws an exception in case of an invalid argument. + * + * @return $this + */ + public function setUserCountry($user_country) { + if (!is_string($user_country)) { + throw new InvalidArgumentException('Invalid user country.'); + } + $this->userCountry = $user_country; + return $this; + } + + /** + * Gets the country of residence of the signatory. + * + * @return string + * Returns the signatory's country of residence if set or NULL if not. + */ + public function getUserCountry() { + return $this->userCountry; + } + + /** + * Sets $this->user. + * + * @param object $user + * The signatory user. Either $user->name or $user->uid must be set. + * + * @return $this + * + * @throws InvalidArgumentException() + * Throws an exception in case of missing user name or UID variables. + */ + public function setUser($user) { + if (!isset($user->name) && !isset($user->uid)) { + throw new InvalidArgumentException('$user->name and $user->uid must be set.'); + } + $this->user = $user; + + return $this; + } + + /** + * Gets $this->user. + * + * @return object + * The signatory user. + * + * @throws InvalidArgumentException() + * Throws an exception in case of missing user object. + */ + public function getUser() { + if (!$this->user) { + if (isset($this->uid)) { + $this->setUser(user_load($this->uid)); + } + else { + throw new InvalidArgumentException('$this->user is not set.'); + } + } + + return $this->user; + } + + /** + * Sets $this->zip. + * + * @param string $zip + * The zip code of the signatory. + * + * @return $this + */ + public function setZip($zip) { + $this->zip = $zip; + + return $this; + } + + /** + * Get $this->zip. + * + * @return string + * The zip code of the signatory. + */ + public function getZip() { + return petitions_data_sanitize_output($this->zip); + } + + /** + * Sets $this->petitionId. + * + * @param string $petition_id + * The petition id to which this signature belongs. + * + * @return $this + */ + public function setPetitionId($petition_id) { + // This test is necessary during the Mongo-to-MySQL transition to avoid + // retrofitting legacy client code to know the difference between entity IDs + // and legacy IDs. + if (petition_is_legacy_id($petition_id)) { + $this->legacyPetitionId = $petition_id; + } + else { + $this->petitionId = $petition_id; + } + return $this; + } + + /** + * Sets $this->legacyPetitionId. + * + * @param string $legacy_petition_id + * The petition id to which this signature belongs. + * + * @return $this + */ + public function setLegacyPetitionId($legacy_petition_id) { + $this->legacyPetitionId = $legacy_petition_id; + return $this; + } + + /** + * Gets the canonical ID of the petition. + * + * If there is a legacy petition ID, that is used, for API backward + * compatibility. + * + * @param bool $legacy_primary + * When TRUE, will return legacyPetitionId first otherwise fallback to petitionId if null. + * When FALSE, will return petitionId first otherwise fallback to legacyPetitionId if null. + * + * @return string + * The petition id to which this signature belongs. + */ + public function getPetitionId($legacy_primary = TRUE) { + if ($legacy_primary) { + if (!is_null($this->legacyPetitionId)) { + return $this->legacyPetitionId; + } + else { + return $this->petitionId; + } + } + else { + if (!is_null($this->petitionId)) { + return $this->petitionId; + } + else { + return $this->legacyPetitionId; + } + } + } + + /** + * Gets the legacy petition ID. + * + * @return string + * Returns the signature's legacy petition ID if set or an empty string if + * not. + */ + public function getLegacyPetitionId() { + return (!empty($this->legacyPetitionId)) ? $this->legacyPetitionId : ''; + } + + /** + * Gets the petition node ID (nid). + * + * @return int + * Returns the signature's petition node ID (nid) if set or 0 if not. + */ + protected function getPetitionEntityId() { + return (!empty($this->petitionId)) ? $this->petitionId : 0; + } + + /** + * Gets the signature in the form of a REST response item array. + * + * @return array + * An array as used by the REST API. + */ + public function toRestResponseItemArray() { + + $output = array( + 'id' => $this->getId(), + 'type' => 'signature', + 'petitionId' => $this->getPetitionId(), + 'name' => $this->getName(), + 'city' => $this->getCity(), + 'state' => $this->getState(), + 'zip' => $this->getZip(), + 'created' => $this->getCreated(), + ); + + return $output; + } + + /** + * Creates a Signature entity from the object. + * + * @return Signature + * Returns a Signature entity created from the object. + */ + public function toEntity() { + $values = array( + 'id' => $this->getEntityId(), + 'uid' => $this->getUid(), + 'petition_id' => $this->getPetitionEntityId(), + 'user_first_name' => $this->getFirstName(), + 'user_last_name' => $this->getLastName(), + 'legacy_id' => $this->getLegacyId(), + 'legacy_petition_id' => $this->getLegacyPetitionId(), + 'timestamp' => $this->getCreated(), + 'user_agent' => $this->getUserAgent(), + 'ip_address' => $this->getIpAddress(), + 'user_city' => $this->getCity(), + 'user_state' => $this->getState(), + 'user_zip' => $this->getZip(), + 'user_username' => $this->getUser()->name, + 'user_country' => $this->getUserCountry(), + ); + // If it already has an entity ID, it's an UPDATE not an INSERT. + if ($this->getEntityId()) { + $values['is_new'] = FALSE; + } + return entity_create('signature_mail', $values); + } + +} diff --git a/modules/custom/petitions_data/classes/SignaturesController.inc b/modules/custom/petitions_data/classes/SignaturesController.inc index b2a3aef60..1a732b0e7 100644 --- a/modules/custom/petitions_data/classes/SignaturesController.inc +++ b/modules/custom/petitions_data/classes/SignaturesController.inc @@ -2,35 +2,181 @@ /** * @file - * Defines SignaturesController class. + * Contains SignaturesController. */ /** - * Class SignaturesController + * Class SignaturesController. * - * Provides methods for interacting with Signature data objects. + * Provides methods for interacting with SignatureItem objects. */ class SignaturesController { /** * Saves a signature. * - * @param Signature $signature + * @param SignatureItem $signature * The signature to be saved. This should have the $petitionId and $uid * properties defined. * - * @return Signature - * Returns Signature object if successful. + * @return SignatureItem|false + * Returns the given SignatureItem, augmented with any newly-given IDs, if + * successful or FALSE if not. * * @throws InvalidArgumentException + * Throws an exception in case of an incomplete signature given. */ - public static function save($signature) { + public static function save(SignatureItem $signature) { if (!$signature->getPetitionId()) { - throw new InvalidArgumentException('The $this->petitionId and $this->uid or $this->user properties must be set before saving a signature.'); + throw new InvalidArgumentException('Cannot save signature without a petition ID (either a legacy ID or an entity ID).'); } - $signature_id = wh_petitions_mongo2mysql_create_new_signature($signature->getPetitionId(), $signature->getUser(), ip_address()); - $signature->setId($signature_id); + if (!$signature->getUid() && !$signature->getUser()) { + throw new InvalidArgumentException('Cannot save signature without a user (either a "user" or "uid" property).'); + } + + // Fail if the signature does not specify a valid petition. + $petition = PetitionsController::load($signature->getPetitionId()); + if (empty($petition)) { + return FALSE; + } + + if (self::isDuplicate($signature)) { + return FALSE; + } + + // Add city, state, and country if missing. + static::ensureLocationDetails($signature); + + // The Mongo save must come first because it adds a legacy ID which the + // MySQL save will use if present. + if (petitions_data_mongo_writes_are_enabled()) { + $signature = self::saveToMongo($signature); + } + + if (petitions_data_mysql_writes_are_enabled()) { + $signature = self::saveToMysql($signature); + } + + return $signature; + } + + /** + * Determines whether a given a signature is a duplicate. + * + * @param SignatureItem $signature + * A signature to test for duplicity. + * + * @return bool + * Returns TRUE if the given signature is a duplicate or FALSE if not. + */ + protected static function isDuplicate(SignatureItem $signature) { + $duplicates = SignaturesSelectQueryFactory::create() + ->setPetitionId($signature->getPetitionId()) + ->setUid($signature->getUid()) + ->execute() + ->getResult(); + return (bool) $duplicates; + } + + /** + * Adds location details to a given signature if missing. + * + * @param SignatureItem $signature + * The signature to be augmented. + */ + protected static function ensureLocationDetails(SignatureItem &$signature) { + if ($signature->getCity() && $signature->getState() && $signature->getUserCountry()) { + // All location data is already present. + return; + } + + if (!$signature->getZip()) { + // No additional location data can be retrieved without a Zip-code. + return; + } + + $locations = wh_zipcodelookup_get_location_details_from_zipcode($signature->getZip()); + if (!empty($locations[0])) { + $signature + ->setCity($locations[0]['city']) + ->setState($locations[0]['state']) + ->setUserCountry($locations[0]['country']); + } + + } + + /** + * Saves a signature to MongoDB. + * + * @param SignatureItem $signature + * The signature to be saved. + * + * @return SignatureItem + * The given signature, augmented with its newly-given legacy ID. + */ + protected static function saveToMongo(SignatureItem $signature) { + // Save the signature. + $legacy_id = wh_petitions_mongo2mysql_create_new_signature($signature->getPetitionId(), $signature->getUser(), $signature->getIpAddress()); + + // If successful, save the new legacy ID back into the SignatureItem. + if ($legacy_id) { + $signature->setLegacyId($legacy_id); + } + // Otherwise log failure. + else { + watchdog('petitions_data', 'Failed to save signature to MongoDB in SignaturesController::saveToMongo: !entity', array( + '!entity' => petitionslog_format_for_watchdog($signature->toEntity()), + )); + } + + return $signature; + } + + /** + * Saves a signature to MySQL. + * + * @param SignatureItem $signature + * The signature to be saved. + * + * @return SignatureItem + * The given signature, augmented with its newly-given legacy ID. + */ + protected static function saveToMysql(SignatureItem $signature) { + // If mongo reads are enabled, we won't have the nid of the petition. + // Try to get it and add to signature if it exists. + if (petitions_data_mongo_reads_are_enabled()) { + $legacy_petition_id = $signature->getLegacyPetitionId(); + if ($legacy_petition_id != '') { + $petition_nid = petition_get_nid($legacy_petition_id); + if ($petition_nid !== FALSE && $petition_nid != 0) { + $signature->setPetitionId($petition_nid); + } + } + } + // Create and save the entity. + $entity = $signature->toEntity(); + try { + $success = $entity->save(); + } + catch (Exception $e) { + watchdog('petitions_data', 'Failed to save signature to MySQL in SignaturesController::saveToMySQL. Signature: !entity Exception: !exception', array( + '!entity' => petitionslog_format_for_watchdog($entity), + '!exception' => petitionslog_format_for_watchdog($e), + )); + return $signature; + } + + // If successful, save the new entity ID back into the SignatureItem. + if ($success) { + $signature->setId($entity->id); + } + // Otherwise log failure. + else { + watchdog('petitions_data', 'Failed to save signature to MySQL in SignaturesController::saveToMySQL. Signature: !entity', array( + '!entity' => petitionslog_format_for_watchdog($entity), + )); + } return $signature; } @@ -40,12 +186,16 @@ class SignaturesController { * * @param string $sid * The signature id. + + * @param bool $realtime + * Whether or not realtime accuracy is required. See + * SignatureSelectQueryFactory::create() for details. Defaults to TRUE. * - * @return Signature - * The loaded signature. + * @return SignatureItem + * The loaded SignatureItem. */ - public static function load($sid) { - return SignaturesController::loadMultiple(array($sid)); + public static function load($sid, $realtime = TRUE) { + return SignaturesController::loadMultiple(array($sid), $realtime); } /** @@ -53,12 +203,16 @@ class SignaturesController { * * @param array $sids * An array signature ids. + + * @param bool $realtime + * Whether or not realtime accuracy is required. See + * SignatureSelectQueryFactory::create() for details. Defaults to TRUE. * * @return array - * An array of Signature objects. + * An array of SignatureItem objects. */ - public static function loadMultiple(array $sids) { - $signatures_query = SignaturesSelectQueryFactory::create(); + public static function loadMultiple(array $sids, $realtime = TRUE) { + $signatures_query = SignaturesSelectQueryFactory::create($realtime); $signatures = $signatures_query->setSignatureIds($sids) ->execute()->getResult(); diff --git a/modules/custom/petitions_data/classes/SignaturesSelectQuery.inc b/modules/custom/petitions_data/classes/SignaturesSelectQuery.inc index 0b64da198..d17636a5f 100644 --- a/modules/custom/petitions_data/classes/SignaturesSelectQuery.inc +++ b/modules/custom/petitions_data/classes/SignaturesSelectQuery.inc @@ -2,11 +2,11 @@ /** * @file - * Defines SignaturesQuery class. + * Contains SignaturesQuery. */ /** - * Class SignaturesQuery. + * Class SignaturesSelectQuery. * * This should contain methods for querying signatures that are NOT specific to * a backend, e.g., MongoDB. @@ -20,13 +20,69 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { protected $startDate; protected $endDate; protected $createdDate; + protected $signatureIds; protected $petitionIds; + protected $uid; + protected $orderByFieldsMap; + protected $orderByDirMap; /** - * Constructor. This must be explicitly called by subclasses. + * These constants are used by the setOrderBy function. + * Mapping is done in SignaturesSelectQueryMysql.inc and + * SignaturesSelectQuerySolr.inc to determine the column names + * for each one of these constants. + */ + const SELECT_QUERY_ORDER_BY_FIELD_TIMESTAMP = 'TIMESTAMP'; + + /** + * Valid signature fields for order by + * + * NOTE: Adding any additional fields requires adding of indexes to database. + * Use of a new field without indexes will cause ADVERSE performance issues. + * + * @var array + */ + protected $validSignaturesOrderByFields = array( + self::SELECT_QUERY_ORDER_BY_FIELD_TIMESTAMP, + ); + + /** + * {@inheritdoc} */ public function __construct() { parent::__construct(); + // Merge signature specific order by fields with common order by fields from SelectQueryBase. + $this->setValidOrderByFields(array_merge($this->getValidOrderByFields(), $this->getValidSignaturesOrderByFields())); + } + + /** + * Getter accessor for $this->validSignaturesOrderByFields. + * + * @return array + * The array of validSignaturesOrderByFields + */ + public function getValidSignaturesOrderByFields() { + return $this->validSignaturesOrderByFields; + } + + /** + * Get accessor for $this->orderByFieldsMap. + * + * @return array + * Array of orderByFieldsMap + */ + public function getOrderByFieldsMap() { + return $this->orderByFieldsMap; + } + + /** + * Get accessor for $this->orderByDirMap. + * + * @return array + * Array of orderByDirMap + */ + public function getOrderByDirMap() { + return $this->orderByDirMap; } /** @@ -45,8 +101,7 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param string $city * City name for signature. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ public function setCity($city) { $this->city = $city; @@ -70,8 +125,7 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param string $state * State name for signature. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ public function setState($state) { $this->state = $state; @@ -79,6 +133,32 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { return $this; } + /** + * Get accessor for $this->uid. + * + * @return int + * A user UID if set or FALSE if not. + */ + protected function getUid() { + return $this->uid; + } + + /** + * Set accessor for $this->uid. + * + * @param int $uid + * A user UID. + * + * @return $this + */ + public function setUid($uid) { + if (!empty($uid)) { + $this->uid = (int) $uid; + } + + return $this; + } + /** * Get accessor for $this->zipcode. * @@ -95,8 +175,7 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param int $zipcode * Postal code to identify locations within the United States. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ public function setZipCode($zipcode) { if (is_numeric($zipcode)) { @@ -122,8 +201,7 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param string $country * Country name for signature. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ public function setCountry($country) { $this->country = $country; @@ -147,8 +225,7 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param int $date * Epoch (UNIX style) time stamp. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ public function setStartDate($date) { if (is_numeric($date)) { @@ -174,8 +251,7 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param int $date * Epoch (UNIX style) time stamp. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ public function setEndDate($date) { if (is_numeric($date)) { @@ -191,8 +267,7 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param int $date * Epoch (UNIX style) time stamp. * - * @return PetitionsQuery - * Returns current instance of object. + * @return $this */ public function setCreatedAt($date) { if (is_numeric($date)) { @@ -215,10 +290,9 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * Set accessor for $this->petitionIds. * * @param array $pids - * An array of petition ids. + * An array of petition IDs. * - * @return PetitionsQuery - * Returns current instance of object. + * @return $this */ public function setPetitionIds(array $pids) { $this->petitionIds = $pids; @@ -242,12 +316,49 @@ abstract class SignaturesSelectQuery extends SelectQueryBase { * @param string $pid * A petition ids. * - * @return PetitionsQuery - * Returns current instance of object. + * @return $this */ public function setPetitionId($pid) { $this->setPetitionIds(array($pid)); return $this; } + + /** + * Set accessor for $this->signatureIds. + * + * @param array $sids + * An array of signature IDs. + * + * @return $this + */ + public function setSignatureIds(array $sids) { + $this->signatureIds = $sids; + + return $this; + } + + /** + * Get accessor for $this->signatureIds. + * + * @return array + * Array of signatureIds. + */ + public function getSignatureIds() { + return $this->signatureIds; + } + + /** + * Pass through to set $this->signatureIds. + * + * @param string $sid + * A signature id. + * + * @return $this + */ + public function setSignatureId($sid) { + $this->setSignatureIds(array($sid)); + + return $this; + } } diff --git a/modules/custom/petitions_data/classes/SignaturesSelectQueryFactory.inc b/modules/custom/petitions_data/classes/SignaturesSelectQueryFactory.inc index 59fb4699d..ccb6c1f07 100644 --- a/modules/custom/petitions_data/classes/SignaturesSelectQueryFactory.inc +++ b/modules/custom/petitions_data/classes/SignaturesSelectQueryFactory.inc @@ -1,21 +1,39 @@ setCollection(mongodb_collection('petition_signatures')); + try { + $collection = mongodb_collection('petition_signatures'); + } + catch (Exception $e) { + petitionslog_event('exception.petitions_data.c968b66'); + } + $this->setCollection($collection); } /** @@ -39,8 +45,7 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { * @param MongoCollection $collection * MongoCollection resource for querying against a collection. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ protected function setCollection($collection) { $this->collection = $collection; @@ -64,8 +69,7 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { * @param array $query * Array of query parameters to get passed to mongodb. * - * @return SignaturesMongoRaw - * Returns current instance of object. + * @return $this */ protected function setQuery(array $query) { $this->query = $query; @@ -80,7 +84,8 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { * Results of query to MongoDB. */ public function execute() { - $results = array(); + $result_arrays = array(); + $result_objects = array(); if ($this->getCreatedDate()) { $this->addCreatedDateToQuery(); @@ -90,11 +95,13 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { $this->addEndDateToQuery(); } + $this->addUidToQuery(); $this->addCityToQuery(); $this->addStateToQuery(); $this->addZipToQuery(); $this->addCountryToQuery(); $this->addPetitionIdsToQuery(); + $this->addSignatureIdsToQuery(); $fields = array( 'uid', @@ -120,8 +127,9 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { ->sort(array('timestamp' => -1)); foreach ($mongo_results as $doc) { - $signature = new Signature($doc['petition_id']); + $signature = new SignatureItem(); $signature + ->setLegacyPetitionId($doc['petition_id']) ->setUid($doc['uid']) ->setId($this->formatReturnId($doc['_id'])) ->setCreated($doc['timestamp']); @@ -143,11 +151,12 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { $signature->setZip($doc['user']['zip']); } - $results[] = $signature->toArray(); + $result_objects[] = $signature; + $result_arrays[] = $signature->toRestResponseItemArray(); } - $this->setCount($mongo_results->count()); - $this->setResult($results); + $this->setResult($result_arrays); + $this->setResultObjects($result_objects); return $this; } @@ -178,6 +187,17 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { } } + /** + * Helper function to add $this->uid as a query parameter. + */ + protected function addUidToQuery() { + if ($this->getUid()) { + $query = $this->getQuery(); + $query += array('uid' => $this->getUid()); + $this->setQuery($query); + } + } + /** * Helper function to add $this->zipcode as a query parameter. */ @@ -264,6 +284,24 @@ class SignaturesSelectQueryMongo extends SignaturesSelectQuery { } } + /** + * Helper function filter results by signature ids. + */ + protected function addSignatureIdsToQuery() { + if ($this->getSignatureIds()) { + $sids = $this->getSignatureIds(); + $mongo_ids = array(); + foreach ($sids as $sid) { + $mongo_ids[] = new MongoId($sid); + } + $query = $this->getQuery(); + $query += array( + '_id' => array('$in' => $mongo_ids), + ); + $this->setQuery($query); + } + } + /** * Helper function to format return response ID field. * diff --git a/modules/custom/petitions_data/classes/SignaturesSelectQueryMysql.inc b/modules/custom/petitions_data/classes/SignaturesSelectQueryMysql.inc new file mode 100644 index 000000000..c6c987981 --- /dev/null +++ b/modules/custom/petitions_data/classes/SignaturesSelectQueryMysql.inc @@ -0,0 +1,266 @@ + 'ASC', + self::SELECT_QUERY_ORDER_BY_DESC => 'DESC', + ); + + /** + * OrderBy MySQL Field mapping + * + * Maps Order By constants to appropriate database column name. + * + * @var array + * An array containing 'field' and 'column' elements. For node columns, only + * 'field' is required. + */ + protected $orderByFieldsMap = array( + self::SELECT_QUERY_ORDER_BY_FIELD_ID => array('field' => 'id'), + self::SELECT_QUERY_ORDER_BY_FIELD_TIMESTAMP => array('field' => 'timestamp'), + ); + + /** + * {@inheritdoc} + */ + public function execute() { + try { + $this->buildQuery(); + $entities = $this->executeQuery(); + $this->buildResult($entities); + } + catch (Exception $e) { + watchdog('petitions_data', 'Exception in SignaturesSelectQueryMysql::execute(): !e', array( + '!e' => petitionslog_format_for_watchdog($e), + )); + } + return $this; + } + + /** + * Builds the query. + */ + protected function buildQuery() { + $this->initializeQuery(); + $this->addArgumentsToQuery(); + } + + /** + * Initializes the basic query. + */ + protected function initializeQuery() { + $this->query = new EntityFieldQueryExtraFields(); + $this->query + ->entityCondition('entity_type', 'signature_mail') + ->addExtraField('', 'legacy_id', 'legacy_id', 'signature_mail') + ->addExtraField('', 'timestamp', 'timestamp', 'signature_mail') + ->addExtraField('', 'petition_id', 'petition_id', 'signature_mail') + ->addExtraField('', 'legacy_petition_id', 'legacy_petition_id', 'signature_mail') + ->addExtraField('', 'uid', 'uid', 'signature_mail') + ->addExtraField('', 'user_username', 'user_username', 'signature_mail') + ->addExtraField('', 'user_first_name', 'user_first_name', 'signature_mail') + ->addExtraField('', 'user_last_name', 'user_last_name', 'signature_mail') + ->addExtraField('', 'user_city', 'user_city', 'signature_mail') + ->addExtraField('', 'user_state', 'user_state', 'signature_mail') + ->addExtraField('', 'user_zip', 'user_zip', 'signature_mail') + ->addExtraField('', 'user_country', 'user_country', 'signature_mail'); + } + + /** + * Adds the orderBy arguments to the query. + */ + protected function addOrderByToQuery() { + if (!$this->getOrderBy()) { + return; + } + + // Order by on signature_mail shouldn't be done in MySQL, throw exception. + throw new Exception('addOrderByToQuery - attempting to use setOrderBy with MySQL on SignatureSelectQuery'); + + // Uncomment below and remove exception to restore order by ability. + + /* + foreach ($this->getOrderBy() as $order_by) { + $field = $order_by->field; + $order_by_dir = $order_by->direction; + if (!$this->isValidOrderByField($field)) { + throw new Exception('addOrderByToQuery - Not a valid field: ' . $field); + } + if (!$this->isValidOrderByDirection($order_by_dir)) { + throw new Exception('addOrderByToQuery - Not a valid direction: ' . $order_by_dir); + } + + $mapped_field = $this->orderByFieldsMap[$field]['field']; + $mapped_dir = $this->orderByDirMap[$order_by_dir]; + $this->query->propertyOrderBy($mapped_field, $mapped_dir); + } + */ + } + + /** + * Adds the supplied arguments to the query. + */ + protected function addArgumentsToQuery() { + $this->addUidToQuery(); + $this->addCityToQuery(); + $this->addStateToQuery(); + $this->addZipcodeToQuery(); + $this->addCountryToQuery(); + $this->addPetitionIdsToQuery(); + $this->addOrderByToQuery(); + $this->addOffsetAndLimitToQuery(); + } + + /** + * Adds the uid argument to the query. + */ + protected function addUidToQuery() { + if ($this->getUid()) { + $this->query->propertyCondition('uid', $this->getUid()); + } + } + + /** + * Adds the city argument to the query. + */ + protected function addCityToQuery() { + if ($this->getCity()) { + $this->query->propertyCondition('user_city', $this->getCity()); + } + } + + /** + * Adds the state argument to the query. + */ + protected function addStateToQuery() { + if ($this->getState()) { + $this->query->propertyCondition('user_state', $this->getState()); + } + } + + /** + * Adds the zipcode argument to the query. + */ + protected function addZipcodeToQuery() { + if ($this->getZipCode()) { + $this->query->propertyCondition('user_zip', $this->getZipCode()); + } + } + + /** + * Adds the country argument to the query. + */ + protected function addCountryToQuery() { + if ($this->getCountry()) { + $this->query->propertyCondition('user_country', $this->getCountry()); + } + } + + /** + * Adds the petition_id argument to the query. + */ + protected function addPetitionIdsToQuery() { + $petition_ids = $this->getPetitionIds(); + if (!empty($petition_ids)) { + $entity_ids = petition_extract_nids_from_petition_ids($petition_ids); + if (!empty($entity_ids)) { + $this->query->propertyCondition('petition_id', $entity_ids, 'IN'); + } + + $legacy_ids = petition_extract_legacy_ids_from_petition_ids($petition_ids); + if (!empty($legacy_ids)) { + $this->query->propertyCondition('legacy_petition_id', $legacy_ids, 'IN'); + } + } + } + + /** + * Adds the offset and limit argument to the query. + */ + protected function addOffsetAndLimitToQuery() { + $this->query->range($this->getOffset(), $this->getLimit()); + } + + /** + * Executes the query. + * + * @return array + * An array of nodes with field data, or an empty array if there are no + * results. + */ + protected function executeQuery() { + $result = $this->query->addTag('petitions_data_debug')->execute(); + + // Return early if no results. + if (empty($result['signature_mail'])) { + return array(); + } + + $entities = $result['signature_mail']; + return $entities; + } + + /** + * Builds the results array from the query results. + * + * @param array $entities + * An array of signature data, as returned by + * SignaturesSelectQueryMysql::executeQuery. + */ + protected function buildResult(array $entities) { + $result_arrays = array(); + $result_objects = array(); + foreach ($entities as $entity) { + $fields = $entity->extraFields; + + $signature = new SignatureItem(); + $signature + ->setPetitionId($fields->petition_id) + ->setUid($fields->uid) + ->setId($fields->entity_id) + ->setCreated($fields->timestamp) + ->setFirstName($fields->user_first_name) + ->setLastName($fields->user_last_name) + ->setCity($fields->user_city) + ->setState($fields->user_state) + ->setZip($fields->user_zip); + if (!empty($fields->legacy_id)) { + $signature->setLegacyId($fields->legacy_id); + } + if (!empty($fields->legacy_petition_id)) { + $signature->setLegacyPetitionId($fields->legacy_petition_id); + } + + $result_objects[] = $signature; + $result_arrays[] = $signature->toRestResponseItemArray(); + } + + $this->setResultObjects($result_objects); + $this->setResult($result_arrays); + + } + +} diff --git a/modules/custom/petitions_data/classes/SignaturesSelectQuerySolr.inc b/modules/custom/petitions_data/classes/SignaturesSelectQuerySolr.inc new file mode 100644 index 000000000..bb6c24b7c --- /dev/null +++ b/modules/custom/petitions_data/classes/SignaturesSelectQuerySolr.inc @@ -0,0 +1,301 @@ + 'asc', + self::SELECT_QUERY_ORDER_BY_DESC => 'desc', + ); + + /** + * OrderBy MySQL Field mapping + * + * Maps Order By constants to appropriate database column name. + * + * @var array + * An array containing 'field' and 'column' elements. For node columns, only + * 'field' is required. + */ + protected $orderByFieldsMap = array( + self::SELECT_QUERY_ORDER_BY_FIELD_ID => array('field' => SIGNATURE_SOLR_FIELD_ID), + self::SELECT_QUERY_ORDER_BY_FIELD_TIMESTAMP => array('field' => SIGNATURE_SOLR_FIELD_TIMESTAMP), + ); + + /** + * The Solr sort parameter. + * + * @var string + */ + protected $sortString = ''; + + /** + * {@inheritdoc} + */ + public function execute() { + try { + $this->buildQuery(); + $response = $this->executeQuery(); + $this->buildResults($response); + } + catch (Exception $e) { + watchdog('petitions_data', 'Exception in SignaturesSelectQuerySolr::execute(): !e', array( + '!e' => petitionslog_format_for_watchdog($e), + )); + } + return $this; + } + + /** + * Builds the query. + */ + protected function buildQuery() { + $this->initializeQuery(); + $this->addArgumentsToQuery(); + } + + /** + * Initializes the basic query. + */ + protected function initializeQuery() { + $this->addQueryParam('bundle', 'signature_mail'); + } + + /** + * Adds User ID to the query. + */ + protected function addUidToQuery() { + $uid = $this->getUid(); + if (!empty($uid)) { + $this->addQueryParam(SIGNATURE_SOLR_FIELD_UID, $uid); + } + } + + /** + * Adds User City to the query. + */ + protected function addCityToQuery() { + $city = $this->getCity(); + if (!empty($city)) { + $this->addQueryParam(SIGNATURE_SOLR_FIELD_USER_CITY, $city); + } + } + + /** + * Adds User State to the query. + */ + protected function addStateToQuery() { + $state = $this->getState(); + if (!empty($state)) { + $this->addQueryParam(SIGNATURE_SOLR_FIELD_USER_STATE, $state); + } + } + + /** + * Adds Zipcode to the query. + */ + protected function addZipcodeToQuery() { + $zip = $this->getZipCode(); + if (!empty($zip)) { + $this->addQueryParam(SIGNATURE_SOLR_FIELD_USER_ZIP, $zip); + } + } + + /** + * Adds User Country the query. + */ + protected function addCountryToQuery() { + $country = $this->getCountry(); + if (!empty($country)) { + $this->addQueryParam(SIGNATURE_SOLR_FIELD_USER_COUNTRY, $country); + } + } + + + /** + * Adds petition IDs to the query. + */ + protected function addPetitionIdsToQuery() { + $petition_ids = $this->getPetitionIds(); + if (!empty($petition_ids)) { + $expressions = array(); + + $nids = petition_extract_nids_from_petition_ids($petition_ids); + if (!empty($nids)) { + $nid_expression = '(' . implode(' OR ', $nids) . ')'; + $expressions[] = SIGNATURE_SOLR_FIELD_PETITION_ID . ':' . $nid_expression; + } + + $legacy_ids = petition_extract_legacy_ids_from_petition_ids($petition_ids); + if (!empty($legacy_ids)) { + $legacy_id_expression = '(' . implode(' OR ', $legacy_ids) . ')'; + $expressions[] = SIGNATURE_SOLR_FIELD_LEGACY_PETITION_ID . ':' . $legacy_id_expression; + } + + if ($expressions) { + $this->params['fq'][] = '(' . implode(' OR ', $expressions) . ')'; + } + // If no expressions could be generated, none of the given petition IDs + // were valid. + else { + throw new PetitionNotFoundException(); + } + } + } + + /** + * Adds the offset and limit arguments to the query. + */ + protected function addOffsetAndLimitToQuery() { + $this->params['start'] = $this->getOffset(); + $this->params['rows'] = $this->getLimit(); + } + + /** + * Adds the sort argument to the query. + */ + protected function addSortToQuery() { + $order_bys = $this->getOrderBy(); + if (empty($order_bys)) { + $this->sortString = SIGNATURE_SOLR_FIELD_TIMESTAMP . ' asc'; + } + else { + foreach ($order_bys as $order_by) { + $field = $order_by->field; + $order_by_dir = $order_by->direction; + if (!$this->isValidOrderByField($field)) { + throw new Exception('addSortToQuery - Not a valid field: ' . $field); + } + if (!$this->isValidOrderByDirection($order_by_dir)) { + throw new Exception('addSortToQuery - Not a valid direction: ' . $order_by_dir); + } + $mapped_field = $this->orderByFieldsMap[$field]['field']; + $mapped_dir = $this->orderByDirMap[$order_by_dir]; + $this->sortString = $mapped_field . ' ' . $mapped_dir . ','; + + } + // Remove comma from end of sortString. + $this->sortString = substr($this->sortString, 0, -1); + } + } + + /** + * Adds the supplied arguments to the query. + */ + protected function addArgumentsToQuery() { + $this->addUidToQuery(); + $this->addCityToQuery(); + $this->addStateToQuery(); + $this->addZipcodeToQuery(); + $this->addCountryToQuery(); + $this->addPetitionIdsToQuery(); + $this->addOffsetAndLimitToQuery(); + $this->addSortToQuery(); + } + + /** + * Adds a Solr search parameter. + * + * @param string $field + * The field to query. + * @param string $value + * The value to query for. + * @param bool $exact + * Whether to require an exact match (a filter query) or not (an ordinary + * query). A filter query can be cached, so an exact search is more + * performant. Defaults to TRUE. + */ + private function addQueryParam($field, $value, $exact = TRUE) { + $query_type = $exact ? 'fq' : 'q'; + $this->params[$query_type][] = "{$field}:{$value}"; + } + + /** + * Executes the query. + * + * @return object + * A search result response object. + */ + protected function executeQuery() { + $query = new SolrBaseQuery('petitions_data', apachesolr_get_solr(), $this->params, $this->sortString); + // Make Solr aware of sorting fields that are available. + foreach ($this->orderByFieldsMap as $order_by) { + $query->setAvailableSort($order_by['field'], array( + 'title' => t($order_by['field']), + 'default' => 'asc', + )); + } + $result = $query->search(); + // @todo Handle a non-200 response code. + return $result->response; + } + + + /** + * Builds the results array from the query results. + * + * @param object $response + * A search result response object, from the return value from + * DrupalSolrQueryInterface::search(). + */ + protected function buildResults($response) { + $result_arrays = array(); + $result_objects = array(); + + foreach ($response->docs as $doc) { + $signature = new SignatureItem(); + $signature + ->setId($doc->{SIGNATURE_SOLR_FIELD_ID}) + ->setUid($doc->{SIGNATURE_SOLR_FIELD_UID}) + ->setPetitionId($doc->{SIGNATURE_SOLR_FIELD_PETITION_ID}) + ->setFirstName($doc->{SIGNATURE_SOLR_FIELD_USER_FIRST_NAME}) + ->setLastName($doc->{SIGNATURE_SOLR_FIELD_USER_LAST_NAME}) + ->setLegacyId($doc->{SIGNATURE_SOLR_FIELD_LEGACY_ID}) + ->setLegacyPetitionId($doc->{SIGNATURE_SOLR_FIELD_LEGACY_PETITION_ID}) + ->setCreated($doc->{SIGNATURE_SOLR_FIELD_TIMESTAMP}) + ->setUserAgent($doc->{SIGNATURE_SOLR_FIELD_USER_AGENT}) + ->setCity($doc->{SIGNATURE_SOLR_FIELD_USER_CITY}) + ->setState($doc->{SIGNATURE_SOLR_FIELD_USER_STATE}) + ->setZip($doc->{SIGNATURE_SOLR_FIELD_USER_ZIP}) + ->setUserCountry($doc->{SIGNATURE_SOLR_FIELD_USER_COUNTRY}); + + if (filter_var($doc->{SIGNATURE_SOLR_FIELD_IP_ADDRESS}, FILTER_VALIDATE_IP)) { + // setIpAddress throws exception if IP not formatted correctly, breaking method chaining. + $signature->setIpAddress($doc->{SIGNATURE_SOLR_FIELD_IP_ADDRESS}); + } + + $result_objects[] = $signature; + $result_arrays[] = $signature->toRestResponseItemArray(); + } + + $this->setResultObjects($result_objects); + $this->setResult($result_arrays); + $this->setCount($response->numFound); + } +} diff --git a/modules/custom/petitions_data/petitions_data.drush.inc b/modules/custom/petitions_data/petitions_data.drush.inc index f60dfa7b7..67ef45ead 100644 --- a/modules/custom/petitions_data/petitions_data.drush.inc +++ b/modules/custom/petitions_data/petitions_data.drush.inc @@ -1,9 +1,57 @@ 'Get the details of a given petition.', + 'arguments' => array( + 'petition_id' => 'A petition ID.', + ), + 'required-arguments' => TRUE, + 'examples' => array( + 'drush petitions-data-petition-get 50a3fd762f2c88cd65000015' => 'Get a petition.', + ), + 'aliases' => array('pdpg'), + ); + + return $items; +} + +/** + * Implements drush_hook_COMMAND_validate(). + * + * @see petitions_data_drush_command() + */ +function drush_petitions_data_petition_get_validate() { + $args = drush_get_arguments(); + $petition_id = $args[1]; + if (!petitions_data_is_valid_petition_id($petition_id)) { + drush_set_error('PETITIONS_DATA_INVALID_PETITION_ID', dt('Invalid petition ID "@id".', array( + '@id' => $petition_id, + ))); + } +} + +/** + * Implements drush_hook_COMMAND(). + * + * @see petitions_data_drush_command() + */ +function drush_petitions_data_petition_get() { + $args = drush_get_arguments(); + $petition_id = $args[1]; + drush_print_r(PetitionsController::load($petition_id)); +} + /** * Implements hook_drush_cache_clear(). */ diff --git a/modules/custom/petitions_data/petitions_data.info b/modules/custom/petitions_data/petitions_data.info index 9c707d9ec..f672b7a8e 100644 --- a/modules/custom/petitions_data/petitions_data.info +++ b/modules/custom/petitions_data/petitions_data.info @@ -3,20 +3,27 @@ description = Data abstraction for use with services to provide a flexible backe core = 7.x package = Petitions API +dependencies[] = apachesolr +dependencies[] = efq_extra_field dependencies[] = wh_petitions ; Petitions Data access layer -files[] = classes/Petition.inc +files[] = classes/PetitionItem.inc +files[] = classes/PetitionNotFoundException.inc files[] = classes/PetitionsController.inc files[] = classes/PetitionsSelectQuery.inc files[] = classes/PetitionsSelectQueryFactory.inc files[] = classes/PetitionsSelectQueryMongo.inc +files[] = classes/PetitionsSelectQueryMysql.inc +files[] = classes/PetitionsSelectQuerySolr.inc files[] = classes/SelectQueryBase.inc -files[] = classes/Signature.inc +files[] = classes/SignatureItem.inc files[] = classes/SignaturesController.inc files[] = classes/SignaturesSelectQuery.inc files[] = classes/SignaturesSelectQueryFactory.inc files[] = classes/SignaturesSelectQueryMongo.inc +files[] = classes/SignaturesSelectQueryMysql.inc +files[] = classes/SignaturesSelectQuerySolr.inc ; Tests files[] = tests/PetitionsDataBaseTestCase.test diff --git a/modules/custom/petitions_data/petitions_data.install b/modules/custom/petitions_data/petitions_data.install index aae1ccaee..10f9582ca 100644 --- a/modules/custom/petitions_data/petitions_data.install +++ b/modules/custom/petitions_data/petitions_data.install @@ -21,3 +21,39 @@ function petitions_data_schema() { $schema['cache_petitions_petitions'] = drupal_get_schema_unprocessed('system', 'cache'); return $schema; } + +/** + * Update shunts. + */ +function petitions_data_update_7301() { + // Set variables directly because the Shunt API is not available in + // hook_update_N(). + variable_set('shunt_mysql_writes', TRUE); + $removed_shunts = array( + 'petition_mongodb_read', + 'petition_mongodb_write', + 'petition_mysql_save', + 'signature_mail_mysql_save', + 'wh_petitions_petition_create', + 'wh_petitions_signature_create', + ); + foreach ($removed_shunts as $shunt) { + variable_del("shunt_{$shunt}"); + } +} + +/** + * PT-1668: Remove unused mongo/mysql shunts + */ +function petitions_data_update_7302() { + $removed_shunts = array( + 'mongo_reads', + 'mongo_writes', + 'mysql_writes', + ); + + foreach ($removed_shunts as $shunt) { + variable_del("shunt_{$shunt}"); + } + +} diff --git a/modules/custom/petitions_data/petitions_data.module b/modules/custom/petitions_data/petitions_data.module index ced415643..5361a3fbc 100644 --- a/modules/custom/petitions_data/petitions_data.module +++ b/modules/custom/petitions_data/petitions_data.module @@ -1,11 +1,12 @@ data; + } + else { + $petition = PetitionsController::load($petition_id); + + // If the petition exists, remove volatile values that are likely to + // become stale before the cache expires and lead to unreliable displays. + if (!empty($petition)) { + unset($petition['signatureCount'], $petition['signaturesNeeded']); + } + + // Cache the result. + cache_set("petition:{$petition_id}", $petition, 'cache_petitions_petitions', CACHE_PERMANENT); + } + } + return $petition; +} + +/** + * Determines whether a given petition ID is valid. + * + * @param mixed $value + * The value to test. + * + * @return bool + * Returns TRUE if the given value is a valid petition ID or FALSE if not. + */ +function petitions_data_is_valid_petition_id($value) { + return is_int($value) || petition_is_legacy_id($value); } /** @@ -141,7 +224,7 @@ function petitions_data_get_nice_url_from_full_url($full_url) { * Returns TRUE if the petition is open and not expired. */ function petitions_data_petition_is_open($petition_id) { - $petition = petitions_data_mongo2mysql_get_petition($petition_id); + $petition = petitions_data_get_petition($petition_id); // Confirm that the petition exists. if (empty($petition)) { @@ -156,6 +239,50 @@ function petitions_data_petition_is_open($petition_id) { return TRUE; } +/** + * Generates a URL for the Petitions website. + * + * For external URLs, use url() instead. + * + * @param string $path + * (optional) The internal path being linked to, such as "node/34". The + * default value is equivalent to passing in '"; + } // JSON_PRETTY_PRINT is only available since PHP 5.4.0. - if (defined('JSON_PRETTY_PRINT')) { - return json_encode($data, JSON_PRETTY_PRINT); + elseif (defined('JSON_PRETTY_PRINT')) { + return '{$data}
'; } else { - return print_r($data, TRUE); + return '' . json_encode($data, JSON_PRETTY_PRINT) . '
'; } } // Otherwise, use (compressed) JSON to get it all on a single line. diff --git a/modules/custom/petitionsmongo2mysqlintegrity/petitionsmongo2mysqlintegrity.module b/modules/custom/petitionsmongo2mysqlintegrity/petitionsmongo2mysqlintegrity.module index f040171bb..53a0634c6 100644 --- a/modules/custom/petitionsmongo2mysqlintegrity/petitionsmongo2mysqlintegrity.module +++ b/modules/custom/petitionsmongo2mysqlintegrity/petitionsmongo2mysqlintegrity.module @@ -13,10 +13,9 @@ function petitionsmongo2mysqlintegrity_form_petition_node_form_alter(&$form, &$form_state, $form_id) { global $user; - $is_mongo_on = !shunt_is_enabled('wh_petitions_petition_create'); $is_not_superuser = $user->uid !=1; - if ($is_mongo_on && $is_not_superuser) { + if (petitions_data_mongo_reads_are_enabled() && $is_not_superuser) { // Remove ability to publish $form['options']['status']['#value'] = 1; $form['options']['#type'] = 'hidden'; @@ -52,10 +51,9 @@ function petitionsmongo2mysqlintegrity_form_node_admin_content_validation(&$form $pub_options = array('publish', 'unpublish'); $admin_link = l('petitions moderation page', 'admin/moderation-tools'); - $is_mongo_on = !shunt_is_enabled('wh_petitions_petition_create'); $is_not_superuser = $user->uid !=1; - if ($is_mongo_on && $is_not_superuser) { + if (petitions_data_mongo_is_on() && $is_not_superuser) { // Throw error if any of our actions are publish/unpublish and any selection is a petition. if (in_array($form_state['values']['operation'], $pub_options)) { foreach ($selected as $choice) { diff --git a/modules/custom/signature/signature.module b/modules/custom/signature/signature.module index 757f6abf3..a9f6ed6b4 100644 --- a/modules/custom/signature/signature.module +++ b/modules/custom/signature/signature.module @@ -5,6 +5,156 @@ * helper function to create sample signatures, and to delete all signatures */ + +// Apache Solr custom field names. Prefixes determine the storage mechanism, +// e.g., "bs_*" for boolean, single-valued and "sm_" is for string, +// multi-valued. See the dynamicField definitions in +// solr-conf/solr-3.x/schema.xml in the apachesolr module for a complete list. + + +define('SIGNATURE_SOLR_FIELD_ID', 'entity_id'); +define('SIGNATURE_SOLR_FIELD_UID', 'is_uid'); +define('SIGNATURE_SOLR_FIELD_PETITION_ID', 'is_petition_id'); +define('SIGNATURE_SOLR_FIELD_USER_FIRST_NAME', 'ss_user_first_name'); +define('SIGNATURE_SOLR_FIELD_USER_LAST_NAME', 'ss_user_last_name'); +define('SIGNATURE_SOLR_FIELD_LEGACY_ID', 'ss_legacy_id'); +define('SIGNATURE_SOLR_FIELD_LEGACY_PETITION_ID', 'ss_legacy_petition_id'); +define('SIGNATURE_SOLR_FIELD_TIMESTAMP', 'is_timestamp'); +define('SIGNATURE_SOLR_FIELD_USER_AGENT', 'ts_user_agent'); +define('SIGNATURE_SOLR_FIELD_IP_ADDRESS', 'ss_ip_address'); +define('SIGNATURE_SOLR_FIELD_USER_CITY', 'ss_user_city'); +define('SIGNATURE_SOLR_FIELD_USER_STATE', 'ss_user_state'); +define('SIGNATURE_SOLR_FIELD_USER_ZIP', 'ss_user_zip'); +define('SIGNATURE_SOLR_FIELD_USER_USERNAME', 'ss_user_username'); +define('SIGNATURE_SOLR_FIELD_USER_COUNTRY', 'ss_user_country'); + +/** + * Implements hook_menu_alter(). + * + * Overrides apachesolr_status_page() to remove stats generating slow queries. + */ +function signature_menu_alter(&$items) { + $items['admin/config/search/apachesolr'] = array( + 'title' => 'Apache Solr search', + 'description' => 'Administer Apache Solr.', + 'page callback' => 'signature_apachesolr_status_page', + 'access arguments' => array('administer search'), + 'weight' => -8, + 'file' => drupal_get_path('module', 'apachesolr') . '/apachesolr.admin.inc', + ); +} + +/** + * Gets information about the fields already in solr index. + * + * @param array $environment + * The environment for which we need to ask the status from + * + * @return array page render array + */ +function signature_apachesolr_status_page($environment = array()) { + if (empty($environment)) { + $env_id = apachesolr_default_environment(); + $environment = apachesolr_environment_load($env_id); + } + else { + $env_id = $environment['env_id']; + } + + // Check for availability + if (!apachesolr_server_status($environment['url'], $environment['service_class'])) { + drupal_set_message(t('The server seems to be unavailable. Please verify the server settings at the settings page', array('!settings_page' => url("admin/config/search/apachesolr/settings/{$environment['env_id']}/edit", array('query' => drupal_get_destination())))), 'warning'); + return ''; + } + + try { + $solr = apachesolr_get_solr($environment["env_id"]); + $solr->clearCache(); + $data = $solr->getLuke(); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + drupal_set_message(nl2br(check_plain($e->getMessage())), "warning"); + $data = new stdClass; + $data->fields = array(); + } + + $messages = array(); + if (isset($data->index->numDocs)) { + try { + // Collect the stats + $stats_summary = $solr->getStatsSummary(); + module_load_include('inc', 'apachesolr', 'apachesolr.index'); + // We need a schema version greater than beta3. This is mostly to catch + // people using the Drupal 6 schema. + if (preg_match('/^drupal-[13]/', $stats_summary['@schema_version'])) { + $minimum = 'drupal-3.0-beta4'; + if (version_compare($stats_summary['@schema_version'], $minimum, '<')) { + drupal_set_message(t('Your schema.xml version is too old. You must update it to at least %minimum and re-index your content.', array('%minimum' => $minimum)), 'error'); + } + } + $pending_msg = $stats_summary['@pending_docs'] ? t('(@pending_docs sent but not yet processed)', $stats_summary) : ''; + $index_msg = $stats_summary['@index_size'] ? t('(@index_size on disk)', $stats_summary) : ''; + $indexed_message = t('@num Items !pending !index_msg', array( + '@num' => $data->index->numDocs, + '!pending' => $pending_msg, + '!index_msg' => $index_msg, + )); + $messages[] = array(t('Indexed'), $indexed_message); + + $messages[] = array(t('Schema'), t('@schema_version', $stats_summary)); + if (!empty($stats_summary['@core_name'])) { + $messages[] = array(t('Solr Core Name'), t('@core_name', $stats_summary)); + } + $messages[] = array(t('Delay'), t('@autocommit_time before updates are processed.', $stats_summary)); + $messages[] = array(t('Pending Deletions'), t('@deletes_total', $stats_summary)); + } + catch (Exception $e) { + watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); + } + } + if (empty($messages)) { + $messages[] = array(t('Error'), t('No data was returned from the server. Check your log messages.')); + } + +// Initializes output with information about which server's setting we are + // editing, as it is otherwise not transparent to the end user. + $output['apachesolr_index_action_status'] = array( + '#prefix' => '' . print_r($data, TRUE) . '
' . t('Please enter at least 3 characters to search by.') . '
'; } - $body_result = $conn->find($body_query, $retrieve_fields)->sort($sort_query)->skip($body_offset)->limit($limit); - foreach ($body_result as $res) { - array_push($petitions, $res); - $count++; + $petitions_query->setTitle($search); + + // Find how many match titles. + $petitions_execute = $petitions_query->setLimit(WH_PETITION_PETITIONS_PER_PAGE)->setOffset($offset)->execute(); + $title_total_results = $petitions_execute->getCount(); + $total = $title_total_results; + + if ($offset < $title_total_results) { + $result = $petitions_execute->getResult(); + + foreach ($result as $res) { + array_push($petitions, $res); + $count++; + } } } - } - else { - // We had a search but found no results, keep the page the same. - if (!empty($search)) { + + if (!$total) { return '' . t('There are no petitions that match this search.') . '
'; } + } + else { + $result = NULL; + $total = 0; // For Trending sort, we have a helper function to do this for us since this logic is used in multiple places. if ($sort == 'trending') { - $total = 0; - $result = wh_petitions_trending_results($conn, $offset, WH_PETITION_PETITIONS_PER_PAGE, $total); + if (petitions_data_mongo_reads_are_enabled()) { + $conn = wh_petitions_mongo_petition_connection(); + $result = wh_petitions_trending_results($conn, $offset, WH_PETITION_PETITIONS_PER_PAGE, $total); + } } - // Otherwise, proceed normally + // Otherwise, proceed normally. else { - $result = $conn->find($query, $retrieve_fields)->sort($sort_query)->skip($offset)->limit(WH_PETITION_PETITIONS_PER_PAGE); - $total = $conn->find($query)->sort($sort_query)->count(); + $petitions_execute = $petitions_query->setLimit(WH_PETITION_PETITIONS_PER_PAGE)->setOffset($offset)->execute(); + $result = $petitions_execute->getResult(); + $total = $petitions_execute->getCount(); } - foreach ($result as $res) { - array_push($petitions, $res); - $count++; + if ($result) { + foreach ($result as $res) { + array_push($petitions, $res); + $count++; + } } } @@ -216,24 +276,23 @@ function wh_petitions_get_petitions(&$sort, &$page, &$cols, &$issues, &$search, $issues = array(0); } - // Theme the petitions + // Theme the petitions. $i = 0; foreach ($petitions as $res) { - $id = $res['_id']->__toString(); $html .= theme('wh_petitions_display_list_petition', array( 'entry_num' => $i, - 'title' => check_plain($res['title']), - 'signature_count' => wh_petitions_format_number($res['signature_count']), - 'petition_link' => l(t('Find out more'), $res['nice_url']), + 'title' => filter_xss($res['title']), + 'signature_count' => wh_petitions_format_number($res['signatureCount']), + 'petition_link' => l(t('Find out more'), $res['url']), 'cols' => intval($cols), - 'petition_id' => check_plain($id), - 'nice_url' => $res['nice_url'], + 'petition_id' => check_plain($res['id']), + 'nice_url' => $res['url'], )); $i++; } if (!empty($issues) && empty($html) && $issues[0] != 0) { - if (sizeof($issues) == 1) { + if (count($issues) == 1) { $html = '' . t('There are no petitions that match this issue.') . '
'; } else { @@ -291,21 +350,50 @@ function wh_petitions_trending_results($conn, $offset, $per_page, &$total) { $ids = variable_get('wh_petitions_trending_ids', array()); if (!empty($ids)) { $id_slice = array_slice($ids, $offset, $per_page); - $query = array( - '_id' => array('$in' => $id_slice), - ); - $retrieve_fields = array('title', 'signature_count', 'published', 'nice_url'); - $result = $conn->find($query, $retrieve_fields); - } - $arr = array(); - foreach ($id_slice as $key => $val) { - foreach ($result as $res) { - if ($res['_id'] == $val) { - array_push($arr, $res); + + if (petitions_data_mongo_reads_are_enabled()) { + $query = array( + '_id' => array('$in' => $id_slice), + ); + $retrieve_fields = array('title', 'signature_count', 'published', 'nice_url'); + $result = $conn->find($query, $retrieve_fields); + $arr = array(); + foreach ($id_slice as $key => $val) { + foreach ($result as $res) { + if ($res['_id'] == $val) { + array_push($arr, $res); + } + } + } + + } + else { + $query = db_select('node', 'n'); + $query->leftJoin('field_data_field_petition_signature_count', 'psc', 'psc.entity_id = n.nid'); + $query->leftJoin('field_data_field_timestamp_published', 'tp', 'tp.entity_id = n.nid'); + $query->leftJoin('url_alias', 'ua', 'substring(ua.source, 6) = n.nid'); + + $query->addField('n', 'title'); + $query->addField('psc', 'field_petition_signature_count_value', 'signature_count'); + $query->addfield('tp', 'field_timestamp_published_value', 'published'); + $query->addField('ua', 'alias', 'nice_url'); + + $query->condition('n.type', 'petition', '='); + $query->condition('n.nid', $id_slice, 'IN'); + $result = $query->execute()->fetchAllAssoc('n.nid'); + + $arr = array(); + foreach ($id_slice as $key => $val) { + foreach ($result as $res) { + if ($res['_id'] == $val) { + array_push($arr, $res); + } + } } } } - $total = sizeof($ids); + + $total = count($ids); return $arr; } @@ -467,7 +555,7 @@ function wh_petitions_petition_detail($petition_id) { $reached_ready = date('M d, Y', $petition['reached_ready']); } - if (!shunt_is_enabled('petition_mysql_save') && function_exists("petitionevents_get_administer_link")) { + if (petitions_data_mysql_writes_are_enabled() && function_exists("petitionevents_get_administer_link")) { $admin_link = petitionevents_get_administer_link(); } @@ -616,9 +704,12 @@ function wh_petitions_petition_detail($petition_id) { } } $signatures = wh_petitions_get_signatures($petition_id, $petition['uid'], $page, $has_more, FALSE, $petition['signature_count'], $last_id); + $last_id = ''; + $previously_found_creator = '0'; if (!empty($signatures)) { - $last_id = $signatures[(sizeof($signatures) -1)]['_id']->__toString(); + $last_id = $signatures[(count($signatures) -1)]['id']; + $previously_found_creator = $signatures[(count($signatures) -1)]['previously_found_creator']; } $signature_html = theme('wh_petitions_display_signatures', array( @@ -629,6 +720,7 @@ function wh_petitions_petition_detail($petition_id) { 'sigs_per_page' => $sigs_per_page, 'last_id' => $last_id, 'nice_url' => $petition['nice_url'], + 'previously_found_creator' => $previously_found_creator, )); // Load the issues and search forms @@ -704,8 +796,25 @@ function wh_petitions_petition_detail($petition_id) { */ function wh_petitions_petition_detail_niceurl($url) { // Load the petition - $conn = wh_petitions_mongo_petition_connection(); $nice_url = check_plain($_GET['q']); + if (!petitions_data_mongo_reads_are_enabled()) { + $nid = ''; + // Find corresponding petition node by legacy path. + $result = db_query('select entity_id from {field_data_field_legacy_path} WHERE field_legacy_path_value = :nice_url AND bundle=\'petition\'', array(':nice_url' => $nice_url)); + $nid = $result->fetchColumn(0); + // Redirect to corresponding petition node, if found. + if (!empty($nid)) { + $petition_node_path = 'node/' . $nid; + drupal_goto($petition_node_path, array(), 301); + } + else { + // If no node petition found and mongo reads are disabled, return not found. + drupal_not_found(); + } + return; + } + + $conn = wh_petitions_mongo_petition_connection(); $petition = $conn->findOne(array('nice_url' => $nice_url), array('title')); if ($petition) { @@ -729,147 +838,185 @@ function wh_petitions_petition_detail_niceurl($url) { * has_more = whether to display the 'more' box * whether we're coming from an ajax request. if so, retrieve 20. otherwise retrieve 19. * last id that we retrieved. use this for faster pagination. ( range query instead of skip is faster according to 10gen ) + * previously_found_creator = whether the creator's signature has been found in previous query results. */ -function wh_petitions_get_signatures($petition_id, $petition_uid, $page, &$has_more, $from_ajax = FALSE, $signature_count = 0, $last_id) { +function wh_petitions_get_signatures($petition_id, $petition_uid, $page, &$has_more, $from_ajax = FALSE, $signature_count = 0, $last_id, $previously_found_creator = FALSE) { + global $user; - $has_user_signed = FALSE; - $user_signature = array(); $signatures = array(); if (empty($page)) { $page = 1; } if ($from_ajax && $page != 1) { - $per_page = 20; + if ($previously_found_creator) { + $per_page = 20; + } + else { + // The creator's signature is always displayed first. + // Get one extra signature in case we also find the creator's + // signature in the following results. + $per_page = 21; + } } else { $per_page = 19; } - $sig_conn = wh_petitions_mongo_petition_signatures_connection(); - - // Find out if a user has signed the petition and capture their signature if they have - if ($user->uid != $petition_uid) { - $query = array( - 'uid' => (int) $user->uid, - 'petition_id' => (string) $petition_id, - ); - $result = $sig_conn->findOne($query); - if (!empty($result)) { - $has_user_signed = TRUE; - $user_signature = $result; - } - } // Get the signatures we should be retrieving if ($page == 1) { $skip = 0; - $max = 18; + $max = 19; + $count_offset = 1; } - else { - $skip = 18 + (($page - 2) * $per_page); - $max = 18 + (($page - 1) * $per_page); + elseif ($previously_found_creator) { + $skip = 19 + (($page - 2) * $per_page); + $max = $skip + $per_page; + $count_offset = 1; } - - $query = array( - 'petition_id' => (string) $petition_id, - ); - - // If this current user has signed this petition, remove them from the possible signatures we retrieve ( they'll always be second on the first page ) - if (!empty($has_user_signed)) { - $query['uid'] = array('$nin' => array((int) $user->uid)); - if ($page == 1) { - $per_page = $per_page - 1; - } - else { - $skip--; - } + else { + $skip = 18 + (($page - 2) * ($per_page - 1)); + $max = $skip + $per_page; + $count_offset = 0; } // If we're on the first page, we want to get the creator's signature to display first. if ($page == 1) { - $creator_signature = $sig_conn->findOne(array('petition_id' => (string) $petition_id, 'uid' => (int) $petition_uid)); + $creator_signature = SignaturesSelectQueryFactory::create() + ->setPetitionId((string)$petition_id) + ->setUid((int)$petition_uid) + ->setLimit(1) + ->execute() + ->getResultObjects(); } - // We don't want to retrieve the creator's signature in any of the queries either. - if (empty($query['uid']) && !empty($petition_uid)) { - $query['uid'] = array('$nin' => array((int) $petition_uid)); - } - else { - array_push($query['uid']['$nin'], (int) $petition_uid); - } + // Use non-realtime Solr to find signatures excluding the creator's. + $signatures = SignaturesSelectQueryFactory::create(FALSE) + ->setPetitionId((string)$petition_id); - // Get the first page of results - if ($page == 1) { - $results = $sig_conn->find($query)->sort(array('_id' => -1))->limit(($per_page - 1)); - } - // If we got the last id from the previous page, use this to query by as it will be faster - else if (!empty($last_id)) { - $query['_id'] = array('$lt' => new MongoId($last_id)); - $results = $sig_conn->find($query)->sort(array('_id' => -1))->limit($per_page); + // Sorting signatures in MySQL is too expensive, so only sort if signatures are served by Solr. + if (petitions_data_solr_signature_queries_are_enabled()) { + $signatures = $signatures + ->setOrderBy(SignaturesSelectQuery::SELECT_QUERY_ORDER_BY_FIELD_TIMESTAMP, SignaturesSelectQuery::SELECT_QUERY_ORDER_BY_DESC); } + $signatures = $signatures->setLimit(($per_page)); + // If we just got a page number, not a last id, do the query anyway using skip - else { - $results = $sig_conn->find($query)->sort(array('_id' => -1))->skip($skip)->limit($per_page); + if ($page != 1) { + $signatures->setOffset($skip); } + $signatures = $signatures->execute() + ->getResultObjects(); - // Put the results into an array - foreach ($results as $res) { - array_push($signatures, $res); + if (empty($signatures) && empty($creator_signature)) { + $has_more = FALSE; + return FALSE; } - // Determine if there are more signatures to display after these - if ($max >= $signature_count) { - $has_more = FALSE; + // Check if we selected the creator's signature again. + // If so, we will remove it so it is not displayed twice. + if (!$previously_found_creator) { + // Look for creator's signature in $signatures + $creator_signature_found = FALSE; + foreach ($signatures as $key => $signature) { + if((int) $signature->getUid() == (int) $petition_uid) { + $creator_signature_found = TRUE; + break; + } + } + if ($creator_signature_found) { + // Remove the creator from retrieved signatures so we don't display it twice. + unset($signatures[$key]); + // Following queries don't need to worry about removing + // creator from results. + $previously_found_creator = TRUE; + } + // If we have still not found it, but have selected more + // signatures than we can display, then remove a signature + // from the array. + elseif ((($page != 1) && count($signatures) == 21) || (($page == 1) && count($signatures) == 19)) { + // Remove the last signature because we have 1 too many. + array_pop($signatures); + } } // If we have a creator's signature, make it the first one. - if (!empty($creator_signature)) { - if (empty($signatures)) { - array_push($signatures, $creator_signature); - } - else { - array_splice($signatures, 0, 1, array_merge(array($creator_signature), array($signatures[0]))); + if (($page == 1) && !empty($creator_signature[0])) { + array_unshift($signatures, $creator_signature[0]); + } + + $result = array(); + $i = 0; + // Put the results into an array + foreach ($signatures as $signature) { + if (empty($signature)) { + continue; } + $result[$i] = array( + 'id' => $signature->getId(), + 'petition_id' => $signature->getPetitionId(), + 'uid' => $signature->getUid(), + 'user' => array( + 'first_name' => $signature->getFirstName(), + 'last_name' => $signature->getLastName(), + 'username' => $signature->getUser()->name, + 'country' => $signature->getUserCountry(), + ), + 'timestamp' => $signature->getCreated(), + ); + + $result[$i]['user']['city'] = $signature->getCity(); + $result[$i]['user']['state'] = $signature->getState(); + $result[$i]['user']['zip'] = $signature->getZip(); + $i++; } - // If this user has signed it and we need to display their signature ( page = 1 ), add their signature - if ($has_user_signed && $page == 1) { - array_splice($signatures, 0, 1, array_merge(array($signatures[0]), array($user_signature))); + // Determine if there are more signatures to display after these + if ($max >= $signature_count) { + $has_more = FALSE; } // Set some themeing variables that will be available to the template for displaying the results. - for ($i=0; $i' . t('Enter the body of the email that will be sent to signors of petitions associated with this response. To preview the email, enter email addresss(es) in the field below and click Preview') . '
' . t('Enter the title of the petition you would like to associate with this response and click Save. To remove a petition, clear the title you would like to remove.') . '
', + ); + + if (!petitions_data_mongo_writes_are_enabled()) { + // mongo2mysql migration is done. Display MySQL reference field. + // Moving the petition reference field into a fieldset. + $form['petitions']['field_petition_id'] = $form['field_petition_id']; + unset($form['field_petition_id']); + foreach ($form['petitions']['field_petition_id'][$form['language']['#value']] as $item_id => $item) { + if (!is_numeric($item_id)) { + continue; } - else { - $form_state['petition_count'] = 0; + + $is_petition_referenced = !empty($item['target_id']['#entity']->field_petition_id[$form['language']['#value']][$item_id]['target_id']); + if ($is_petition_referenced) { + $petition_nid = $item['target_id']['#entity']->field_petition_id[$form['language']['#value']][$item_id]['target_id']; + $view = l(t('View'), 'node/' . $petition_nid, array('attributes' => array('target' => '_blank'))); + $form['petitions']['field_petition_id'][$form['language']['#value']][$item_id]['#prefix'] = '' . t('Enter the title of the petition you would like to associate with this response and click Save. To remove a petition, clear the title you would like to remove.') . '
', + '#suffix' => '
', + ); + // Prepend custom submit handler, so it runs first. + array_unshift($form['actions']['submit']['#submit'], 'wh_response_mongo2mysql_petition_submit'); + + // Make this the active tab by default. + $form['#additional_settings__active_tab'] = array('petitions'); + } + // This is the create a response form, so just add in a "save and stay" + // button, since responses must be created before petitions can be added. + else { + // Text at the top of the box. + $form['petitions']['save'] = array( + '#type' => 'submit', + '#prefix' => '' . t('You must save a response before you can add petitions. Click below to save this response and return to this screen to begin associating petitions.') . '
', - '#suffix' => '
', - ); - $form['actions']['submit']['#submit'][] = 'wh_response_petition_submit'; - - // Make this the active tab by default - $form['#additional_settings__active_tab'] = array('petitions'); - } - // This is the create a response form, so just add in a "save and stay" button, since responses must be created before petitions can be added. - else { - // Add the Petitions vertical tab - $form['petitions'] = array( - '#type' => 'fieldset', - '#title' => t('Petitions'), - '#tree' => TRUE, - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#group' => 'additional_settings', - '#weight' => 100, - '#attributes' => array('class' => array('petitions')), - ); - - // Text at the top of the box. - $form['petitions']['save'] = array( - '#type' => 'submit', - '#prefix' => '' . t('You must save a response before you can add petitions. Click below to save this response and return to this screen to begin associating petitions.') . '
' . t('There are no responses that match this issue.') . '
'; } else { $html = '' . t('There are no responses that match these issues.') . '
'; } - } - + } + if ($count == 0 && !empty($search)) { return '' . t('There are no responses that match this search.') . '
'; } @@ -181,24 +188,33 @@ function wh_response_more_responses($sort, $page, $cols, $issues) { $total = 0; $search = arg(6); $html = wh_response_get_responses($sort, $page, $cols, $issues, $search, $count, $total); - + // Display the 'More' link if we had responses on this page, plus the total count is more than the total count we've displayed so far. Otherwise, display a bar with the total count of results. if ($total > 0) { - $next_page = ''; - if ($count > 0 && ($total > ($page * WH_RESPONSE_RESPONSES_PER_PAGE))) { - $count = $count + (WH_RESPONSE_RESPONSES_PER_PAGE * ($page - 1)); - $next_page = 'responses/' . $sort . '/' . ($page + 1) . '/' . $cols . '/' . implode('+', $issues) . '/' . urlencode($search); - $html .= '