Skip to content

Commit 2bd468a

Browse files
authored
Merge pull request #2768 from drgrice1/pg-critic
Add "PG Critic" to the PG problem editor.
2 parents 251ab1b + 7009663 commit 2bd468a

9 files changed

Lines changed: 184 additions & 62 deletions

File tree

bin/check_modules.pl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ =head1 DESCRIPTION
8888
Net::OAuth
8989
Opcode
9090
Pandoc
91+
Perl::Critic
9192
Perl::Tidy
9293
PHP::Serialization
9394
Pod::Simple::Search

htdocs/js/PGProblemEditor/pgproblemeditor.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,15 @@
161161
?.addEventListener('change', () => (deleteBackupCheck.checked = true));
162162
}
163163

164+
const renderArea = document.getElementById('pgedit-render-area');
165+
166+
const scrollToRenderArea = () => {
167+
// Scroll to the top of the render window if the current scroll position is below that.
168+
const renderAreaRect = renderArea.getBoundingClientRect();
169+
const topBarHeight = document.querySelector('.webwork-logo')?.getBoundingClientRect().height ?? 0;
170+
if (renderAreaRect.top < topBarHeight) window.scrollBy(0, renderAreaRect.top - topBarHeight);
171+
};
172+
164173
// Send a request to the server to perltidy the current PG code in the CodeMirror editor.
165174
const tidyPGCode = () => {
166175
const request_object = { courseID: document.getElementsByName('courseID')[0]?.value };
@@ -235,15 +244,43 @@
235244
.catch((err) => showMessage(`Error: ${err?.message ?? err}`));
236245
};
237246

247+
// Send a request to the server to run the PG critic in the CodeMirror editor.
248+
const runPGCritic = () => {
249+
const request_object = { courseID: document.getElementsByName('courseID')[0]?.value };
250+
251+
const user = document.getElementsByName('user')[0];
252+
if (user) request_object.user = user.value;
253+
const sessionKey = document.getElementsByName('key')[0];
254+
if (sessionKey) request_object.key = sessionKey.value;
255+
256+
request_object.rpc_command = 'runPGCritic';
257+
request_object.pgCode =
258+
webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? '';
259+
260+
fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) })
261+
.then((response) => response.json())
262+
.then((data) => {
263+
if (data.error) throw new Error(data.error);
264+
if (!data.result_data) throw new Error('An invalid response was received.');
265+
renderArea.innerHTML = data.result_data.html;
266+
scrollToRenderArea();
267+
})
268+
.catch((err) => showMessage(`Error: ${err?.message ?? err}`));
269+
};
270+
238271
document.getElementById('take_action')?.addEventListener('click', async (e) => {
239-
if (document.getElementById('current_action')?.value === 'format_code') {
272+
if (document.getElementById('current_action')?.value === 'code_maintenance') {
240273
e.preventDefault();
241-
if (document.querySelector('input[name="action.format_code"]:checked').value == 'tidyPGCode') {
274+
if (document.querySelector('input[name="action.code_maintenance"]:checked').value === 'tidyPGCode') {
242275
tidyPGCode();
243276
} else if (
244-
document.querySelector('input[name="action.format_code"]:checked').value == 'convertCodeToPGML'
277+
document.querySelector('input[name="action.code_maintenance"]:checked').value === 'convertCodeToPGML'
245278
) {
246279
convertCodeToPGML();
280+
} else if (
281+
document.querySelector('input[name="action.code_maintenance"]:checked').value === 'runPGCritic'
282+
) {
283+
runPGCritic();
247284
}
248285
return;
249286
}
@@ -306,7 +343,6 @@
306343
});
307344

308345
const renderURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/render_rpc`;
309-
const renderArea = document.getElementById('pgedit-render-area');
310346
const fileType = document.getElementsByName('file_type')[0]?.value;
311347

312348
// This is either the div containing the CodeMirror editor or the problemContents textarea in the case that
@@ -391,11 +427,7 @@
391427
}
392428

393429
adjustIFrameHeight();
394-
395-
// Scroll to the top of the render window if the current scroll position is below that.
396-
const renderAreaRect = renderArea.getBoundingClientRect();
397-
const topBarHeight = document.querySelector('.webwork-logo')?.getBoundingClientRect().height ?? 0;
398-
if (renderAreaRect.top < topBarHeight) window.scrollBy(0, renderAreaRect.top - topBarHeight);
430+
scrollToRenderArea();
399431
});
400432

401433
const render = () =>

lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ the submit button pressed (the action).
9090
Requested actions and aliases
9191
View/Reload action = view
9292
Generate Hardcopy: action = hardcopy
93-
Format Code: action = format_code
93+
Code Maintenance: action = code_maintenance
9494
Save: action = save
9595
Save as: action = save_as
9696
Append: action = add_problem
@@ -118,15 +118,15 @@ use SampleProblemParser qw(getSampleProblemCode generateMetadata);
118118
use constant DEFAULT_SEED => 123456;
119119

120120
# Editor tabs
121-
use constant ACTION_FORMS => [qw(view hardcopy format_code save save_as add_problem revert)];
121+
use constant ACTION_FORMS => [qw(view hardcopy code_maintenance save save_as add_problem revert)];
122122
use constant ACTION_FORM_TITLES => {
123-
view => x('View/Reload'),
124-
hardcopy => x('Generate Hardcopy'),
125-
format_code => x('Format Code'),
126-
save => x('Save'),
127-
save_as => x('Save As'),
128-
add_problem => x('Append'),
129-
revert => x('Revert'),
123+
view => x('View/Reload'),
124+
hardcopy => x('Generate Hardcopy'),
125+
code_maintenance => x('Code Maintenance'),
126+
save => x('Save'),
127+
save_as => x('Save As'),
128+
add_problem => x('Append'),
129+
revert => x('Revert'),
130130
};
131131

132132
my $BLANKPROBLEM = 'newProblem.pg';
@@ -847,9 +847,9 @@ sub view_handler ($c) {
847847
return;
848848
}
849849

850-
# The format_code action is handled by javascript. This is provided just in case
850+
# The code_maintenance action is handled by javascript. This is provided just in case
851851
# something goes wrong and the handler is called.
852-
sub format_code_handler { }
852+
sub code_maintenance_handler { }
853853

854854
sub hardcopy_handler ($c) {
855855
# Redirect to problem editor page.

lib/WebworkWebservice.pm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ sub command_permission {
255255
putPastAnswer => 'problem_grader',
256256
tidyPGCode => 'access_instructor_tools',
257257
convertCodeToPGML => 'access_instructor_tools',
258+
runPGCritic => 'access_instructor_tools',
258259

259260
# WebworkWebservice::RenderProblem
260261
renderProblem => 'webservice_render_problem',

lib/WebworkWebservice/ProblemActions.pm

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use Data::Structure::Util qw(unbless);
88

99
use WeBWorK::PG::Tidy qw(pgtidy);
1010
use WeBWorK::PG::ConvertToPGML qw(convertToPGML);
11+
use WeBWorK::PG::Critic qw(critiquePGCode);
1112

1213
sub getUserProblem {
1314
my ($invocant, $self, $params) = @_;
@@ -165,4 +166,18 @@ sub convertCodeToPGML {
165166

166167
}
167168

169+
sub runPGCritic {
170+
my ($invocant, $self, $params) = @_;
171+
172+
return {
173+
ra_out => {
174+
html => $self->c->render_to_string(
175+
template => 'ContentGenerator/Instructor/PGProblemEditor/pg_critic',
176+
violations => [ critiquePGCode($params->{pgCode}) ]
177+
)
178+
},
179+
text => 'The script pg-critic has been run successfully.'
180+
};
181+
}
182+
168183
1;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
% last unless $c->{is_pg};
2+
<div>
3+
<div class="form-check">
4+
<%= radio_button 'action.code_maintenance' => 'tidyPGCode',
5+
id => 'action_code_maintenance_perltidy', class => 'form-check-input', checked => undef =%>
6+
<%= label_for 'action_code_maintenance_perltidy', class => 'form-check-label', begin =%>
7+
<%== maketext('Reformat the code using perltidy.') =%>
8+
<% end =%>
9+
<a class="help-popup" data-bs-content="<%== maketext('Perltidy is a reformatting '
10+
. 'function that attempts to format code in a standard way. It does not change '
11+
. 'the functionality of the code and in general is desired to have a common problem layout.') =%>"
12+
data-bs-placement="top" data-bs-toggle="popover" role="button">
13+
<i aria-hidden="true" class="fas fa-question-circle"></i>
14+
<span class="visually-hidden"><%= maketext('Perltidy Help') %></span>
15+
</a>
16+
</div>
17+
<div class="form-check">
18+
<%= radio_button 'action.code_maintenance' => 'convertCodeToPGML',
19+
id => 'action_code_maintenance_convert_PGML', class => 'form-check-input'=%>
20+
<%= label_for 'action_code_maintenance_convert_PGML', class => 'form-check-label', begin =%>
21+
<%== maketext('Convert the code to PGML') =%>
22+
<% end =%>
23+
<a class="help-popup" data-bs-content="<%== maketext('This option converts the text blocks '
24+
. 'in the problem code to PGML and updates the loadMacros to include PGML and drop others. '
25+
. 'This can be used as a first pass of the conversion, however the author will still need '
26+
. 'to ensure the problem functions. One area of attention should be the answer blanks, '
27+
. 'which may not be converted correctly.') =%>"
28+
data-bs-placement="top" data-bs-toggle="popover" role="button">
29+
<i aria-hidden="true" class="fas fa-question-circle"></i>
30+
<span class="visually-hidden"><%= maketext('PGML Conversion Help') %></span>
31+
</a>
32+
</div>
33+
<div class="form-check">
34+
<%= radio_button 'action.code_maintenance' => 'runPGCritic',
35+
id => 'action_code_maintenance_run_pgcritic', class => 'form-check-input'=%>
36+
<%= label_for 'action_code_maintenance_run_pgcritic', class => 'form-check-label', begin =%>
37+
<%== maketext('Analyze code with PG Critic') =%>
38+
<% end =%>
39+
<a class="help-popup" data-bs-content="<%== maketext('This option analyzes the code with PG Critic '
40+
. 'which gives suggestions on using modern PG constructs and ensures that the code conforms '
41+
. 'to current best-practices.') =%>"
42+
data-bs-placement="top" data-bs-toggle="popover" role="button">
43+
<i aria-hidden="true" class="fas fa-question-circle"></i>
44+
<span class="visually-hidden"><%= maketext('PG Critic Help') %></span>
45+
</a>
46+
</div>
47+
</div>

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

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
% Perl::Critic::Violation::set_format('%m at line %l, column %c.');
2+
%
3+
<div class="m-3 overflow-auto">
4+
<h2><%= maketext('PG Critic Violations') %></h2>
5+
% my @pgCriticViolations = grep { $_->policy =~ /^Perl::Critic::Policy::PG::/ } @$violations;
6+
% if (@pgCriticViolations) {
7+
<h3 class="mt-2"><%= maketext('The following PG issues should be fixed:') %></h3>
8+
<ul class="list-group">
9+
% for (@pgCriticViolations) {
10+
<li class="list-group-item">
11+
<div><%= $_->to_string %></div>
12+
<div>
13+
<%= $_->explanation->{explanation} %>
14+
<%== maketext(
15+
'See [_1].',
16+
link_to(
17+
($_->policy =~ s/^Perl::Critic::Policy:://r)
18+
=> pod_viewer => { filePath => 'lib/' . ($_->policy =~ s/::/\//gr) . '.pm' },
19+
target => '_blank'
20+
)
21+
) =%>
22+
</div>
23+
% if (ref($_->explanation->{sampleProblems}) eq 'ARRAY' && @{ $_->explanation->{sampleProblems} }) {
24+
<div>
25+
<%== maketext('Related sample [plural,_1,problem]:',
26+
scalar(@{ $_->explanation->{sampleProblems} })) %>
27+
<%= c(map {
28+
link_to(
29+
$_->[0] => sample_problem_viewer => { filePath => $_->[1] },
30+
target => '_blank'
31+
)
32+
} @{ $_->explanation->{sampleProblems} })->join(', ') =%>
33+
</div>
34+
% }
35+
</li>
36+
% }
37+
</ul>
38+
%}
39+
% my @perlCriticViolations = grep { $_->policy !~ /^Perl::Critic::Policy::PG::/ } @$violations;
40+
% if (@perlCriticViolations) {
41+
<h3 class="mt-2"><%= maketext('The following general Perl issues should be fixed:') %></h3>
42+
<ul class="list-group">
43+
% for (@perlCriticViolations) {
44+
<li class="list-group-item">
45+
<div><%= $_->to_string %></div>
46+
<div>
47+
See <%= link_to(
48+
($_->policy =~ s/^Perl::Critic::Policy:://r) => 'https://metacpan.org/pod/' . $_->policy,
49+
target => '_blank'
50+
) %>.
51+
</div>
52+
</li>
53+
% }
54+
</ul>
55+
%}
56+
</div>

templates/HelpFiles/InstructorPGProblemEditor.html.ep

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,19 +156,22 @@
156156
. 'generating the PDF file using LaTeX.') =%>
157157
</p>
158158
<p>
159-
<%= maketext('You can also click "Edit Selected Theme" to edit a hardcopy theme. The new theme will be saved to '
160-
. 'the templates/hardcopyThemes folder.') =%>
159+
<%= maketext('You can also click "Edit Selected Theme" to edit a hardcopy theme. The new theme will be saved '
160+
. 'to the templates/hardcopyThemes folder.') =%>
161161
</p>
162162
</dd>
163163

164-
<dt><%= maketext('Format Code') %></dt>
164+
<dt><%= maketext('Code Maintenance') %></dt>
165165
<dd>
166-
<%= maketext('Reformat the code using perltidy or a conversion to PGML. Using perltidy will change the code '
167-
. 'in the editor window, and save changes to the temporary file. In some cases (if the code contains '
168-
. 'backslashes or double tildes) this can result in odd spacing in the code. The convert to PGML '
169-
. 'feature changes the code in text blocks in the code to use PGML features. Generally the conversion of '
170-
. 'many of the formatting and LaTeX is performed correctly, however answer blanks need attention. In '
171-
. 'either case, make sure to inspect the formatted code, and edit further or revert if needed.') =%>
166+
<%= maketext('Three code maintenance tools can be utilized. First, the code can be reformatted using perltidy. '
167+
. 'Using perltidy will change the code in the editor window, and save changes to the temporary file. In some '
168+
. 'cases (if the code contains backslashes or double tildes) this can result in odd spacing in the code. '
169+
. 'Second the code can be converted to PGML. This changes the code in text blocks to use PGML features. '
170+
. 'Generally the conversion of much of the formatting and LaTeX is performed correctly. However, answer '
171+
. 'blanks need attention. In any case, make sure to inspect the formatted code, and edit further or revert '
172+
. 'if needed. Third, the code can be analyzed by the "PG Critic." This checks that the code does not use '
173+
. 'old or deprecated features of PG and conforms to current best-practices in problem authoring, and offers '
174+
. 'suggestions on how to fix issues that are found.') =%>
172175
</dd>
173176

174177
<dt><%= maketext('Save') %></dt>

0 commit comments

Comments
 (0)