From 182fc16021917f665e91a8d43abbfde1daada711 Mon Sep 17 00:00:00 2001 From: Paul Driver Date: Fri, 22 Jul 2011 14:41:34 -0500 Subject: [PATCH] added WaitForUserConfirmation activity --- docs/changelog/7.x.x.txt | 1 + docs/upgrades/upgrade_7.10.20-7.10.21.pl | 17 +- lib/WebGUI/Operation.pm | 1 + lib/WebGUI/Operation/User.pm | 32 +++ lib/WebGUI/Workflow/Activity.pm | 23 ++ .../Activity/WaitForUserConfirmation.pm | 270 ++++++++++++++++++ .../Activity_WaitForUserConfirmation.pm | 74 +++++ sbin/testEnvironment.pl | 1 + t/Workflow/Activity/WaitForUserConfirmation.t | 110 +++++++ 9 files changed, 526 insertions(+), 3 deletions(-) create mode 100644 lib/WebGUI/Workflow/Activity/WaitForUserConfirmation.pm create mode 100644 lib/WebGUI/i18n/English/Activity_WaitForUserConfirmation.pm create mode 100644 t/Workflow/Activity/WaitForUserConfirmation.t diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index aa590d5b2..5714648c6 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -1,6 +1,7 @@ 7.10.21 - added #9668 extension template variable to attachment loops for the following assets: Article,Post,Event,File,Form::Attachments,Folder + - added WaitForUserConfirmation workflow activity - added the optional WebGUI::Content::PDFGenerator, not enabled by default (see the module's documentation). - fixed #12204: Default forum notification template produces invalid HTML diff --git a/docs/upgrades/upgrade_7.10.20-7.10.21.pl b/docs/upgrades/upgrade_7.10.20-7.10.21.pl index 0e4b75f1e..0d9612b79 100644 --- a/docs/upgrades/upgrade_7.10.20-7.10.21.pl +++ b/docs/upgrades/upgrade_7.10.20-7.10.21.pl @@ -29,12 +29,23 @@ my $quiet; # this line required my $session = start(); # this line required - -# upgrade functions go here - +addWaitForConfirmationWorkflow($session); finish($session); # this line required +#---------------------------------------------------------------------------- +sub addWaitForConfirmationWorkflow { + my $session = shift; + my $c = $session->config; + my $exists = $c->get('workflowActivities/WebGUI::User'); + my $class = 'WebGUI::Workflow::Activity::WaitForUserConfirmation'; + unless (grep { $_ eq $class } @$exists) { + print "Adding WaitForUserConfirmation workflow..." unless $quiet; + $c->addToArray('workflowActivities/WebGUI::User' => $class); + print "Done!\n" unless $quiet; + } +} + #---------------------------------------------------------------------------- # Describe what our function does #sub exampleFunction { diff --git a/lib/WebGUI/Operation.pm b/lib/WebGUI/Operation.pm index d3c56f3a3..2e9273dfa 100644 --- a/lib/WebGUI/Operation.pm +++ b/lib/WebGUI/Operation.pm @@ -201,6 +201,7 @@ sub getOperations { 'ajaxDeleteUser' => 'User', 'ajaxUpdateUser' => 'User', 'becomeUser' => 'User', + 'confirmUserEmail' => 'User', 'deleteUser' => 'User', 'editUser' => 'User', 'editUserSave' => 'User', diff --git a/lib/WebGUI/Operation/User.pm b/lib/WebGUI/Operation/User.pm index db4bc5c51..30ebbb93e 100644 --- a/lib/WebGUI/Operation/User.pm +++ b/lib/WebGUI/Operation/User.pm @@ -418,6 +418,38 @@ sub www_ajaxCreateUser { #------------------------------------------------------------------- +=head2 www_confirmUserEmail ( ) + +Process links clicked from mails sent out by the WaitForUserConfmration +workflow activity. + +=cut + +sub www_confirmUserEmail { + my $session = shift; + my $f = $session->form; + my $instanceId = $f->get('instanceId'); + my $token = $f->get('token'); + my $actId = $f->get('activityId'); + my $activity = WebGUI::Workflow::Activity->new($session, $actId) + or die; + my $instance = WebGUI::Workflow::Instance->new($session, $instanceId) + or die; + if ($activity->confirm($instance, $token)) { + my $msg = $activity->get('okMessage'); + unless ($msg) { + my $i18n = WebGUI::International->new($session, 'WebGUI'); + $msg = $i18n->get('ok'); + } + return $session->style->userStyle($msg); + } + else { + return $session->privilege->noAccess; + } +} + +#------------------------------------------------------------------- + =head2 www_ajaxDeleteUser ( ) Delete a user using a web service. diff --git a/lib/WebGUI/Workflow/Activity.pm b/lib/WebGUI/Workflow/Activity.pm index b872d7b3c..b9dfd42eb 100644 --- a/lib/WebGUI/Workflow/Activity.pm +++ b/lib/WebGUI/Workflow/Activity.pm @@ -76,6 +76,29 @@ These methods are available from this class: #------------------------------------------------------------------- +=head2 changeWorkflow ( $workflowId, $instance, $skipDelete ) + +Kicks a new workflow in a new instance with the same object the current +instance has, deleting the old instance unless you say otherwise. + +=cut + +sub changeWorkflow { + my ($self, $workflowId, $instance, $skipDelete) = @_; + WebGUI::Workflow::Instance->create( + $self->session, { + workflowId => $workflowId, + methodName => $instance->get('methodName'), + className => $instance->get('className'), + parameters => $instance->get('parameters'), + priority => $instance->get('priority'), + } + )->start(1); + $instance->delete() unless $skipDelete; +} + +#------------------------------------------------------------------- + =head2 cleanup ( ) Override this activity to add a cleanup routine to be run if an instance diff --git a/lib/WebGUI/Workflow/Activity/WaitForUserConfirmation.pm b/lib/WebGUI/Workflow/Activity/WaitForUserConfirmation.pm new file mode 100644 index 000000000..da7404be9 --- /dev/null +++ b/lib/WebGUI/Workflow/Activity/WaitForUserConfirmation.pm @@ -0,0 +1,270 @@ +package WebGUI::Workflow::Activity::WaitForUserConfirmation; + +use warnings; +use strict; + +use base 'WebGUI::Workflow::Activity'; +use WebGUI::Asset::Template; +use WebGUI::International; +use WebGUI::Inbox::Message; +use WebGUI::Macro; +use Kwargs; +use Tie::IxHash; + +#----------------------------------------------------------------- + +=head2 confirm ( $instance, $token ) + +Returns true (and sets the workflow as done) if the token matches the one we +generated for the email. + +=cut + +sub confirm { + my ($self, $instance, $token) = @_; + my $id = $self->getId; + return 0 unless $token eq $instance->getScratch("$id-token"); + $instance->setScratch("$id-status", 'done'); + return 1; +} + +#----------------------------------------------------------------- + +=head2 definition ( ) + +See WebGUI::Workflow::Activity::definition for details. + +=cut + +sub definition { + my ($class, $session, $def) = @_; + my $i18n = WebGUI::International->new( + $session, 'Activity_WaitForUserConfirmation' + ); + + tie my %props, 'Tie::IxHash', ( + emailFrom => { + fieldType => 'user', + defaultValue => 3, + }, + emailSubject => { + fieldType => 'text', + defaultValue => 'Confirmation Email', + }, + template => { + fieldType => 'textarea', + defaultValue => $i18n->get('your template goes here'), + }, + templateParser => { + fieldType => 'templateParser', + defaultValue => $session->config->get('defaultTemplateParser'), + }, + okMessage => { + fieldType => 'HTMLArea', + }, + waitBetween => { + fieldType => 'interval', + defaultValue => 60*5 + }, + expireAfter => { + fieldType => 'interval', + defaultValue => 60*60*24*7, + }, + doOnExpire => { + fieldType => 'workflow', + type => 'WebGUI::User', + none => 1, + } + ); + + for my $key (keys %props) { + $props{$key}{label} = $i18n->get("$key label"); + $props{$key}{hoverHelp} = $i18n->get("$key hoverHelp"); + } + + push @$def, { + name => $i18n->get('topicName'), + properties => \%props, + }; + + return $class->SUPER::definition( $session, $def ); +} + +#----------------------------------------------------------------- + +=head2 execute ( ) + +See WebGUI::Workflow::Activity::execute for details. + +=cut + +sub execute { + my ($self, $object, $instance) = @_; + my $id = $self->getId; + my $statk = "$id-status"; + my $start = "$id-started"; + my $status = $instance->getScratch($statk); + my $subject = $self->get('emailSubject'); + my $parser = $self->get('templateParser'); + WebGUI::Macro::process(\$subject); + my $body = WebGUI::Asset::Template->processRaw( + $self->session, + $self->get('template'), + $self->getTemplateVariables($object, $instance), + $parser, + ); + WebGUI::Macro::process(\$body); + unless ($status) { + $instance->setScratch($start => $self->now); + $self->sendEmail( + from => $self->get('emailFrom'), + to => $object->userId, + subject => $subject, + body => $body, + ); + $instance->setScratch($statk => 'waiting'); + return $self->wait; + } + return $self->COMPLETE if $status eq 'done' || $status eq 'expired'; + if ($status eq 'waiting') { + my $end = $instance->getScratch($start) + $self->get('expireAfter'); + if ($self->now > $end) { + $self->expire($instance); + $instance->setScratch($statk => 'expired'); + return $self->COMPLETE; + } + return $self->wait; + } + $self->session->log->error("Unknown status: $status"); + return $self->ERROR; +} + +#----------------------------------------------------------------- + +=head2 expire ( $instance ) + +Deletes the workflow instance and kicks off a configured workflow if there is +one. + +=cut + +sub expire { + my ($self, $instance) = @_; + if (my $id = $self->get('doOnExpire')) { + $self->changeWorkflow($id, $instance); + } + else { + $instance->delete(); + } +} + +#----------------------------------------------------------------- + +=head2 getTemplateVariables ( $object, $instance ) + +Returns the variables to be used in rendering the email template. + +=cut + +sub getTemplateVariables { + my ($self, $object, $instance) = @_; + + my $user = $object->get; + + # Kill all humans. I means references. Currently there seems to be a bug + # in _rewriteVars in some of the template plugins that disallows us from + # using arrayrefs with just strings in them, which is a common occurrence + # in profile fields. When that bug gets fixed, we can (and should) take + # this out. + delete @{$user}{grep {ref $user->{$_} } keys %$user}; + + return { + user => $user, + link => $self->link($instance), + } +} + +#----------------------------------------------------------------- + +=head2 link ( $instance ) + +Returns the URL that needs to be visited by the user. + +=cut + +sub link { + my ($self, $instance) = @_; + my $url = $self->session->url; + my $aid = $self->getId; + my $iid = $instance->getId; + my $token = $instance->getScratch("$aid-token"); + $instance->setScratch("$aid-token", $token = $self->token) unless $token; + my $path = $url->page( + "op=confirmUserEmail;instanceId=$iid;token=$token;activityId=$aid" + ); + return $url->getSiteURL . $url->gateway($path); +} + +#----------------------------------------------------------------- + +=head2 now ( ) + +Just returns the current time, nice for testing. + +=cut + +sub now { time } + +#----------------------------------------------------------------- + +=head2 sendEmail ( { from, to, subject, body } ) + +Takes a user to send email from, to, with a subject and a body all as +keywords. Mostly here for testing, it just calls +WebGUI::Inbox::Message->create() with proper arguments. 'from' and 'to' are +userIds, not user objects. + +=cut + +sub sendEmail { + my ($self, $from, $to, $subject, $body) = kwn @_, 1, + qw(from to subject body); + + WebGUI::Inbox::Message->create( + $self->session, { + message => $body, + subject => $subject, + status => 'pending', + userId => $to, + sentBy => $from, + } + ); +} + +#----------------------------------------------------------------- + +=head2 token ( ) + +Returns a random string to use as a token in the confirmation link + +=cut + +sub token { + my $self = shift; + $self->session->id->generate; +} + +#----------------------------------------------------------------- + +=head2 wait ( ) + +Waits for the configured waitBetween interval. + +=cut + +sub wait { + my $self = shift; + return $self->WAITING($self->get('waitBetween')); +} + +1; diff --git a/lib/WebGUI/i18n/English/Activity_WaitForUserConfirmation.pm b/lib/WebGUI/i18n/English/Activity_WaitForUserConfirmation.pm new file mode 100644 index 000000000..bcca378d6 --- /dev/null +++ b/lib/WebGUI/i18n/English/Activity_WaitForUserConfirmation.pm @@ -0,0 +1,74 @@ +package WebGUI::i18n::English::Activity_WaitForUserConfirmation; + +use strict; + +our $I18N = { + 'doOnExpire hoverHelp' => { + message => q{Workflow to run after the waiting period has expired.}, + lastUpdated => 1311365415, + }, + 'doOnExpire label' => { + message => q{Do On Expire}, + lastUpdated => 1311365395, + }, + 'emailFrom hoverHelp' => { + message => q{Which user should the confirmation email be from?}, + lastUpdated => 1311363981, + }, + 'emailFrom label' => { + message => q{Email From}, + lastUpdated => 1311363958, + }, + 'emailSubject label' => { + message => q{Email Subject}, + lastUpdated => 1311363994, + }, + 'expireAfter hoverHelp' => { + message => q{How long should we wait for the user to respond?}, + lastUpdated => 1311363900, + }, + 'expireAfter label' => { + message => q{Expire After}, + lastUpdated => 1311363885, + }, + 'okMessage label' => { + message => q{Confirmation Message}, + lastUpdated => 1311612584, + }, + 'okMessage hoverHelp' => { + message => q{Message to display to the user when he clicks the confirm link}, + lastUpdated => 1311612632, + }, + 'template hoverHelp' => { + message => q{Raw template code for the body of the email goes here.}, + lastUpdated => 1311364201, + }, + 'template label' => { + message => q{Template}, + lastUpdated => 1311364181, + }, + 'templateParser label' => { + message => q{Template Parser}, + lastUpdated => 1311364015, + }, + 'topicName' => { + message => q{Wait For User Confirmation}, + lastUpdated => 1311364913, + }, + 'waitBetween hoverHelp' => { + message => q{How long should we wait in between checks to see if the user has clicked the link?}, + lastUpdated => 1311363934, + }, + 'waitBetween label' => { + message => q{Wait Interval}, + lastUpdated => 1311363920, + }, + 'your template goes here' => { + message => q{Your template goes here!}, + lastUpdated => 1311365274, + }, +}; + +1; + +#vim:ft=perl diff --git a/sbin/testEnvironment.pl b/sbin/testEnvironment.pl index f2aaf3814..68cfec516 100755 --- a/sbin/testEnvironment.pl +++ b/sbin/testEnvironment.pl @@ -155,6 +155,7 @@ checkModule('IO::Socket::SSL', ); checkModule('Net::Twitter', "3.13006" ); checkModule('PerlIO::eol', "0.14" ); checkModule('Monkey::Patch', '0.03' ); +checkModule('Kwargs', ); checkModule('Data::ICal', '0.16' ); checkModule('common::sense', '3.2' ); checkModule('Geo::Coder::Googlev3', '0.07' ); diff --git a/t/Workflow/Activity/WaitForUserConfirmation.t b/t/Workflow/Activity/WaitForUserConfirmation.t new file mode 100644 index 000000000..3a5e0f366 --- /dev/null +++ b/t/Workflow/Activity/WaitForUserConfirmation.t @@ -0,0 +1,110 @@ +use warnings; +use strict; + +use FindBin; +use lib "$FindBin::Bin/../../lib"; +use lib "$FindBin::Bin/../../t/lib"; + +use WebGUI::Test; +use Test::More tests => 28; +use Test::MockObject; +use Test::MockObject::Extends; +use WebGUI::Workflow::Activity; +use Kwargs; +use URI; + +my $session = WebGUI::Test->session; + +my $act = WebGUI::Workflow::Activity->newByPropertyHashRef( + $session, { + className => 'WebGUI::Workflow::Activity::WaitForUserConfirmation', + activityId => 'test-activity', + expireAfter => 60*60*24, + waitBetween => 60*5, + emailFrom => 3, + emailSubject => 'Confirmation Email', + templateParser => 'WebGUI::Asset::Template::TemplateToolkit', + template => 'Hey [% user.firstName %] [% user.lastName %], ' + . 'click $link!', + } +); + +is $act->wait, $act->WAITING(60*5), 'wait helper method'; + +$act = Test::MockObject::Extends->new($act); + +my (%scratch, %profile); +%profile = ( + email => 'target@test.com', + firstName => 'Target', + lastName => 'Targetson', +); + +my $user = Test::MockObject->new + ->mock(get => sub { \%profile }) + ->mock(userId => sub { 'test-user-id' }); + +my $workflow = Test::MockObject->new + ->mock(setScratch => sub { $scratch{$_[1]} = $_[2] }) + ->mock(getScratch => sub { $scratch{$_[1]} }) + ->mock(getId => sub { 'test-workflow' }); + +my ($expired, $sent) = (0,0); +$act->mock(sendEmail => sub { $sent++ }) + ->mock(expire => sub { $expired++ }) + ->mock(now => sub { 100 }) + ->mock(token => sub { 'test-token' }); + +my $st = 'test-activity-status'; + +sub ex { $act->execute($user, $workflow) } +sub clr { + delete @scratch{'test-activity-started', $st}; + $sent = 0; + $expired = 0; +} + +is ex, $act->wait, 'from scratch returns waiting'; +is $sent, 1, 'one email sent'; +is $scratch{$st}, 'waiting', 'scratch is waiting'; +is $scratch{'test-activity-started'}, 100, 'started at mocked time'; +is ex, $act->wait, 'still waiting'; +is $sent, 1, 'did not send second email'; +is $scratch{$st}, 'waiting', 'scratch still waiting'; +$scratch{$st} = 'done'; +is ex, $act->COMPLETE, 'returns complete after done'; +is ex, $act->COMPLETE, 'forever'; +is $expired, 0, 'not expired though'; +clr; +is $act->execute($user, $workflow), $act->wait, 'waiting after clear'; +is $sent, 1, 'one email sent'; +$act->mock(now => sub { 60*60*24+101 }); +is ex, $act->COMPLETE, 'complete after expired'; +is $scratch{$st}, 'expired', 'expired status'; +is $expired, 1, 'expire called'; + +clr; +my ($self, $to, $from, $subject, $body); +$act->mock( + sendEmail => sub { + ($self, $to, $from, $subject, $body) = kwn @_, 1, + qw(to from subject body); + } +); +ex; +is $to, $user->userId, 'to'; +is $from, 3, 'from'; +is $subject, 'Confirmation Email', 'subject'; +my $link = URI->new($act->link($workflow)); +my %p = $link->query_form; +is $body, "Hey Target Targetson, click $link!", 'body'; +is $p{token}, 'test-token', 'token in link'; +is $p{instanceId}, 'test-workflow', 'instance id in link'; +is $p{activityId}, 'test-activity', 'activity id in link'; +$act->unmock('token'); +is $act->link($workflow), $link, 'token only generated once'; + +ok !$act->confirm($workflow, 'not-the-token'), 'bad token'; +is $scratch{$st}, 'waiting', 'wait after bad'; +ok $act->confirm($workflow, 'test-token'), 'good token'; +is $scratch{$st}, 'done', 'done after good';