Skip to content

Commit

Permalink
Move achievement item use form to ProblemSet page.
Browse files Browse the repository at this point in the history
Move the forms to use achievement items to a modal on the problem
set page.  If any achievement item is usable on the current set,
a button to "Use Achievement Reward" will be available, and open
a modal which lists all achievement items that can be applied.

The Achievements page still lists all rewards the student has earned,
and provides instructions to use them from the problem assignment they
wish to use the achievement on.
  • Loading branch information
somiaj committed Feb 2, 2025
1 parent ada1b90 commit e9c4625
Show file tree
Hide file tree
Showing 28 changed files with 841 additions and 1,293 deletions.
33 changes: 0 additions & 33 deletions htdocs/js/AchievementItems/achievementitems.js

This file was deleted.

130 changes: 95 additions & 35 deletions lib/WeBWorK/AchievementItems.pm
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package WeBWorK::AchievementItems;
use Mojo::Base -signatures;

use WeBWorK::Utils qw(thaw_base64);
use WeBWorK::Utils qw(nfreeze_base64 thaw_base64);

# List of available achievement items. Make sure to add any new items to this list. Furthermore, the elements in this
# list have to match the class name of the achievement item classes loaded below.
Expand Down Expand Up @@ -44,65 +44,125 @@ use constant ITEMS => [ qw(

=head2 NAME
This is the base class for achievement times. This defines an interface for all of the achievement items. Each
achievement item will have a name, a description, a method for creating an html form to get its inputs called print_form
and a method for applying those inputs called use_item.
This is the base class for achievement times. This defines an interface for all of the achievement items.
Each achievement item will have an id, a name, a description, and the three methods can_use (checks if the
item can be used on the given set), print_form (prints the form to use the item), and use_item.
Note: the ID has to match the name of the class.
The global method UserItems returns an array of all achievement items available to the given user. If no
set is included, a list of all earned achievement items is return. If provided a set and corresponding problem
or test version records, a list of items usable on the current set and records paired with an input form to
use the item is returned. This method will also process any posts to use the achievement item.
=cut

sub id ($c) { return $c->{id}; }
sub name ($c) { return $c->{name}; }
sub description ($c) { return $c->{description}; }
sub id ($self) { return $self->{id}; }
sub name ($self) { return $self->{name}; }
sub count ($self) { return $self->{count}; }
sub description ($self) { return $self->{description}; }

# Method to find all achievement items available to the given user.
# If $set is undefined return an array reference of all earned items.
# If $set is defined, return an array reference of the usable items
# for the given $set and problem or test versions records. Each item
# is paired with its input form to use the item.
sub UserItems ($c, $userName, $set, $records) {
my $db = $c->db;

# This is a global method that returns all of the provided users items.
sub UserItems ($userName, $db, $ce) {
# return unless the user has global achievement data
my $globalUserAchievement = $db->getGlobalUserAchievement($userName);
# When acting as another user, achievement items can be listed but not used.
return if $set && $userName ne $c->param('user');

return unless ($globalUserAchievement->frozen_hash);
# Return unless the user has global achievement data.
my $globalUserAchievement = $c->{globalData} // $db->getGlobalUserAchievement($userName);
return unless $globalUserAchievement && $globalUserAchievement->frozen_hash;

my $globalData = thaw_base64($globalUserAchievement->frozen_hash);
my $globalData = thaw_base64($globalUserAchievement->frozen_hash);
my $use_item_id = $c->param('use_achievement_item_id') // '';
my @items;

# Get a new item object for each type of item.
for my $item (@{ +ITEMS }) {
push(@items, [ "WeBWorK::AchievementItems::$item"->new, $globalData->{$item} ])
if ($globalData->{$item});
next unless $globalData->{$item};
my $achievementItem = "WeBWorK::AchievementItems::$item"->new;
$achievementItem->{count} = $globalData->{$item};

# Return list of achievements items if $set is not defined.
unless ($set) {
push(@items, $achievementItem);
next;
}
next unless $achievementItem->can_use($set, $records);

# Use the achievement item.
if ($use_item_id eq $item) {
my $message = $achievementItem->use_item($set, $records, $c);
if ($message) {
$globalData->{$item}--;
$achievementItem->{count}--;
$globalUserAchievement->frozen_hash(nfreeze_base64($globalData));
$db->putGlobalUserAchievement($globalUserAchievement);
$c->addgoodmessage($c->maketext('[_1] succesffuly used. [_2]', $achievementItem->name, $message));
}
}

push(@items, [ $achievementItem, $use_item_id ? '' : $achievementItem->print_form($set, $records, $c) ]);
}

# If an achievement item has been used, double check if the achievement items can still be used
# since the item count could now be zero or an achievement item has altered the set/records.
# Input forms are also built here to account for any possible change.
if ($set && $use_item_id) {
my @new_items;
for (@items) {
my $item = $_->[0];
next unless $item->{count} && $item->can_use($set, $records);
push(@new_items, [ $item, $item->print_form($set, $records, $c) ]);
}
return \@new_items;
}
return \@items;
}

# Method that returns a string with the achievement name and number of remaining items.
sub remaining_title ($self, $c) {
if ($self->count > 1) {
return $c->maketext('[_1] ([_2] remaining)', $c->maketext($self->name), $self->count);
} elsif ($self->count < 0) {
return $c->maketext('[_1] (unlimited reusability)', $c->maketext($self->name));
} else {
return $c->maketext('[_1] (1 remains)', $c->maketext($self->name));
}
}

# Utility method for outputing a form row with a label and popup menu.
# The id, label_text, and values are required parameters.
sub form_popup_menu_row ($c, %options) {
my %params = (
id => '',
label_text => '',
label_attr => {},
values => [],
menu_attr => {},
menu_container_attr => {},
add_container => 1,
id => '',
first_item => '',
label_text => '',
label_attr => {},
values => [],
menu_attr => {},
add_container => 1,
%options
);

$params{label_attr}{class} //= 'col-4 col-form-label';
$params{menu_attr}{class} //= 'form-select';
$params{menu_container_attr}{class} //= 'col-8';
$params{label_attr}{class} //= 'col-form-label';
$params{menu_attr}{class} //= 'form-select';

my $row_contents = $c->c(
$c->label_for($params{id} => $params{label_text}, %{ $params{label_attr} }),
$c->tag(
'div',
%{ $params{menu_container_attr} },
$c->select_field($params{id} => $params{values}, id => $params{id}, %{ $params{menu_attr} })
)
)->join('');
unshift(@{ $params{values} }, [ $params{first_item} => '' ]) if $params{first_item};

my $row_contents = $c->tag(
'div',
class => 'form-floating',
$c->c(
$c->select_field($params{id} => $params{values}, %{ $params{menu_attr} }),
$c->label_for($params{id} => $params{label_text}, %{ $params{label_attr} })
)->join('')
);

return $params{add_container} ? $c->tag('div', class => 'row mb-3', $row_contents) : $row_contents;
return $params{add_container} ? $c->tag('div', class => 'my-3', $row_contents) : $row_contents;
}

END {
Expand Down
75 changes: 24 additions & 51 deletions lib/WeBWorK/AchievementItems/AddNewTestGW.pm
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ use Mojo::Base 'WeBWorK::AchievementItems', -signatures;

# Item to allow students to take an additional version of a test within its test version interval

use WeBWorK::Utils qw(x nfreeze_base64 thaw_base64);
use WeBWorK::Utils::DateTime qw(before between);
use WeBWorK::Utils::Sets qw(format_set_name_display);
use WeBWorK::Utils qw(x);
use WeBWorK::Utils::DateTime qw(between);

sub new ($class) {
return bless {
Expand All @@ -33,60 +32,34 @@ sub new ($class) {
}, $class;
}

sub print_form ($self, $sets, $setProblemIds, $c) {
my $db = $c->db;

my @openGateways;

# Find the template sets of open gateway quizzes.
for my $set (@$sets) {
push(@openGateways, [ format_set_name_display($set->set_id) => $set->set_id ])
if $set->assignment_type =~ /gateway/
&& $set->set_id !~ /,v\d+$/
&& between($set->open_date, $set->due_date);
}

return unless @openGateways;
sub can_use ($self, $set, $records) {
return
$set->assignment_type =~ /gateway/
&& $set->set_id !~ /,v\d+$/
&& between($set->open_date, $set->due_date)
&& $set->versions_per_interval > 0;
}

return $c->c(
$c->tag('p', $c->maketext('Add a new version for which test?')),
WeBWorK::AchievementItems::form_popup_menu_row(
$c,
id => 'adtgw_gw_id',
label_text => $c->maketext('Test Name'),
values => \@openGateways,
menu_attr => { dir => 'ltr' }
sub print_form ($self, $set, $records, $c) {
return $c->tag(
'p',
$c->maketext(
'Increase the number of versions from [_1] to [_2] for this test.',
$set->versions_per_interval,
$set->versions_per_interval + 1
)
)->join('');
);
}

sub use_item ($self, $userName, $c) {
my $db = $c->db;
my $ce = $c->ce;

# Validate data
my $globalUserAchievement = $db->getGlobalUserAchievement($userName);
return 'No achievement data?!?!?!' unless $globalUserAchievement->frozen_hash;

my $globalData = thaw_base64($globalUserAchievement->frozen_hash);
return "You are $self->{id} trying to use an item you don't have" unless $globalData->{ $self->{id} };

my $setID = $c->param('adtgw_gw_id');
return 'You need to input a Test Name' unless defined $setID;

my $set = $db->getMergedSet($userName, $setID);
my $userSet = $db->getUserSet($userName, $setID);
return q{Couldn't find that set!} unless $set && $userSet;

# Add an additional version per interval to the set.
$userSet->versions_per_interval($set->versions_per_interval + 1) unless $set->versions_per_interval == 0;
sub use_item ($self, $set, $records, $c) {
# Increase the number of versions per interval by 1.
my $db = $c->db;
my $userSet = $db->getUserSet($set->user_id, $set->set_id);
$set->versions_per_interval($set->versions_per_interval + 1);
$userSet->versions_per_interval($set->versions_per_interval);
$db->putUserSet($userSet);

$globalData->{ $self->{id} }--;
$globalUserAchievement->frozen_hash(nfreeze_base64($globalData));
$db->putGlobalUserAchievement($globalUserAchievement);

return;
return $c->maketext('One additional test version added to this test.');
}

1;
Loading

0 comments on commit e9c4625

Please sign in to comment.