From 667f0bbc762967d72d9ca4f4cf6551ceb7da993b Mon Sep 17 00:00:00 2001
From: Peter Staab
Date: Fri, 20 Jun 2025 12:09:43 -0700
Subject: [PATCH 1/4] This will update the pgeditor to use the pgcritic to
analyze a problem.
---
htdocs/js/PGProblemEditor/pgproblemeditor.js | 26 ++++
lib/WebworkWebservice.pm | 1 +
lib/WebworkWebservice/ProblemActions.pm | 22 +++-
.../PGProblemEditor/format_code_form.html.ep | 14 ++
.../PGProblemEditor/pg_critic.html.ep | 121 ++++++++++++++++++
5 files changed, 182 insertions(+), 2 deletions(-)
create mode 100644 templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
diff --git a/htdocs/js/PGProblemEditor/pgproblemeditor.js b/htdocs/js/PGProblemEditor/pgproblemeditor.js
index f48214574f..8e9fa78a54 100644
--- a/htdocs/js/PGProblemEditor/pgproblemeditor.js
+++ b/htdocs/js/PGProblemEditor/pgproblemeditor.js
@@ -231,6 +231,30 @@
.catch((err) => showMessage(`Error: ${err?.message ?? err}`));
};
+ // Send a request to the server to run the PG critic in the CodeMirror editor.
+ const runPGCritic = () => {
+ const request_object = { courseID: document.getElementsByName('courseID')[0]?.value };
+
+ const user = document.getElementsByName('user')[0];
+ if (user) request_object.user = user.value;
+ const sessionKey = document.getElementsByName('key')[0];
+ if (sessionKey) request_object.key = sessionKey.value;
+
+ request_object.rpc_command = 'runPGCritic';
+ request_object.pgCode =
+ webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? '';
+
+ fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) })
+ .then((response) => response.json())
+ .then((data) => {
+ renderArea.innerHTML = data.result_data.html;
+ })
+ .catch((err) => {
+ console.log(err);
+ showMessage(`Error: ${err?.message ?? err}`);
+ });
+ };
+
document.getElementById('take_action')?.addEventListener('click', async (e) => {
if (document.getElementById('current_action')?.value === 'format_code') {
e.preventDefault();
@@ -240,6 +264,8 @@
document.querySelector('input[name="action.format_code"]:checked').value == 'convertCodeToPGML'
) {
convertCodeToPGML();
+ } else if (document.querySelector('input[name="action.format_code"]:checked').value == 'runPGCritic') {
+ runPGCritic();
}
return;
}
diff --git a/lib/WebworkWebservice.pm b/lib/WebworkWebservice.pm
index 1bd113b619..f44e4e5e5c 100644
--- a/lib/WebworkWebservice.pm
+++ b/lib/WebworkWebservice.pm
@@ -270,6 +270,7 @@ sub command_permission {
putPastAnswer => 'problem_grader',
tidyPGCode => 'access_instructor_tools',
convertCodeToPGML => 'access_instructor_tools',
+ runPGCritic => 'access_instructor_tools',
# WebworkWebservice::RenderProblem
renderProblem => 'proctor_quiz_login',
diff --git a/lib/WebworkWebservice/ProblemActions.pm b/lib/WebworkWebservice/ProblemActions.pm
index 7684872367..9ec13c57ec 100644
--- a/lib/WebworkWebservice/ProblemActions.pm
+++ b/lib/WebworkWebservice/ProblemActions.pm
@@ -21,8 +21,9 @@ use warnings;
use Data::Structure::Util qw(unbless);
-use WeBWorK::PG::Tidy qw(pgtidy);
-use WeBWorK::PG::ConvertToPGML qw(convertToPGML);
+use WeBWorK::PG::Tidy qw(pgtidy);
+use WeBWorK::PG::ConvertToPGML qw(convertToPGML);
+use WeBWorK::PG::PGProblemCritic qw(analyzePGcode);
sub getUserProblem {
my ($invocant, $self, $params) = @_;
@@ -180,4 +181,21 @@ sub convertCodeToPGML {
}
+sub runPGCritic {
+ my ($invocant, $self, $params) = @_;
+ my $pg_critic_results = analyzePGcode($params->{pgCode});
+
+ my $html_output = $self->c->render_to_string(
+ template => 'ContentGenerator/Instructor/PGProblemEditor/pg_critic',
+ results => $pg_critic_results
+ );
+
+ return {
+ ra_out => {
+ html => $html_output
+ },
+ text => 'The script pg-critic has been run successfully.'
+ };
+}
+
1;
diff --git a/templates/ContentGenerator/Instructor/PGProblemEditor/format_code_form.html.ep b/templates/ContentGenerator/Instructor/PGProblemEditor/format_code_form.html.ep
index 69da3d5a95..0ddab6d09f 100644
--- a/templates/ContentGenerator/Instructor/PGProblemEditor/format_code_form.html.ep
+++ b/templates/ContentGenerator/Instructor/PGProblemEditor/format_code_form.html.ep
@@ -30,4 +30,18 @@
<%= maketext('PGML Conversion Help') %>
+
+ <%= radio_button 'action.format_code' => 'runPGCritic',
+ id => 'action_format_code_run_pgcritic', class => 'form-check-input'=%>
+ <%= label_for 'action_format_code_run_pgcritic', class => 'form-check-label', begin =%>
+ <%== maketext('Run the PG Critic Analyzer') =%>
+ <% end =%>
+
+
+ <%= maketext('PG Critic Help') %>
+
+
diff --git a/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
new file mode 100644
index 0000000000..278ce91bcf
--- /dev/null
+++ b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
@@ -0,0 +1,121 @@
+
+
PG Critic Results
+
+
Metadata
+
+
The following lists required metadata. If any is missing, the given tag must be filled in.
+ However, make sure that the categories are correct, especially if the problem has been
+ copied.
+
+% sub showIcon { my $show = shift;
+% return $show ? q!! : q!!;
+%}
+
+
+
+
DBsubject
<%== showIcon($results->{metadata}{DBsection}) %>
+
DBchapter
<%== showIcon($results->{metadata}{DBchapter}) %>
+
DBsection
<%== showIcon($results->{metadata}{DBsection}) %>
+
Keywords
<%== showIcon($results->{metadata}{KEYWORDS}) %>
+
+
+
You done good with the following
+
+
+
+% if ($results->{good}{PGML}) {
+
PGML
This problem uses PGML, the current preferred way to write problem (text), solution and hint
+ blocks.
+%}
+% if ($results->{good}{solution}) {
+
Solutions
This problem has a solution block. Every problem should have solutions that the
+ student can view after the answer data.
+%}
+% if ($results->{good}{hint}) {
+
Hints
This problem has a hint. This can be helpful for students after attempting the problem
+ a few times (this can be set by the instructor).
+%}
+
This problem has the following deprecated macros: <%= join(', ',@{$results->{deprecated_macros}} ) %>
+
+
These should be removed from the problem in that these macros will be deleted from PG in a future
+ version. The functions from these macros may be listed below to help aid in transitioning away from
+ these macros.
+% }
+
+
+% my $has_bad_features = 0;
+% $has_bad_features += $results->{bad}{$_} for (keys %{$results->{bad}});
+
+% # <%== dumper $results->{bad} %>
+
+% if ($has_bad_features) {
+
You can improve on the following:
+
There are features in this problem that contain old or deprecated features. The following
+list gives feedback of how the problem can be improved.
+%}
+
+
+% if ($results->{bad}{BEGIN_TEXT}) {
+
This problem contains older formatting blocks like BEGIN_TEXT. Consider use PGML.
+ In the Format Code section of the PG Editor, the "Convert to PGML" should be used
+ as a start to get the problem switched.
+
+%}
+% if ($results->{bad}{beginproblem}) {
+
This problem contains the line TEXT(beginproblem()). This is no longer necessary and should be removed.
+%}
+% if ($results->{bad}{context_texstrings}) {
+
This problem contains the line Context()->texStrings;. This is no longer necessary and should be removed.
+%}
+% if ($results->{bad}{oldtable}) {
+
This problem contains the deprecated begintable command. This is not assessible and often cannot be
+ converted to hardcopy. This table should be written using nicetables or a PGML table.
+%}
+% if ($results->{bad}{showPartialCorrect}) {
+
This problem contains the line $showPartialCorrectAnswers = 1. This is enabled by default and needed only
+ if set to 0.
+% }
+% if (!$results->{good}{solution}) {
+
This problem does not have a solution. Consider adding one.
This problem contains the functioins num_cmp, str_cmp or fun_cmp.
+ These are old ways of checking answers. These should be converted to MathObjects.
+% }
+% if ($results->{bad}{multiple_loadmacros}) {
+
This problem contains two loadMacros function call. Combine the function
+ calls and make sure that all macros are needed for your problem.
+% }
+% if ($results->{bad}{old_multiple_choice}) {
+
+% }
+% if ($results->{bad}{lines_below_enddocument}) {
+
There is content (code or other text), below the ENDDOCUMENT() line. Although this
+ is ignored, there shouldn't be content in this area.
+% }
+
+
+
+
+
From 3fa727c6f69412e78f53d761cd390e69d6bb2c87 Mon Sep 17 00:00:00 2001
From: Peter Staab
Date: Fri, 20 Jun 2025 13:53:15 -0700
Subject: [PATCH 2/4] Improvements part of the pgcritic report now shows up
if the only problem is that solutions are not shown.
---
.../Instructor/PGProblemEditor/pg_critic.html.ep | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
index 278ce91bcf..b18b1cf167 100644
--- a/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
+++ b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
@@ -54,13 +54,10 @@
these macros.
% }
-
% my $has_bad_features = 0;
% $has_bad_features += $results->{bad}{$_} for (keys %{$results->{bad}});
-% # <%== dumper $results->{bad} %>
-
-% if ($has_bad_features) {
+% if ($has_bad_features || !$results->{good}{solution}) {
You can improve on the following:
There are features in this problem that contain old or deprecated features. The following
list gives feedback of how the problem can be improved.
From ef458598ebd4bfce1524d9656c7d404f03ecdf4b Mon Sep 17 00:00:00 2001
From: Peter Staab
Date: Fri, 27 Jun 2025 16:59:53 -0400
Subject: [PATCH 3/4] Improved work on the PG critic interface to the PGEditor.
---
.../PGProblemEditor/pg_critic.html.ep | 75 ++++++++++++-------
1 file changed, 49 insertions(+), 26 deletions(-)
diff --git a/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
index b18b1cf167..f75fabe741 100644
--- a/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
+++ b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
@@ -25,22 +25,40 @@
Keywords
<%== showIcon($results->{metadata}{KEYWORDS}) %>
-
You done good with the following
-
+% my $pos = $results->{positive};
+% if ($pos->{PGML} || $pos->{solution} || $pos->{hint}) {
+
Good aspects of this problems are the following
+% }
-% if ($results->{good}{PGML}) {
+% if ($pos->{PGML}) {
PGML
This problem uses PGML, the current preferred way to write problem (text), solution and hint
blocks.
-%}
-% if ($results->{good}{solution}) {
+% }
+% if ($pos->{solution}) {
Solutions
This problem has a solution block. Every problem should have solutions that the
student can view after the answer data.
-%}
-% if ($results->{good}{hint}) {
+% }
+% if ($pos->{hint}) {
Hints
This problem has a hint. This can be helpful for students after attempting the problem
a few times (this can be set by the instructor).
-%}
+% }
+% # list of the positive contexts:
+% my @good_contexts = grep { $pos->{contexts}{$_} } keys %{$pos->{parsers}};
+% if (@good_contexts) {
+
Modern Contexts
This problem uses the following modern contexts:
+ <%= join(', ', @good_contexts) %>
+% }
+% my @good_parsers = grep { $pos->{parsers}->{$_} } keys %{$pos->{parsers}};
+% if (@good_parsers) {
+
Modern Parsers
This problem uses features of the following modern parsers:
+ <%= join(', ', @good_parsers) %>
+% }
+% my @good_macros = grep { $pos->{macros}->{$_} } keys %{$pos->{macros}};
+% if (@good_macros) {
+
Modern Macros
This problem uses functionality from the following modern macros:
+ <%= join(', ', @good_macros) %>
+% }
@@ -55,59 +73,64 @@
% }
% my $has_bad_features = 0;
-% $has_bad_features += $results->{bad}{$_} for (keys %{$results->{bad}});
+% $has_bad_features += $results->{negative}{$_} for (keys %{$results->{negative}});
-% if ($has_bad_features || !$results->{good}{solution}) {
+% if ($has_bad_features || !$pos->{solution}) {
You can improve on the following:
There are features in this problem that contain old or deprecated features. The following
list gives feedback of how the problem can be improved.
%}
-% if ($results->{bad}{BEGIN_TEXT}) {
+% if ($results->{negative}{BEGIN_TEXT}) {
This problem contains older formatting blocks like BEGIN_TEXT. Consider use PGML.
In the Format Code section of the PG Editor, the "Convert to PGML" should be used
as a start to get the problem switched.
%}
-% if ($results->{bad}{beginproblem}) {
+% if ($results->{negative}{beginproblem}) {
This problem contains the line TEXT(beginproblem()). This is no longer necessary and should be removed.
%}
-% if ($results->{bad}{context_texstrings}) {
+% if ($results->{negative}{context_texstrings}) {
This problem contains the line Context()->texStrings;. This is no longer necessary and should be removed.
%}
-% if ($results->{bad}{oldtable}) {
+% if ($results->{negative}{oldtable}) {
This problem contains the deprecated begintable command. This is not assessible and often cannot be
converted to hardcopy. This table should be written using nicetables or a PGML table.
%}
-% if ($results->{bad}{showPartialCorrect}) {
+% if ($results->{negative}{showPartialCorrect}) {
This problem contains the line $showPartialCorrectAnswers = 1. This is enabled by default and needed only
if set to 0.
% }
-% if (!$results->{good}{solution}) {
+% if (!$pos->{solution}) {
This problem does not have a solution. Consider adding one.
% }
-% if ($results->{bad}{fun_cmp} || $results->{bad}{str_cmp} || $results->{bad}{num_cmp}) {
+% if ($results->{negative}{fun_cmp} || $results->{negative}{str_cmp} || $results->{negative}{num_cmp}) {
This problem contains the functioins num_cmp, str_cmp or fun_cmp.
These are old ways of checking answers. These should be converted to MathObjects.
% }
-% if ($results->{bad}{multiple_loadmacros}) {
+% if ($results->{negative}{multiple_loadmacros}) {
This problem contains two loadMacros function call. Combine the function
calls and make sure that all macros are needed for your problem.
% }
-% if ($results->{bad}{old_multiple_choice}) {
+% if ($results->{negative}{macros}{PGchoicemacros}) {
% }
-% if ($results->{bad}{lines_below_enddocument}) {
+% if ($results->{negative}{macros}{PGgraphmacros}) {
+
This problem uses PGgraphmacros a old plotting library. Consider using
+ Plots.pl
+ or PGtikz.pl
+%}
+% if ($results->{negative}{lines_below_enddocument}) {
There is content (code or other text), below the ENDDOCUMENT() line. Although this
is ignored, there shouldn't be content in this area.
% }
From bfc499de9c5e9910889618b783e9a1a17f51b343 Mon Sep 17 00:00:00 2001
From: Peter Staab
Date: Tue, 8 Jul 2025 14:05:25 -0400
Subject: [PATCH 4/4] Add randomness to the list of criteria to be checked.
---
.../PGProblemEditor/pg_critic.html.ep | 62 ++++++++++---------
1 file changed, 34 insertions(+), 28 deletions(-)
diff --git a/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
index f75fabe741..d5a4e77f1d 100644
--- a/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
+++ b/templates/ContentGenerator/Instructor/PGProblemEditor/pg_critic.html.ep
@@ -33,31 +33,35 @@
% if ($pos->{PGML}) {
PGML
This problem uses PGML, the current preferred way to write problem (text), solution and hint
- blocks.
+ blocks.
% }
% if ($pos->{solution}) {
Solutions
This problem has a solution block. Every problem should have solutions that the
- student can view after the answer data.
+ student can view after the answer data.
% }
% if ($pos->{hint}) {
Hints
This problem has a hint. This can be helpful for students after attempting the problem
- a few times (this can be set by the instructor).
+ a few times (this can be set by the instructor).
+% }
+% if ($pos->{randomness}) {
+
Randomness
This problem uses randomness. This is desired to give to a class of students, each
+ of whom may have a different problem.
% }
% # list of the positive contexts:
% my @good_contexts = grep { $pos->{contexts}{$_} } keys %{$pos->{parsers}};
% if (@good_contexts) {
Modern Contexts
This problem uses the following modern contexts:
- <%= join(', ', @good_contexts) %>
This problem uses functionality from the following modern macros:
- <%= join(', ', @good_macros) %>
+ <%= join(', ', @good_macros) %>
% }
@@ -68,25 +72,24 @@
This problem has the following deprecated macros: <%= join(', ',@{$results->{deprecated_macros}} ) %>
These should be removed from the problem in that these macros will be deleted from PG in a future
- version. The functions from these macros may be listed below to help aid in transitioning away from
- these macros.
+ version. The functions from these macros may be listed below to help aid in transitioning away from
+ these macros.
% }
% my $has_bad_features = 0;
% $has_bad_features += $results->{negative}{$_} for (keys %{$results->{negative}});
% if ($has_bad_features || !$pos->{solution}) {
-
You can improve on the following:
-
There are features in this problem that contain old or deprecated features. The following
-list gives feedback of how the problem can be improved.
+
You can improve on the following:
+
There are features in this problem that contain old or deprecated features. The following
+ list gives feedback of how the problem can be improved.
%}
% if ($results->{negative}{BEGIN_TEXT}) {
This problem contains older formatting blocks like BEGIN_TEXT. Consider use PGML.
- In the Format Code section of the PG Editor, the "Convert to PGML" should be used
- as a start to get the problem switched.
-
+ In the Format Code section of the PG Editor, the "Convert to PGML" should be used
+ as a start to get the problem switched.
%}
% if ($results->{negative}{beginproblem}) {
This problem contains the line TEXT(beginproblem()). This is no longer necessary and should be removed.
@@ -96,43 +99,46 @@ list gives feedback of how the problem can be improved.
%}
% if ($results->{negative}{oldtable}) {
This problem contains the deprecated begintable command. This is not assessible and often cannot be
- converted to hardcopy. This table should be written using nicetables or a PGML table.
+ converted to hardcopy. This table should be written using nicetables or a PGML table.
%}
% if ($results->{negative}{showPartialCorrect}) {
This problem contains the line $showPartialCorrectAnswers = 1. This is enabled by default and needed only
- if set to 0.
+ if set to 0.
% }
% if (!$pos->{solution}) {
This problem does not have a solution. Consider adding one.
% }
+% if (!$pos->{randomness}) {
+
This problem does not have randomness. Consider adding variables that take on random values.
+% }
% if ($results->{negative}{fun_cmp} || $results->{negative}{str_cmp} || $results->{negative}{num_cmp}) {
This problem contains the functioins num_cmp, str_cmp or fun_cmp.
- These are old ways of checking answers. These should be converted to MathObjects.
+ These are old ways of checking answers. These should be converted to MathObjects.
% }
% if ($results->{negative}{multiple_loadmacros}) {
This problem contains two loadMacros function call. Combine the function
- calls and make sure that all macros are needed for your problem.
+ calls and make sure that all macros are needed for your problem.
% }
% if ($results->{negative}{macros}{PGchoicemacros}) {