Skip to content

Commit bb23dbc

Browse files
committed
PG Problem Editor file selector
This changes the PG Problem Editor to not assume a file type when opened without a file type specified. So when the problem editor is opened from the site navigation link it does not directly open to the blank file template. Instead it opens to a file chooser page. On that page, three options are presented. The user may choose a "New Problem Template", a "File" and give a path relative to the course templates directory, or a "Sample Problem" and pick a sample problem file from a dropdown. In order to support sample problems a new file type needed to be implemented for the problem editor. The sample problem parsing module for PG is used to strip out the documentation when the problem is loaded into the editor. However, I observed that the current code for that used by the sample problem viewer to offer a sample problem file for download is highly innefficient. It currently reads all of the sample problem files, then reads all of the PG macros, and then parses the desired problem stripping out documentation, and finally serves the file. So a corresponding PG pull request adds a `getSampleProblemCode` method that simply reads only the desired sample problem and efficiently strips out the documentation without needing the macros and sample problem metadata. So instead of needing to read 253 files (the number of macros plus the number of sample problems currently), it only reads 1.
1 parent d77931e commit bb23dbc

File tree

9 files changed

+178
-41
lines changed

9 files changed

+178
-41
lines changed

htdocs/js/PGProblemEditor/pgproblemeditor.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,57 @@
11
(() => {
2+
const fileChooserForm = document.forms['pg-editor-file-chooser'];
3+
if (fileChooserForm) {
4+
const newProblemRadio = document.getElementById('new_problem');
5+
6+
const sourceFilePathInput = fileChooserForm.elements['sourceFilePath'];
7+
const filePathRadio = document.getElementById('file_path');
8+
9+
const sampleProblemFileSelect = fileChooserForm.elements['sampleProblemFile'];
10+
const sampleProblemRadio = document.getElementById('sample_problem');
11+
12+
newProblemRadio?.addEventListener('change', () => {
13+
if (newProblemRadio.checked) {
14+
sampleProblemFileSelect.required = false;
15+
sourceFilePathInput.required = false;
16+
}
17+
});
18+
19+
if (filePathRadio && sourceFilePathInput) {
20+
const filePathSelected = () => {
21+
sampleProblemFileSelect.required = false;
22+
sourceFilePathInput.required = true;
23+
filePathRadio.checked = true;
24+
};
25+
filePathRadio.addEventListener('change', () => {
26+
if (filePathRadio.checked) filePathSelected();
27+
});
28+
sourceFilePathInput.addEventListener('focusin', filePathSelected);
29+
}
30+
if (sampleProblemRadio && sampleProblemFileSelect) {
31+
const sampleProblemSelected = () => {
32+
sampleProblemFileSelect.required = true;
33+
sourceFilePathInput.required = false;
34+
sampleProblemRadio.checked = true;
35+
};
36+
sampleProblemRadio.addEventListener('change', () => {
37+
if (sampleProblemRadio.checked) sampleProblemSelected();
38+
});
39+
sampleProblemFileSelect.addEventListener('change', sampleProblemSelected);
40+
sampleProblemFileSelect.addEventListener('focusin', sampleProblemSelected);
41+
}
42+
43+
fileChooserForm.addEventListener('submit', (e) => {
44+
if (!fileChooserForm.checkValidity()) {
45+
e.preventDefault();
46+
e.stopPropagation();
47+
}
48+
49+
fileChooserForm.classList.add('was-validated');
50+
});
51+
52+
return;
53+
}
54+
255
// Add a container for message toasts.
356
const toastContainer = document.createElement('div');
457
toastContainer.classList.add('toast-container', 'position-fixed', 'bottom-0', 'end-0', 'p-3');

lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ The "stationary" source for this problem is stored in the assets/pg directory
9090
and defined in defaults.config as
9191
$webworkFiles{screenSnippets}{blankProblem}
9292
93+
=item sample_problem
94+
95+
This is a special case which allows one to edit a sample PG problem. These
96+
are problems located in the pg/tutorial/sample_problems directory.
97+
9398
=back
9499
95100
=head2 Action
@@ -123,6 +128,7 @@ use WeBWorK::Utils::Files qw(surePathToFile readFile path_is_subdir);
123128
use WeBWorK::Utils::Instructor qw(assignProblemToAllSetUsers addProblemToSet);
124129
use WeBWorK::Utils::JITAR qw(seq_to_jitar_id jitar_id_to_seq);
125130
use WeBWorK::Utils::Sets qw(format_set_name_display);
131+
use SampleProblemParser qw(getSampleProblemCode generateMetadata);
126132

127133
use constant DEFAULT_SEED => 123456;
128134

@@ -191,11 +197,11 @@ sub pre_header_initialize ($c) {
191197
} else {
192198
$c->{file_type} = 'problem';
193199
}
194-
} else {
195-
$c->{file_type} = 'blank_problem';
196200
}
197201
}
198202

203+
return unless $c->{file_type};
204+
199205
# Clean up sourceFilePath and check that sourceFilePath is relative to the templates folder
200206
if ($c->{file_type} eq 'source_path_for_problem_file') {
201207
$c->{sourceFilePath} = $c->getRelativeSourceFilePath($c->param('sourceFilePath'));
@@ -242,6 +248,11 @@ sub initialize ($c) {
242248
$c->stash->{actionFormTitles} = ACTION_FORM_TITLES();
243249
$c->stash->{hardcopyLabels} = [];
244250

251+
unless ($c->{file_type}) {
252+
$c->stash->{sampleProblemMetadata} = generateMetadata("$ce->{pg_dir}/tutorial/sample-problems");
253+
return;
254+
}
255+
245256
# Tell the templates if we are working on a PG file
246257
$c->{is_pg} = !$c->{file_type} || ($c->{file_type} ne 'course_info' && $c->{file_type} ne 'hardcopy_theme');
247258

@@ -265,7 +276,7 @@ sub initialize ($c) {
265276
));
266277
}
267278

268-
if ($c->{file_type} eq 'blank_problem') {
279+
if ($c->{file_type} eq 'blank_problem' || $c->{file_type} eq 'sample_problem') {
269280
$c->addbadmessage($c->maketext('This file is a template. You may use "Save As" to create a new file.'));
270281
} elsif ($c->{inputFilePath} =~ /$BLANKPROBLEM$/) {
271282
$c->addbadmessage($c->maketext(
@@ -298,7 +309,8 @@ sub initialize ($c) {
298309
eval { $problemContents = readFile($c->{editFilePath}) };
299310
$problemContents = $@ if $@;
300311
$c->{inputFilePath} = $c->{editFilePath};
301-
312+
} elsif (path_is_subdir($c->{editFilePath}, "$ce->{pg_dir}/tutorial/sample-problems")) {
313+
$problemContents = getSampleProblemCode($c->{editFilePath});
302314
} else {
303315
$c->stash->{file_error} = $c->maketext('The given file path is not a valid location.');
304316
}
@@ -387,12 +399,14 @@ sub page_title ($c) {
387399

388400
# Convert initial path component to [TMPL], [COURSE], or [WW].
389401
sub shortPath ($c, $file) {
390-
my $tmpl = $c->ce->{courseDirs}{templates};
391-
my $root = $c->ce->{courseDirs}{root};
392-
my $ww = $c->ce->{webworkDirs}{root};
402+
my $tmpl = $c->ce->{courseDirs}{templates};
403+
my $root = $c->ce->{courseDirs}{root};
404+
my $ww = $c->ce->{webworkDirs}{root};
405+
my $sample = $c->ce->{pg_dir} . '/tutorial/sample-problems';
393406
$file =~ s|^$tmpl|[TMPL]|;
394407
$file =~ s|^$root|[COURSE]|;
395408
$file =~ s|^$ww|[WW]|;
409+
$file =~ s|^$sample|[SAMPLE]|;
396410

397411
return $file;
398412
}
@@ -416,6 +430,7 @@ sub determineTempEditFilePath ($c, $path) {
416430
my $templatesDirectory = $c->ce->{courseDirs}{templates};
417431
my $tmpEditFileDirectory = $c->getTempEditFileDirectory();
418432
my $hardcopyThemesDir = $c->ce->{webworkDirs}{hardcopyThemes};
433+
my $pgRoot = $c->ce->{pg_dir};
419434

420435
$c->addbadmessage($c->maketext('The path to the original file should be absolute.'))
421436
unless $path =~ m|^/|;
@@ -429,6 +444,9 @@ sub determineTempEditFilePath ($c, $path) {
429444
} elsif ($path eq $c->ce->{webworkFiles}{screenSnippets}{blankProblem}) {
430445
# Handle the case of the blank problem in snippets.
431446
$path = "$tmpEditFileDirectory/blank.$setID.$user.tmp";
447+
} elsif ($path =~ m|^$pgRoot/tutorial/sample-problems/(.*\.pg)$|) {
448+
# Handle the case of a sample problem.
449+
$path = "$tmpEditFileDirectory/$1.$user.tmp";
432450
} elsif ($path eq $c->ce->{webworkFiles}{hardcopySnippets}{setHeader}) {
433451
# Handle the case of the screen header in snippets.
434452
$path = "$tmpEditFileDirectory/screenHeader.$setID.$user.tmp";
@@ -450,7 +468,6 @@ sub determineTempEditFilePath ($c, $path) {
450468
}
451469

452470
# Determine the original path to a file corresponding to a temporary edit file.
453-
# Returns a path that is relative to the template directory.
454471
sub determineOriginalEditFilePath ($c, $path) {
455472
my $ce = $c->ce;
456473

@@ -511,6 +528,8 @@ sub getFilePaths ($c) {
511528
$editFilePath = "$ce->{courseDirs}{templates}/$ce->{courseFiles}{course_info}";
512529
} elsif ($c->{file_type} eq 'blank_problem') {
513530
$editFilePath = $ce->{webworkFiles}{screenSnippets}{blankProblem};
531+
} elsif ($c->{file_type} eq 'sample_problem') {
532+
$editFilePath = "$ce->{pg_dir}/tutorial/sample-problems/" . $c->param('sampleProblemFile');
514533
} elsif ($c->{file_type} eq 'hardcopy_theme') {
515534
$editFilePath = "$ce->{courseDirs}{hardcopyThemes}/" . $c->param('hardcopy_theme');
516535
if (!-e $editFilePath) {
@@ -787,8 +806,8 @@ sub view_handler ($c) {
787806
sourceFilePath => $relativeTempFilePath
788807
}
789808
));
790-
} elsif ($c->{file_type} eq 'blank_problem') {
791-
# Redirect to Problem.pm.pm.
809+
} elsif ($c->{file_type} eq 'blank_problem' || $c->{file_type} eq 'sample_problem') {
810+
# Redirect to Problem.pm.
792811
$c->authen->flash(status_message => $c->{status_message}->join(''));
793812
$c->reply_with_redirect($c->systemLink(
794813
$c->url_for('problem_detail', setID => 'Undefined_Set', problemID => 1),
@@ -843,10 +862,9 @@ sub view_handler ($c) {
843862
return;
844863
}
845864

846-
# The hardcopy and format_code actions are handled by javascript. These are provided just in case
847-
# something goes wrong and the actions are called.
848-
sub hardcopy_action { }
849-
sub format_code_action { }
865+
# The format_code action is handled by javascript. This is provided just in case
866+
# something goes wrong and the handler is called.
867+
sub format_code_handler { }
850868

851869
sub hardcopy_handler ($c) {
852870
# Redirect to problem editor page.

lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ use Mojo::Base 'WeBWorK::ContentGenerator', -signatures;
1919
use File::Basename qw(basename);
2020
use Pod::Simple::Search;
2121

22-
use SampleProblemParser qw(parseSampleProblem generateMetadata);
22+
use WeBWorK::Utils::Files qw(path_is_subdir);
23+
use SampleProblemParser qw(parseSampleProblem generateMetadata getSampleProblemCode);
2324

2425
=head1 NAME
2526
@@ -44,9 +45,17 @@ Render the requestedSampleProblem or one of the indexes.
4445
sub renderSampleProblem ($c) {
4546
my $pg_root = $c->ce->{pg_dir};
4647

47-
(undef, my $macro_files) = Pod::Simple::Search->new->inc(0)->survey("$pg_root/macros");
48-
my %macro_locations = map { basename($_) => $_ =~ s!$pg_root/macros/!!r } keys %$macro_files;
49-
my $metadata = generateMetadata("$pg_root/tutorial/sample-problems", macro_locations => \%macro_locations);
48+
if ($c->stash->{filePath} =~ /\.pg$/) {
49+
my $sampleProblemFile = "$pg_root/tutorial/sample-problems/" . $c->stash->{filePath};
50+
return $c->render(data => $c->maketext('File not found.'))
51+
unless path_is_subdir($sampleProblemFile, $c->ce->{pg_dir} . '/tutorial/sample-problems')
52+
&& -r $sampleProblemFile;
53+
54+
# Render the .pg file as a downloadable file.
55+
return $c->render_file(data => getSampleProblemCode($sampleProblemFile));
56+
}
57+
58+
my $metadata = generateMetadata("$pg_root/tutorial/sample-problems");
5059

5160
if (grep { $c->stash->{filePath} eq $_ } qw(categories techniques subjects macros)) {
5261
my %labels = (
@@ -79,25 +88,14 @@ sub renderSampleProblem ($c) {
7988
label => $labels{ $c->stash->{filePath} },
8089
list => $list
8190
);
82-
} elsif ($c->stash->{filePath} =~ /\.pg$/) {
83-
unless ($metadata->{ basename($c->stash->{filePath}) }) {
84-
return $c->render(data => $c->maketext('File not found.'));
85-
}
86-
87-
# Render the .pg file as a downloadable file.
88-
return $c->render_file(
89-
data => parseSampleProblem(
90-
"$pg_root/tutorial/sample-problems/" . $c->stash->{filePath},
91-
metadata => $metadata,
92-
pod_root => $c->url_for('pod_index'),
93-
pg_doc_home => $c->url_for('sample_problem_index')
94-
)->{code}
95-
);
9691
} else {
9792
unless ($metadata->{ basename($c->stash->{filePath}) . '.pg' }) {
9893
$c->render(data => $c->maketext('Sample problem not found.'));
9994
}
10095

96+
(undef, my $macro_files) = Pod::Simple::Search->new->inc(0)->survey("$pg_root/macros");
97+
my %macro_locations = map { basename($_) => $_ =~ s!$pg_root/macros/!!r } keys %$macro_files;
98+
10199
# Render a problem with its documentation.
102100
my $problemFile = "$pg_root/tutorial/sample-problems/" . $c->stash->{filePath} . '.pg';
103101
return $c->render(

templates/ContentGenerator/Instructor/PGProblemEditor.html.ep

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@
2222
% last;
2323
% }
2424
%
25+
% if (!$c->{file_type}) {
26+
%= include("ContentGenerator/Instructor/PGProblemEditor/file_chooser");
27+
% last;
28+
% }
29+
%
2530
% if (stash('file_error')) {
2631
<div class="alert alert-danger p-1 mb-0"><%= stash('file_error') %></div>
2732
% last;
2833
% }
2934
%
3035
% my %titles = (
3136
% blank_problem => x('Editing <strong>new problem</strong> template "[_1]".'),
37+
% sample_problem => x('Editing <strong>sample problem</strong> file "[_1]".'),
3238
% set_header => x('Editing <strong>set header</strong> file "[_1]".'),
3339
% hardcopy_header => x('Editing <strong>hardcopy header</strong> file "[_1]".'),
3440
% hardcopy_theme => x('Editing <strong>hardcopy theme</strong> file "[_1]".'),
@@ -85,6 +91,9 @@
8591
% if (not_blank($c->{tempFilePath})) {
8692
<%= hidden_field temp_file_path => $c->{tempFilePath} =%>
8793
% }
94+
% if ($c->{file_type} eq 'sample_problem' && param('sampleProblemFile')) {
95+
<%= hidden_field sampleProblemFile => param('sampleProblemFile') =%>
96+
% }
8897
% if ($c->{file_type} eq 'hardcopy_theme') {
8998
<%= hidden_field hardcopy_theme => param('hardcopy_theme') =%>
9099
% }

templates/ContentGenerator/Instructor/PGProblemEditor/add_problem_form.html.ep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
% use WeBWorK::Utils::Sets qw(format_set_name_display);
22
%
3-
% last unless $c->{is_pg};
3+
% last unless $c->{is_pg} && $c->{file_type} ne 'blank_problem' && $c->{file_type} ne 'sample_problem';
44
%
55
% my $allSetNames = [ map { $_->[0] =~ s/^set|\.def$//gr } $db->listGlobalSetsWhere({}, 'set_id') ];
66
%
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<h2 class="my-3">Choose File to Edit</h2>
2+
%
3+
<%= form_for current_route, method => 'POST', name => 'pg-editor-file-chooser',
4+
class => 'needs-validation', novalidate => undef, begin =%>
5+
<div class="row align-items-center mb-3">
6+
<div class="col-auto">
7+
<div class="form-check">
8+
<%= radio_button file_type => 'blank_problem',
9+
id => 'new_problem', checked => undef, class => 'form-check-input' %>
10+
<%= label_for new_problem => maketext('New problem template'), class => 'form-check-label' =%>
11+
</div>
12+
</div>
13+
</div>
14+
<div class="row align-items-center mb-3">
15+
<div class="col-auto">
16+
<div class="form-check">
17+
<%= radio_button file_type => 'source_path_for_problem_file',
18+
id => 'file_path', class => 'form-check-input' %>
19+
<%= label_for file_path => maketext('File:'), class => 'form-check-label' =%>
20+
</div>
21+
</div>
22+
<div class="col-auto" dir="ltr">
23+
<div class="editor-save-path input-group input-group-sm">
24+
<%= label_for source_file_path => '[TMPL]/', class => 'input-group-text' =%>
25+
<%= text_field sourceFilePath => '',
26+
id => 'source_file_path', class => 'form-control form-control-sm', size => 60, dir => 'ltr' =%>
27+
<div class="invalid-feedback">
28+
<%= maketext('Plese enter a file with path relative to the course templates directory.') %>
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
<div class="row align-items-center mb-3">
34+
<div class="col-auto">
35+
<div class="form-check">
36+
<%= radio_button file_type => 'sample_problem',
37+
id => 'sample_problem', class => 'form-check-input' %>
38+
<%= label_for sample_problem => maketext('Sample problem:'), class => 'form-check-label' =%>
39+
</div>
40+
</div>
41+
<div class="col-auto">
42+
<div class="col-auto">
43+
<%= select_field sampleProblemFile => [
44+
[maketext('Choose Sample Problem') => '', selected => undef, disabled => undef],
45+
map { [ $sampleProblemMetadata->{$_}{name} => "$sampleProblemMetadata->{$_}{dir}/$_" ] }
46+
sort { $sampleProblemMetadata->{$a}{name} cmp $sampleProblemMetadata->{$b}{name} }
47+
keys %$sampleProblemMetadata
48+
], id => 'sample_problem_file', class => 'form-select', 'aria-labelledby' => 'sample_problem' =%>
49+
<div class="invalid-feedback"><%= maketext('Please select a problem.') %></div>
50+
</div>
51+
</div>
52+
</div>
53+
<div><%= submit_button maketext('Open'), class => 'btn btn-primary' =%></div>
54+
<%= end =%>

templates/ContentGenerator/Instructor/PGProblemEditor/save_as_form.html.ep

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
% # Don't show the save as form when editing an existing course info file.
88
% last if $c->{file_type} eq 'course_info' && -e $c->{editFilePath};
99
%
10-
% my $isBlank = $c->{file_type} eq 'blank_problem';
10+
% my $isBlank = $c->{file_type} eq 'blank_problem' || $c->{file_type} eq 'sample_problem';
1111
% my $isHardcopyTheme = $c->{file_type} eq 'hardcopy_theme';
1212
% my $templatesDir = $ce->{courseDirs}{templates};
13-
% my $shortFilePath = $isBlank ? 'newProblem.pg' : $c->{editFilePath} =~ s|^$templatesDir/||r;
14-
% $shortFilePath = $isHardcopyTheme ? 'hardcopyThemes/' . ($c->{editFilePath} =~ s|^.*\/||r) : $shortFilePath;
13+
% my $shortFilePath =
14+
% $isBlank ? 'newProblem.pg'
15+
% : $isHardcopyTheme ? 'hardcopyThemes/' . ($c->{editFilePath} =~ s|^.*\/||r)
16+
% : $c->{editFilePath} =~ s|^$templatesDir/||r;
1517
%
1618
% # Suggest that modifications be saved to the "local" subdirectory if its not in a writeable directory
1719
% $shortFilePath = "local/$shortFilePath" unless $isBlank || $isHardcopyTheme || -w dirname($c->{editFilePath});
@@ -21,9 +23,9 @@
2123
%
2224
% my $probNum = $c->{file_type} eq 'problem' ? $c->{problemID} : 'header';
2325
%
24-
% # Don't add or replace problems to sets if the set is the Undefined_Set or if the problem is the blank_problem.
25-
% my $can_add_problem_to_set =
26-
% not_blank($c->{setID}) && $c->{setID} ne 'Undefined_Set' && $c->{file_type} ne 'blank_problem';
26+
% # Don't add or replace problems to sets if the set is the Undefined_Set or
27+
% # if the problem is the blank_problem or a sample problem.
28+
% my $can_add_problem_to_set = not_blank($c->{setID}) && $c->{setID} ne 'Undefined_Set' && !$isBlank;
2729
%
2830
% my $prettyProbNum = $probNum;
2931
% if ($c->{setID}) {

templates/ContentGenerator/Instructor/PGProblemEditor/save_form.html.ep

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
% # Can't save blank problems without changing names, and can't save if lacking write permissions.
2-
% last unless $c->{file_type} ne 'blank_problem' && $c->{editFilePath} !~ /newProblem\.pg$/ && -w $c->{editFilePath};
2+
% last if $c->{file_type} eq 'blank_problem'
3+
% || $c->{file_type} eq 'sample_problem'
4+
% || $c->{editFilePath} =~ /newProblem\.pg$/
5+
% || !-w $c->{editFilePath};
36
%
47
<div>
58
<div class="mb-2">

0 commit comments

Comments
 (0)