diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 210c0555a2..2c372d5c0a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: ["https://webwork.maa.org/wiki"] +custom: ['https://wiki.openwebwork.org/wiki'] diff --git a/.github/workflows/check-formats.yml b/.github/workflows/check-formats.yml index 092dabe309..5c06bdbc10 100644 --- a/.github/workflows/check-formats.yml +++ b/.github/workflows/check-formats.yml @@ -18,26 +18,26 @@ jobs: image: perl:5.38 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install dependencies - run: cpanm -n Perl::Tidy@20240903 + run: cpanm -n Perl::Tidy@20260204 - name: Run perltidy shell: bash run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" shopt -s extglob globstar nullglob - perltidy --pro=./.perltidyrc -b -bext='/' ./**/*.p[lm] ./**/*.t && git diff --exit-code + perltidy --pro=./.perltidyrc -b -bext='/' ./**/*.p[lm] ./**/*.t ./**/*.at && git diff --exit-code prettier: name: Check JavaScript, style, and HTML file formatting with prettier runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' - name: Install Dependencies run: cd htdocs && npm ci --ignore-scripts - name: Check formatting with prettier diff --git a/DockerfileStage2 b/DockerfileStage2 index 4706f5bfb5..e196cd2f6b 100644 --- a/DockerfileStage2 +++ b/DockerfileStage2 @@ -33,7 +33,7 @@ RUN echo Cloning branch $PG_BRANCH branch from $PG_GIT_URL \ # We need to change FROM before setting the ENV variables. -FROM webwork-base:forWW220 +FROM webwork-base:forWW221 ENV WEBWORK_URL=/webwork2 \ WEBWORK_ROOT_URL=http://localhost:8080 \ diff --git a/LICENSE b/LICENSE index 821f53097f..5f4df6a5fb 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Online Homework Delivery System Version 2.* - Copyright 2000-2025, The WeBWorK Project + Copyright 2000-2026, The WeBWorK Project All rights reserved. @@ -13,19 +13,19 @@ Software Foundation; either version 2, or (at your option) any later version, or - b) the "Artistic License" which comes with this package. + b) the "Artistic License" 1.0 which comes with this package. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the Artistic License for more details. - You should have received a copy of the Artistic License with this - package, in the file named "Artistic" inside the `doc` folder. If not, we'll be glad to provide - one. + You should have received a copy of the Artistic License 1.0 with this + package, in the file named "Artistic" inside the `doc` folder. If not, + you can find a copy at https://github.com/openwebwork/webwork2/blob/main/doc/Artistic + or https://perlfoundation.org/artistic-license-10.html. You should also have received a copy of the GNU General Public License - along with this program in the file named "Copying" inside the `doc` folder. If not, write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - 02111-1307, USA or visit their web page on the internet at - http://www.gnu.org/copyleft/gpl.html. + along with this program in the file named "Copying" inside the `doc` folder. + If not, you can find a copy at https://github.com/openwebwork/webwork2/blob/main/doc/Copying + or https://www.gnu.org/licenses/old-licenses/gpl-2.0.html. diff --git a/README.md b/README.md index db8e9a149c..7d213d39db 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,35 @@ - WeBWorK - Online Homework Delivery System - Version 2.* - Branch: github.com/openwebwork - - https://webwork.maa.org/wiki/Release_notes_for_WeBWorK_2.20 - Copyright 2000-2025, The WeBWorK Project - https://openwebwork.org/ - All rights reserved. - # Welcome to WeBWorK -WeBWorK is an open-source online homework system for math and sciences courses. WeBWorK is supported by the MAA and the NSF and comes with an Open Problem Library (OPL) of over 30,000 homework problems. Problems in the OPL target most lower division undergraduate math courses, some advanced courses and some other STEM subjects. Supported courses include college algebra, discrete mathematics, probability and statistics, single and multivariable calculus, differential equations, linear algebra and complex analysis. Find out more at the main WeBWorK [webpage](https://openwebwork.org). +WeBWorK is an open-source online homework system for math and sciences courses. WeBWorK is supported by the MAA and the +NSF and comes with an Open Problem Library (OPL) of over 30,000 homework problems. Problems in the OPL target most lower +division undergraduate math courses, some advanced courses and some other STEM subjects. Supported courses include +college algebra, discrete mathematics, probability and statistics, single and multivariable calculus, differential +equations, linear algebra and complex analysis. Find out more at the main WeBWorK [webpage](https://openwebwork.org). ## Information for Users -New users interested in getting started with their own WeBWorK server, or instructors looking to learn more about how to use WeBWorK in their classes, should take a look at one of the following resources: +New users interested in getting started with their own WeBWorK server, or instructors looking to learn more about how to +use WeBWorK in their classes, should take a look at one of the following resources: -* The [WeBWorK project home page](https://openwebwork.org/) - General information and resources including announcements of events and important project news -* [WeBWorK wiki](https://webwork.maa.org/wiki/Main_Page) - The main WeBWorK wiki -* [Installing WeBWorK](https://webwork.maa.org/wiki/Manual_Installation_Guides) - Installing WeBWorK -* [Instructors](https://webwork.maa.org/wiki/Instructors) - Information for Instructors -* [Problem Authors](https://webwork.maa.org/wiki/Authors) - Information for Problem Authors -* [Forum](http://webwork.maa.org/moodle/mod/forum/index.php?id=3) - The WeBWorK Forum for getting help from the community -* [Frequently Asked Questions](https://github.com/openwebwork/webwork2/wiki/Frequently-Asked-Questions) - A list of frequently asked questions. +- The [WeBWorK project home page](https://openwebwork.org/) - General information and resources including announcements + of events and important project news +- [WeBWorK wiki](https://wiki.openwebwork.org/wiki/Main_Page) - The main WeBWorK wiki +- [Installing WeBWorK](https://wiki.openwebwork.org/wiki/Manual_Installation_Guides) - Installing WeBWorK +- [Instructors](https://wiki.openwebwork.org/wiki/Instructors) - Information for Instructors +- [Problem Authors](https://wiki.openwebwork.org/wiki/Authors) - Information for Problem Authors +- [Forum](https://forums.openwebwork.org/mod/forum/index.php?id=3) - The WeBWorK Forum for getting help from the + community +- [Frequently Asked Questions](https://github.com/openwebwork/webwork2/wiki/Frequently-Asked-Questions) - A list of + frequently asked questions. ## Information for Downloading -* Installation manuals can be found at https://webwork.maa.org/wiki/Category:Installation_Manuals +- See the [installation manuals](https://wiki.openwebwork.org/wiki/Category:Installation_Manuals). ## Information For Developers -* People interested in developing new features for WeBWorK can start at https://webwork.maa.org/wiki/Category:Developers, or start a [discussion on GitHub](https://github.com/openwebwork/webwork2/discussions) to engage with the current developers. -* People interested in developing new problems for WeBWorK should visit [Problem Authors](http://webwork.maa.org/wiki/Authors). +- People interested in developing new features for WeBWorK can start at the wiki + [development page](https://wiki.openwebwork.org/wiki/Category:Developers), or start a + [discussion on GitHub](https://github.com/openwebwork/webwork2/discussions) to engage with the current developers. +- People interested in developing new problems for WeBWorK should visit the wiki + [problem authoring page](https://wiki.openwebwork.org/wiki/Authors). diff --git a/VERSION b/VERSION index 450fad3c87..b6669a5671 100644 --- a/VERSION +++ b/VERSION @@ -1,4 +1,4 @@ -$WW_VERSION = '2.20'; -$WW_COPYRIGHT_YEARS = '1996-2025'; +$WW_VERSION = '2.21'; +$WW_COPYRIGHT_YEARS = '1996-2026'; 1; diff --git a/assets/hardcopyThemes/basic.xml b/assets/hardcopyThemes/basic.xml index 46b8b8f7ab..34a859df84 100644 --- a/assets/hardcopyThemes/basic.xml +++ b/assets/hardcopyThemes/basic.xml @@ -3,7 +3,7 @@ This theme produces hardcopies with minimal page headers displaying the set -title and useername, and minimal problem numbering. +title and username, and minimal problem numbering. diff --git a/assets/hardcopyThemes/basicTwoCol.xml b/assets/hardcopyThemes/basicTwoCol.xml index ccb11b0dc9..44b1a455ee 100644 --- a/assets/hardcopyThemes/basicTwoCol.xml +++ b/assets/hardcopyThemes/basicTwoCol.xml @@ -3,7 +3,7 @@ This theme produces hardcopies with minimal page headers displaying the set -title and useername, and minimal problem numbering. With two columns. +title and username, and minimal problem numbering. With two columns. diff --git a/assets/pg/PGMLLab/PGML-lab.pg b/assets/pg/PGMLLab/PGML-lab.pg index b58c76614c..931fe40867 100644 --- a/assets/pg/PGMLLab/PGML-lab.pg +++ b/assets/pg/PGMLLab/PGML-lab.pg @@ -682,9 +682,9 @@ TEXT(tag( >> At the right >> Several lines combined. - >> right justfied + >> right justified - >> Or a whole paragaph + >> Or a whole paragraph that is pushed to the right >> Or two lines diff --git a/assets/pg/Student_Orientation/discriminant.png b/assets/pg/Student_Orientation/discriminant.png new file mode 100644 index 0000000000..636d7719af Binary files /dev/null and b/assets/pg/Student_Orientation/discriminant.png differ diff --git a/assets/pg/Student_Orientation/enteringMath.pg b/assets/pg/Student_Orientation/enteringMath.pg index 8db6992a59..0e20c0d9fc 100644 --- a/assets/pg/Student_Orientation/enteringMath.pg +++ b/assets/pg/Student_Orientation/enteringMath.pg @@ -21,9 +21,9 @@ expression automatically is converted to look like [::pi/sqrt(x+1)::]. [_]{Conte Occasionally you will want to answer with raw text that should not be converted into a math expression. For example, if the answer is a word. For this kind of answer, you should enter "text mode". There is a "Tt" button on the palette that -enters and exits text mode. You can also use the [|"|]* character to enter text mode, and then exit with the [|tab|]* -key or right arrow. Try answering the following with and without using text mode. The answer is [|pirate|]*: -[_]{Context("Numeric")->strings->add(pirate => {}), 'pirate'}{16}. +enters and exits text mode. You can also use the double quote character ([|"|]*) to enter text mode, and then exit with +the [|tab|]* key or right arrow. Try answering the following with and without using text mode. The answer is [|web|]*: +[_]{Context("Numeric")->strings->add(web => {}), 'web'}{16}. The palette tool might be a distraction, especially to keyboard-only users. You can disable it by right-clicking (or control-clicking) in an answer blank. To "right click" without a mouse: diff --git a/assets/pg/Student_Orientation/explorerfull.png b/assets/pg/Student_Orientation/explorerfull.png index 6b52703fcc..a563a3b4e7 100644 Binary files a/assets/pg/Student_Orientation/explorerfull.png and b/assets/pg/Student_Orientation/explorerfull.png differ diff --git a/assets/pg/Student_Orientation/explorerpiece.png b/assets/pg/Student_Orientation/explorerpiece.png index a4fb755b36..36c4b884d5 100644 Binary files a/assets/pg/Student_Orientation/explorerpiece.png and b/assets/pg/Student_Orientation/explorerpiece.png differ diff --git a/assets/pg/Student_Orientation/mathInteraction.pg b/assets/pg/Student_Orientation/mathInteraction.pg index 3cf56a7111..162750be9e 100644 --- a/assets/pg/Student_Orientation/mathInteraction.pg +++ b/assets/pg/Student_Orientation/mathInteraction.pg @@ -26,37 +26,54 @@ Once focus is on a math expression, using the space bar will activate the "MathJ by right-clicking (Windows/Linux) or [|control|]*-clicking (MacOS) a piece of math. Try activating the MathJax menu now. It should look like the following. ->> [!MathJax contextual menu!]{'mathjaxmenu.png'} << +>> [!MathJax contextual menu!]{'mathjaxmenu.png'}{140} << There are many features that help you to engage with math content. Explore the menu options to survey what is available. -We will now point out a few important features. In the main menu, there is a "Math Settings" submenu. The "Zoom Trigger" -and "Zoom Factor" items allow you to control if/how math content is magnified. Magnification may help users with some -vision disabilities see the content better. And it may help all users to see some details of math notation better. Take -a moment to explore these settings and select options that you would be comfortable with. (Of course you can change -these settings at any time.) - -Also in the main menu, there is an "Accessibility" submenu. In that menu, if accessibility is not already activated, -select "Activate". After activating this, you may need to refresh the web page to see the math expression again. Now you -have the option to see math content verbalized. To do this, place focus onto a math expression and hit [|enter|]*. -* At first, the entire expression is highlighted and there will be a verbal rendering of the expression. -[!Speech string for the quadratic formula!]{'explorerfull.png'}{600} -* Use the down arrow to navigate "down" into a smaller piece of the math expression. -* Use the left/right arrows to navigate to similar pieces of the math expression. -* At any time you can navigate back "up" to a larger part of the expression, or "down" into smaller pieces. For example, -you can see a verbalization for just this part of the expression above: -[!Speech string for the radicand of the quadratic formula!]{'explorerpiece.png'}{600} -* Return to the MathJax menu, Accessibility submenu, to explore options for how this explorer tool works. -* Under "Speech" you will find options to use MathSpeak, ClearSpeak, or ChromeVox rules. The default is to use -"MathSpeak verbose" rules, which try try to read math "literally" without context. For example, it reads [`(1,3)`] as -"left parenthesis 1 comma 3 right parenthesis". Other speech rules can produce more meaningful verbal renderings. For -example with the right ClearSpeak settings, the same math expression produces "the point with coordinates 1 comma 3" -or "the interval from 1 to 3 not including 1 or 3". +### Enlarging Math + +In the main menu, there is a "Math Settings" submenu. The "Zoom Trigger" and "Zoom Factor" items allow you to control +if/how math content is magnified. Magnification may help users with some vision disabilities see the content better. +And it may help all users to see some details of math notation better. Take a moment to explore these settings and +select options that you would be comfortable with. (Of course you can change these settings at any time.) + +### Navigating Math + +When focus is on a piece of math content, there is a small help icon in the upper right. Click this icon to see a guide +for how to navigate inside the math content. This can help you understand the meaning of the math content. For example, +here we have navigated to the piece of math content inside the radical: + +[!Highlighting the discriminant in the quadratic formula!]{'discriminant.png'}{600} + +### Subtitles for Math + +Also in the main menu, there is an "Accessibility" submenu. Accessibility is enabled by default, but you can also select +"Show Subtitles" in the "Speech" submenu. This allows you to see math content verbalized. To do this, place focus onto +a math expression and hit [|enter|]*. At first, the entire expression is highlighted and there will be a verbal +rendering of the expression. +[!Speech string for the quadratic formula!]{'explorerfull.png'}{735} + +As you navigate within the math using arrows, you can see verbalizations of the corresponding pieces: + +[!Speech string for the radicand of the quadratic formula!]{'explorerpiece.png'}{735} + +Under "Speech" you will find options for MathSpeak and ClearSpeak rules. The default is to use "MathSpeak verbose" +rules, which try try to read math "literally" without context. For example, it reads [`(1,3)`] as "left parenthesis 1 +comma 3 right parenthesis". Other speech rules can produce more meaningful verbal renderings. For example with the right +ClearSpeak settings, the same math expression produces "the point with coordinates 1 comma 3" or "the interval from 1 to +3 not including 1 or 3". + +### Voicing Math Out Loud + +When focus is on math content, use the [|s|]* character to hear the math spoken. If you would like math to always be +spoken whenever focus lands on math, check Auto Voicing in the Speech menu. + +### Disabling Tab Indexing Some keyboard-navigating users might find it undesirable for each piece of math content to be tab-indexed. If this is -the case, then in the "Accessibility" sub menu you can uncheck "Include in Tab Order". Just note that in order to undo -this and make math content tabbable again, you will need to access the menu, and so you will need some way other than -tabbing to bring focus back to a piece of math content. +the case, then in the "Options" submenu of the "Accessibility" submenu you can uncheck "Include in Tab Order" and +"Semantic Enrichment". Just note that in order to undo this and make math content tabbable again, you will need to +access the menu, and so you will need some way other than tabbing to bring focus back to a piece of math content. END_BODY $images = <dirname->dirname; } @@ -33,20 +33,21 @@ my $ce = WeBWorK::CourseEnvironment->new({ webwork_dir => $ENV{WEBWORK_ROOT} }); print "\nDownloading the latest OPL release.\n"; runScript("$ENV{WEBWORK_ROOT}/bin/download-OPL-metadata-release.pl"); -if ($ce->{problemLibrary}{showLibraryLocalStats} || - $ce->{problemLibrary}{showLibraryGlobalStats}) { - print "\nUpdating Library Statistics.\n"; - runScript("$ENV{WEBWORK_ROOT}/bin/update-OPL-statistics.pl"); - - print "\nLoading global statistics (if possible).\n"; - runScript("$ENV{WEBWORK_ROOT}/bin/load-OPL-global-statistics.pl"); - - if ( $ENV{SKIP_UPLOAD_OPL_STATISTICS} ) { - print "\nSkipping upload-OPL-statistics as requested\n"; - } else { - print "\nSharing aggregated statistics\n"; - runScript("$ENV{WEBWORK_ROOT}/bin/upload-OPL-statistics.pl"); - } +if ($ce->{problemLibrary}{showLibraryLocalStats} + || $ce->{problemLibrary}{showLibraryGlobalStats}) +{ + print "\nUpdating Library Statistics.\n"; + runScript("$ENV{WEBWORK_ROOT}/bin/update-OPL-statistics.pl"); + + print "\nLoading global statistics (if possible).\n"; + runScript("$ENV{WEBWORK_ROOT}/bin/load-OPL-global-statistics.pl"); + + if ($ENV{SKIP_UPLOAD_OPL_STATISTICS}) { + print "\nSkipping upload-OPL-statistics as requested\n"; + } else { + print "\nSharing aggregated statistics\n"; + runScript("$ENV{WEBWORK_ROOT}/bin/upload-OPL-statistics.pl"); + } } print "\nDone.\n"; diff --git a/bin/OPL-update-legacy b/bin/OPL-update-legacy index 39232570c9..d47790d6b3 100755 --- a/bin/OPL-update-legacy +++ b/bin/OPL-update-legacy @@ -19,55 +19,54 @@ use File::Basename; use Cwd; use DBI; - #(maximum varchar length is 255 for mysql version < 5.0.3. - #You can increase path length to 4096 for mysql > 5.0.3) +#(maximum varchar length is 255 for mysql version < 5.0.3. +#You can increase path length to 4096 for mysql > 5.0.3) # Taxonomy global variables # Make a hash of hashes of hashes to record what is legal # Also create list for json file -my $taxo={}; +my $taxo = {}; #my $taxsubs = []; ### Data for creating the database tables my %OPLtables = ( - dbsubject => 'OPL_DBsubject', - dbchapter => 'OPL_DBchapter', - dbsection => 'OPL_DBsection', - author => 'OPL_author', - path => 'OPL_path', - pgfile => 'OPL_pgfile', - keyword => 'OPL_keyword', - pgfile_keyword => 'OPL_pgfile_keyword', - textbook => 'OPL_textbook', - chapter => 'OPL_chapter', - section => 'OPL_section', - problem => 'OPL_problem', - morelt => 'OPL_morelt', - pgfile_problem => 'OPL_pgfile_problem', + dbsubject => 'OPL_DBsubject', + dbchapter => 'OPL_DBchapter', + dbsection => 'OPL_DBsection', + author => 'OPL_author', + path => 'OPL_path', + pgfile => 'OPL_pgfile', + keyword => 'OPL_keyword', + pgfile_keyword => 'OPL_pgfile_keyword', + textbook => 'OPL_textbook', + chapter => 'OPL_chapter', + section => 'OPL_section', + problem => 'OPL_problem', + morelt => 'OPL_morelt', + pgfile_problem => 'OPL_pgfile_problem', ); - my %NPLtables = ( - dbsubject => 'NPL-DBsubject', - dbchapter => 'NPL-DBchapter', - dbsection => 'NPL-DBsection', - author => 'NPL-author', - path => 'NPL-path', - pgfile => 'NPL-pgfile', - keyword => 'NPL-keyword', - pgfile_keyword => 'NPL-pgfile-keyword', - textbook => 'NPL-textbook', - chapter => 'NPL-chapter', - section => 'NPL-section', - problem => 'NPL-problem', - morelt => 'NPL-morelt', - pgfile_problem => 'NPL-pgfile-problem', + dbsubject => 'NPL-DBsubject', + dbchapter => 'NPL-DBchapter', + dbsection => 'NPL-DBsection', + author => 'NPL-author', + path => 'NPL-path', + pgfile => 'NPL-pgfile', + keyword => 'NPL-keyword', + pgfile_keyword => 'NPL-pgfile-keyword', + textbook => 'NPL-textbook', + chapter => 'NPL-chapter', + section => 'NPL-section', + problem => 'NPL-problem', + morelt => 'NPL-morelt', + pgfile_problem => 'NPL-pgfile-problem', ); BEGIN { use Mojo::File qw(curfile); - use Env qw(WEBWORK_ROOT); + use Env qw(WEBWORK_ROOT); $WEBWORK_ROOT = curfile->dirname->dirname; } @@ -84,17 +83,17 @@ my $ce = WeBWorK::CourseEnvironment->new({ webwork_dir => $ENV{WEBWORK_ROOT} }); # decide whether the mysql installation can handle # utf8mb4 and that should be used for the OPL -my $ENABLE_UTF8MB4 = ($ce->{ENABLE_UTF8MB4})?1:0; -print "using utf8mb4 \n\n" if $ENABLE_UTF8MB4; +my $ENABLE_UTF8MB4 = ($ce->{ENABLE_UTF8MB4}) ? 1 : 0; +print "using utf8mb4 \n\n" if $ENABLE_UTF8MB4; # The DBD::MariaDB driver should not get the # mysql_enable_utf8mb4 or mysql_enable_utf8 settings, # but DBD::mysql should. my %utf8_parameters = (); -if ( $ce->{database_driver} =~ /^mysql$/i ) { +if ($ce->{database_driver} =~ /^mysql$/i) { # Only needed for older DBI:mysql driver - if ( $ENABLE_UTF8MB4 ) { + if ($ENABLE_UTF8MB4) { $utf8_parameters{mysql_enable_utf8mb4} = 1; } else { $utf8_parameters{mysql_enable_utf8} = 1; @@ -112,8 +111,8 @@ my $dbh = DBI->connect( }, ); -my $character_set=''; -$character_set=($ENABLE_UTF8MB4)?"utf8mb4":"utf8"; +my $character_set = ''; +$character_set = ($ENABLE_UTF8MB4) ? "utf8mb4" : "utf8"; $dbh->prepare("SET NAMES '$character_set'")->execute(); print "using character set $character_set to build OPL database\n"; @@ -125,13 +124,13 @@ $contribRoot =~ s|/+$||; print "using libraryRoot $libraryRoot\n"; print "using contribRoot $contribRoot\n"; print "WEBWORK_ROOT $ENV{WEBWORK_ROOT}\n"; -my $libraryVersion = $ce->{problemLibrary}->{version}; +my $libraryVersion = $ce->{problemLibrary}->{version}; my $db_storage_engine = $ce->{problemLibrary_db}->{storage_engine}; my $verbose = 0; -my $cnt2 = 0; +my $cnt2 = 0; # Can force library version -$libraryVersion = $ARGV[0] if(@ARGV); +$libraryVersion = $ARGV[0] if (@ARGV); # auto flush printing my $old_fh = select(STDOUT); @@ -139,15 +138,15 @@ $| = 1; select($old_fh); sub dbug { - my $msg = shift; + my $msg = shift; my $insignificance = shift || 2; - print $msg if($verbose>=$insignificance); + print $msg if ($verbose >= $insignificance); } ##Figure out which set of tables to use my %tables; -if($libraryVersion eq '2.5') { +if ($libraryVersion eq '2.5') { %tables = %OPLtables; my $lib = 'OPL'; warn "Library version is $libraryVersion; using OPLtables!\n"; @@ -158,29 +157,36 @@ if($libraryVersion eq '2.5') { } @create_tables = ( -[$tables{dbsubject}, ' + [ + $tables{dbsubject}, ' DBsubject_id int(15) NOT NULL auto_increment, name varchar(245) NOT NULL, KEY DBsubject (name), PRIMARY KEY (DBsubject_id) -'], -[$tables{dbchapter}, ' +' + ], + [ + $tables{dbchapter}, ' DBchapter_id int(15) NOT NULL auto_increment, name varchar(245) NOT NULL, DBsubject_id int(15) DEFAULT 0 NOT NULL, KEY DBchapter (name), KEY (DBsubject_id), PRIMARY KEY (DBchapter_id) -'], -[$tables{dbsection}, ' +' + ], + [ + $tables{dbsection}, ' DBsection_id int(15) NOT NULL auto_increment, name varchar(245) NOT NULL, DBchapter_id int(15) DEFAULT 0 NOT NULL, KEY DBsection (name), KEY (DBchapter_id), PRIMARY KEY (DBsection_id) -'], -[$tables{author}, ' +' + ], + [ + $tables{author}, ' author_id int (15) NOT NULL auto_increment, institution tinyblob, lastname varchar (255) NOT NULL, @@ -188,16 +194,20 @@ if($libraryVersion eq '2.5') { email varchar (255), KEY author (lastname(100), firstname(100)), PRIMARY KEY (author_id) -'], -[$tables{path}, ' +' + ], + [ + $tables{path}, ' path_id int(15) NOT NULL auto_increment, path varchar(245) NOT NULL, machine varchar(255), user varchar(255), KEY (path), PRIMARY KEY (path_id) -'], -[$tables{pgfile}, ' +' + ], + [ + $tables{pgfile}, ' pgfile_id int(15) NOT NULL auto_increment, DBsection_id int(15) NOT NULL, author_id int(15), @@ -211,20 +221,26 @@ if($libraryVersion eq '2.5') { static TINYINT, MO TINYINT, PRIMARY KEY (pgfile_id) -'], -[$tables{keyword}, ' +' + ], + [ + $tables{keyword}, ' keyword_id int(15) NOT NULL auto_increment, keyword varchar(245) NOT NULL, KEY (keyword), PRIMARY KEY (keyword_id) -'], -[$tables{pgfile_keyword}, ' +' + ], + [ + $tables{pgfile_keyword}, ' pgfile_id int(15) DEFAULT 0 NOT NULL, keyword_id int(15) DEFAULT 0 NOT NULL, KEY pgfile_keyword (keyword_id, pgfile_id), KEY pgfile (pgfile_id) -'], -[$tables{textbook}, ' +' + ], + [ + $tables{textbook}, ' textbook_id int (15) NOT NULL auto_increment, title varchar (255) NOT NULL, edition int (15) DEFAULT 0 NOT NULL, @@ -233,8 +249,10 @@ if($libraryVersion eq '2.5') { isbn char (15), pubdate varchar (255), PRIMARY KEY (textbook_id) -'], -[$tables{chapter}, ' +' + ], + [ + $tables{chapter}, ' chapter_id int (15) NOT NULL auto_increment, textbook_id int (15), number int(3), @@ -243,8 +261,10 @@ if($libraryVersion eq '2.5') { KEY (textbook_id, name), KEY (number), PRIMARY KEY (chapter_id) -'], -[$tables{section}, ' +' + ], + [ + $tables{section}, ' section_id int(15) NOT NULL auto_increment, chapter_id int (15), number int(3), @@ -253,8 +273,10 @@ if($libraryVersion eq '2.5') { KEY (chapter_id, name), KEY (number), PRIMARY KEY section (section_id) -'], -[$tables{problem}, ' +' + ], + [ + $tables{problem}, ' problem_id int(15) NOT NULL auto_increment, section_id int(15), number int(4) NOT NULL, @@ -262,20 +284,26 @@ if($libraryVersion eq '2.5') { #KEY (page, number), KEY (section_id), PRIMARY KEY (problem_id) -'], -[$tables{morelt}, ' +' + ], + [ + $tables{morelt}, ' morelt_id int(15) NOT NULL auto_increment, name varchar(245) NOT NULL, DBsection_id int(15), leader int(15), # pgfile_id of the MLT leader KEY (name), PRIMARY KEY (morelt_id) -'], -[$tables{pgfile_problem}, ' +' + ], + [ + $tables{pgfile_problem}, ' pgfile_id int(15) DEFAULT 0 NOT NULL, problem_id int(15) DEFAULT 0 NOT NULL, PRIMARY KEY (pgfile_id, problem_id) -']); +' + ] +); ### End of database data @@ -288,20 +316,19 @@ $dbh->do("DROP TABLE IF EXISTS `NPL-pgfile-institution`"); for my $tableinfo (@create_tables) { my $tabname = $tableinfo->[0]; my $tabinit = $tableinfo->[1]; - my $query = "DROP TABLE IF EXISTS `$tabname`"; + my $query = "DROP TABLE IF EXISTS `$tabname`"; $dbh->do($query); $query = "CREATE TABLE `$tabname` ( $tabinit ) ENGINE=$db_storage_engine CHARACTER SET $character_set"; $dbh->do($query); - if($lib eq 'OPL') { + if ($lib eq 'OPL') { $old_tabname = $tabname; $old_tabname =~ s/OPL/NPL/; $old_tabname =~ s/_/-/g; $query = "DROP TABLE IF EXISTS `$old_tabname`"; - $dbh -> do($query); + $dbh->do($query); } } - print "Mysql database reinitialized.\n"; # From pgfile @@ -318,53 +345,55 @@ print "Mysql database reinitialized.\n"; # Get an id, and add entry to the database if needed sub safe_get_id { - my $tablename = shift; - my $idname = shift; - my $whereclause = shift; - my $wherevalues = shift; - my $addifnew = shift; + my $tablename = shift; + my $idname = shift; + my $whereclause = shift; + my $wherevalues = shift; + my $addifnew = shift; my @insertvalues = @_; #print "\nCalled with $tablename, $idname, $whereclause, [".join(',', @$wherevalues)."], (".join(',', @insertvalues).")\n"; - for my $j (0..$#insertvalues) { + for my $j (0 .. $#insertvalues) { $insertvalues[$j] =~ s/"/\\\"/g; } - my $query = "SELECT $idname FROM `$tablename` ".$whereclause; - my $sth = $dbh->prepare($query); + my $query = "SELECT $idname FROM `$tablename` " . $whereclause; + my $sth = $dbh->prepare($query); $sth->execute(@$wherevalues); my $idvalue, @row; - unless(@row = $sth->fetchrow_array()) { + unless (@row = $sth->fetchrow_array()) { return 0 unless $addifnew; - for my $j (0..$#insertvalues) { + for my $j (0 .. $#insertvalues) { #print "Looking at ".$insertvalues[$j]."\n"; if ($insertvalues[$j] ne "") { - $insertvalues[$j] = '"'.$insertvalues[$j].'"'; + $insertvalues[$j] = '"' . $insertvalues[$j] . '"'; } else { $insertvalues[$j] = NULL; } } - $dbh->do("INSERT INTO `$tablename` VALUES(". join(',',@insertvalues) .")"); - dbug "INSERT INTO $tablename VALUES( ".join(',',@insertvalues).")\n"; + $dbh->do("INSERT INTO `$tablename` VALUES(" . join(',', @insertvalues) . ")"); + dbug "INSERT INTO $tablename VALUES( " . join(',', @insertvalues) . ")\n"; $sth = $dbh->prepare($query); $sth->execute(@$wherevalues); @row = $sth->fetchrow_array(); } $idvalue = $row[0]; - return($idvalue); + return ($idvalue); } sub isvalid { my $tags = shift; - if(not defined $taxo->{$tags->{DBsubject}}) { - print "\nInvalid subject ".$tags->{DBsubject}."\n"; + if (not defined $taxo->{ $tags->{DBsubject} }) { + print "\nInvalid subject " . $tags->{DBsubject} . "\n"; return 0; } - if(not ($tags->{DBchapter} eq 'Misc.') and not defined $taxo->{$tags->{DBsubject}}->{$tags->{DBchapter}}) { - print "\nInvalid chapter ".$tags->{DBchapter}."\n"; + if (not($tags->{DBchapter} eq 'Misc.') and not defined $taxo->{ $tags->{DBsubject} }->{ $tags->{DBchapter} }) { + print "\nInvalid chapter " . $tags->{DBchapter} . "\n"; return 0; } - if(not ($tags->{DBsection} eq 'Misc.') and not defined $taxo->{$tags->{DBsubject}}->{$tags->{DBchapter}}->{$tags->{DBsection}}) { - print "\nInvalid section ".$tags->{DBsection}."\n"; + if (not($tags->{DBsection} eq 'Misc.') + and not defined $taxo->{ $tags->{DBsubject} }->{ $tags->{DBchapter} }->{ $tags->{DBsection} }) + { + print "\nInvalid section " . $tags->{DBsection} . "\n"; return 0; } return 1; @@ -372,39 +401,50 @@ sub isvalid { #### First read in textbook information -if(open(IN, '<:encoding(UTF-8)', "$libraryRoot/Textbooks")) { +if (open(IN, '<:encoding(UTF-8)', "$libraryRoot/Textbooks")) { print "Reading in textbook data from Textbooks in the library $libraryRoot.\n"; - my %textinfo = ( TitleText => '', EditionText =>'', AuthorText=>''); - my $bookid = undef; + my %textinfo = (TitleText => '', EditionText => '', AuthorText => ''); + my $bookid = undef; while (my $line = ) { $line =~ s|#*$||; - if($line =~ /^\s*(.*?)\s*>>>\s*(.*?)\s*$/) { # Should have chapter or section information + if ($line =~ /^\s*(.*?)\s*>>>\s*(.*?)\s*$/) { # Should have chapter or section information my $chapsec = $1; - my $title = $2; - if($chapsec=~ /(\d+)\.(\d+)/) { # We have a section - if(defined($bookid)) { - my $query = "SELECT chapter_id FROM `$tables{chapter}` WHERE textbook_id = \"$bookid\" AND number = \"$1\""; + my $title = $2; + if ($chapsec =~ /(\d+)\.(\d+)/) { # We have a section + if (defined($bookid)) { + my $query = + "SELECT chapter_id FROM `$tables{chapter}` WHERE textbook_id = \"$bookid\" AND number = \"$1\""; my $chapid = $dbh->selectrow_array($query); - if(defined($chapid)) { - my $sectid = safe_get_id($tables{section}, 'section_id', - qq(WHERE chapter_id = ? and name = ?), [$chapid, $title], 1, "", $chapid, $2, $title, ""); + if (defined($chapid)) { + my $sectid = safe_get_id( + $tables{section}, 'section_id', + qq(WHERE chapter_id = ? and name = ?), + [ $chapid, $title ], + 1, "", $chapid, $2, $title, "" + ); } else { - print "Cannot enter section $chapsec because textbook information is missing the chapter entry\n"; + print + "Cannot enter section $chapsec because textbook information is missing the chapter entry\n"; } } else { print "Cannot enter section $chapsec because textbook information is incomplete\n"; } - } else { # We have a chapter entry - if(defined($bookid)) { - my $chapid = safe_get_id($tables{chapter}, 'chapter_id', - qq(WHERE textbook_id = ? AND number = ?), [$bookid, $chapsec], 1, "", $bookid, $chapsec, $title, ""); - - # Add dummy section entry for problems tagged to the chapter - # without a section - $query = "SELECT section_id FROM `$tables{section}` WHERE chapter_id = \"$chapid\" AND number = -1"; - my $sectid = $dbh->selectrow_array($query); - if (!defined($sectid)) { - $dbh->do("INSERT INTO `$tables{section}` + } else { # We have a chapter entry + if (defined($bookid)) { + my $chapid = safe_get_id( + $tables{chapter}, 'chapter_id', + qq(WHERE textbook_id = ? AND number = ?), + [ $bookid, $chapsec ], + 1, "", $bookid, $chapsec, $title, "" + ); + + # Add dummy section entry for problems tagged to the chapter + # without a section + $query = "SELECT section_id FROM `$tables{section}` WHERE chapter_id = \"$chapid\" AND number = -1"; + my $sectid = $dbh->selectrow_array($query); + if (!defined($sectid)) { + $dbh->do( + "INSERT INTO `$tables{section}` VALUES( NULL, \"$chapid\", @@ -412,31 +452,36 @@ if(open(IN, '<:encoding(UTF-8)', "$libraryRoot/Textbooks")) { \"\", NULL )" - ); + ); dbug "INSERT INTO section VALUES(\"\", \"$chapid\", \"-1\", \"\", \"\" )\n"; - } + } } else { print "Cannot enter chapter $chapsec because textbook information is incomplete\n"; } } - } elsif($line =~ /^\s*(TitleText|EditionText|AuthorText)\(\s*'(.*?)'\s*\)/) { + } elsif ($line =~ /^\s*(TitleText|EditionText|AuthorText)\(\s*'(.*?)'\s*\)/) { # Textbook information, maybe new my $type = $1; - if(defined($textinfo{$type})) { # signals new text - %textinfo = ( TitleText => undef, - EditionText =>undef, - AuthorText=> undef); + if (defined($textinfo{$type})) { # signals new text + %textinfo = ( + TitleText => undef, + EditionText => undef, + AuthorText => undef + ); $textinfo{$type} = $2; $bookid = undef; } else { $textinfo{$type} = $2; - if(defined($textinfo{TitleText}) and - defined($textinfo{AuthorText}) and - defined($textinfo{EditionText})) { - my $query = "SELECT textbook_id FROM `$tables{textbook}` WHERE title = \"$textinfo{TitleText}\" AND edition = \"$textinfo{EditionText}\" AND author=\"$textinfo{AuthorText}\""; + if (defined($textinfo{TitleText}) + and defined($textinfo{AuthorText}) + and defined($textinfo{EditionText})) + { + my $query = + "SELECT textbook_id FROM `$tables{textbook}` WHERE title = \"$textinfo{TitleText}\" AND edition = \"$textinfo{EditionText}\" AND author=\"$textinfo{AuthorText}\""; $bookid = $dbh->selectrow_array($query); if (!defined($bookid)) { - $dbh->do("INSERT INTO `$tables{textbook}` + $dbh->do( + "INSERT INTO `$tables{textbook}` VALUES( NULL, \"$textinfo{TitleText}\", @@ -446,8 +491,9 @@ if(open(IN, '<:encoding(UTF-8)', "$libraryRoot/Textbooks")) { NULL, NULL )" - ); - dbug "INSERT INTO textbook VALUES( \"\", \"$textinfo{TitleText}\", \"$textinfo{EditionText}\", \"$textinfo{AuthorText}\", \"\", \"\", \"\" )\n"; + ); + dbug + "INSERT INTO textbook VALUES( \"\", \"$textinfo{TitleText}\", \"$textinfo{EditionText}\", \"$textinfo{AuthorText}\", \"\", \"\", \"\" )\n"; $bookid = $dbh->selectrow_array($query); } } @@ -463,18 +509,18 @@ if(open(IN, '<:encoding(UTF-8)', "$libraryRoot/Textbooks")) { #### End of textbooks #### Next read in the taxonomy -my $clsep = '<<<'; +my $clsep = '<<<'; my $clinner = '__'; -my @cllist = (); +my @cllist = (); # Record full taxonomy for tagging menus (does not include cross-lists) my $tagtaxo = []; -my ($chaplist, $seclist) = ([],[]); +my ($chaplist, $seclist) = ([], []); my $canopenfile = 0; -if(open(IN, '<:encoding(UTF-8)', "$libraryRoot/Taxonomy2")) { +if (open(IN, '<:encoding(UTF-8)', "$libraryRoot/Taxonomy2")) { print "Reading in OPL taxonomy from Taxonomy2 in the library $libraryRoot.\n"; $canopenfile = 1; -} elsif(open(IN, '<:encoding(UTF-8)', "$libraryRoot/Taxonomy")) { +} elsif (open(IN, '<:encoding(UTF-8)', "$libraryRoot/Taxonomy")) { print "Reading in OPL taxonomy from Taxonomy in the library $libraryRoot.\n"; $canopenfile = 1; } else { @@ -484,20 +530,20 @@ if(open(IN, '<:encoding(UTF-8)', "$libraryRoot/Taxonomy2")) { } # Taxonomy is a subset of Taxonomy2, so we can use the same code either way -if($canopenfile) { - my ($cursub,$curchap); # these are strings - my ($subj, $chap, $sect); # these are indeces - while(my $line = ) { +if ($canopenfile) { + my ($cursub, $curchap); # these are strings + my ($subj, $chap, $sect); # these are indeces + while (my $line = ) { $line =~ /^(\t*)/; my $count = length($1); my $oktag = 1; chomp($line); - if($line =~ m/$clsep/) { + if ($line =~ m/$clsep/) { $oktag = 0; my @cross = split $clsep, $line; @cross = map(trim($_), @cross); - if(scalar(@cross) > 1) { - push @cllist, [join($clinner, ($cursub,$curchap,$cross[0])) ,$cross[1]]; + if (scalar(@cross) > 1) { + push @cllist, [ join($clinner, ($cursub, $curchap, $cross[0])), $cross[1] ]; } $line = $cross[0]; } @@ -507,31 +553,38 @@ if($canopenfile) { # but crosslists are not put in the heierarchy of legal tags # instead they go in a list of crosslists to deal with after # the full taxonomy is read in - if($count == 0) { #DBsubject + if ($count == 0) { #DBsubject $cursub = $line; - if($oktag) { + if ($oktag) { $taxo->{$line} = {}; - ($chaplist, $seclist) = ([],[]); - push @{$tagtaxo}, {name=>$line, subfields=>$chaplist}; + ($chaplist, $seclist) = ([], []); + push @{$tagtaxo}, { name => $line, subfields => $chaplist }; } - $subj = safe_get_id($tables{dbsubject}, 'DBsubject_id', - qq(WHERE name = ?), [$line], 1, "", $line); - } elsif($count == 1) { #DBchapter - if($oktag) { + $subj = safe_get_id($tables{dbsubject}, 'DBsubject_id', qq(WHERE name = ?), [$line], 1, "", $line); + } elsif ($count == 1) { #DBchapter + if ($oktag) { $taxo->{$cursub}->{$line} = {}; - $seclist=[]; - push @{$chaplist}, {name=>$line, subfields=>$seclist}; + $seclist = []; + push @{$chaplist}, { name => $line, subfields => $seclist }; } $curchap = $line; - $chap = safe_get_id($tables{dbchapter}, 'DBchapter_id', - qq(WHERE name = ? and DBsubject_id = ?), [$line, $subj], 1, "", $line, $subj); - } else { #DBsection - if($oktag) { + $chap = safe_get_id( + $tables{dbchapter}, 'DBchapter_id', + qq(WHERE name = ? and DBsubject_id = ?), + [ $line, $subj ], + 1, "", $line, $subj + ); + } else { #DBsection + if ($oktag) { $taxo->{$cursub}->{$curchap}->{$line} = []; - push @{$seclist}, {name=>$line}; + push @{$seclist}, { name => $line }; } - $sect = safe_get_id($tables{dbsection}, 'DBsection_id', - qq(WHERE name = ? and DBchapter_id = ?), [$line, $chap], 1, "", $line, $chap); + $sect = safe_get_id( + $tables{dbsection}, 'DBsection_id', + qq(WHERE name = ? and DBchapter_id = ?), + [ $line, $chap ], + 1, "", $line, $chap + ); } } close(IN); @@ -540,16 +593,16 @@ if($canopenfile) { #### Save the official taxonomy in json format my $webwork_htdocs = $ce->{webworkDirs}{htdocs}; -my $file = "$webwork_htdocs/DATA/tagging-taxonomy.json"; +my $file = "$webwork_htdocs/DATA/tagging-taxonomy.json"; -writeJSONtoFile($tagtaxo,$file); +writeJSONtoFile($tagtaxo, $file); print "Saved taxonomy to $file.\n"; #### Now deal with cross-listed sections for my $clinfo (@cllist) { my @scs = split /$clinner/, $clinfo->[1]; - if(defined $taxo->{$scs[0]}->{$scs[1]}->{$scs[2]}) { - push @{$taxo->{$scs[0]}->{$scs[1]}->{$scs[2]}}, $clinfo->[0]; + if (defined $taxo->{ $scs[0] }->{ $scs[1] }->{ $scs[2] }) { + push @{ $taxo->{ $scs[0] }->{ $scs[1] }->{ $scs[2] } }, $clinfo->[0]; } else { print "Faulty cross-list: pointing to $scs[0] / $scs[1] / $scs[2]\n"; } @@ -561,8 +614,8 @@ print "Number of files processed:\n"; #### Now search for tagged problems #recursive search for all pg files -File::Find::find( { wanted => \&pgfiles, follow_fast => 1 }, $libraryRoot ); -File::Find::find( { wanted => \&pgfiles, follow_fast => 1 }, $contribRoot ); +File::Find::find({ wanted => \&pgfiles, follow_fast => 1 }, $libraryRoot); +File::Find::find({ wanted => \&pgfiles, follow_fast => 1 }, $contribRoot); sub trim { my $str = shift; @@ -576,14 +629,14 @@ sub kwtidy { $s =~ s/\W//g; $s =~ s/_//g; $s = lc($s); - return($s); + return ($s); } sub keywordcleaner { my $string = shift; - my @spl1 = split /,/, $string; - my @spl2 = map(kwtidy($_), @spl1); - return(@spl2); + my @spl1 = split /,/, $string; + my @spl2 = map(kwtidy($_), @spl1); + return (@spl2); } # Save on passing these values around @@ -594,22 +647,28 @@ sub maybenewtext { my $textno = shift; return if defined($textinfo{$textno}); # So, not defined yet - $textinfo{$textno} = { title => '', author =>'', edition =>'', - section => '', chapter =>'', problems => [] }; + $textinfo{$textno} = { + title => '', + author => '', + edition => '', + section => '', + chapter => '', + problems => [] + }; } # process each file returned by the find command. sub pgfiles { my $name = $File::Find::name; my ($text, $edition, $textauthor, $textsection, $textproblem); - %textinfo=(); + %textinfo = (); my @textproblems = (-1); -#print "\n$name"; + #print "\n$name"; if ($name =~ /\.pg$/) { $cnt2++; - printf("%6d", $cnt2) if(($cnt2 % 100) == 0); - print "\n" if(($cnt2 % 1000) == 0); + printf("%6d", $cnt2) if (($cnt2 % 100) == 0); + print "\n" if (($cnt2 % 1000) == 0); my $pgfile = basename($name); my $pgpath = dirname($name); @@ -629,48 +688,68 @@ sub pgfiles { # DBsubject table - if(isvalid($tags)) { - my $DBsubject_id = safe_get_id($tables{dbsubject}, 'DBsubject_id', - qq(WHERE BINARY name = ?), [$tags->{DBsubject}], 1, "", $tags->{DBsubject}); - if(not $DBsubject_id) { + if (isvalid($tags)) { + my $DBsubject_id = safe_get_id( + $tables{dbsubject}, 'DBsubject_id', + qq(WHERE BINARY name = ?), + [ $tags->{DBsubject} ], + 1, "", $tags->{DBsubject} + ); + if (not $DBsubject_id) { print "\nInvalid subject '$tags->{DBsubject}' in $name\n"; return; } # DBchapter table - $DBchapter_id = safe_get_id($tables{dbchapter}, 'DBchapter_id', - qq(WHERE BINARY name = ? and DBsubject_id = ?), [$tags->{DBchapter}, $DBsubject_id], 1, "", $tags->{DBchapter}, $DBsubject_id); - if(not $DBchapter_id) { + $DBchapter_id = safe_get_id( + $tables{dbchapter}, 'DBchapter_id', + qq(WHERE BINARY name = ? and DBsubject_id = ?), + [ $tags->{DBchapter}, $DBsubject_id ], + 1, "", $tags->{DBchapter}, $DBsubject_id + ); + if (not $DBchapter_id) { print "\nInvalid chapter '$tags->{DBchapter}' in $name\n"; return; } # DBsection table - $aDBsection_id = safe_get_id($tables{dbsection}, 'DBsection_id', - qq(WHERE BINARY name = ? and DBchapter_id = ?), [$tags->{DBsection}, $DBchapter_id], 1, "", $tags->{DBsection}, $DBchapter_id); - if(not $aDBsection_id) { + $aDBsection_id = safe_get_id( + $tables{dbsection}, 'DBsection_id', + qq(WHERE BINARY name = ? and DBchapter_id = ?), + [ $tags->{DBsection}, $DBchapter_id ], + 1, "", $tags->{DBsection}, $DBchapter_id + ); + if (not $aDBsection_id) { print "\nInvalid section '$tags->{DBsection}' in $name\n"; return; } - } else { # tags are not valid, error printed by validation part + } else { # tags are not valid, error printed by validation part print "File $name\n"; return; } - my @DBsection_ids=($aDBsection_id); + my @DBsection_ids = ($aDBsection_id); # Now add crosslisted section - my @CL_array = @{$taxo->{$tags->{DBsubject}}->{$tags->{DBchapter}}->{$tags->{DBsection}}}; + my @CL_array = @{ $taxo->{ $tags->{DBsubject} }->{ $tags->{DBchapter} }->{ $tags->{DBsection} } }; for my $clsect (@CL_array) { my @np = split /$clinner/, $clsect; @np = map(trim($_), @np); - my $new_dbsubj_id = safe_get_id($tables{dbsubject}, 'DBsubject_id', - qq(WHERE name = ?), [$np[0]], 1, "", $np[0]); - my $new_dbchap_id = safe_get_id($tables{dbchapter}, 'DBchapter_id', - qq(WHERE name = ? and DBsubject_id = ?), [$np[1], $new_dbsubj_id], 1, "", $np[1], $new_dbsubj_id); - my $new_dbsect_id = safe_get_id($tables{dbsection}, 'DBsection_id', - qq(WHERE name = ? and DBchapter_id = ?), [$np[2], $new_dbchap_id], 1, "", $np[2], $new_dbchap_id); + my $new_dbsubj_id = + safe_get_id($tables{dbsubject}, 'DBsubject_id', qq(WHERE name = ?), [ $np[0] ], 1, "", $np[0]); + my $new_dbchap_id = safe_get_id( + $tables{dbchapter}, 'DBchapter_id', + qq(WHERE name = ? and DBsubject_id = ?), + [ $np[1], $new_dbsubj_id ], + 1, "", $np[1], $new_dbsubj_id + ); + my $new_dbsect_id = safe_get_id( + $tables{dbsection}, 'DBsection_id', + qq(WHERE name = ? and DBchapter_id = ?), + [ $np[2], $new_dbchap_id ], + 1, "", $np[2], $new_dbchap_id + ); push @DBsection_ids, $new_dbsect_id; } @@ -678,22 +757,25 @@ sub pgfiles { $tags->{Author} =~ /(.*?)\s(\w+)\s*$/; my $firstname = $1; - my $lastname = $2; + my $lastname = $2; #remove leading and trailing spaces from firstname, which includes any middle name too. $firstname =~ s/^\s*//; $firstname =~ s/\s*$//; - $lastname =~ s/^\s*//; - $lastname =~ s/\s*$//; + $lastname =~ s/^\s*//; + $lastname =~ s/\s*$//; my $author_id = 0; - if($lastname) { - $author_id = safe_get_id($tables{author}, 'author_id', - qq(WHERE lastname = ? AND firstname = ?), [$lastname, $firstname], 1, "", $tags->{Institution}, $lastname, $firstname,""); + if ($lastname) { + $author_id = safe_get_id( + $tables{author}, 'author_id', + qq(WHERE lastname = ? AND firstname = ?), + [ $lastname, $firstname ], + 1, "", $tags->{Institution}, $lastname, $firstname, "" + ); } # path table - my $path_id = safe_get_id($tables{path}, 'path_id', - qq(WHERE path = ?), [$pgpath], 1, "", $pgpath, "", ""); + my $path_id = safe_get_id($tables{path}, 'path_id', qq(WHERE path = ?), [$pgpath], 1, "", $pgpath, "", ""); # pgfile table -- set 4 defaults first @@ -706,29 +788,38 @@ sub pgfiles { my @pgfile_ids = (); for my $DBsection_id (@DBsection_ids) { - my $pgfile_id = safe_get_id($tables{pgfile}, 'pgfile_id', + my $pgfile_id = safe_get_id( + $tables{pgfile}, 'pgfile_id', qq(WHERE filename = ? AND path_id = ? AND DBsection_id = ? AND libraryroot = ?), - [$pgfile, $path_id, $DBsection_id, $pglib], 1, "", $DBsection_id, $author_id, - $tags->{Institution}, $pglib, $path_id, $pgfile, 0, $level, $lang, $static, $mathobj); + [ $pgfile, $path_id, $DBsection_id, $pglib ], 1, "", $DBsection_id, $author_id, + $tags->{Institution}, $pglib, $path_id, $pgfile, 0, $level, $lang, $static, $mathobj + ); push @pgfile_ids, $pgfile_id; } # morelt table my $morelt_id; - if($tags->{MLT}) { + if ($tags->{MLT}) { for my $DBsection_id (@DBsection_ids) { - $morelt_id = safe_get_id($tables{morelt}, 'morelt_id', - qq(WHERE name = ?), [$tags->{MLT}], 1, "", $tags->{MLT}, $DBsection_id, ""); + $morelt_id = safe_get_id( + $tables{morelt}, 'morelt_id', qq(WHERE name = ?), [ $tags->{MLT} ], + 1, "", $tags->{MLT}, $DBsection_id, + "" + ); for my $pgfile_id (@pgfile_ids) { - $dbh->do("UPDATE `$tables{pgfile}` SET - morelt_id = \"$morelt_id\" WHERE pgfile_id = \"$pgfile_id\" "); + $dbh->do( + "UPDATE `$tables{pgfile}` SET + morelt_id = \"$morelt_id\" WHERE pgfile_id = \"$pgfile_id\" " + ); dbug "UPDATE pgfile morelt_id for $pgfile_id to $morelt_id\n"; - if($tags->{MLTleader}) { - $dbh->do("UPDATE `$tables{morelt}` SET - leader = \"$pgfile_id\" WHERE morelt_id = \"$morelt_id\" "); + if ($tags->{MLTleader}) { + $dbh->do( + "UPDATE `$tables{morelt}` SET + leader = \"$pgfile_id\" WHERE morelt_id = \"$morelt_id\" " + ); dbug "UPDATE morelt leader for $morelt_id to $pgfile_id\n"; } } @@ -737,49 +828,54 @@ sub pgfiles { # keyword table, and problem_keyword many-many table - foreach my $keyword (@{$tags->{keywords}}) { + foreach my $keyword (@{ $tags->{keywords} }) { $keyword =~ s/[\'\"]//g; $keyword = kwtidy($keyword); - # skip it if it is empty - next unless $keyword; - my $keyword_id = safe_get_id($tables{keyword}, 'keyword_id', - qq(WHERE keyword = ?), [$keyword], 1, "", $keyword); + # skip it if it is empty + next unless $keyword; + my $keyword_id = + safe_get_id($tables{keyword}, 'keyword_id', qq(WHERE keyword = ?), [$keyword], 1, "", $keyword); for my $pgfile_id (@pgfile_ids) { - $query = "SELECT pgfile_id FROM `$tables{pgfile_keyword}` WHERE keyword_id = \"$keyword_id\" and pgfile_id=\"$pgfile_id\""; + $query = + "SELECT pgfile_id FROM `$tables{pgfile_keyword}` WHERE keyword_id = \"$keyword_id\" and pgfile_id=\"$pgfile_id\""; my $ok = $dbh->selectrow_array($query); if (!defined($ok)) { - $dbh->do("INSERT INTO `$tables{pgfile_keyword}` + $dbh->do( + "INSERT INTO `$tables{pgfile_keyword}` VALUES( \"$pgfile_id\", \"$keyword_id\" )" - ); + ); dbug "INSERT INTO pgfile_keyword VALUES( \"$pgfile_id\", \"$keyword_id\" )\n"; } } - } #end foreach keyword + } #end foreach keyword # Textbook section # problem table contains textbook problems - for my $texthashref (@{$tags->{textinfo}}) { + for my $texthashref (@{ $tags->{textinfo} }) { # textbook table - $text = $texthashref->{TitleText}; + $text = $texthashref->{TitleText}; $edition = $texthashref->{EditionText} || 0; $edition =~ s/[^\d\.]//g; $textauthor = $texthashref->{AuthorText}; - next unless($text and $textauthor); + next unless ($text and $textauthor); my $chapnum = $texthashref->{chapter} || -1; - my $secnum = $texthashref->{section} || -1; - $query = "SELECT textbook_id FROM `$tables{textbook}` WHERE title = \"$text\" AND edition = \"$edition\" AND author=\"$textauthor\""; + my $secnum = $texthashref->{section} || -1; + $query = + "SELECT textbook_id FROM `$tables{textbook}` WHERE title = \"$text\" AND edition = \"$edition\" AND author=\"$textauthor\""; my $textbook_id = $dbh->selectrow_array($query); + if (!defined($textbook_id)) { - # make sure edition is an integer - $edition = 0 unless $edition; - $dbh->do("INSERT INTO `$tables{textbook}` + # make sure edition is an integer + $edition = 0 unless $edition; + $dbh->do( + "INSERT INTO `$tables{textbook}` VALUES( NULL, \"$text\", @@ -789,37 +885,45 @@ sub pgfiles { NULL, NULL )" - ); - dbug "INSERT INTO textbook VALUES( \"\", \"$text\", \"$edition\", \"$textauthor\", \"\", \"\", \"\" )\n"; + ); + dbug + "INSERT INTO textbook VALUES( \"\", \"$text\", \"$edition\", \"$textauthor\", \"\", \"\", \"\" )\n"; dbug "\nLate add into $tables{textbook} \"$text\", \"$edition\", \"$textauthor\"\n", 1; $textbook_id = $dbh->selectrow_array($query); } # chapter weak table of textbook - $query = "SELECT chapter_id FROM `$tables{chapter}` WHERE textbook_id = \"$textbook_id\" AND number = \"$chapnum\""; + $query = + "SELECT chapter_id FROM `$tables{chapter}` WHERE textbook_id = \"$textbook_id\" AND number = \"$chapnum\""; my $chapter_id = $dbh->selectrow_array($query); if (!defined($chapter_id) && defined($textbook_id)) { - $dbh->do("INSERT INTO `$tables{chapter}` + $dbh->do( + "INSERT INTO `$tables{chapter}` VALUES( NULL, \"$textbook_id\", - \"".$chapnum."\", + \"" . $chapnum . "\", \"$tags->{DBchapter}\", NULL )" - ); - dbug "\nLate add into $tables{chapter} \"$text\", \"$edition\", \"$textauthor\", $chapnum $tags->{chapter} from $name\n", 1; - dbug "INSERT INTO chapter VALUES(\"\", \"$textbook_id\", \"".$chapnum."\", \"$tags->{DBchapter}\", \"\" )\n"; + ); + dbug + "\nLate add into $tables{chapter} \"$text\", \"$edition\", \"$textauthor\", $chapnum $tags->{chapter} from $name\n", + 1; + dbug "INSERT INTO chapter VALUES(\"\", \"$textbook_id\", \"" . $chapnum + . "\", \"$tags->{DBchapter}\", \"\" )\n"; $chapter_id = $dbh->selectrow_array($query); } # section weak table of textbook # $tags->{DBsection} = '' if ($secnum < 0); - $query = "SELECT section_id FROM `$tables{section}` WHERE chapter_id = \"$chapter_id\" AND number = \"$secnum\""; + $query = + "SELECT section_id FROM `$tables{section}` WHERE chapter_id = \"$chapter_id\" AND number = \"$secnum\""; my $section_id = $dbh->selectrow_array($query); if (!defined($section_id) && defined($chapter_id) && defined($textbook_id)) { - $dbh->do("INSERT INTO `$tables{section}` + $dbh->do( + "INSERT INTO `$tables{section}` VALUES( NULL, \"$chapter_id\", @@ -827,41 +931,48 @@ sub pgfiles { \"$tags->{DBsection}\", NULL )" - ); - dbug "INSERT INTO section VALUES(\"\", \"$textbook_id\", \"$secnum\", \"$tags->{DBsection}\", \"\" )\n"; - dbug "\nLate add into $tables{section} \"$text\", \"$edition\", \"$textauthor\", $secnum $tags->{DBsection} from $name\n", 1; + ); + dbug + "INSERT INTO section VALUES(\"\", \"$textbook_id\", \"$secnum\", \"$tags->{DBsection}\", \"\" )\n"; + dbug + "\nLate add into $tables{section} \"$text\", \"$edition\", \"$textauthor\", $secnum $tags->{DBsection} from $name\n", + 1; $section_id = $dbh->selectrow_array($query); } - @textproblems = @{$texthashref->{problems}}; + @textproblems = @{ $texthashref->{problems} }; if ($section_id) { for my $tp (@textproblems) { - $query = "SELECT problem_id FROM `$tables{problem}` WHERE section_id = \"$section_id\" AND number = \"$tp\""; + $query = + "SELECT problem_id FROM `$tables{problem}` WHERE section_id = \"$section_id\" AND number = \"$tp\""; my $problem_id = $dbh->selectrow_array($query); if (!defined($problem_id)) { - $dbh->do("INSERT INTO `$tables{problem}` + $dbh->do( + "INSERT INTO `$tables{problem}` VALUES( NULL, \"$section_id\", \"$tp\", NULL )" - ); + ); dbug "INSERT INTO problem VALUES( \"\", \"$section_id\", \"$tp\", \"\" )\n"; $problem_id = $dbh->selectrow_array($query); } # pgfile_problem table associates pgfiles with textbook problems for my $pgfile_id (@pgfile_ids) { - $query = "SELECT problem_id FROM `$tables{pgfile_problem}` WHERE problem_id = \"$problem_id\" AND pgfile_id = \"$pgfile_id\""; + $query = + "SELECT problem_id FROM `$tables{pgfile_problem}` WHERE problem_id = \"$problem_id\" AND pgfile_id = \"$pgfile_id\""; my $pg_problem_id = $dbh->selectrow_array($query); if (!defined($pg_problem_id)) { - $dbh->do("INSERT INTO `$tables{pgfile_problem}` + $dbh->do( + "INSERT INTO `$tables{pgfile_problem}` VALUES( \"$pgfile_id\", \"$problem_id\" )" - ); + ); dbug "INSERT INTO pgfile_problem VALUES( \"$pgfile_id\", \"$problem_id\" )\n"; } } @@ -869,11 +980,12 @@ sub pgfiles { } #reset tag vars, they may not match the next text/file - $textauthor=""; $textsection=""; + $textauthor = ""; + $textsection = ""; } - } else { # This file was not tagged - # Message if not a pointer - # print STDERR "File $name is not tagged\n" if not $tags->isplaceholder(); + } else { # This file was not tagged + # Message if not a pointer + # print STDERR "File $name is not tagged\n" if not $tags->isplaceholder(); ; } } @@ -892,7 +1004,7 @@ my $dbsects = $dbh->selectall_arrayref("SELECT DBsection_id from `$tables{dbsect for my $sect (@{$dbsects}) { $sect = $sect->[0]; my $srar = $dbh->selectall_arrayref("SELECT * FROM `$tables{pgfile}` WHERE DBsection_id=$sect"); - if(scalar(@{$srar})==0) { + if (scalar(@{$srar}) == 0) { $dbh->do("DELETE FROM `$tables{dbsection}` WHERE DBsection_id=$sect"); } } @@ -901,7 +1013,7 @@ my $dbchaps = $dbh->selectall_arrayref("SELECT DBchapter_id from `$tables{dbchap for my $chap (@{$dbchaps}) { $chap = $chap->[0]; my $srar = $dbh->selectall_arrayref("SELECT * FROM `$tables{dbsection}` WHERE DBchapter_id=$chap"); - if(scalar(@{$srar})==0) { + if (scalar(@{$srar}) == 0) { $dbh->do("DELETE FROM `$tables{dbchapter}` WHERE DBchapter_id=$chap"); } } @@ -912,13 +1024,14 @@ for my $chap (@{$dbchaps}) { $dbh->disconnect; -if ($ce->{problemLibrary}{showLibraryLocalStats} || - $ce->{problemLibrary}{showLibraryGlobalStats}) { - print "\nUpdating Library Statistics.\n"; - do $ENV{WEBWORK_ROOT}.'/bin/update-OPL-statistics.pl'; +if ($ce->{problemLibrary}{showLibraryLocalStats} + || $ce->{problemLibrary}{showLibraryGlobalStats}) +{ + print "\nUpdating Library Statistics.\n"; + do $ENV{WEBWORK_ROOT} . '/bin/update-OPL-statistics.pl'; - print "\nLoading global statistics (if possible).\n"; - do $ENV{WEBWORK_ROOT}.'/bin/load-OPL-global-statistics.pl'; + print "\nLoading global statistics (if possible).\n"; + do $ENV{WEBWORK_ROOT} . '/bin/load-OPL-global-statistics.pl'; } print "\nDone.\n"; diff --git a/bin/addcourse b/bin/addcourse index 42ea11ac5d..ae65783715 100755 --- a/bin/addcourse +++ b/bin/addcourse @@ -49,7 +49,7 @@ use Getopt::Long; BEGIN { use Mojo::File qw(curfile); - use Env qw(WEBWORK_ROOT); + use Env qw(WEBWORK_ROOT); $WEBWORK_ROOT = curfile->dirname->dirname; } @@ -59,9 +59,9 @@ use lib "$ENV{WEBWORK_ROOT}/lib"; use WeBWorK::CourseEnvironment; use WeBWorK::File::Classlist; -use WeBWorK::Utils qw(runtime_use cryptPassword); +use WeBWorK::Utils qw(runtime_use cryptPassword); use WeBWorK::Utils::CourseManagement qw(addCourse); -use WeBWorK::File::Classlist qw(parse_classlist); +use WeBWorK::File::Classlist qw(parse_classlist); use WeBWorK::DB::Record::User; use WeBWorK::DB::Record::Password; use WeBWorK::DB::Record::PermissionLevel; @@ -124,6 +124,8 @@ if ($users) { $record{permission} = $ce->{userRoles}{professor}; } + $record{accommodation_time_factor} = 1; + delete $professors{$user_id}; push @users, diff --git a/bin/change_user_id b/bin/change_user_id index 2ec44a5034..a68f538871 100755 --- a/bin/change_user_id +++ b/bin/change_user_id @@ -1,18 +1,19 @@ #!/usr/bin/env perl + +# Sometimes a webwork user id changes. This script transfers the webwork data for the old user_id to the new user_id. # -#Sometimes a webwork user id changes. This script transfers the webwork data for the old user_id to the new user_id -# Update database tables -#user -#permission -#password -#key -#set_user -#problem_user -#set_locations_user -#global_achievement_user -#achievement_user +# The script updates the following database database tables +# user +# permission +# password +# key +# set_user +# problem_user +# set_locations_user +# global_achievement_user +# achievement_user # -# Update answer_log +# and updates the answer_log. use strict; use warnings; @@ -21,7 +22,7 @@ use File::Basename; BEGIN { use Mojo::File qw(curfile); - use Env qw(WEBWORK_ROOT); + use Env qw(WEBWORK_ROOT); $WEBWORK_ROOT = curfile->dirname->dirname; } @@ -32,13 +33,13 @@ use WeBWorK::CourseEnvironment; use WeBWorK::DB; use Data::Dumper; -if((scalar(@ARGV) != 3)) { - print "\nSyntax is: change_user_id course_id old_user_id new_user_id"; - print "\n (e.g. newpassword MAT_123 jjones jsmith\n\n"; - exit(); +if ((scalar(@ARGV) != 3)) { + print "\nSyntax is: change_user_id course_id old_user_id new_user_id"; + print "\n (e.g. newpassword MAT_123 jjones jsmith\n\n"; + exit(); } -my $courseID = shift; +my $courseID = shift; my $old_user_id = shift; my $new_user_id = shift; @@ -50,63 +51,63 @@ my $ce = WeBWorK::CourseEnvironment->new({ my $db = WeBWorK::DB->new($ce); die "Error: $old_user_id does not exist!" unless $db->existsUser($old_user_id); -unless($db->existsUser($new_user_id)) { - my $user = $db->getUser($old_user_id); - $user->{user_id}=$new_user_id; - $user->{comment} = $user->{comment}."Record created from $old_user_id record"; - $db->addUser($user); +unless ($db->existsUser($new_user_id)) { + my $user = $db->getUser($old_user_id); + $user->{user_id} = $new_user_id; + $user->{comment} = $user->{comment} . "Record created from $old_user_id record"; + $db->addUser($user); } -unless($db->existsPassword($new_user_id)) { - my $password = $db->getPassword($old_user_id); - $password->{user_id} = $new_user_id; - $db->addPassword($password); +if (!$db->existsPassword($new_user_id) && (my $password = $db->getPassword($old_user_id))) { + $password->{user_id} = $new_user_id; + $db->addPassword($password); } -unless($db->existsPermissionLevel($new_user_id)) { - my $permission = $db->getPermissionLevel($old_user_id); - $permission->{user_id} = $new_user_id; - $db->addPermissionLevel($permission); +unless ($db->existsPermissionLevel($new_user_id)) { + my $permission = $db->getPermissionLevel($old_user_id); + $permission->{user_id} = $new_user_id; + $db->addPermissionLevel($permission); } my @old_user_sets = $db->listUserSets($old_user_id); -foreach(@old_user_sets) { - my $set_id = $_; - my $new_set = $db->newUserSet; - $new_set->user_id($new_user_id); - $new_set->set_id($set_id); - eval{$db->addUserSet($new_set)}; - my $old_set = $db->getUserSet($old_user_id,$set_id); - foreach(keys %$old_set) { - next if /user_id|set_id/; - $new_set->$_($old_set->$_); - } - - $db->putUserSet($new_set) unless $db->existsUserSet($new_user_id,$set_id); - my @global_problems = grep { defined $_} $db->getAllGlobalProblems($set_id); - foreach(@global_problems) { - if($db->existsUserProblem($old_user_id,$set_id,$_->{problem_id})) { - my $old_user_problem = $db->getUserProblem($old_user_id,$set_id,$_->{problem_id}); - my $new_user_problem = $db->newUserProblem; - $new_user_problem->user_id($new_user_id); - $new_user_problem->set_id($set_id); - $new_user_problem->problem_id($_->{problem_id}); - $db->addUserProblem($new_user_problem) unless $db->existsUserProblem($new_user_id,$set_id,$_->{problem_id}); - foreach(keys %$old_user_problem) { - next if /(user_id|set_id|problem_id)/; - $new_user_problem->$_($old_user_problem->$_); - } - $db->putUserProblem($new_user_problem); - } - } +foreach (@old_user_sets) { + my $set_id = $_; + my $new_set = $db->newUserSet; + $new_set->user_id($new_user_id); + $new_set->set_id($set_id); + eval { $db->addUserSet($new_set) }; + my $old_set = $db->getUserSet($old_user_id, $set_id); + foreach (keys %$old_set) { + next if /user_id|set_id/; + $new_set->$_($old_set->$_); + } + + $db->putUserSet($new_set) unless $db->existsUserSet($new_user_id, $set_id); + my @global_problems = grep { defined $_ } $db->getAllGlobalProblems($set_id); + foreach (@global_problems) { + if ($db->existsUserProblem($old_user_id, $set_id, $_->{problem_id})) { + my $old_user_problem = $db->getUserProblem($old_user_id, $set_id, $_->{problem_id}); + my $new_user_problem = $db->newUserProblem; + $new_user_problem->user_id($new_user_id); + $new_user_problem->set_id($set_id); + $new_user_problem->problem_id($_->{problem_id}); + $db->addUserProblem($new_user_problem) + unless $db->existsUserProblem($new_user_id, $set_id, $_->{problem_id}); + foreach (keys %$old_user_problem) { + next if /(user_id|set_id|problem_id)/; + $new_user_problem->$_($old_user_problem->$_); + } + $db->putUserProblem($new_user_problem); + } + } } my $answer_log = $ce->{courseFiles}->{logs}->{answer_log}; -my $dirname = dirname($answer_log); -copy($answer_log,"$dirname/answer_log.bak"); -open(my $in,'<',"$dirname/answer_log.bak") or die "Can't open $dirname/answer_log.bak:$!"; -open(my $out,'>',$answer_log); -while(<$in>) { - s/$old_user_id/$new_user_id/g; - print $out $_; +my $dirname = dirname($answer_log); +copy($answer_log, "$dirname/answer_log.bak"); +open(my $in, '<', "$dirname/answer_log.bak") or die "Can't open $dirname/answer_log.bak:$!"; +open(my $out, '>', $answer_log); +while (<$in>) { + s/$old_user_id/$new_user_id/g; + print $out $_; } diff --git a/bin/check_latex b/bin/check_latex index 8c5a6ee567..a806cea296 100755 --- a/bin/check_latex +++ b/bin/check_latex @@ -13,8 +13,8 @@ use feature 'say'; BEGIN { use Mojo::File qw(curfile); - use YAML::XS qw(LoadFile); - use Env qw(WEBWORK_ROOT PG_ROOT); + use YAML::XS qw(LoadFile); + use Env qw(WEBWORK_ROOT PG_ROOT); $WEBWORK_ROOT = curfile->dirname->dirname->to_string; @@ -51,7 +51,9 @@ my $latex_cmd = . shell_quote($ce->{webworkDirs}{assetsTex}) . ':' . shell_quote($ce->{pg}{directories}{assetsTex}) . ': ' . $ce->{externalPrograms}{latex2pdf} - . ' -interaction nonstopmode check_latex_exam.tex >> check_latex.nfo 2>&1'; + . ' -interaction nonstopmode check_latex_exam.tex >> check_latex.nfo 2>&1 &&' + . "TEXINPUTS=$ENV{WEBWORK_ROOT}/bin: $ce->{externalPrograms}{latex2pdf}" + . ' -interaction nonstopmode check_latex_standalone.tex > check_latex.nfo 2>&1'; if ((system $latex_cmd) >> 8) { if (open(my $fh, '<', "$temp_dir/check_latex.nfo")) { diff --git a/bin/check_latex_article.tex b/bin/check_latex_article.tex index 76036857ee..21d58de3e6 100644 --- a/bin/check_latex_article.tex +++ b/bin/check_latex_article.tex @@ -8,7 +8,7 @@ \usepackage{multicol} \usepackage{tcolorbox} -\usepackage[active,textmath,displaymath]{preview} % needed for dvipng +\usepackage[textmath,displaymath]{preview} % needed for dvipng \setlength{\columnsep}{.25in} \setlength{\columnseprule}{.4pt} diff --git a/bin/check_latex_exam.tex b/bin/check_latex_exam.tex index bd48c01ef5..384a8ba693 100644 --- a/bin/check_latex_exam.tex +++ b/bin/check_latex_exam.tex @@ -9,7 +9,7 @@ \usepackage{bidi} \fi -\usepackage[active,textmath,displaymath]{preview} % needed for dvipng +\usepackage[textmath,displaymath]{preview} % needed for dvipng \setlength{\columnsep}{.25in} \setlength{\columnseprule}{.4pt} diff --git a/bin/check_latex_standalone.tex b/bin/check_latex_standalone.tex new file mode 100644 index 0000000000..7f8daeecd6 --- /dev/null +++ b/bin/check_latex_standalone.tex @@ -0,0 +1,22 @@ +\documentclass{standalone} + +\usepackage[svgnames]{xcolor} +\usepackage{tikz} +\usepackage{pgfplots} +\usetikzlibrary{arrows.meta,plotmarks,calc,spath3} +\usepgfplotslibrary{fillbetween} +\pgfplotsset{compat = 1.18} + +\begin{document} + +\begin{tikzpicture} + \begin{axis} + \addplot[name path=pathA, domain=-3:3, draw=none, mark=triangle*] {(x^2)}; + \draw[{Stealth}-{Stealth}, spath/use=pathA]; + \draw ($(-1, 0) + (10pt, 10pt)$) -- ($(1, 3) - (10pt, 10pt)$); + \addplot[name path=pathB, domain=-3:3] {(-x^2 + 3)}; + \addplot[green, fill opacity=0.3] fill between[of=pathA and pathB]; + \end{axis} +\end{tikzpicture} + +\end{document} diff --git a/bin/check_modules.pl b/bin/check_modules.pl index 3b73cee2bf..661e8e7331 100755 --- a/bin/check_modules.pl +++ b/bin/check_modules.pl @@ -9,9 +9,37 @@ =head1 SYNOPSIS check_modules.pl [options] - Options: - -m|--modules Check that the perl modules needed by webwork2 can be loaded. - -p|--programs Check that the programs needed by webwork2 exist. +Options: + +=over 4 + +=item C<-m|--modules> + +Check that the perl modules needed by webwork2 can be loaded. + +=item C<-p|--programs> + +Check that the programs needed by webwork2 exist. + +=item C<-d|--distribution> + +Specify your linux distribution. Currently supported options are + +=over 4 + +=item C + +Tested on ubuntu 24. May work for other distributions using the apt package +manager + +=item C + +For RedHat Enterprise Linux and equivalents with the EPEL and CodeReady +Builder repositories enabled (e.g. Rocky Linux, Oracle Linux) + +=back + +=back Both programs and modules are checked if no options are given. @@ -30,98 +58,496 @@ =head1 DESCRIPTION use Getopt::Long qw(:config bundling); use Pod::Usage; -my @modulesList = qw( - Archive::Tar - Archive::Zip - Archive::Zip::SimpleZip - Benchmark - Carp - Class::Accessor - Crypt::JWT - Crypt::PK::RSA - Data::Dump - Data::Dumper - Data::Structure::Util - Data::UUID - Date::Format - Date::Parse - DateTime - DBI - Digest::MD5 - Digest::SHA - Email::Address::XS - Email::Sender::Transport::SMTP - Email::Stuffer - Errno - Exception::Class - File::Copy - File::Copy::Recursive - File::Fetch - File::Find - File::Find::Rule - File::Path - File::Spec - File::stat - File::Temp - Future::AsyncAwait - GD - GD::Barcode::QRcode - Getopt::Long - Getopt::Std - HTML::Entities - HTTP::Async - IO::File - Iterator - Iterator::Util - Locale::Maketext::Lexicon - Locale::Maketext::Simple - LWP::Protocol::https - MIME::Base32 - MIME::Base64 - Math::Random::Secure - Minion - Minion::Backend::SQLite - Mojolicious - Mojolicious::Plugin::NotYAMLConfig - Mojolicious::Plugin::RenderFile - Net::IP - Net::OAuth - Opcode - Pandoc - Perl::Tidy - PHP::Serialization - Pod::Simple::Search - Pod::Simple::XHTML - Pod::Usage - Pod::WSDL - Scalar::Util - SOAP::Lite - Socket - SQL::Abstract - String::ShellQuote - SVG - Text::CSV - Text::Wrap - Tie::IxHash - Time::HiRes - Time::Zone - Types::Serialiser - URI::Escape - UUID::Tiny - XML::LibXML - XML::Parser - XML::Parser::EasyTree - XML::Writer - YAML::XS +my %modulesList = ( + 'Archive::Tar' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Archive-Tar' + } + }, + 'Archive::Zip' => { + package => { + ubuntu => 'libarchive-zip-perl', + rhel => 'perl-Archive-Zip' + } + }, + 'Archive::Zip::SimpleZip' => { + package => { + rhel => 'perl-Archive-Zip-SimpleZip' + } + }, + 'Benchmark' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Benchmark' + } + }, + 'Carp' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Carp' + } + }, + 'Class::Accessor' => { + package => { + ubuntu => 'libclass-accessor-perl', + rhel => 'perl-Class-Accessor' + } + }, + 'Crypt::JWT' => { + package => { + ubuntu => 'libcrypt-jwt-perl', + rhel => 'perl-Crypt-JWT' + } + }, + 'Crypt::PK::RSA' => { + package => { + ubuntu => 'libcryptx-perl', + rhel => 'perl-CryptX' + } + }, + 'DBI' => { + package => { + ubuntu => 'libdbi-perl', + rhel => 'perl-DBI' + } + }, + 'Data::Dump' => { + package => { + ubuntu => 'libdata-dump-perl', + rhel => 'perl-Data-Dump' + } + }, + 'Data::Dumper' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Data-Dumper' + } + }, + 'Data::Structure::Util' => { + package => { + ubuntu => 'libdata-structure-util-perl' + } + }, + 'Data::UUID' => { + package => { + ubuntu => 'libossp-uuid-perl', + rhel => 'perl-Data-UUID' + } + }, + 'Date::Format' => { + package => { + ubuntu => 'libtimedate-perl', + rhel => 'perl-TimeDate' + } + }, + 'Date::Parse' => { + package => { + ubuntu => 'libtimedate-perl', + rhel => 'perl-TimeDate' + } + }, + 'DateTime' => { + package => { + ubuntu => 'libdatetime-perl', + rhel => 'perl-DateTime' + } + }, + 'Digest::MD5' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Digest-MD5' + } + }, + 'Digest::SHA' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Digest-SHA' + } + }, + 'Email::Address::XS' => { + package => { + ubuntu => 'libemail-address-xs-perl', + rhel => 'perl-Email-Address-XS' + } + }, + 'Email::Sender::Transport::SMTP' => { + package => { + ubuntu => 'libemail-sender-perl', + rhel => 'perl-Email-Sender' + } + }, + 'Email::Stuffer' => { + package => { + ubuntu => 'libemail-stuffer-perl' + } + }, + 'Errno' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Errno' + } + }, + 'Exception::Class' => { + package => { + ubuntu => 'libexception-class-perl', + rhel => 'perl-Exception-Class' + } + }, + 'File::Copy' => { + package => { + ubuntu => 'perl', + rhel => 'perl-File-Copy' + } + }, + 'File::Copy::Recursive' => { + package => { + ubuntu => 'libfile-copy-recursive-perl', + rhel => 'perl-File-Copy-Recursive' + } + }, + 'File::Fetch' => { + package => { + ubuntu => 'perl', + rhel => 'perl-File-Fetch' + } + }, + 'File::Find' => { + package => { + ubuntu => 'perl', + rhel => 'perl-File-Find' + } + }, + 'File::Find::Rule' => { + package => { + ubuntu => 'libfile-find-rule-perl', + rhel => 'perl-File-Find-Rule' + } + }, + 'File::Path' => { + package => { + ubuntu => 'perl', + rhel => 'perl-File-Path' + } + }, + 'File::Spec' => { + package => { + ubuntu => 'perl-base', + rhel => 'perl-PathTools' + } + }, + 'File::Temp' => { + package => { + ubuntu => 'perl', + rhel => 'perl-File-Temp' + } + }, + 'File::stat' => { + package => { + ubuntu => 'perl', + rhel => 'perl-File-stat' + } + }, + 'Future::AsyncAwait' => { + minversion => '0.52', + package => { + ubuntu => 'libfuture-asyncawait-perl' + } + }, + 'GD' => { + package => { + ubuntu => 'libgd-perl', + rhel => 'perl-GD' + } + }, + 'GD::Barcode::QRcode' => { + package => { + ubuntu => 'libgd-barcode-perl' + } + }, + 'Getopt::Long' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Getopt-Long' + } + }, + 'Getopt::Std' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Getopt-Std' + } + }, + 'HTML::Entities' => { + package => { + ubuntu => 'libhtml-parser-perl', + rhel => 'perl-HTML-Parser' + } + }, + 'HTTP::Async' => { + package => { + ubuntu => 'libhttp-async-perl' + } + }, + 'IO::File' => { + package => { + ubuntu => 'perl-base', + rhel => 'perl-IO' + } + }, + 'Iterator' => { + package => { + ubuntu => 'libiterator-perl', + } + }, + 'Iterator::Util' => { + package => { + ubuntu => 'libiterator-util-perl' + } + }, + 'LWP::Protocol::https' => { + minversion => '6.06', + package => { + ubuntu => 'liblwp-protocol-https-perl', + rhel => 'perl-LWP-Protocol-https' + } + }, + 'Locale::Maketext::Lexicon' => { + package => { + ubuntu => 'liblocale-maketext-lexicon-perl' + } + }, + 'Locale::Maketext::Simple' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Locale-Maketext-Simple' + } + }, + 'MIME::Base32' => { + package => { + ubuntu => 'libmime-base32-perl' + } + }, + 'MIME::Base64' => { + package => { + ubuntu => 'perl', + rhel => 'perl-MIME-Base64' + } + }, + 'Math::Random::Secure' => { + package => { + ubuntu => 'libmath-random-secure-perl', + rhel => 'perl-Math-Random-Secure' + } + }, + 'Minion' => { + package => { + ubuntu => 'libminion-perl' + } + }, + 'Minion::Backend::SQLite' => { + package => { + ubuntu => 'libminion-backend-sqlite-perl' + } + }, + 'Mojolicious' => { + minversion => '9.34', + incompatibleVersions => [ '9.43', '9.44', '9.45', '9.46' ], + package => { + ubuntu => 'libmojolicious-perl', + rhel => 'perl-Mojolicious' + } + }, + 'Mojolicious::Plugin::NotYAMLConfig' => { + package => { + ubuntu => 'libmojolicious-perl', + rhel => 'perl-Mojolicious' + } + }, + 'Mojolicious::Plugin::RenderFile' => { + package => { + ubuntu => 'libmojolicious-plugin-renderfile-perl' + } + }, + 'Net::IP' => { + package => { + ubuntu => 'libnet-ip-perl', + rhel => 'perl-Net-IP' + } + }, + 'Net::OAuth' => { + package => { + ubuntu => 'libnet-oauth-perl', + rhel => 'perl-Net-OAuth' + } + }, + 'Opcode' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Opcode' + } + }, + 'PHP::Serialization' => { + package => { + ubuntu => 'libphp-serialization-perl', + rhel => 'perl-PHP-Serialization' + } + }, + 'Pandoc' => { + package => { + ubuntu => 'libpandoc-wrapper-perl' + } + }, + 'Perl::Critic' => { + package => { + ubuntu => 'libperl-critic-perl', + rhel => 'perl-Perl-Critic' + } + }, + 'Perl::Tidy' => { + package => { + ubuntu => 'perltidy', + rhel => 'perltidy' + } + }, + 'Pod::Simple::Search' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Pod-Simple' + } + }, + 'Pod::Simple::XHTML' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Pod-Simple' + } + }, + 'Pod::Usage' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Pod-Usage' + } + }, + 'Pod::WSDL' => { + package => { + ubuntu => 'libpod-wsdl-perl' + } + }, + 'SOAP::Lite' => { + package => { + ubuntu => 'libsoap-lite-perl', + rhel => 'perl-SOAP-Lite' + } + }, + 'SQL::Abstract' => { + minversion => '2', + package => { + ubuntu => 'libsql-abstract-perl', + rhel => 'perl-SQL-Abstract' + } + }, + 'SVG' => { + package => { + ubuntu => 'libsvg-perl', + } + }, + 'Scalar::Util' => { + package => { + ubuntu => 'perl-base', + rhel => 'perl-Scalar-List-Utils' + } + }, + 'Socket' => { + package => { + ubuntu => 'perl-base', + rhel => 'perl-Socket' + } + }, + 'String::ShellQuote' => { + package => { + ubuntu => 'libstring-shellquote-perl', + rhel => 'perl-String-ShellQuote' + } + }, + 'Text::CSV' => { + package => { + ubuntu => 'libtext-csv-perl', + rhel => 'perl-Text-CSV' + } + }, + 'Text::Wrap' => { + package => { + ubuntu => 'perl-base', + rhel => 'perl-Text-Tabs+Wrap' + } + }, + 'Tie::IxHash' => { + package => { + ubuntu => 'libtie-ixhash-perl', + rhel => 'perl-Tie-IxHash' + } + }, + 'Time::HiRes' => { + package => { + ubuntu => 'perl', + rhel => 'perl-Time-HiRes' + } + }, + 'Time::Zone' => { + package => { + ubuntu => 'libtimedate-perl', + rhel => 'perl-TimeDate' + } + }, + 'Types::Serialiser' => { + package => { + ubuntu => 'libtypes-serialiser-perl', + rhel => 'perl-Types-Serialiser' + } + }, + 'URI::Escape' => { + package => { + ubuntu => 'liburi-perl', + rhel => 'perl-URI' + } + }, + 'UUID::Tiny' => { + package => { + ubuntu => 'libuuid-tiny-perl', + rhel => 'perl-UUID-Tiny' + } + }, + 'XML::LibXML' => { + package => { + ubuntu => 'libxml-libxml-perl', + rhel => 'perl-XML-LibXML' + } + }, + 'XML::Parser' => { + package => { + ubuntu => 'libxml-parser-perl', + rhel => 'perl-XML-Parser' + } + }, + 'XML::Parser::EasyTree' => { + package => { + ubuntu => 'libxml-parser-easytree-perl' + } + }, + 'XML::Writer' => { + package => { + ubuntu => 'libxml-writer-perl', + rhel => 'perl-XML-Writer' + } + }, + 'YAML::XS' => { + package => { + ubuntu => 'libyaml-libyaml-perl', + rhel => 'perl-YAML-LibYAML' + } + } ); -my %moduleVersion = ( - 'Future::AsyncAwait' => 0.52, - 'IO::Socket::SSL' => 2.007, - 'LWP::Protocol::https' => 6.06, - 'Mojolicious' => 9.34, - 'SQL::Abstract' => 2.000000 -); +my %cpanm_package = (ubuntu => 'cpanminus', rhel => 'perl-App-cpanminus'); my @programList = qw( convert @@ -141,19 +567,37 @@ =head1 DESCRIPTION dvipng ); -my ($test_modules, $test_programs, $show_help); +my ($test_modules, $test_programs, $packagetype, $show_help); GetOptions( - 'm|modules' => \$test_modules, - 'p|programs' => \$test_programs, - 'h|help' => \$show_help, + 'm|modules' => \$test_modules, + 'p|programs' => \$test_programs, + 'd|distribution=s' => \$packagetype, + 'h|help' => \$show_help, ); + pod2usage(2) if $show_help; +if ($packagetype && $packagetype ne 'rhel' && $packagetype ne 'ubuntu') { + die 'packagetype must be one of \'ubuntu\' or \'rhel\''; +} + +if (!$packagetype) { + if (determine_distribution()) { + say 'Distribution was not specified. Detected that the distribution is ' . $packagetype . ' or equivalent.'; + } else { + say 'Distribution was not specified and could not be detected. Use the -d option to specify your distribution.'; + } +} + +my %packagemgrcommand = (ubuntu => 'sudo apt install ', rhel => 'sudo dnf install '); + $test_modules = $test_programs = 1 unless $test_programs || $test_modules; my @PATH = split(/:/, $ENV{PATH}); +my (@missing_packages, @missing_modules); + check_modules() if $test_modules; say '' if $test_modules && $test_programs; check_apps() if $test_programs; @@ -166,6 +610,45 @@ sub which { return; } +sub determine_distribution { + my %os_attrs; + + #code adapted from Sys::OsRelease + if (open my $fh, "<", '/etc/os-release') { + while (my $line = <$fh>) { + chomp $line; # remove trailing nl + if (substr($line, -1, 1) eq "\r") { + $line = substr($line, 0, -1); # remove trailing cr + } + + # skip comments and blank lines + if ($line =~ /^ \s+ #/x or $line =~ /^ \s+ $/x) { + next; + } + + # read attribute assignment lines + if ($line =~ /^ ([A-Z0-9_]+) = "(.*)" $/x + or $line =~ /^ ([A-Z0-9_]+) = '(.*)' $/x + or $line =~ /^ ([A-Z0-9_]+) = (.*) $/x) + { + next if $1 eq "_config"; # don't overwrite _config + $os_attrs{ lc($1) } = $2; + } + } + close $fh; + if ($os_attrs{id} =~ 'ubuntu' || ($os_attrs{id_like} && $os_attrs{id_like} =~ 'ubuntu')) { + $packagetype = 'ubuntu'; + return 1; + } elsif ($os_attrs{id} =~ 'fedora' || ($os_attrs{id_like} && $os_attrs{id_like} =~ 'fedora')) { + $packagetype = 'rhel'; + return 1; + } + } else { + return 0; + } + +} + sub check_modules { say "Checking for modules required by WeBWorK..."; @@ -181,27 +664,54 @@ sub check_modules { my $file = ($module =~ s|::|/|gr) . '.pm'; if ($@ =~ /Can't locate $file in \@INC/) { say "** $module not found in \@INC"; + if ($packagetype) { + if ($modulesList{$module}{package}{$packagetype}) { + push(@missing_packages, $modulesList{$module}{package}{$packagetype}); + } else { + push(@missing_modules, $module); + } + } } else { say "** $module found, but failed to load: $@"; } - } elsif (defined($moduleVersion{$module}) - && version->parse(${ $module . '::VERSION' }) < version->parse($moduleVersion{$module})) + } elsif (defined($modulesList{$module}{minversion}) + && version->parse(${ $module . '::VERSION' }) < version->parse($modulesList{$module}{minversion})) + { + $moduleNotFound = 1; + say "** $module found, but not version $modulesList{$module}{minversion} or better"; + } elsif ( + ref $modulesList{$module}{incompatibleVersions} eq 'ARRAY' + && (grep { version->parse(${ $module . '::VERSION' }) == version->parse($_) } + @{ $modulesList{$module}{incompatibleVersions} }) + ) { $moduleNotFound = 1; - say "** $module found, but not version $moduleVersion{$module} or better"; + say "** $module found, but is an incompatible version (incompatible versions: " + . join(', ', @{ $modulesList{$module}{incompatibleVersions} }) . ')'; } else { say " $module found and loaded"; } use strict 'refs'; }; - for my $module (@modulesList) { + for my $module (sort keys(%modulesList)) { $checkModule->($module); } if ($moduleNotFound) { say ''; - say 'Some requred modules were not found, could not be loaded, or were not at the sufficient version.'; + say 'Some required modules were not found, could not be loaded, or were not a sufficient version.'; + if (@missing_modules || @missing_packages) { + say 'You can try to install the missing modules with the following command(s):' . "\n"; + if (@missing_modules) { + say 'sudo cpanm ' . join(' ', @missing_modules) . "\n"; + } + if (@missing_packages) { + say $packagemgrcommand{$packagetype} . join(' ', @missing_packages) . "\n"; + } + say 'If the cpanm command is not installed, you can try installing it using the following command:' . "\n"; + say $packagemgrcommand{$packagetype} . $cpanm_package{$packagetype}; + } say 'Exiting as this is required to check the database driver and programs.'; exit 0; } @@ -246,8 +756,8 @@ sub check_apps { my $node_version_str = qx/node -v/; my ($node_version) = $node_version_str =~ m/v(\d+)\./; - say "\n**The version of node should be at least 18. You have version $node_version." - if $node_version < 18; + say "\n**The version of node should be at least 22. You have version $node_version." + if $node_version < 22; return; } diff --git a/bin/delcourse b/bin/delcourse index a04871be27..65d6dec565 100755 --- a/bin/delcourse +++ b/bin/delcourse @@ -29,7 +29,7 @@ use warnings; BEGIN { use Mojo::File qw(curfile); - use Env qw(WEBWORK_ROOT); + use Env qw(WEBWORK_ROOT); $WEBWORK_ROOT = curfile->dirname->dirname; } diff --git a/bin/dev_scripts/PODtoHTML.pm b/bin/dev_scripts/PODtoHTML.pm deleted file mode 100644 index a922314011..0000000000 --- a/bin/dev_scripts/PODtoHTML.pm +++ /dev/null @@ -1,201 +0,0 @@ -package PODtoHTML; - -use strict; -use warnings; -use utf8; - -use Pod::Simple::Search; -use Mojo::Template; -use Mojo::DOM; -use Mojo::Collection qw(c); -use File::Path qw(make_path); -use File::Basename qw(dirname); -use IO::File; -use POSIX qw(strftime); - -use WeBWorK::Utils::PODParser; - -our @sections = ( - doc => 'Documentation', - bin => 'Scripts', - macros => 'Macros', - lib => 'Libraries', -); -our %macro_names = ( - answers => 'Answers', - contexts => 'Contexts', - core => 'Core', - deprecated => 'Deprecated', - graph => 'Graph', - math => 'Math', - misc => 'Miscellaneous', - parsers => 'Parsers', - ui => 'User Interface' -); - -sub new { - my ($invocant, %o) = @_; - my $class = ref $invocant || $invocant; - - my @section_list = ref($o{sections}) eq 'ARRAY' ? @{ $o{sections} } : @sections; - my $section_hash = {@section_list}; - my $section_order = [ map { $section_list[ 2 * $_ ] } 0 .. $#section_list / 2 ]; - delete $o{sections}; - - my $self = { - %o, - idx => {}, - section_hash => $section_hash, - section_order => $section_order, - macros_hash => {}, - }; - return bless $self, $class; -} - -sub convert_pods { - my $self = shift; - my $source_root = $self->{source_root}; - my $dest_root = $self->{dest_root}; - - my $regex = join('|', map {"^$_"} @{ $self->{section_order} }); - - my ($name2path, $path2name) = Pod::Simple::Search->new->inc(0)->limit_re(qr!$regex!)->survey($self->{source_root}); - for (keys %$path2name) { - print "Processing file: $_\n" if $self->{verbose} > 1; - $self->process_pod($_, $name2path); - } - - $self->write_index("$dest_root/index.html"); - - return; -} - -sub process_pod { - my ($self, $pod_path, $pod_files) = @_; - - my $pod_name; - - my ($subdir, $filename) = $pod_path =~ m|^$self->{source_root}/(?:(.*)/)?(.*)$|; - - my ($subdir_first, $subdir_rest) = ('', ''); - - if (defined $subdir) { - if ($subdir =~ m|/|) { - ($subdir_first, $subdir_rest) = $subdir =~ m|^([^/]*)/(.*)|; - } else { - $subdir_first = $subdir; - } - } - - $pod_name = (defined $subdir_rest ? "$subdir_rest/" : '') . $filename; - if ($filename =~ /\.pl$/) { - $filename =~ s/\.pl$/.html/; - } elsif ($filename =~ /\.pod$/) { - $pod_name =~ s/\.pod$//; - $filename =~ s/\.pod$/.html/; - } elsif ($filename =~ /\.pm$/) { - $pod_name =~ s/\.pm$//; - $pod_name =~ s|/+|::|g; - $filename =~ s/\.pm$/.html/; - } elsif ($filename !~ /\.html$/) { - $filename .= '.html'; - } - - $pod_name =~ s/^(\/|::)//; - - my $html_dir = $self->{dest_root} . (defined $subdir ? "/$subdir" : ''); - my $html_path = "$html_dir/$filename"; - my $html_rel_path = defined $subdir ? "$subdir/$filename" : $filename; - - $self->update_index($subdir, $html_rel_path, $pod_name); - make_path($html_dir); - my $html = $self->do_pod2html( - pod_path => $pod_path, - pod_name => $pod_name, - pod_files => $pod_files - ); - my $fh = IO::File->new($html_path, '>:encoding(UTF-8)') - or die "Failed to open file '$html_path' for writing: $!\n"; - print $fh $html; - - return; -} - -sub update_index { - my ($self, $subdir, $html_rel_path, $pod_name) = @_; - - $subdir =~ s|/.*$||; - my $idx = $self->{idx}; - my $sections = $self->{section_hash}; - if ($subdir eq 'macros') { - $idx->{macros} = []; - if ($pod_name =~ m!^(.+)/(.+)$!) { - push @{ $self->{macros_hash}{$1} }, [ $html_rel_path, $2 ]; - } else { - push @{ $idx->{doc} }, [ $html_rel_path, $pod_name ]; - } - } elsif (exists $sections->{$subdir}) { - push @{ $idx->{$subdir} }, [ $html_rel_path, $pod_name ]; - } else { - warn "no section for subdir '$subdir'\n"; - } - - return; -} - -sub write_index { - my ($self, $out_path) = @_; - - my $fh = IO::File->new($out_path, '>:encoding(UTF-8)') or die "Failed to open index '$out_path' for writing: $!\n"; - print $fh Mojo::Template->new(vars => 1)->render_file( - "$self->{template_dir}/category-index.mt", - { - title => 'POD for ' . ($self->{source_root} =~ s|^.*/||r), - base_url => $self->{dest_url}, - pod_index => $self->{idx}, - sections => $self->{section_hash}, - section_order => $self->{section_order}, - macros => $self->{macros_hash}, - macros_order => [ sort keys %{ $self->{macros_hash} } ], - macro_names => \%macro_names, - date => strftime('%a %b %e %H:%M:%S %Z %Y', localtime) - } - ); - - return; -} - -sub do_pod2html { - my ($self, %o) = @_; - - my $psx = WeBWorK::Utils::PODParser->new($o{pod_files}); - $psx->{source_root} = $self->{source_root}; - $psx->{verbose} = $self->{verbose}; - $psx->{assert_html_ext} = 1; - $psx->{base_url} = ($self->{dest_url} // '') . '/' . (($self->{source_root} // '') =~ s|^.*/||r); - $psx->output_string(\my $html); - $psx->html_header(''); - $psx->html_footer(''); - $psx->parse_file($o{pod_path}); - - my $dom = Mojo::DOM->new($html); - my $podIndexUL = $dom->at('ul[id="index"]'); - my $podIndex = $podIndexUL ? $podIndexUL->find('ul[id="index"] > li') : c(); - for (@$podIndex) { - $_->attr({ class => 'nav-item' }); - $_->at('a')->attr({ class => 'nav-link p-0' }); - for (@{ $_->find('ul') }) { - $_->attr({ class => 'nav flex-column w-100' }); - } - for (@{ $_->find('li') }) { - $_->attr({ class => 'nav-item' }); - $_->at('a')->attr({ class => 'nav-link p-0' }); - } - } - my $podHTML = $podIndexUL ? $podIndexUL->remove : $html; - - return Mojo::Template->new(vars => 1)->render_file("$self->{template_dir}/pod.mt", - { title => $o{pod_name}, base_url => dirname($psx->{base_url}), index => $podIndex, content => $podHTML }); -} - -1; diff --git a/bin/dev_scripts/generate-ww-pg-pod.pl b/bin/dev_scripts/generate-ww-pg-pod.pl index f1591ae485..d20902a171 100755 --- a/bin/dev_scripts/generate-ww-pg-pod.pl +++ b/bin/dev_scripts/generate-ww-pg-pod.pl @@ -9,17 +9,16 @@ =head1 SYNOPSIS generate-ww-pg-pod.pl [options] Options: - -p|--pg-root Directory containing a git clone of pg. - If this option is not set, then the environment - variable $PG_ROOT will be used if it is set. -o|--output-dir Directory to save the output files to. (required) -b|--base-url Base url location used on server. (default: /) This is needed for internal POD links to work correctly. -v|--verbose Increase the verbosity of the output. (Use multiple times for more verbosity.) -Note that --pg-root must be provided or the PG_ROOT environment variable set -if the POD for pg is desired. +Note that C must be set in the C file, or +if that file does not exist then the clone of the PG repository must be located +at C as defined in the C +file. =head1 DESCRIPTION @@ -30,37 +29,47 @@ =head1 DESCRIPTION use strict; use warnings; +my ($webwork_root, $pg_root); + +BEGIN { + use File::Basename qw(dirname); + use Cwd qw(abs_path); + use YAML::XS qw(LoadFile); + + $webwork_root = abs_path(dirname(dirname(dirname(__FILE__)))); + + # Load the configuration file to obtain the PG root directory. + my $config_file = "$webwork_root/conf/webwork2.mojolicious.yml"; + $config_file = "$webwork_root/conf/webwork2.mojolicious.dist.yml" unless -e $config_file; + my $config = LoadFile($config_file); + + $pg_root = $config->{pg_dir}; +} + use Getopt::Long qw(:config bundling); use Pod::Usage; -my ($pg_root, $output_dir, $base_url); +my ($output_dir, $base_url); my $verbose = 0; GetOptions( - 'p|pg-root=s' => \$pg_root, 'o|output-dir=s' => \$output_dir, 'b|base-url=s' => \$base_url, 'v|verbose+' => \$verbose ); -$pg_root = $ENV{PG_ROOT} if !$pg_root; - -pod2usage(2) unless $output_dir; +pod2usage(2) unless $output_dir && $pg_root && -d $pg_root; $base_url = "/" if !$base_url; use Mojo::Template; use IO::File; use File::Copy; -use File::Path qw(make_path remove_tree); -use File::Basename qw(dirname); -use Cwd qw(abs_path); - -use lib dirname(dirname(dirname(__FILE__))) . '/lib'; -use lib dirname(__FILE__); +use File::Path qw(make_path remove_tree); -use PODtoHTML; +use lib "$webwork_root/lib"; +use lib "$pg_root/lib"; -my $webwork_root = abs_path(dirname(dirname(dirname(__FILE__)))); +use WeBWorK::Utils::PODtoHTML; for my $dir ($webwork_root, $pg_root) { next unless $dir && -d $dir; @@ -73,10 +82,10 @@ =head1 DESCRIPTION write_index($index_fh); make_path("$output_dir/assets"); -copy("$webwork_root/htdocs/js/PODViewer/podviewer.css", "$output_dir/assets/podviewer.css"); -print "copying $webwork_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.css\n" if $verbose; -copy("$webwork_root/htdocs/js/PODViewer/podviewer.js", "$output_dir/assets/podviewer.js"); -print "copying $webwork_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.js\n" if $verbose; +copy("$pg_root/htdocs/js/PODViewer/podviewer.css", "$output_dir/assets/podviewer.css"); +print "copying $pg_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.css\n" if $verbose; +copy("$pg_root/htdocs/js/PODViewer/podviewer.js", "$output_dir/assets/podviewer.js"); +print "copying $pg_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.js\n" if $verbose; sub process_dir { my $source_dir = shift; @@ -89,12 +98,15 @@ sub process_dir { remove_tree($dest_dir); make_path($dest_dir); - my $htmldocs = PODtoHTML->new( - source_root => $source_dir, - dest_root => $dest_dir, - template_dir => "$webwork_root/bin/dev_scripts/pod-templates", - dest_url => $base_url, - verbose => $verbose + my $htmldocs = WeBWorK::Utils::PODtoHTML->new( + source_root => $source_dir, + dest_root => $dest_dir, + template_dir => "$pg_root/assets/pod-templates", + dest_url => $base_url, + home_url => $base_url, + home_url_link_name => 'WeBWorK POD Home', + page_url => $base_url . ($source_dir =~ s|^.*/||r), + verbose => $verbose ); $htmldocs->convert_pods; diff --git a/bin/dev_scripts/pod-templates/category-index.mt b/bin/dev_scripts/pod-templates/category-index.mt deleted file mode 100644 index a5d980d599..0000000000 --- a/bin/dev_scripts/pod-templates/category-index.mt +++ /dev/null @@ -1,95 +0,0 @@ - - -% - - - - <%= $title %> - - - - - - -% - - - - <%= $title %> - - - - - - % - % my ($index, $macro_index, $content, $macro_content) = ('', '', '', ''); - % for my $macro (@$macros_order) { - % my $new_index = begin - <%= $macro_names->{$macro} // $macro %> - % end - % $macro_index .= $new_index->(); - % my $new_content = begin - <%= $macro_names->{$macro} // $macro %> - - % for my $file (sort { $a->[1] cmp $b->[1] } @{ $macros->{$macro} }) { - <%= $file->[1] %> - % } - - % end - % $macro_content .= $new_content->(); - % } - % for my $section (@$section_order) { - % next unless defined $pod_index->{$section}; - % my $new_index = begin - <%= $sections->{$section} %> - % if ($section eq 'macros') { - - <%= $macro_index %> - - % } - % end - % $index .= $new_index->(); - % my $new_content = begin - <%= $sections->{$section} %> - - % if ($section eq 'macros') { - <%= $macro_content =%> - % } else { - % for my $file (sort { $a->[1] cmp $b->[1] } @{ $pod_index->{$section} }) { - - <%= $file->[1] %> - - % } - % } - - % end - % $content .= $new_content->(); - % } - % - - - - <%= $content =%> - Generated <%= $date %> - - - -% - diff --git a/bin/dev_scripts/pod-templates/main-index.mt b/bin/dev_scripts/pod-templates/main-index.mt index d9bfe39e8d..6a6d2f1e7f 100644 --- a/bin/dev_scripts/pod-templates/main-index.mt +++ b/bin/dev_scripts/pod-templates/main-index.mt @@ -4,19 +4,18 @@ - - + WeBWorK/PG POD % - + WeBWorK/PG POD - - + + (Plain Old Documentation) @@ -32,7 +31,7 @@ % } - + %
Generated <%= $date %>