Contributors: Reset profile hours when pledge deactivated.

See https://github.com/WordPress/five-for-the-future/issues/169
This commit is contained in:
Ian Dunn 2022-04-28 15:05:44 -07:00
parent 834c62c0d0
commit c6d7bbb7c1
No known key found for this signature in database
GPG key ID: 99B971B50343CBCB
6 changed files with 338 additions and 4 deletions

View file

@ -166,6 +166,22 @@ function add_pledge_contributors( $pledge_id, $contributors ) {
return $results; return $results;
} }
/**
* Remove all of the contributors for the given pledge.
*
* Some contributors are sponsored by multiple companies. They'll have a `5ftf_contributor` post for each company,
* but only the post associated with the given pledge should be removed.
*/
function remove_pledge_contributors( int $pledge_id ) : void {
$contributors = get_pledge_contributors( $pledge_id, 'all' );
foreach ( $contributors as $status_group ) {
foreach ( $status_group as $contributor ) {
remove_contributor( $contributor->ID );
}
}
}
/** /**
* Remove a contributor post from a pledge. * Remove a contributor post from a pledge.
* *
@ -186,6 +202,21 @@ function remove_contributor( $contributor_post_id ) {
Email\send_contributor_removed_email( $pledge_id, $contributor ); Email\send_contributor_removed_email( $pledge_id, $contributor );
} }
$has_additional_sponsors = get_posts( array(
'post_type' => CPT_ID,
'title' => $contributor->post_title,
'post_status' => 'publish',
) );
// `pending` contributors never confirmed they were associated with the company, so their profile data isn't
// tied to the pledge, and shouldn't be reset. If a user has multiple sponsors, we don't know which hours are
// sponsored by which company, so just leave them all.
if ( 'publish' === $old_status && ! $has_additional_sponsors ) {
$user = get_user_by( 'login', $contributor->post_title );
XProfile\reset_contribution_data( $user->ID );
}
/** /**
* Action: Fires when a contributor is removed from a pledge. * Action: Fires when a contributor is removed from a pledge.
* *

View file

@ -128,13 +128,23 @@ function send_contributor_confirmation_emails( $pledge_id, $contributor_id = nul
function send_contributor_removed_email( $pledge_id, $contributor ) { function send_contributor_removed_email( $pledge_id, $contributor ) {
$pledge = get_post( $pledge_id ); $pledge = get_post( $pledge_id );
$subject = "Removed from {$pledge->post_title} Five for the Future pledge"; $subject = "Removed from {$pledge->post_title} Five for the Future pledge";
$message = "Howdy {$contributor->post_title},\n\n"; $user = get_user_by( 'login', $contributor->post_title );
$message .= sprintf(
'This email is to notify you that your WordPress.org contributor profile is no longer linked to %1$ss Five for the Future pledge. If this is unexpected news, its best to reach out directly to %1$s with questions. Have a great day!', $message = sprintf( '
Howdy %1$s,
This email is to notify you that your WordPress.org contributor profile is no longer linked to %2$ss Five for the Future pledge. If this is unexpected news, its best to reach out directly to %2$s with questions.
If they were the only sponsor linked to your account, then the "Hours Per Week" and "Contributor Teams" fields on your profile have been reset, so that teams have accurate data. If you still plan on contributing without sponsorship, please revisit your profile and enter your new hours and teams.
https://profiles.wordpress.org/me/profile/edit/group/5/
Have a great day!',
$contributor->post_title,
$pledge->post_title $pledge->post_title
); );
$message = str_replace( "\t", '', trim( $message ) );
$user = get_user_by( 'login', $contributor->post_title );
send_email( $user->user_email, $subject, $message, $pledge_id ); send_email( $user->user_email, $subject, $message, $pledge_id );
} }

View file

@ -387,6 +387,10 @@ function deactivate( $pledge_id, $notify = false, $reason = '' ) {
do_action( FiveForTheFuture\PREFIX . '_deactivated_pledge', $pledge_id, $notify, $reason, $result ); do_action( FiveForTheFuture\PREFIX . '_deactivated_pledge', $pledge_id, $notify, $reason, $result );
if ( ! is_wp_error( $result ) ) {
Contributor\remove_pledge_contributors( $pledge_id );
}
return $result; return $result;
} }

View file

@ -170,3 +170,21 @@ function get_contributor_user_data( $user_id ) {
return $formatted_data; return $formatted_data;
} }
/**
* Reset the 5ftF data on a user's profile.
*/
function reset_contribution_data( $user_id ) : void {
global $wpdb;
$wpdb->query( $wpdb->prepare( '
DELETE FROM `bpmain_bp_xprofile_data`
WHERE
user_id = %d AND
field_id IN ( %d, %d, %d )',
$user_id,
FIELD_IDS['sponsored'],
FIELD_IDS['hours_per_week'],
FIELD_IDS['team_names'],
) );
}

View file

@ -6,6 +6,9 @@ use const WordPressDotOrg\FiveForTheFuture\Pledge\CPT_ID as PLEDGE_POST_TYPE;
defined( 'WPINC' ) || die(); defined( 'WPINC' ) || die();
/**
* @group auth
*/
class Test_Auth extends WP_UnitTestCase { class Test_Auth extends WP_UnitTestCase {
// phpcs:ignore PSR2.Classes.PropertyDeclaration.Multiple // phpcs:ignore PSR2.Classes.PropertyDeclaration.Multiple
protected static $pledge, $action, $page, $action_url, $token; protected static $pledge, $action, $page, $action_url, $token;

View file

@ -0,0 +1,268 @@
<?php
use WordPressDotOrg\FiveForTheFuture\{ Contributor, Pledge, XProfile };
defined( 'WPINC' ) || die();
/**
* These are integration tests rather than unit tests. They target the the highest functions in the call stack
* in order to test everything beneath them, to the extent that that's practical. `INPUT_POST` can't be mocked,
* so functions that reference it can't be used.
*
* @group contributor
*/
class Test_Contributor extends WP_UnitTestCase {
protected static $user_jane_id;
protected static $user_ashish_id;
protected static $page_for_organizations;
protected static $pledge_bluehost_id;
protected static $pledge_10up_id;
/**
* Run once when class loads.
*/
public static function set_up_before_class() {
global $wpdb;
parent::set_up_before_class();
$wpdb->query( "
CREATE TABLE `bpmain_bp_xprofile_data` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`field_id` bigint unsigned NOT NULL DEFAULT '0',
`user_id` bigint unsigned NOT NULL DEFAULT '0',
`value` longtext NOT NULL,
`last_updated` datetime NOT NULL DEFAULT '1970-01-01 00:00:00',
PRIMARY KEY (`id`),
KEY `field_id` (`field_id`),
KEY `user_id` (`user_id`)
)
ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3
" );
self::$user_jane_id = self::factory()->user->create( array(
'user_login' => 'jane',
'user_email' => 'jane@example.org',
) );
self::$user_ashish_id = self::factory()->user->create( array(
'user_login' => 'ashish',
'user_email' => 'ashish@example.org',
) );
self::$page_for_organizations = get_post( self::factory()->post->create( array(
'post_type' => 'page',
'post_title' => 'For Organizations',
'post_status' => 'publish',
) ) );
$GLOBALS['post'] = self::$page_for_organizations; // `create_new_pledge()` assumes this exists.
self::$pledge_10up_id = Pledge\create_new_pledge( '10up' );
self::$pledge_bluehost_id = Pledge\create_new_pledge( 'BlueHost' );
wp_update_post( array(
'ID' => self::$pledge_bluehost_id,
'post_status' => 'publish',
) );
wp_update_post( array(
'ID' => self::$pledge_10up_id,
'post_status' => 'publish',
) );
}
/**
* Run before every test.
*/
public function set_up() {
global $wpdb;
parent::set_up();
$wpdb->query( 'TRUNCATE TABLE `bpmain_bp_xprofile_data` ' );
$wpdb->query( $wpdb->prepare( "
INSERT INTO `bpmain_bp_xprofile_data`
(`id`, `field_id`, `user_id`, `value`, `last_updated`)
VALUES
(NULL, 29, %d, '40', '2019-12-02 10:00:00' ),
(NULL, 30, %d, 'a:1:{i:0;s:9:\"Core Team\";}', '2019-12-03 11:00:00' ),
(NULL, 29, %d, '35', '2019-12-02 10:00:00' ),
(NULL, 30, %d, 'a:1:{i:0;s:18:\"Documentation Team\";}', '2019-12-03 11:00:00' )",
self::$user_jane_id,
self::$user_jane_id,
self::$user_ashish_id,
self::$user_ashish_id
) );
reset_phpmailer_instance();
}
/**
* Run once after all tests are finished.
*/
public static function tear_down_after_class() {
global $wpdb;
parent::tear_down_after_class();
$wpdb->query( 'DROP TABLE `bpmain_bp_xprofile_data` ' );
}
/**
* @covers ::remove_pledge_contributors
* @covers ::remove_contributors
*/
public function test_data_reset_once_no_active_sponsors() : void {
// Setup scenario where Jane is sponsored by two companies.
$mailer = tests_retrieve_phpmailer_instance();
$jane = get_user_by( 'id', self::$user_jane_id );
$jane_contribution = XProfile\get_contributor_user_data( $jane->ID );
$tenup = get_post( self::$pledge_10up_id );
$bluehost = get_post( self::$pledge_bluehost_id );
$tenup_contributors = Contributor\add_pledge_contributors( $tenup->ID, array( $jane->user_login ) );
$bluehost_contributors = Contributor\add_pledge_contributors( $bluehost->ID, array( $jane->user_login ) );
$tenup_jane_id = $tenup_contributors[ $jane->user_login ];
$bluehost_jane_id = $bluehost_contributors[ $jane->user_login ];
wp_update_post( array(
'ID' => $tenup_jane_id,
'post_status' => 'publish',
) );
wp_update_post( array(
'ID' => $bluehost_jane_id,
'post_status' => 'publish',
) );
$bluehost_jane = get_post( $bluehost_jane_id );
$this->assertSame( 'publish', $bluehost->post_status );
$this->assertSame( 'publish', $bluehost_jane->post_status );
$this->assertSame( 40, $jane_contribution['hours_per_week'] );
$this->assertContains( 'Core Team', $jane_contribution['team_names'] );
// Deactivating a pledge shouldn't trigger a data resets if they have another active sponsor.
Pledge\deactivate( $bluehost->ID, false );
$bluehost = get_post( $bluehost->ID );
$bluehost_jane = get_post( $bluehost_jane->ID );
$tenup_jane = get_post( $tenup_jane_id );
$jane_contribution = XProfile\get_contributor_user_data( $jane->ID );
$this->assertSame( Pledge\DEACTIVE_STATUS, $bluehost->post_status );
$this->assertSame( 'trash', $bluehost_jane->post_status );
$this->assertSame( 'publish', $tenup_jane->post_status );
$this->assertSame( 40, $jane_contribution['hours_per_week'] );
$this->assertContains( 'Core Team', $jane_contribution['team_names'] );
$this->assertContains( $jane->user_email, $mailer->mock_sent[0]['to'][0] );
$this->assertSame( "Removed from $bluehost->post_title Five for the Future pledge", $mailer->mock_sent[0]['subject'] );
// Once the last sponsor has been deactivated, contribution data should be reset.
Pledge\deactivate( $tenup->ID, false );
$tenup = get_post( $tenup->ID );
$tenup_jane = get_post( $tenup_jane_id );
$jane_contribution = XProfile\get_contributor_user_data( $jane->ID );
$this->assertSame( Pledge\DEACTIVE_STATUS, $tenup->post_status );
$this->assertSame( 'trash', $tenup_jane->post_status );
$this->assertSame( 0, $jane_contribution['hours_per_week'] );
$this->assertEmpty( $jane_contribution['team_names'] );
$this->assertContains( $jane->user_email, $mailer->mock_sent[1]['to'][0] );
$this->assertSame( "Removed from $tenup->post_title Five for the Future pledge", $mailer->mock_sent[1]['subject'] );
}
/**
* @covers ::remove_pledge_contributors
* @covers ::remove_contributors
*/
public function test_data_not_reset_when_unconfirmed_sponsor() : void {
// Setup scenario where Jane was invited to join a company but didn't respond.
$mailer = tests_retrieve_phpmailer_instance();
$jane = get_user_by( 'id', self::$user_jane_id );
$jane_contribution = XProfile\get_contributor_user_data( $jane->ID );
$tenup = get_post( self::$pledge_10up_id );
$tenup_contributors = Contributor\add_pledge_contributors( $tenup->ID, array( $jane->user_login ) );
$tenup_jane_id = $tenup_contributors[ $jane->user_login ];
wp_update_post( array(
'ID' => $tenup_jane_id,
'post_status' => 'pending',
) );
$tenup_jane = get_post( $tenup_jane_id );
$this->assertSame( 'publish', $tenup->post_status );
$this->assertSame( 'pending', $tenup_jane->post_status );
$this->assertSame( 40, $jane_contribution['hours_per_week'] );
$this->assertContains( 'Core Team', $jane_contribution['team_names'] );
// Deactivating a pledge shouldn't trigger a data resets if they haven't confirmed their connection to the company.
Pledge\deactivate( $tenup->ID, false );
$tenup = get_post( $tenup->ID );
$tenup_jane = get_post( $tenup_jane_id );
$jane_contribution = XProfile\get_contributor_user_data( $jane->ID );
$this->assertSame( Pledge\DEACTIVE_STATUS, $tenup->post_status );
$this->assertSame( 'trash', $tenup_jane->post_status );
$this->assertSame( 40, $jane_contribution['hours_per_week'] );
$this->assertContains( 'Core Team', $jane_contribution['team_names'] );
$this->assertEmpty( $mailer->mock_sent );
}
/**
* @covers ::remove_contributors
*/
public function test_data_reset_when_single_contributor_removed_from_pledge() : void {
// Setup scenario where Jane and Ashish are sponsored by a company.
$mailer = tests_retrieve_phpmailer_instance();
$jane = get_user_by( 'id', self::$user_jane_id );
$jane_contribution = XProfile\get_contributor_user_data( $jane->ID );
$ashish = get_user_by( 'id', self::$user_ashish_id );
$ashish_contribution = XProfile\get_contributor_user_data( $ashish->ID );
$tenup = get_post( self::$pledge_10up_id );
$tenup_contributors = Contributor\add_pledge_contributors( $tenup->ID, array( $jane->user_login, $ashish->user_login ) );
$tenup_jane_id = $tenup_contributors[ $jane->user_login ];
$tenup_ashish_id = $tenup_contributors[ $ashish->user_login ];
wp_update_post( array(
'ID' => $tenup_jane_id,
'post_status' => 'publish',
) );
wp_update_post( array(
'ID' => $tenup_ashish_id,
'post_status' => 'publish',
) );
$tenup_jane = get_post( $tenup_jane_id );
$tenup_ashish = get_post( $tenup_ashish_id );
$this->assertSame( 'publish', $tenup_ashish->post_status );
$this->assertSame( 'publish', $tenup_jane->post_status );
$this->assertSame( 40, $jane_contribution['hours_per_week'] );
$this->assertContains( 'Core Team', $jane_contribution['team_names'] );
$this->assertSame( 35, $ashish_contribution['hours_per_week'] );
$this->assertContains( 'Documentation Team', $ashish_contribution['team_names'] );
// Removing Jane should reset her data, but leave Ashish unaffected.
Contributor\remove_contributor( $tenup_jane_id );
$tenup_jane = get_post( $tenup_jane_id );
$jane_contribution = XProfile\get_contributor_user_data( $jane->ID );
$tenup_ashish = get_post( $tenup_ashish_id );
$ashish_contribution = XProfile\get_contributor_user_data( $ashish->ID );
$this->assertSame( 'trash', $tenup_jane->post_status );
$this->assertSame( 'publish', $tenup_ashish->post_status );
$this->assertSame( 0, $jane_contribution['hours_per_week'] );
$this->assertEmpty( $jane_contribution['team_names'] );
$this->assertSame( 35, $ashish_contribution['hours_per_week'] );
$this->assertContains( 'Documentation Team', $ashish_contribution['team_names'] );
$this->assertCount( 1, $mailer->mock_sent );
$this->assertContains( $jane->user_email, $mailer->mock_sent[0]['to'][0] );
$this->assertSame( "Removed from $tenup->post_title Five for the Future pledge", $mailer->mock_sent[0]['subject'] );
}
}