Skip to content

Commit 268d5e3

Browse files
committed
Better keep track of which sets are complete in achievements.
Store the setID of all completed sets in the globalHash when evaluating achievements. This allows achievements to use this data vs just counting the number of completed sets. One use case is being able to exclude optional sets, such as review sets, from some achievements without completely excluding them from all achievements. In addition saving all the setIDs can avoid a double counting completed sets, as there is currently no check to ensure a set is not counted multiple times.
1 parent 0b1fa1c commit 268d5e3

1 file changed

Lines changed: 37 additions & 32 deletions

File tree

lib/WeBWorK/AchievementEvaluator.pm

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ sub checkForAchievements ($problem_in, $c, %options) {
2323
# Make a copy of the problem so that local modifications do not persist.
2424
our $problem = $db->newUserProblem($problem_in);
2525

26-
# Date and time for course timezone (may differ from the server timezone)
27-
# Saved into separate array
26+
# Date and time for course timezone (may differ from the server timezone).
27+
# Saved into separate array:
2828
# https://metacpan.org/pod/DateTime
2929
my $dtCourseTime = DateTime->from_epoch(epoch => time(), time_zone => $ce->{siteDefaults}{timezone} || 'local');
3030

31-
# Set up variables and get achievements
31+
# Set up variables and get achievements.
3232
my $cheevoMessage = $c->c;
3333
my $user_id = $problem->user_id;
3434
my $set_id = $problem->set_id;
3535

36-
# exit early if the set is to be ignored by achievements
36+
# Exit early if the set is to be ignored by achievements.
3737
foreach my $excludedSet (@{ $ce->{achievementExcludeSet} }) {
3838
return '' if $set_id eq $excludedSet;
3939
}
@@ -45,20 +45,20 @@ sub checkForAchievements ($problem_in, $c, %options) {
4545
my $isGatewaySet = ($set->assignment_type =~ /gateway/) ? 1 : 0;
4646
my $isJitarSet = ($set->assignment_type eq 'jitar') ? 1 : 0;
4747

48-
# If its a gateway set get the current version
48+
# If its a gateway set get the current version.
4949
if ($isGatewaySet) {
5050
$set = $db->getSetVersion($user_id, $set_id, $options{setVersion});
5151
}
5252

53-
# If no global data then initialize
53+
# If no global data then initialize.
5454
if (not $globalUserAchievement) {
5555
$globalUserAchievement = $db->newGlobalUserAchievement();
5656
$globalUserAchievement->user_id($user_id);
5757
$globalUserAchievement->achievement_points(0);
5858
$db->addGlobalUserAchievement($globalUserAchievement);
5959
}
6060

61-
#These need to be "our" so that they can share to the safe container
61+
# These need to be "our" so that they can share to the safe container.
6262
our $counter;
6363
our $maxCounter;
6464
our $achievementPoints = $globalUserAchievement->achievement_points;
@@ -75,21 +75,21 @@ sub checkForAchievements ($problem_in, $c, %options) {
7575

7676
my $compartment = WeBWorK::WWSafe->new;
7777

78-
#initialize things that are ""
78+
# Initialize things that are "".
7979
if (not $achievementPoints) {
8080
$achievementPoints = 0;
8181
$globalUserAchievement->achievement_points(0);
8282
}
8383

84-
#Methods alowed in the safe container
84+
# Methods allowed in the safe container.
8585
$compartment->permit(qw(time localtime));
8686

87-
#Thaw_Base64 globalData hash
87+
# Thaw_Base64 globalData hash.
8888
if ($globalUserAchievement->frozen_hash) {
8989
$globalData = thaw_base64($globalUserAchievement->frozen_hash);
9090
}
9191

92-
#Generate hash of user achievements:
92+
# Generate hash of user achievements:
9393
foreach my $achievement (@achievements) {
9494
next unless $achievement->enabled;
9595
my $userAchievement = $db->getUserAchievement($user_id, $achievement->achievement_id);
@@ -132,19 +132,24 @@ sub checkForAchievements ($problem_in, $c, %options) {
132132
}
133133
}
134134

135-
$globalData->{completeSets}++ if ($allcorrect);
135+
# Store the setID of all completed sets so they can be used in achievements and not double counted.
136+
$globalData->{completedSetIds} //= {};
137+
if ($allcorrect) {
138+
++$globalData->{completeSets} unless $globalData->{completedSetIds}{$set_id};
139+
$globalData->{completedSetIds}{$set_id} = 1;
140+
}
136141

137-
# get the problem tags if its not a gatway
138-
# if it is a gateway get rid of $problem since it doensn't make sense
142+
# Get the problem tags if its not a gateway.
143+
# If it is a gateway get rid of $problem since it doesn't make sense.
139144
if ($isGatewaySet) {
140145
$problem = undef;
141146
} else {
142147
my $templateDir = $ce->{courseDirs}{templates};
143148
$tags = WeBWorK::Utils::Tags->new($templateDir . '/' . $problem->source_file());
144149
}
145150

146-
#These variables are shared with the safe compartment. The achievement evaulators
147-
# have access too
151+
# These variables are shared with the safe compartment.
152+
# The achievement evaluators have access too:
148153
# $problem - the problem data;
149154
# @setProblems - the problem data for everything from this set;
150155
# $localData - the hash that is used only for this achievement
@@ -161,7 +166,7 @@ sub checkForAchievements ($problem_in, $c, %options) {
161166
$compartment->share(qw( $problem @setProblems $localData $maxCounter $userAchievements
162167
$globalData $counter $nextLevelPoints $set $achievementPoints $tags @courseDateTime));
163168

164-
#load any preamble code
169+
# Load any preamble code.
165170
my $preamble = '';
166171
my $source;
167172
if (-e "$ce->{courseDirs}{achievements}/$ce->{achievementPreambleFile}") {
@@ -170,27 +175,27 @@ sub checkForAchievements ($problem_in, $c, %options) {
170175
$preamble = <$PREAMB>;
171176
close($PREAMB);
172177
}
173-
#loop through the various achievements, see if they have been obtained,
178+
# Loop through the various achievements, see if they have been obtained.
174179
foreach my $achievement (@achievements) {
175-
#skip achievements not assigned, not enabled, and that are already earned, or if it doesn't match the set type
180+
# Skip achievements not assigned, not enabled, and that are already earned, or if it doesn't match the set type.
176181
next unless $achievement->enabled;
177182
my $achievement_id = $achievement->achievement_id;
178-
next unless ($db->existsUserAchievement($user_id, $achievement_id));
183+
next unless $db->existsUserAchievement($user_id, $achievement_id);
179184
my $userAchievement = $db->getUserAchievement($user_id, $achievement_id);
180-
next if ($userAchievement->earned);
185+
next if $userAchievement->earned;
181186
my $setType = $set->assignment_type;
182187
next unless $achievement->assignment_type =~ /$setType/;
183188

184-
#thaw_base64 localData hash
189+
# Thaw_base64 localData hash.
185190
if ($userAchievement->frozen_hash) {
186191
$localData = thaw_base64($userAchievement->frozen_hash);
187192
}
188193

189-
#recover counter information (for progress bar achievements)
194+
# Recover counter information (for progress bar achievements).
190195
$counter = $userAchievement->counter;
191196
$maxCounter = $achievement->max_counter;
192197

193-
#check the achievement using Safe
198+
# Check the achievement using Safe.
194199
my $sourceFilePath = $ce->{courseDirs}{achievements} . '/' . $achievement->test;
195200
if (-e $sourceFilePath) {
196201
local $/ = undef;
@@ -205,11 +210,11 @@ sub checkForAchievements ($problem_in, $c, %options) {
205210
my $earned = $compartment->reval($preamble . "\n" . $source);
206211
warn "There were errors in achievement $achievement_id\n" . $@ if $@;
207212

208-
#if we have a new achievement then update achievement points
213+
# If we have a new achievement then update achievement points.
209214
if ($earned) {
210215
$userAchievement->earned(1);
211216

212-
# update userAchievements hash with earned status.
217+
# Update userAchievements hash with earned status.
213218
$userAchievements->{$achievement_id} = $earned;
214219

215220
if ($achievement->category eq 'level') {
@@ -223,14 +228,14 @@ sub checkForAchievements ($problem_in, $c, %options) {
223228
push(@$cheevoMessage, $c->include('AchievementEvaluator/cheevoMessage', achievement => $achievement));
224229

225230
my $points = $achievement->points;
226-
#just in case points is an uninitialized variable
231+
# Just in case points is an uninitialized variable.
227232
$points = 0 unless $points;
228233

229234
$globalUserAchievement->achievement_points($globalUserAchievement->achievement_points + $points);
230-
#this variable is shared and should be considered iffy
235+
# This variable is shared and should be considered iffy.
231236
$achievementPoints += $points;
232237

233-
# if email_template is defined, send an email to the user
238+
# If email_template is defined, send an email to the user.
234239
$c->minion->enqueue(
235240
send_achievement_email => [ {
236241
recipient => $user_id,
@@ -245,14 +250,14 @@ sub checkForAchievements ($problem_in, $c, %options) {
245250
) if ($ce->{mail}{achievementEmailFrom} && $achievement->email_template);
246251
}
247252

248-
#update counter, nfreeze_base64 localData and store
253+
# Update counter, nfreeze_base64 localData and store.
249254
$userAchievement->counter($counter);
250255
$userAchievement->frozen_hash(nfreeze_base64($localData));
251256
$db->putUserAchievement($userAchievement);
252257

253-
} #end for loop
258+
} # End for loop.
254259

255-
#nfreeze_base64 globalData and store
260+
# nfreeze_base64 globalData and store.
256261
$globalUserAchievement->frozen_hash(nfreeze_base64($globalData));
257262
$db->putGlobalUserAchievement($globalUserAchievement);
258263

0 commit comments

Comments
 (0)