diff --git a/bin/check_modules.pl b/bin/check_modules.pl index ee8f919a35..02ae0b5b2c 100755 --- a/bin/check_modules.pl +++ b/bin/check_modules.pl @@ -44,6 +44,7 @@ Dancer Dancer::Plugin::Database Data::Dumper + Data::Compare Data::UUID Date::Format Date::Parse diff --git a/webwork3/bin/app.pl b/webwork3/bin/app.pl index 517d540189..550b4778d5 100755 --- a/webwork3/bin/app.pl +++ b/webwork3/bin/app.pl @@ -1,151 +1,159 @@ #!/usr/bin/env perl -use Dancer; -use Dancer::Plugin::Database; BEGIN {$ENV{MOD_PERL_API_VERSION}=2} -# link to WeBWorK code libraries -use lib config->{webwork_dir}.'/lib'; -use lib config->{pg_dir}.'/lib'; - -use WeBWorK::CourseEnvironment; -use WeBWorK::DB; -use WeBWorK::Authen; - -## note: Routes::Authenication must be passed first -use Routes::Authentication qw/buildSession setCourseEnvironment setCookie/; -use Routes::Course; -use Routes::Library; -use Routes::ProblemSets; -use Routes::User; -use Routes::Settings; -use Routes::PastAnswers; - -set serializer => 'JSON'; - -hook 'before' => sub { - - # for my $key (keys(%{request->params})){ - # my $value = defined(params->{$key}) ? params->{$key} : ''; - # debug($key . " : " . $value); - # } - -}; - -## right now, this is to help handshaking between the original webservice and dancer. -## it does nothing except sets the session using the hook 'before' above. - -post '/handshake' => sub { - - - debug "in /handshake"; - - setCourseEnvironment(params->{course_id}); - - debug session; - buildSession(); - if (! session 'logged_in'){ - send_error('You are no longer logged in. You may need to reauthenticate.',419); - } - - return {msg => "If you get this message the handshaking between Dancer and WW2 worked."}; -}; - - -post '/courses/:course_id/login' => sub { - - my $authen = new WeBWorK::Authen(vars->{ce}); - $authen->set_params({ - user => params->{user}, - password => params->{password}, - key => params->{session_key} - }); - - my $result = $authen->verify(); - - if($result){ - my $key = $authen->create_session(params->{user}); - - session user => params->{user}; - session key => $key; - - my $permission = vars->{db}->getPermissionLevel(session->{user}); - session permission => $permission->{permission}; - session timestamp => time(); - - setCookie(); - - return {session_key=>$key, user=>params->{user},logged_in=>1}; - - } else { - return {logged_in=>0}; - } -}; - - -post '/courses/:course_id/logout' => sub { - - my $deleteKey = vars->{db}->deleteKey(session 'user'); - my $sessionDestroy = session->destroy; - - my $hostname = vars->{ce}->{server_root_url}; - $hostname =~ s/https?:\/\///; - - if ($hostname ne "localhost" && $hostname ne "127.0.0.1") { - cookie "WeBWorK.CourseAuthen." . params->{course_id} => "", domain=>$hostname, expires => "-1 hour"; - } else { - cookie "WeBWorK.CourseAuthen." . params->{course_id} => "", expires => "-1 hour"; - } - - return {logged_in=>0}; -}; - - -get '/app-info' => sub { - return { - environment=>config->{environment}, - port=>config->{port}, - content_type=>config->{content_type}, - startup_info=>config->{startup_info}, - server=>config->{server}, - appdir=>config->{appdir}, - template=>config->{template}, - logger=>config->{logger}, - session=>config->{session}, - session_expires=>config->{session_expires}, - session_name=>config->{session_name}, - session_secure=>config->{session_secure}, - session_is_http_only=>config->{session_is_http_only}, - }; -}; - -get '/courses/:course_id/info' => sub { - - setCourseEnvironment(params->{course_id}); - - return { - course_id => params->{course_id}, - webwork_dir => vars->{ce}->{webwork_dir}, - webworkURLs => vars->{ce}->{webworkURLs}, - webworkDirs => vars->{ce}->{webworkDirs} - }; - -}; - - -sub checkCourse { - if (! defined(session->{course})) { - if (defined(params->{course_id})) { - session->{course} = params->{course_id}; - } else { - send_error("The course has not been defined. You may need to authenticate again",401); - } - - } - - var ce => WeBWorK::CourseEnvironment->new({webwork_dir => config->{webwork_dir}, courseName=> session->{course}}); - -} - - -Dancer->dance; +use Dancer; +use WeBWorK3; + + + +#package WeBWorK3; +#use Dancer; +#use Dancer::Plugin::Database; +# +## link to WeBWorK code libraries +#use lib config->{webwork_dir}.'/lib'; +#use lib config->{pg_dir}.'/lib'; +# +#use WeBWorK::CourseEnvironment; +#use WeBWorK::DB; +#use WeBWorK::Authen; +# +### note: Routes::Authenication must be passed first +#use Routes::Authentication qw/buildSession setCourseEnvironment setCookie/; +#use Routes::Course; +#use Routes::Library; +#use Routes::ProblemSets; +#use Routes::User; +#use Routes::Settings; +#use Routes::PastAnswers; +# +#set serializer => 'JSON'; +# +#hook 'before' => sub { +# +# # for my $key (keys(%{request->params})){ +# # my $value = defined(params->{$key}) ? params->{$key} : ''; +# # debug($key . " : " . $value); +# # } +# +#}; +# +### right now, this is to help handshaking between the original webservice and dancer. +### it does nothing except sets the session using the hook 'before' above. +# +#post '/handshake' => sub { +# +# +# debug "in /handshake"; +# +# setCourseEnvironment(params->{course_id}); +# +# debug session; +# buildSession(); +# if (! session 'logged_in'){ +# send_error('You are no longer logged in. You may need to reauthenticate.',419); +# } +# +# return {msg => "If you get this message the handshaking between Dancer and WW2 worked."}; +#}; +# +# +#post '/courses/:course_id/login' => sub { +# +# my $authen = new WeBWorK::Authen(vars->{ce}); +# $authen->set_params({ +# user => params->{user}, +# password => params->{password}, +# key => params->{session_key} +# }); +# +# my $result = $authen->verify(); +# +# if($result){ +# my $key = $authen->create_session(params->{user}); +# +# session user => params->{user}; +# session key => $key; +# +# my $permission = vars->{db}->getPermissionLevel(session->{user}); +# session permission => $permission->{permission}; +# session timestamp => time(); +# +# setCookie(); +# +# return {session_key=>$key, user=>params->{user},logged_in=>1}; +# +# } else { +# return {logged_in=>0}; +# } +#}; +# +# +#post '/courses/:course_id/logout' => sub { +# +# my $deleteKey = vars->{db}->deleteKey(session 'user'); +# my $sessionDestroy = session->destroy; +# +# my $hostname = vars->{ce}->{server_root_url}; +# $hostname =~ s/https?:\/\///; +# +# if ($hostname ne "localhost" && $hostname ne "127.0.0.1") { +# cookie "WeBWorKCourseAuthen." . params->{course_id} => "", domain=>$hostname, expires => "-1 hour"; +# } else { +# cookie "WeBWorKCourseAuthen." . params->{course_id} => "", expires => "-1 hour"; +# } +# +# return {logged_in=>0}; +#}; +# +# +#get '/app-info' => sub { +# return { +# appname => config->{appname}, +# environment=>config->{environment}, +# port=>config->{port}, +# content_type=>config->{content_type}, +# startup_info=>config->{startup_info}, +# server=>config->{server}, +# appdir=>config->{appdir}, +# template=>config->{template}, +# logger=>config->{logger}, +# session=>config->{session}, +# session_expires=>config->{session_expires}, +# session_name=>config->{session_name}, +# session_secure=>config->{session_secure}, +# session_is_http_only=>config->{session_is_http_only}, +# }; +#}; +# +#get '/courses/:course_id/info' => sub { +# +# setCourseEnvironment(params->{course_id}); +# +# return { +# course_id => params->{course_id}, +# webwork_dir => vars->{ce}->{webwork_dir}, +# webworkURLs => vars->{ce}->{webworkURLs}, +# webworkDirs => vars->{ce}->{webworkDirs} +# }; +# +#}; +# +# +#sub checkCourse { +# if (! defined(session->{course})) { +# if (defined(params->{course_id})) { +# session->{course} = params->{course_id}; +# } else { +# send_error("The course has not been defined. You may need to authenticate again",401); +# } +# +# } +# +# var ce => WeBWorK::CourseEnvironment->new({webwork_dir => config->{webwork_dir}, courseName=> session->{course}}); +# +#} + + +dance; diff --git a/webwork3/lib/Routes/Course.pm b/webwork3/lib/Routes/Course.pm index 42b73fd483..b2f06f1b84 100644 --- a/webwork3/lib/Routes/Course.pm +++ b/webwork3/lib/Routes/Course.pm @@ -4,24 +4,22 @@ # ## -package Routes::Course; +#package Routes::Course; -use strict; -use warnings; -use Dancer ':syntax'; -use Dancer::Plugin::Ajax; +#use strict; +#use warnings; +#use Dancer ':syntax'; +##use Dancer::Plugin::Ajax; use Dancer::FileUtils qw /read_file_content path/; use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash/; use WeBWorK::Utils::CourseManagement qw(listCourses listArchivedCourses addCourse deleteCourse renameCourse); use WeBWorK::Utils::CourseIntegrityCheck qw(checkCourseTables); use Utils::CourseUtils qw/getAllUsers getCourseSettings getAllSets/; -# use Utils::CourseUtils qw/getCourseSettings/; -use Routes::Authentication qw/buildSession checkPermissions setCookie/; -use Data::Dumper; +use Utils::Authentication qw/buildSession checkPermissions setCookie setCourseEnvironment/; -our $PERMISSION_ERROR = "You don't have the necessary permissions."; +our $PERMISSION_ERROR = "You don't have the necessary permissions."; ### # @@ -34,6 +32,9 @@ our $PERMISSION_ERROR = "You don't have the necessary permissions."; get '/courses' => sub { + #debug 'in GET /courses/'; + + setCourseEnvironment(""); my @courses = listCourses(vars->{ce}); return \@courses; @@ -57,12 +58,15 @@ get '/courses/:course_id' => sub { # template 'course_home.tt', {course_id=>params->{course_id}}; if(request->is_ajax){ - + + setCourseEnvironment(params->{course_id}); my $ce2 = new WeBWorK::CourseEnvironment({ webwork_dir => vars->{ce}->{webwork_dir}, courseName => params->{course_id}, }); + + my $coursePath = vars->{ce}->{webworkDirs}->{courses} . "/" . params->{course_id}; @@ -75,14 +79,12 @@ get '/courses/:course_id' => sub { my $CIchecker = new WeBWorK::Utils::CourseIntegrityCheck(ce=>$ce2); if (params->{checkCourseTables}){ ($tables_ok,$dbStatus) = $CIchecker->checkCourseTables(params->{course_id}); - } + return { coursePath => $coursePath, tables_ok => $tables_ok, dbStatus => $dbStatus, + message => "Course exists."}; + } else { + return {course_id => params->{course_id}, message=> "Course exists."}; + } - return { - coursePath => $coursePath, - tables_ok => $tables_ok, - dbStatus => $dbStatus - - }; } else { my $session = {}; @@ -111,19 +113,23 @@ get '/courses/:course_id' => sub { post '/courses/:new_course_id' => sub { - checkPermissions(15,session->{user}); - + setCourseEnvironment("admin"); # this will make sure that the user is associated with the admin course. + checkPermissions(10,session->{user}); ## maybe this should be at 15? But is admin=15? + + + my $coursesDir = vars->{ce}->{webworkDirs}->{courses}; my $courseDir = "$coursesDir/" . params->{new_course_id}; ## This is a hack to get a new CourseEnviromnet. Use of %WeBWorK::SeedCE doesn't work. my $ce2 = new WeBWorK::CourseEnvironment({ - webwork_dir => vars->{ce}->{webwork_dir}, - courseName => params->{course_id}, + webwork_dir => vars->{ce}->{webwork_dir}, + courseName => params->{new_course_id}, }); - - + + + # return an error if the course already exists if (-e $courseDir) { @@ -135,15 +141,6 @@ post '/courses/:new_course_id' => sub { my $dbLayoutName = $ce2->{dbLayoutName}; my $db2 = new WeBWorK::DB($ce2->{dbLayouts}->{$dbLayoutName}); - # for my $table (keys %$db2) - # { - # my $tableName = $db2->{$table}; - # my $database_table_exists = ($db2->{$table}->tableExists) ? 1:0; - # debug "$table : $database_table_exists \n"; - # } - - ## what's a good way to tell if the database already exists? - my $userTableExists = ($db2->{user}->tableExists) ? 1: 0; if ($userTableExists){ @@ -151,8 +148,6 @@ post '/courses/:new_course_id' => sub { } - - # fail if the course ID contains invalid characters send_error("Invalid characters in course ID: " . params->{new_course_id} . " (valid characters are [-A-Za-z0-9_])",424) @@ -186,6 +181,7 @@ post '/courses/:new_course_id' => sub { user_id => params->{new_userID}, permission => "10", ); + push @users, [ $User, $Password, $PermissionLevel ]; my %courseOptions = ( dbLayoutName => "sql_single" ); @@ -194,9 +190,9 @@ post '/courses/:new_course_id' => sub { dbOptions=> params->{db_options}, users=>\@users}; - my $addCourse = addCourse(%{$options}); + addCourse(%{$options}); - return $addCourse; + return {courseID => params->{new_course_id}, message => "Course created successfully."}; }; @@ -206,21 +202,17 @@ post '/courses/:new_course_id' => sub { put '/courses/:course_id' => sub { - checkPermissions(15,session->{user}); +setCourseEnvironment("admin"); + checkPermissions(10,session->{user}); ## This is a hack to get a new CourseEnviromnet. Use of %WeBWorK::SeedCE doesn't work. - my $ce2 = new WeBWorK::CourseEnvironment({ - webwork_url => vars->{ce}->{webwork_url}, - webwork_dir => vars->{ce}->{webwork_dir}, - pg_dir => vars->{ce}->{pg_dir}, - webwork_htdocs_url => vars->{ce}->{webwork_htdocs_url}, - webwork_htdocs_dir => vars->{ce}->{webwork_htdocs_dir}, - webwork_courses_url => vars->{ce}->{webwork_courses_url}, - webwork_courses_dir => vars->{ce}->{webwork_courses_dir}, - courseName => params->{new_course_id}, + my $ce2 = new WeBWorK::CourseEnvironment({ + webwork_dir => vars->{ce}->{webwork_dir}, + courseName => params->{course_id}, }); + my %courseOptions = ( dbLayoutName => "sql_single" ); @@ -239,32 +231,25 @@ put '/courses/:course_id' => sub { del '/courses/:course_id' => sub { - checkPermissions(15,session->{user}); - - # my $coursesDir = vars->{ce}->{webworkDirs}->{courses}; - # my $courseDir = "$coursesDir/" . params->{course_id}; + setCourseEnvironment("admin"); + checkPermissions(10,session->{user}); ## This is a hack to get a new CourseEnviromnet. Use of %WeBWorK::SeedCE doesn't work. - my $ce2 = new WeBWorK::CourseEnvironment({ - webwork_url => vars->{ce}->{webwork_url}, - webwork_dir => vars->{ce}->{webwork_dir}, - pg_dir => vars->{ce}->{pg_dir}, - webwork_htdocs_url => vars->{ce}->{webwork_htdocs_url}, - webwork_htdocs_dir => vars->{ce}->{webwork_htdocs_dir}, - webwork_courses_url => vars->{ce}->{webwork_courses_url}, - webwork_courses_dir => vars->{ce}->{webwork_courses_dir}, + my $ce2 = new WeBWorK::CourseEnvironment({ + webwork_dir => vars->{ce}->{webwork_dir}, courseName => params->{course_id}, }); + my %courseOptions = ( dbLayoutName => "sql_single" ); my $options = { courseID => params->{course_id}, ce=>$ce2, courseOptions=>\%courseOptions, dbOptions=> params->{db_options}}; - my $delCourse = deleteCourse(%{$options}); + deleteCourse(%{$options}); - return $delCourse; + return {course_id => params->{course_id}, message => "Course deleted."}; }; @@ -286,6 +271,7 @@ post '/courses/:course_id/session' => sub { get '/courses/:course_id/manager' => sub { + # read the course manager configuration file to set up the main and side panes my $configFilePath = path(config->{webwork_dir},"webwork3","public","js","apps","CourseManager","config.json"); @@ -381,9 +367,9 @@ get '/courses/:course_id/manager' => sub { buildSession($userID,$sessKey); if(session 'logged_in'){ - $settings = getCourseSettings(); - $sets = getAllSets(); - $users = getAllUsers(); + $settings = getCourseSettings(vars->{ce}); + $sets = getAllSets(vars->{db},vars->{ce}); + $users = getAllUsers(vars->{db},vars->{ce}); } else { session->destroy(); } diff --git a/webwork3/lib/Routes/Library.pm b/webwork3/lib/Routes/Library.pm index 58a5d10a14..e3269626e0 100644 --- a/webwork3/lib/Routes/Library.pm +++ b/webwork3/lib/Routes/Library.pm @@ -4,29 +4,23 @@ # ## -package Routes::Library; +#package Routes::Library; -use strict; -use warnings; -use Dancer ':syntax'; +#use strict; +#use warnings; +#use Dancer ':syntax'; use Dancer::Plugin::Database; use Path::Class; use File::Find::Rule; use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash/; use Utils::LibraryUtils qw/list_pg_files searchLibrary getProblemTags render/; use Utils::ProblemSets qw/record_results/; -use Routes::Authentication qw/checkPermissions setCourseEnvironment/; +use Utils::Authentication qw/checkPermissions setCourseEnvironment/; use WeBWorK::DB::Utils qw(global2user); use WeBWorK::Utils::Tasks qw(fake_user fake_set fake_problem); use WeBWorK::PG::Local; use WeBWorK::Constants; -# use constant MY_PROBLEMS => ' My Problems '; -# use constant MAIN_PROBLEMS => ' Unclassified Problems '; -# use constant fakeSetName => "Undefined_Set"; -# use constant fakeUserName => "Undefined_User"; - - get '/Library/subjects' => sub { my $webwork_dir = config->{webwork_dir}; @@ -400,9 +394,8 @@ any ['get', 'post'] => '/renderer/courses/:course_id/problems/:problem_id' => su setCourseEnvironment(params->{course_id}); my $renderParams = {}; - - - $renderParams->{displayMode} = param('displayMode') || vars->{ce}->{pg}{options}{displayMode}; + + $renderParams->{displayMode} = params->{displayMode} || vars->{ce}->{pg}{options}{displayMode}; $renderParams->{problemSeed} = defined(params->{problemSeed}) ? params->{problemSeed} : 1; $renderParams->{showHints} = 0; $renderParams->{showSolutions} = 0; @@ -429,13 +422,13 @@ any ['get', 'post'] => '/renderer/courses/:course_id/problems/:problem_id' => su $renderParams->{problem}->{source_file} = "Library/" . $path_header . "/" . $problem_info->{filename}; } - return render($renderParams); + return render(vars->{ce},$renderParams); }; ### # -# Problem render. Given information about the problem (problem_id, set_id, course_id, or path) return the +# Problem render for a UserProblem. Given information about the problem (problem_id, set_id, course_id, or path) return the # HTML for the problem. # # The displayMode parameter will determine the exact HTML code that is returned (images, MathJax, plain, PDF) @@ -444,13 +437,16 @@ any ['get', 'post'] => '/renderer/courses/:course_id/problems/:problem_id' => su # ### -any ['get', 'post'] => '/renderer/courses/:course_id/sets/:set_id/problems/:problem_id' => sub { +any ['get', 'post'] => '/renderer/courses/:course_id/users/:user_id/sets/:set_id/problems/:problem_id' => sub { send_error("The set " . params->{set_id} . " does not exist.",404) unless vars->{db}->existsGlobalSet(params->{set_id}); send_error("The problem with id " . params->{problem_id} . " does not exist in set " . params->{set_id},404) unless vars->{db}->existsGlobalProblem(params->{set_id},params->{problem_id}); + send_error("The user " . params->{user_id} . " is not assigned to the set " . params->{set_id} . ".") + unless vars->{db}->existsUserProblem(params->{user_id},params->{set_id},params->{problem_id}); + my $renderParams = {}; @@ -471,15 +467,14 @@ any ['get', 'post'] => '/renderer/courses/:course_id/sets/:set_id/problems/:prob $renderParams->{showAnswers} = 0; } else { - $renderParams->{showHints} = defined(param('show_hints'))? param('show_hints') : 0; - $renderParams->{showSolutions} = defined(param('show_solutions'))? param('show_solutions') : 0; - $renderParams->{showAnswers} = defined(param('show_answers'))? param('show_answers') : 0; + $renderParams->{showHints} = defined(param('show_hints'))? int(param('show_hints')) : 0; + $renderParams->{showSolutions} = defined(param('show_solutions'))? int(param('show_solutions')) : 0; + $renderParams->{showAnswers} = defined(param('show_answers'))? int(param('show_answers')) : 0; } - $renderParams->{problem} = vars->{db}->getMergedProblem(params->{effectiveUser}|| session->{user}, - params->{set_id},params->{problem_id}); - $renderParams->{user} = vars->{db}->getUser(params->{effectiveUser}|| session->{user}); - $renderParams->{set} = vars->{db}->getUserSet(params->{effectiveUser}|| session->{user},params->{set_id}); + $renderParams->{problem} = vars->{db}->getMergedProblem(params->{user_id},params->{set_id},params->{problem_id}); + $renderParams->{user} = vars->{db}->getUser(params->{user_id}); + $renderParams->{set} = vars->{db}->getMergedSet(params->{user_id},params->{set_id}); my $results = render($renderParams); diff --git a/webwork3/lib/Routes/PastAnswers.pm b/webwork3/lib/Routes/PastAnswers.pm index 25d5e1674a..a11ae9d099 100644 --- a/webwork3/lib/Routes/PastAnswers.pm +++ b/webwork3/lib/Routes/PastAnswers.pm @@ -10,7 +10,7 @@ use strict; use warnings; use Dancer ':syntax'; use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash/; -use Routes::Authentication qw/checkPermissions/; +use Utils::Authentication qw/checkPermissions/; our $PERMISSION_ERROR = "You don't have the necessary permissions."; diff --git a/webwork3/lib/Routes/ProblemSets.pm b/webwork3/lib/Routes/ProblemSets.pm index 331bf2a8c2..0616f60b90 100644 --- a/webwork3/lib/Routes/ProblemSets.pm +++ b/webwork3/lib/Routes/ProblemSets.pm @@ -4,25 +4,31 @@ # ## -package ProblemSets; +package Routes::ProblemSets; use strict; use warnings; use Dancer ':syntax'; +use Dancer::FileUtils qw/read_file_content dirname path/; +use File::Slurp qw/write_file/; +use WeBWorK::Utils::Tasks qw(fake_user fake_set fake_problem); +use Utils::LibraryUtils qw/render/; use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash convertBooleans/; -use Utils::ProblemSets qw/reorderProblems addGlobalProblems addUserSet addUserProblems deleteProblems createNewUserProblem/; +use Utils::ProblemSets qw/reorderProblems addGlobalProblems addUserSet addUserProblems deleteProblems createNewUserProblem + renumber_problems updateProblems getGlobalSet putGlobalSet putUserSet getUserSet + putUserProblem + @time_props @set_props @boolean_set_props @user_set_props @problem_props/; use WeBWorK::Utils qw/parseDateTime decodeAnswers/; -use Array::Utils qw(array_minus); -use Routes::Authentication qw/checkPermissions setCourseEnvironment/; +use Array::Utils qw/array_minus/; +use List::MoreUtils qw/first_value/; +use Utils::Authentication qw/checkPermissions setCourseEnvironment/; use Utils::CourseUtils qw/getCourseSettings/; use Dancer::Plugin::Database; use Dancer::Plugin::Ajax; use List::Util qw/first max/; -our @set_props = qw/set_id set_header hardcopy_header open_date reduced_scoring_date due_date answer_date visible enable_reduced_scoring assignment_type attempts_per_version time_interval versions_per_interval version_time_limit version_creation_time version_last_attempt_time problem_randorder hide_score hide_score_by_problem hide_work time_limit_cap restrict_ip relax_restrict_ip restricted_login_proctor/; -our @user_set_props = qw/user_id set_id psvn set_header hardcopy_header open_date reduced_scoring_date due_date answer_date visible enable_reduced_scoring assignment_type description restricted_release restricted_status attempts_per_version time_interval versions_per_interval version_time_limit version_creation_time problem_randorder version_last_attempt_time problems_per_page hide_score hide_score_by_problem hide_work time_limit_cap restrict_ip relax_restrict_ip restricted_login_proctor hide_hint/; -our @problem_props = qw/problem_id flags value max_attempts source_file/; -our @boolean_set_props = qw/visible enable_reduced_scoring/; + + ### # return all problem sets (as objects) for course *course_id* @@ -34,20 +40,10 @@ our @boolean_set_props = qw/visible enable_reduced_scoring/; get '/courses/:course_id/sets' => sub { checkPermissions(10,session->{user}); - - my @globalSetNames = vars->{db}->listGlobalSets; - my @globalSets = vars->{db}->getGlobalSets(@globalSetNames); - - for my $set (@globalSets){ - my @globalProblems = vars->{db}->getAllGlobalProblems($set->{set_id}); - $set->{problems} = convertArrayOfObjectsToHash(\@globalProblems); - my @userNames = vars->{db}->listSetUsers($set->{set_id}); - $set->{assigned_users} = \@userNames; - $set->{_id} = $set->{set_id}; - } + my @allGlobalSets = map { getGlobalSet(vars->{db},vars->{ce},$_)} @globalSetNames; - return convertArrayOfObjectsToHash(\@globalSets,\@boolean_set_props); + return convertArrayOfObjectsToHash(\@allGlobalSets,\@boolean_set_props); }; @@ -62,20 +58,13 @@ get '/courses/:course_id/sets/:set_id' => sub { checkPermissions(10,session->{user}); - my $globalSet = vars->{db}->getGlobalSet(param('set_id')); - my @userNamesFromDB = vars->{db}->listSetUsers(params->{set_id}); - my @problemsFromDB = vars->{db}->getAllGlobalProblems(params->{set_id}); - - my $setResults = convertObjectToHash($globalSet,\@boolean_set_props); + my $globalSet = getGlobalSet(vars->{db},vars->{ce},params->{set_id}); - $setResults->{assigned_users} = \@userNamesFromDB; - $setResults->{problems} = convertArrayOfObjectsToHash(\@problemsFromDB); - - return $setResults; + return convertObjectToHash($globalSet,\@boolean_set_props); }; #### -# create a new problem set *set_id* for course *course_id* +# create a new problem set or update an existing problem set *set_id* for course *course_id* # # any property can be set by assigning that property a value # @@ -84,65 +73,34 @@ get '/courses/:course_id/sets/:set_id' => sub { # permission > Student ## -post '/courses/:course_id/sets/:set_id' => sub { - checkPermissions(10,session->{user}); - - # call validator directly instead - - if (params->{set_id} !~ /^[\w\_.-]+$/) { - send_error("The set name must only contain A-Za-z0-9_-.",403); - } - - send_error("The set name: " . param('set_id'). " already exists.",404) if (vars->{db}->existsGlobalSet(param('set_id'))); - - my $set = vars->{db}->newGlobalSet(); - for my $key (@set_props) { - $set->{$key} = params->{$key} if defined(params->{$key}); - } - - vars->{db}->addGlobalSet($set); - - for my $user(@{params->{assigned_users}}){ - addUserSet($user,params->{set_id}); - } - - addGlobalProblems(params->{set_id},params->{problems}); - addUserProblems(params->{set_id},params->{problems},params->{assigned_users}); - - my @globalProblems = vars->{db}->getAllGlobalProblems(params->{set_id}); - - my $returnSet = convertObjectToHash($set,\@boolean_set_props); - - - $returnSet->{assigned_users} = params->{assigned_users}; - $returnSet->{problems} = convertArrayOfObjectsToHash(\@globalProblems); - $returnSet->{_id} = params->{set_id}; - return $returnSet; +any ['post', 'put'] => '/courses/:course_id/sets/:set_id' => sub { -}; - -put '/courses/:course_id/sets/:set_id' => sub { - + debug 'in put or post /courses/:course_id/sets/:set_id'; checkPermissions(10,session->{user}); - send_error("The set name: " . param('set_id'). " does not exist.",404) - if (! vars->{db}->existsGlobalSet(params->{set_id})); - - #### - # - # set all the parameters sent from the client as new properties. - # - ## + # set all of the new parameters sent from the client my %allparams = params; - my $set = vars->{db}->getGlobalSet(params->{set_id}); - my $setFromClient = convertBooleans(\%allparams,\@boolean_set_props); - for my $key (@set_props){ - $set->{$key} = $setFromClient->{$key}; + + my $problems_from_client = params->{problems}; + + if(request->is_post()){ + if (params->{set_id} !~ /^[\w\_.-]+$/) { + send_error("The set name must only contain A-Za-z0-9_-.",403); + } + + send_error("The set name: " . param('set_id'). " already exists.",404) if (vars->{db}->existsGlobalSet(param('set_id'))); + my $set = vars->{db}->newGlobalSet(); + $set->{set_id} = params->{set_id}; + vars->{db}->addGlobalSet($set); + } else { + send_error("The set name: " . param('set_id'). " does not exist.",404) + if (! vars->{db}->existsGlobalSet(params->{set_id})); } - my $result = vars->{db}->putGlobalSet($set); - + + putGlobalSet(vars->{db},vars->{ce},\%allparams); + ## # # Take care of the assigned users @@ -154,45 +112,42 @@ put '/courses/:course_id/sets/:set_id' => sub { my @usersToDelete = array_minus(@userNamesFromDB,@{params->{assigned_users}}); for my $user(@usersToAdd){ - addUserSet($user,params->{set_id}); + addUserSet(vars->{db},$user,params->{set_id}); } for my $user (@usersToDelete){ vars->{db}->deleteUserSet($user,params->{set_id}); } # handle the global problems. - + my @problemsFromDB = vars->{db}->getAllGlobalProblems(params->{set_id}); - if(scalar(@problemsFromDB) == scalar(@{params->{problems}})){ # then perhaps the problems need to be reordered. - reorderProblems(params->{assigned_users}); + if(params->{_reorder}){ # reorder the problems + debug "the problems are being reordered"; + reorderProblems(vars->{db},params->{set_id},params->{problems},params->{assigned_users}); } elsif (scalar(@problemsFromDB) < scalar(@{params->{problems}})) { # problems have been added addGlobalProblems(params->{set_id},params->{problems}); - } else { # problems have been deleted. - deleteProblems(params->{set_id},params->{problems}); + addUserProblems(vars->{db},params->{set_id},params->{problems},params->{assigned_users}); + } elsif(params->{_delete_problem_id}) { # problems have been deleted. + deleteProblems(vars->{db},params->{set_id},params->{problems},params->{assigned_users}, + params->{_delete_problem_id}); + } else { # problem may have been updated + updateProblems(vars->{db},params->{set_id},params->{problems}); } - my @globalProblems = vars->{db}->getAllGlobalProblems(params->{set_id}); - - addUserProblems(params->{set_id},params->{problems},params->{assigned_users}); - - ## why is this here? it doesn't do anything. - - if (scalar(@usersToDelete)>0){ - debug "Deleting users to set " . params->{set_id}; - debug join("; ", @usersToDelete); + my $returnSet = getGlobalSet(vars->{db},vars->{ce},params->{set_id}); + + for my $set (@{$returnSet->{problems}}){ + ## return the rendered data that was sent from the client. + my $set_from_client = first_value { $set->{source_file} eq $_->{source_file} && + $set->{problem_id} eq $_->{problem_id} } @$problems_from_client; + $set->{data} = $set_from_client->{data} if defined($set_from_client); + $set->{problem_seed} = $set_from_client->{problem_seed} if defined($set_from_client->{problem_seed}); } + + $returnSet->{pg_password} = $allparams{pg_password} if defined($allparams{pg_password}); - my $setFromDB = vars->{db}->getGlobalSet(params->{set_id}); - - my $returnSet = convertObjectToHash($setFromDB,\@boolean_set_props); - - - $returnSet->{assigned_users} = params->{assigned_users}; - $returnSet->{problems} = convertArrayOfObjectsToHash(\@globalProblems); - $returnSet->{_id} = params->{set_id}; - - return $returnSet; + return convertObjectToHash($returnSet); }; @@ -231,7 +186,7 @@ del '/courses/:course_id/sets/:set_id' => sub { ## # -# Get a list of all user_id's assigned to set *set_id* in course *course_id* +# Get an array of user sets for all users in course :course_id for set :set_id # # return: array of properties. ## @@ -243,15 +198,9 @@ get '/courses/:course_id/sets/:set_id/users' => sub { my @userIDs = vars->{db}->listSetUsers(params->{set_id}); - my @sets = (); - - foreach my $user_id (@userIDs){ - my $userSet = convertObjectToHash(vars->{db}->getMergedSet($user_id,params->{set_id}),\@boolean_set_props); - $userSet->{_id} = $user_id; - push(@sets,$userSet); - } - - return \@sets; + my @sets = map { getUserSet(vars->{db},$_,params->{set_id});} @userIDs; + + return \@sets; }; @@ -338,7 +287,7 @@ del '/courses/:course_id/sets/:set_id/users' => sub { # # permission > Student # -# The users are assigned by setting the users_assigned parameter to a comma delimited list of user_id's. +# The users are assigned by setting the users_assigned parameter an array ref of user_id's. # ##### @@ -486,25 +435,8 @@ put '/courses/:course_id/users/:user_id/sets/:set_id' => sub { # check to see if the user has already been assigned and skip the addition if exists already. - my $userSet = vars->{db}->getUserSet(params->{user_id},params->{set_id}); - send_error("The user " . params->{user_id} . " has not been assigned problem set " . params->{set_id} . ".") - unless $userSet; - - # get the global problem set to determine if the value has changed - my $globalSet = vars->{db}->getGlobalSet(params->{set_id}); - - for my $key (@user_set_props) { - my $globalValue = $globalSet->{$key} || ""; - # check to see if the value differs from the global value. If so, set it else delete it. - $userSet->{$key} = params->{$key} if defined(params->{$key}); - delete $userSet->{$key} if $globalValue eq $userSet->{$key} && $key ne "set_id"; - - } - vars->{db}->putUserSet($userSet); - - my $mergedSet = vars->{db}->getMergedSet(params->{user_id},params->{set_id}); - - return convertObjectToHash($mergedSet,\@boolean_set_props); + my %allparams = request->params; + return putUserSet(vars->{db},\%allparams); }; @@ -575,9 +507,9 @@ get '/courses/:course_id/users/:user_id/sets' => sub { checkPermissions(10,session->{user}); - my @userSetNames = vars->{db}->listUserSets(param('user_id')); - my @userSets = vars->{db}->getMergedSets(map { [params->{user_id}, $_]} @userSetNames); - + my @setIDs = vars->{db}->listUserSets(param('user_id')); + my @userSets = map { getUserSet(vars->{db},vars->{ce},params->{user_id},$_) } @setIDs; + return convertArrayOfObjectsToHash(\@userSets); }; @@ -645,12 +577,10 @@ get '/courses/:course_id/sets/:set_id/users/all/problems' => sub { send_error("The set " . param('set_id'). " doesn't exist for course " . param("course_id"),404) unless vars->{db}->existsGlobalSet(params->{set_id}); - my @allUsers = vars->{db}->listSetUsers(params->{set_id}); - my @userSets = (); - for my $userID (@allUsers){ - my $userSet = convertObjectToHash(vars->{db}->getUserSet($userID,params->{set_id})); - - my @problems = vars->{db}->getAllMergedUserProblems($userID,params->{set_id}); + my @allUserIDs = vars->{db}->listSetUsers(params->{set_id}); + my @userSets = map { + my $userSet = getUserSet(vars->{db},vars->{ce},$_,params->{set_id}); + my @problems = vars->{db}->getAllMergedUserProblems($_,params->{set_id}); my @userProblems = (); for my $problem (@problems){ my @lastAnswers = decodeAnswers($problem->{last_answer}); @@ -659,8 +589,7 @@ get '/courses/:course_id/sets/:set_id/users/all/problems' => sub { push(@userProblems,$prob); } $userSet->{problems} = \@userProblems; - push(@userSets,$userSet); - } + } @allUserIDs; return \@userSets; }; @@ -683,8 +612,7 @@ get '/courses/:course_id/users/:user_id/sets/all/problems' => sub { my @userSetNames = vars->{db}->listUserSets(params->{user_id}); my @userSets = (); for my $setID (@userSetNames){ - my $userSet = convertObjectToHash(vars->{db}->getUserSet(params->{user_id},$setID)); - + my $userSet = getUserSet(vars->{db},vars->{ce}, params->{user_id},$setID); my @problems = vars->{db}->getAllMergedUserProblems(params->{user_id},$setID); my @userProblems = (); for my $problem (@problems){ @@ -725,6 +653,7 @@ get '/courses/:course_id/sets/:set_id/users/:user_id/problems' => sub { for my $problem (@problems){ my @lastAnswers = decodeAnswers($problem->{last_answer}); $problem->{last_answer} = \@lastAnswers; + $problem->{_id} = params->{set_id} . ":" . params->{user_id} . ":" . $problem->{problem_id}; } return convertArrayOfObjectsToHash(\@problems); @@ -895,7 +824,7 @@ del '/courses/:course_id/sets/:set_id/problems/:problem_id' => sub { # # get '/courses/:course_id/status/usersets' # -# This returns the status of each problem sets in the course course_id. If the userProblems match +# This returns the status of each problem set in the course course_id. If the userProblems match # the global problems then a 1 is returned for the problem_status for each set or a 0 if not. # # This is mainly used for troubleshooting where there are inconsistencies in the problem set databases @@ -919,16 +848,16 @@ get '/courses/:course_id/status/usersets' => sub { my @setOkay = (); my @userNames = vars->{db}->listSetUsers($set->{set_id}); - my @userSets = map { {user_id=>$_}} @userNames; - + my @userSets = map { {user_id=>$_}} @userNames; + for my $userSet (@userSets){ my @userProblems = vars->{db}->listUserProblems($userSet->{user_id},$set->{set_id}); $userSet->{problems} = \@userProblems; - push(@setOkay,(@userProblems ~~ @problems && @problems ~~ @userProblems)?1:0); + push(@setOkay,(join("|",@userProblems) eq join("|",@problems))?1:0); } - #$set->{userSets} = \@userSets; - $set->{problem_status} = (0 ~~ @setOkay)?0:1; + my @okays = grep { $_ == 0 } @setOkay; + $set->{problem_status} = scalar(@setOkay)==0?0:1; $set->{problem_length} = scalar(@problems); } @@ -954,8 +883,6 @@ post '/courses/:course_id/fix/usersets' => sub { checkPermissions(10,session->{user}); my $p = vars->{db}->getUserProblem("profa","HW5.2",6); - debug $p; - debug defined($p->{problem_seed}); my @setNames = vars->{db}->listGlobalSets; my @sets = map { {set_id=>$_} } @setNames; @@ -1020,12 +947,49 @@ get '/courses/:course_id/users/:user_id/sets/:set_id/problems/:problem_id' => su send_error("The user " . params->{user_id} . " is not assigned to set " . params->{set_id}) unless vars->{db}->existsUserSet(params->{user_id},params->{set_id}); - my $problem = convertObjectToHash(vars->{db}->getUserProblem(params->{user_id},params->{set_id},params->{problem_id})); + my $problem = convertObjectToHash(vars->{db}->getMergedProblem(params->{user_id},params->{set_id},params->{problem_id})); my @answers = decodeAnswers($problem->{last_answer}); $problem->{last_answer} = \@answers; return $problem; +}; + +### +# +# update the problem :problem_id for user :user_id for set :set_id in course :course_id +# +### + +put '/courses/:course_id/users/:user_id/sets/:set_id/problems/:problem_id' => sub { + checkPermissions(10,session->{user}); + send_error("The problem set with name: " . params->{set_id} . " does not exist.",404) + unless vars->{db}->existsGlobalSet(params->{set_id}); + + send_error("The problem with id " . params->{problem_id} . " doesn't exist in set " . params->{set_id},404) + unless vars->{db}->existsGlobalProblem(params->{set_id},params->{problem_id}); + + send_error("The user " . params->{user_id} . " is not assigned to set " . params->{set_id}) + unless vars->{db}->existsUserSet(params->{user_id},params->{set_id}); + + my $problem = vars->{db}->getMergedProblem(params->{user_id},params->{set_id},params->{problem_id}); + for my $key (@problem_props){ + $problem->{$key} = params->{$key} + } + + putUserProblem(vars->{db}, $problem); + + return convertObjectToHash(vars->{db}->getMergedProblem(params->{user_id},params->{set_id} + ,params->{problem_id})); +}; + +### redirect to the above put if the parameters are out of order: + +put '/courses/:course_id/sets/:set_id/users/:user_id/problems/:problem_id' => sub { + redirect '/courses/' . params->{course_id} . '/users/' . params->{user_id} .'/sets/' . params->{set_id} . + '/problems/ ' . params->{problem_id}; + + }; ### @@ -1121,8 +1085,116 @@ get '/courses/:course_id/pgeditor' => sub { pagename=>"Simple Editor",user=>session->{user}}; }; +#### +# +# get /courses/:course_id/headers +# +# returns an array of possible header files for a given course. +# +#### + +get '/courses/:course_id/headers' => sub { + + checkPermissions(10,session->{user}); + + my $templateDir = vars->{ce}->{courseDirs}->{templates}; + my $include = qr/header.*\.pg$/i; + my $skipDIRS = join("|", keys %{ vars->{ce}->{courseFiles}->{problibs} }); + my $skip = qr/^(?:$skipDIRS|svn)$/; + + my $rule = File::Find::Rule->new; + $rule->or($rule->new->directory->name($skip)->prune->discard,$rule->new); #skip the directories that match $skip + my @files = $rule->file()->name($include)->in($templateDir); + + # return the files relative to the templates/ directory. + my @relativeFiles = map { my @dirs = split(params->{course_id}."/templates/",$_); $dirs[1];} @files; + return \@relativeFiles; +}; + + + +#### +# +# get,put,post /courses/:course_id/sets/:set_id/setheader +# +# gets, creates a new or updates the set header for the problem set :set_id +# +#### + +any ['get', 'put'] => '/courses/:course_id/sets/:set_id/setheader' => sub { + checkPermissions(10,session->{user}); + + if (!vars->{db}->existsGlobalSet(param('set_id'))){ + send_error("The problem set with name: " . param('set_id'). " does not exist.",404); + } + + my $globalSet = vars->{db}->getGlobalSet(param('set_id')); + my $templateDir = vars->{ce}->{courseDirs}->{templates}; + + my $setHeader = $globalSet->{set_header}; + my $setHeaderFile; + if($setHeader eq 'defaultHeader' || ! defined($setHeader) || $setHeader eq ''){ + $setHeader = 'defaultHeader'; + $setHeaderFile = vars->{ce}->{webworkFiles}->{screenSnippets}->{setHeader}; + } else { + $setHeaderFile = path(dirname($templateDir),'templates',$setHeader); + } + + debug $setHeaderFile; + + my $hardcopyHeader = $globalSet->{hardcopy_header}; + my $hardcopyHeaderFile; + if(! defined($hardcopyHeader) || $hardcopyHeader eq ''){ + $hardcopyHeader = 'defaultHeader'; + $hardcopyHeaderFile = vars->{ce}->{webworkFiles}->{hardcopySnippets}->{setHeader}; + } else { + $hardcopyHeaderFile = path(dirname($templateDir),'templates',$hardcopyHeader); + } + my $headerContent = params->{set_header_content}; + my $hardcopyHeaderContent = params->{hardcopy_header_content}; + if(request->is_put()){ + # first determine if the header files are global or local + if($setHeader ne 'defaultHeader'){ + write_file($setHeaderFile,params->{set_header_content}); + } + if($hardcopyHeader ne 'defaultHeader'){ + write_file($hardcopyHeaderFile,params->{hardcopy_header_content}); + } + } + + $headerContent = read_file_content($setHeaderFile); + $hardcopyHeaderContent = read_file_content($hardcopyHeaderFile); + + my $mergedSet = vars->{db}->getMergedSet(session->{user},params->{set_id}); + + my $renderParams = { + displayMode => param('displayMode') || vars->{ce}->{pg}{options}{displayMode}, + problemSeed => defined(params->{problemSeed}) ? params->{problemSeed} : 1, + showHints=> 0, + showSolutions=>0, + showAnswers=>0, + user=>vars->{db}->getUser(session->{user}), + set=>$mergedSet, + problem=>fake_problem(vars->{db}) }; + + + + # check to see if the problem_path is defined + $renderParams->{problem}->{source_file} = $setHeaderFile; + + my $ren = render(vars->{ce},$renderParams); + my $setHeaderHTML = $ren->{text}; + $renderParams->{problem}->{source_file} = $hardcopyHeaderFile; + $ren = render(vars->{ce},$renderParams); + my $hardcopyHeaderHTML = $ren->{text}; + + return {_id=>params->{set_id},set_header=>$setHeader,hardcopy_header=>$hardcopyHeader, + set_header_content=>$headerContent, hardcopy_header_content=>$hardcopyHeaderContent, + set_header_html=>$setHeaderHTML, hardcopy_header_html=>$hardcopyHeaderHTML + }; +}; diff --git a/webwork3/lib/Routes/Settings.pm b/webwork3/lib/Routes/Settings.pm index db9bd40167..83f37524b8 100644 --- a/webwork3/lib/Routes/Settings.pm +++ b/webwork3/lib/Routes/Settings.pm @@ -11,7 +11,8 @@ our $PERMISSION_ERROR = "You don't have the necessary permissions."; use strict; use warnings; use Utils::CourseUtils qw/getCourseSettings/; -use Routes::Authentication qw/checkPermissions/; +use Utils::Authentication qw/checkPermissions/; +use Utils::GeneralUtils qw/writeConfigToFile getCourseSettingsWW2/; use Dancer ':syntax'; #### @@ -68,7 +69,7 @@ put '/courses/:course_id/settings/:setting_id' => sub { #debug "in PUT /course/:course_id/settings/:setting_id"; - my $ConfigValues = getCourseSettingsWW2(); + my $ConfigValues = getCourseSettingsWW2(vars->{ce}); foreach my $oneConfig (@$ConfigValues) { foreach my $hash (@$oneConfig) { if (ref($hash)=~/HASH/){ @@ -88,127 +89,6 @@ put '/courses/:course_id/settings/:setting_id' => sub { }; -# the following are used for loading settings in the WW2 way. -# we should change the settings so they are stored as a JSON file instead. This -# eliminate the need for these subroutines. - -sub getCourseSettingsWW2 { - - my $ConfigValues = vars->{ce}->{ConfigValues}; - - # get the list of theme folders in the theme directory and remove . and .. - my $themeDir = vars->{ce}->{webworkDirs}{themes}; - opendir(my $dh, $themeDir) || die "can't opendir $themeDir: $!"; - my $themes =[grep {!/^\.{1,2}$/} sort readdir($dh)]; - - - foreach my $oneConfig (@$ConfigValues) { - foreach my $hash (@$oneConfig) { - if (ref($hash) eq "HASH") { - my $string = $hash->{var}; - if ($string =~ m/^\w+$/) { - $string =~ s/^(\w+)$/\{$1\}/; - } else { - $string =~ s/^(\w+)/\{$1\}->/; - } - $hash->{value} = eval('vars->{ce}->' . $string); - - if ($hash->{var} eq 'defaultTheme'){ - $hash->{values} = $themes; - } - } - } - } - - - my $tz = DateTime::TimeZone->new( name => vars->{ce}->{siteDefaults}->{timezone}); - my $dt = DateTime->now(); - - my @tzabbr = ("tz_abbr", $tz->short_name_for_datetime( $dt )); - - push(@$ConfigValues, \@tzabbr); - - return $ConfigValues; -} - - - -sub writeConfigToFile { - - my ($ce,$config) = @_; - - my $filename = $ce->{courseDirs}->{root} . "/simple.conf"; - debug("Write to file: " . $filename); - - my $fileoutput = "#!perl -# This file is automatically generated by WeBWorK's web-based -# configuration module. Do not make changes directly to this -# file. It will be overwritten the next time configuration -# changes are saved.\n\n"; - - - # read in the file - - my @raw_data =(); - if (-e $filename){ - open(DAT, $filename) || die("Could not open file!"); - @raw_data=; - close(DAT); - } - - my $line; - my $varFound = 0; - - foreach $line (@raw_data) - { - chomp $line; - if ($line =~ /^\$/) { - my ($var,$value) = ($line =~ /^\$(.*)\s+=\s+(.*);$/); - if ($var eq $config->{var}){ - $fileoutput .= writeLine($config->{var},$config->{value}); - $varFound = 1; - } else { - $fileoutput .= writeLine($var,$value); - } - } - } - - if (! $varFound) { - $fileoutput .= writeLine($config->{var},$config->{value}); - } - - my $writeFileErrors; - eval { - local *OUTPUTFILE; - if( open OUTPUTFILE, ">", $filename) { - print OUTPUTFILE $fileoutput; - close OUTPUTFILE; - } else { - $writeFileErrors = "I could not open $fileoutput". - "We will not be able to make configuration changes unless the permissions are set so that the web server can write to this file."; - } - }; # any errors are caught in the next block - - $writeFileErrors = $@ if $@; - - if ($writeFileErrors){ - return {error=>$writeFileErrors}; - } else { - if($config->{type} eq 'boolean'){ - $config->{value} = $config->{value} ? JSON::true : JSON::false; - } - return $config; - } -} - -sub writeLine { - my ($var,$value) = @_; - my $val = (ref($value) =~/ARRAY/) ? to_json($value,{pretty=>0}): $value; - $val = "'".$val . "'" if ($val =~ /^[a-zA-Z\s]+$/); - #$val =~ s/'//g; - return "\$" . $var . " = " . $val . ";\n"; -} - 1; diff --git a/webwork3/lib/Routes/User.pm b/webwork3/lib/Routes/User.pm index 856aafa1ba..d57a166e20 100644 --- a/webwork3/lib/Routes/User.pm +++ b/webwork3/lib/Routes/User.pm @@ -10,9 +10,8 @@ use strict; use warnings; use Dancer ':syntax'; use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash convertBooleans/; -use Routes::Authentication qw/checkPermissions/; -use WeBWorK::GeneralUtils qw/cryptPassword/; -use Data::Dumper; +use Utils::Authentication qw/checkPermissions/; +use WeBWorK::Utils qw/cryptPassword/; our @user_props = qw/first_name last_name student_id user_id email_address permission status section recitation comment displayMode showOldAnswers useMathView/; diff --git a/webwork3/lib/Routes/Authentication.pm b/webwork3/lib/Utils/Authentication.pm similarity index 82% rename from webwork3/lib/Routes/Authentication.pm rename to webwork3/lib/Utils/Authentication.pm index 0c095fd573..f96b181912 100644 --- a/webwork3/lib/Routes/Authentication.pm +++ b/webwork3/lib/Utils/Authentication.pm @@ -1,41 +1,14 @@ -### Library routes -## -# These are the routes for all library functions in the RESTful webservice -# -## - -package Routes::Authentication; - -use strict; -use warnings; -use Dancer ':syntax'; -use Dancer::Plugin::Database; -use WeBWorK::Constants; -use Data::Dumper; - +package Utils::Authentication; use base qw(Exporter); + our @EXPORT = (); -our @EXPORT_OK = qw(checkPermissions setCourseEnvironment buildSession setCookie); -our $PERMISSION_ERROR = "You don't have the necessary permissions."; +our @EXPORT_OK = qw(setCourseEnvironment buildSession checkPermissions setCookie); -## the following routes is matched for any URL starting with /courses. It is used to load the -# CourseEnvironment -# -# Note: for this to match before others, make sure this package is loaded before others. -# +use Dancer ':syntax'; -any ['get','put','post','delete'] => '/courses/*/**' => sub { - my ($courseID) = splat; - setCourseEnvironment($courseID); - pass; -}; +our $PERMISSION_ERROR = "You don't have the necessary permissions."; -any ['get','post'] => '/renderer/courses/*/**' => sub { - my ($courseID) = splat; - setCourseEnvironment($courseID); - pass; -}; sub setCourseEnvironment { @@ -56,12 +29,11 @@ sub setCourseEnvironment { } sub buildSession { - my ($userID,$sessKey) = @_; if(! vars->{db}){ send_error("The database object DB is not defined. Make sure that you call setCourseEnvironment first.",404); } - + ## need to check that the session hasn't expired. if (!defined(session 'user')) { @@ -71,7 +43,7 @@ sub buildSession { send_error("The user is not defined. You may need to authenticate again",401); } } - + my $key = vars->{db}->getKey(session 'user'); my $timeLastLoggedIn = 0; @@ -157,6 +129,7 @@ sub create_session { } + ### # # This sets the cookie in the WW2 style to allow for seamless transfer back and forth. @@ -175,3 +148,6 @@ sub setCookie { } + + +1; diff --git a/webwork3/lib/Utils/Convert.pm b/webwork3/lib/Utils/Convert.pm index ea06ceae79..9f869a0d0f 100644 --- a/webwork3/lib/Utils/Convert.pm +++ b/webwork3/lib/Utils/Convert.pm @@ -4,7 +4,6 @@ package Utils::Convert; use base qw(Exporter); use JSON; -use Data::Dumper; our @EXPORT = (); our @EXPORT_OK = qw(convertObjectToHash convertArrayOfObjectsToHash convertBooleans); @@ -27,9 +26,8 @@ sub convertObjectToHash { $boolean_props = [] unless defined($boolean_props); - for my $key (keys %{$obj}){ - if(grep(/^$key$/,@{$boolean_props})){ + if(grep(/^$key$/,@{$boolean_props})){ $s->{$key} = $obj->{$key} ? JSON::true : JSON::false; } else { $s->{$key} = $obj->{$key}; @@ -40,16 +38,21 @@ sub convertObjectToHash { } -### I (pstaab) don't think this is necessary. I think dancer takes care of it through constants true and false +## convert from the JSON::true or JSON::false to 1/0. sub convertBooleans { my ($obj,$boolean_props) = @_; - - for my $key (@{$boolean_props}){ - $obj->{$key} = $obj->{$key} ? 1: 0; + + my $s = {}; + + for my $key (keys %{$obj}){ + if(grep(/^$key$/,@{$boolean_props})){ + $s->{$key} = $obj->{$key} ?1: 0; + } else { + $s->{$key} = $obj->{$key}; + } } - - return $obj; + return $s; } diff --git a/webwork3/lib/Utils/CourseUtils.pm b/webwork3/lib/Utils/CourseUtils.pm index 9f48a85d50..fd5eacd5d1 100644 --- a/webwork3/lib/Utils/CourseUtils.pm +++ b/webwork3/lib/Utils/CourseUtils.pm @@ -3,11 +3,10 @@ package Utils::CourseUtils; use base qw(Exporter); -use Dancer ':syntax'; +#use Dancer ':syntax'; #use Dancer::Plugin::Database; use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash/; use Utils::ProblemSets qw/getGlobalSet/; -use Data::Dumper; our @EXPORT = (); our @EXPORT_OK = qw(getCourseSettings getAllSets getAllUsers); @@ -17,34 +16,36 @@ our @EXPORT_OK = qw(getCourseSettings getAllSets getAllUsers); ## get all of the user information to send to the client via a script tag in the output_JS subroutine below sub getAllSets { + my ($db,$ce) = @_; - my @setNames = vars->{db}->listGlobalSets; - my @sets = map { getGlobalSet($_) } @setNames; + my @setNames = $db->listGlobalSets; + my @sets = map { getGlobalSet($db,$ce,$_) } @setNames; return \@sets; } -# get all users for the course +# get all users (except login proctors) for the course sub getAllUsers { + my ($db,$ce) = @_; - my @tempArray = vars->{db}->listUsers; - my @userInfo = vars->{db}->getUsers(@tempArray); - my $numGlobalSets = vars->{db}->countGlobalSets; + my @userIDs = $db->listUsers; + my @userInfo = $db->getUsers(@userIDs); + my $numGlobalSets = $db->countGlobalSets; my @allUsers = (); - my %permissionsHash = reverse %{vars->{ce}->{userRoles}}; + my %permissionsHash = reverse %{$ce->{userRoles}}; foreach my $u (@userInfo) { - my $PermissionLevel = vars->{db}->getPermissionLevel($u->{'user_id'}); + my $PermissionLevel = $db->getPermissionLevel($u->{'user_id'}); $u->{permission} = $PermissionLevel->{permission}; my $studid= $u->{student_id}; - my $key = vars->{db}->getKey($u->{'user_id'}); + my $key = $db->getKey($u->{'user_id'}); $u->{student_id} = "$studid"; # make sure that the student_id is returned as a string. - $u->{num_user_sets} = vars->{db}->listUserSets($studid) . "/" . $numGlobalSets; - $u->{logged_in} = ($key and time <= $key->timestamp()+vars->{ce}->{sessionKeyTimeout}) ? JSON::true : JSON::false; + $u->{num_user_sets} = $db->listUserSets($studid) . "/" . $numGlobalSets; + $u->{logged_in} = ($key and time <= $key->timestamp()+$ce->{sessionKeyTimeout}) ? JSON::true : JSON::false; # convert the user $u to a hash @@ -55,15 +56,18 @@ sub getAllUsers { $s->{$key} = $u->{$key} } - my $showOldAnswers = ($u->{showOldAnswers} eq '') ? vars->{ce}{pg}{options}{showOldAnswers}: $u->{showOldAnswers}; + my $showOldAnswers = ($u->{showOldAnswers} eq '') ? $ce->{pg}{options}{showOldAnswers}: $u->{showOldAnswers}; $s->{showOldAnswers} = $showOldAnswers ? JSON::true : JSON::false; - my $useMathView = ($u->{useMathView} eq '')? vars->{ce}{pg}{options}{useMathView} : $u->{useMathView}; + my $useMathView = ($u->{useMathView} eq '')? $ce->{pg}{options}{useMathView} : $u->{useMathView}; $s->{useMathView} = $useMathView ? JSON::true : JSON::false; $s->{_id} = $s->{user_id}; - push(@allUsers,$s); + if(! ($s->{user_id} =~ /^set_id:/)){ # filter out login proctors. + push(@allUsers,$s); + } + } return \@allUsers; @@ -73,11 +77,13 @@ sub getAllUsers { sub getCourseSettings { - my $ConfigValues = vars->{ce}->{ConfigValues}; + my $ce = shift; + + my $ConfigValues = $ce->{ConfigValues}; my @settings = (); # get the list of theme folders in the theme directory and remove . and .. - my $themeDir = vars->{ce}->{webworkDirs}{themes}; + my $themeDir = $ce->{webworkDirs}{themes}; opendir(my $dh, $themeDir) || die "can't opendir $themeDir: $!"; my $themes =[grep {!/^\.{1,2}$/} sort readdir($dh)]; @@ -94,7 +100,7 @@ sub getCourseSettings { } else { $string =~ s/^(\w+)/\{$1\}->/; } - $setting->{value} = eval('vars->{ce}->' . $string); + $setting->{value} = eval('$ce->' . $string); if ($hash->{var} eq 'defaultTheme'){ $setting->{value} = $themes; } diff --git a/webwork3/lib/Utils/GeneralUtils.pm b/webwork3/lib/Utils/GeneralUtils.pm new file mode 100644 index 0000000000..21da5ad6ab --- /dev/null +++ b/webwork3/lib/Utils/GeneralUtils.pm @@ -0,0 +1,144 @@ +package Utils::GeneralUtils; +use base qw(Exporter); + +### this is a number of subrotines from the webwork2 version of WeBWorK::Utils + +use Dancer ':syntax'; +use strict; +use warnings; +use DateTime; +use DateTime::TimeZone; + +our @EXPORT = (); + +our @EXPORT_OK = qw(writeConfigToFile getCourseSettingsWW2); + + + +# the following are used for loading settings in the WW2 way. +# we should change the settings so they are stored as a JSON file instead. This +# eliminate the need for these subroutines. + +### pstaab: I think a nearly identical version of this is in Utils::CourseUtils + +sub getCourseSettingsWW2 { + my $ce = shift; + + my $ConfigValues = $ce->{ConfigValues}; + + # get the list of theme folders in the theme directory and remove . and .. + my $themeDir = $ce->{webworkDirs}{themes}; + opendir(my $dh, $themeDir) || die "can't opendir $themeDir: $!"; + my $themes =[grep {!/^\.{1,2}$/} sort readdir($dh)]; + + + foreach my $oneConfig (@$ConfigValues) { + foreach my $hash (@$oneConfig) { + if (ref($hash) eq "HASH") { + my $string = $hash->{var}; + if ($string =~ m/^\w+$/) { + $string =~ s/^(\w+)$/\{$1\}/; + } else { + $string =~ s/^(\w+)/\{$1\}->/; + } + $hash->{value} = eval('$ce->' . $string); + + if ($hash->{var} eq 'defaultTheme'){ + $hash->{values} = $themes; + } + } + } + } + + + my $tz = DateTime::TimeZone->new( name => $ce->{siteDefaults}->{timezone}); + my $dt = DateTime->now(); + + my @tzabbr = ("tz_abbr", $tz->short_name_for_datetime( $dt )); + + push(@$ConfigValues, \@tzabbr); + + return $ConfigValues; +} + + + +sub writeConfigToFile { + + my ($ce,$config) = @_; + + my $filename = $ce->{courseDirs}->{root} . "/simple.conf"; + + my $fileoutput = "#!perl +# This file is automatically generated by WeBWorK's web-based +# configuration module. Do not make changes directly to this +# file. It will be overwritten the next time configuration +# changes are saved.\n\n"; + + + # read in the file + + my @raw_data =(); + if (-e $filename){ + open(DAT, $filename) || die("Could not open file!"); + @raw_data=; + close(DAT); + } + + my $line; + my $varFound = 0; + + foreach $line (@raw_data) + { + chomp $line; + if ($line =~ /^\$/) { + my ($var,$value) = ($line =~ /^\$(.*)\s+=\s+(.*);$/); + if ($var eq $config->{var}){ + $fileoutput .= writeLine($config->{var},$config->{value}); + $varFound = 1; + } else { + $fileoutput .= writeLine($var,$value); + } + } + } + + if (! $varFound) { + $fileoutput .= writeLine($config->{var},$config->{value}); + } + + my $writeFileErrors; + eval { + local *OUTPUTFILE; + if( open OUTPUTFILE, ">", $filename) { + print OUTPUTFILE $fileoutput; + close OUTPUTFILE; + } else { + $writeFileErrors = "I could not open $fileoutput". + "We will not be able to make configuration changes unless the permissions are set so that the web server can write to this file."; + } + }; # any errors are caught in the next block + + $writeFileErrors = $@ if $@; + + if ($writeFileErrors){ + return {error=>$writeFileErrors}; + } else { + if($config->{type} eq 'boolean'){ + $config->{value} = $config->{value} ? JSON::true : JSON::false; + } + return $config; + } +} + +sub writeLine { + my ($var,$value) = @_; + my $val = (ref($value) =~/ARRAY/) ? to_json($value,{pretty=>0}): $value; + $val = "'".$val . "'" if ($val =~ /^[\w\s\/]+$/); + #$val =~ s/'//g; + return "\$" . $var . " = " . $val . ";\n"; +} + + + + +1; \ No newline at end of file diff --git a/webwork3/lib/Utils/LibraryUtils.pm b/webwork3/lib/Utils/LibraryUtils.pm index c03a1ab06c..d322b9f10b 100644 --- a/webwork3/lib/Utils/LibraryUtils.pm +++ b/webwork3/lib/Utils/LibraryUtils.pm @@ -6,10 +6,12 @@ use base qw(Exporter); use Path::Class qw/file dir/; use Dancer ':syntax'; use Dancer::Plugin::Database; -use Data::Dumper; -use WeBWorK::GeneralUtils qw(readDirectory); +use WeBWorK::Utils qw(readDirectory); +use WeBWorK3::PG::Local; our @EXPORT = (); our @EXPORT_OK = qw(list_pg_files searchLibrary getProblemTags render); +our @answerFields = qw/preview_latex_string done original_student_ans preview_text_string ans_message + student_ans error_flag score correct_ans ans_label error_message _filter_name type ans_name/; my %ignoredir = ( '.' => 1, '..' => 1, 'CVS' => 1, 'tmpEdit' => 1, @@ -23,11 +25,7 @@ my %ignoredir = ( ### sub render { - - debug "in general render sub"; - - my $renderParams = shift; - + my ($ce,$renderParams) = @_; my @anskeys = split(";",params->{answer_fields} || ""); $renderParams->{formFields}= {}; @@ -38,21 +36,22 @@ sub render { $renderParams->{formFields}->{effectiveUser} = params->{effectiveUser} || session->{user}; # remove any pretty garbage around the problem - local vars->{ce}->{pg}{specialPGEnvironmentVars}{problemPreamble} = {TeX=>'',HTML=>''}; - local vars->{ce}->{pg}{specialPGEnvironmentVars}{problemPostamble} = {TeX=>'',HTML=>''}; + local $ce->{pg}{specialPGEnvironmentVars}{problemPreamble} = {TeX=>'',HTML=>''}; + local $ce->{pg}{specialPGEnvironmentVars}{problemPostamble} = {TeX=>'',HTML=>''}; my $translationOptions = { displayMode => $renderParams->{displayMode}, showHints => $renderParams->{showHints}, showSolutions => $renderParams->{showSolutions}, + showAnswers => $renderParams->{showAnswers}, refreshMath2img => defined(param("refreshMath2img")) ? param("refreshMath2img") : 0 , processAnswers => defined(param("processAnswers")) ? param("processAnswers") : 1 }; - my $pg = new WeBWorK::PG( - vars->{ce}, + my $pg = new WeBWorK3::PG::Local( + $ce, $renderParams->{user}, params->{session_key}, $renderParams->{set}, @@ -75,14 +74,16 @@ sub render { # extract the important parts of the answer, but don't send the correct_ans if not requested. - for my $key (@anskeys){ - for my $field (qw(preview_latex_string done original_student_ans preview_text_string ans_message student_ans error_flag score correct_ans ans_label error_message _filter_name type ans_name)) { + for my $key (@{$pg->{flags}->{ANSWER_ENTRY_ORDER}}){ + $answers->{$key} = {}; + for my $field (@answerFields) { if ($field ne 'correct_ans' || $renderParams->{showAnswers}){ $answers->{$key}->{$field} = $pg->{answers}->{$key}->{$field}; } + } } - + my $flags = {}; ## skip the CODE reference which appears in the PROBLEM_GRADER_TO_USE. I don't think this is useful for @@ -109,6 +110,15 @@ sub render { debug_messages => \@pgdebug_messages, internal_debug_messages => \@internal_debug_messages, }; + + if($problem_hash->{errors}){ + my $text = qq|
An error occurred while processing this problem. + Click here + to show details of the error.
"; + + $problem_hash->{text} = $text; + } return $problem_hash; @@ -458,8 +468,6 @@ sub getProblemTags { # my ($top,$base,$dir,$probLib) = @_; -# debug "top: $top base: $base dir: $dir probLib: $probLib \n"; -# debug Dumper($probLib); # my @lis = readDirectory("$base/$dir"); # return () if grep /^=library-ignore$/, @lis; # return () if !$top && grep /^=library-no-combine$/, @lis; @@ -471,8 +479,6 @@ sub getProblemTags { # my @dirs = grep {!$ignoredir{$_} and -d "$base/$dir/$_"} @lis; # if ($top == 1) {@dirs = grep {!$problib->{$_}} @dirs} -# debug Dumper(@dirs); - # foreach my $subdir (@dirs) {push(@pgs, get_library_pgs(0,"$base/$dir",$subdir,$probLib))} # return () unless $top || (scalar(@pgs) == 1 && $others) || grep /^=library-combine-up$/, @lis; @@ -525,4 +531,6 @@ sub munge_pg_file_path { # an error so the user knows there is a troublesome path in the # set.def file. return($pg_path); -} \ No newline at end of file +} + +1; \ No newline at end of file diff --git a/webwork3/lib/Utils/ProblemSets.pm b/webwork3/lib/Utils/ProblemSets.pm index d179cfc2cd..6a2f9d8e35 100644 --- a/webwork3/lib/Utils/ProblemSets.pm +++ b/webwork3/lib/Utils/ProblemSets.pm @@ -3,23 +3,60 @@ package Utils::ProblemSets; use base qw(Exporter); -use Dancer ':syntax'; -use Data::Dumper; + use List::Util qw(first); -use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash/; +use List::MoreUtils qw/first_index indexes/; +use Dancer ':syntax'; +use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash convertBooleans/; +use WeBWorK::Utils qw/writeCourseLog encodeAnswers writeLog cryptPassword/; +use Array::Utils qw/array_minus/; + +our @set_props = qw/set_id set_header hardcopy_header open_date reduced_scoring_date due_date answer_date visible + enable_reduced_scoring assignment_type description attempts_per_version time_interval + versions_per_interval version_time_limit version_creation_time version_last_attempt_time + problem_randorder hide_score hide_score_by_problem hide_work time_limit_cap restrict_ip + relax_restrict_ip restricted_login_proctor hide_hint/; + +our @user_set_props = qw/user_id set_id psvn set_header hardcopy_header open_date reduced_scoring_date due_date + answer_date visible enable_reduced_scoring assignment_type description restricted_release + restricted_status attempts_per_version time_interval versions_per_interval version_time_limit + version_creation_time problem_randorder version_last_attempt_time problems_per_page + hide_score hide_score_by_problem hide_work time_limit_cap restrict_ip relax_restrict_ip + restricted_login_proctor hide_hint/; +our @problem_props = qw/problem_id flags value max_attempts status source_file/; +our @boolean_set_props = qw/visible enable_reduced_scoring hide_hint time_limit_cap problem_randorder/; + +our @user_problem_props = qw/user_id set_id problem_id source_file value max_attempts showMeAnother + showMeAnotherCount flags problem_seed status attempted last_answer num_correct num_incorrect + sub_status flags/; + our @EXPORT = (); our @EXPORT_OK = qw(reorderProblems addGlobalProblems deleteProblems addUserProblems addUserSet - createNewUserProblem getGlobalSet record_results); -our @boolean_set_props = qw/visible enable_reduced_scoring/; - + createNewUserProblem getGlobalSet record_results renumber_problems updateProblems shiftTime + unshiftTime putGlobalSet putUserSet getUserSet putUserProblem + @time_props @set_props @user_set_props @problem_props @boolean_set_props); + sub getGlobalSet { - my ($setName) = @_; - my $set = vars->{db}->getGlobalSet($setName); + my ($db,$ce,$setName) = @_; + my $set = $db->getGlobalSet($setName); my $problemSet = convertObjectToHash($set,\@boolean_set_props); - my @users = vars->{db}->listSetUsers($setName); - my @problems = vars->{db}->getAllGlobalProblems($setName); + my @users = $db->listSetUsers($setName); + my @problems = $db->getAllGlobalProblems($setName); + for my $problem (@problems){ + $problem->{_id} = $problem->{set_id} . ":" . $problem->{problem_id}; # this helps backbone on the client side + } + + my $proctor_id = "set_id:".$set->{set_id}; + if($db->existsUser($proctor_id)){ + if($db->getPassword($proctor_id)){ + $problemSet->{pg_password}='******'; + } + } + + + $problemSet->{assigned_users} = \@users; $problemSet->{problems} = convertArrayOfObjectsToHash(\@problems); $problemSet->{_id} = $setName; # this is needed so that backbone works with the server. @@ -27,43 +64,193 @@ sub getGlobalSet { return $problemSet; } + ### # -# This reorders the problems +# This puts/updates the global set with properties in the hash ref $set +# +### -sub reorderProblems { +sub putGlobalSet { + my ($db,$ce,$set) = @_; + + my $set_from_db = $db->getGlobalSet($set->{set_id}); + $set = convertBooleans($set,\@boolean_set_props); + + for my $key (@set_props){ + $set_from_db->{$key} = $set->{$key} if defined($set->{$key}); + } + + ## if the set is a proctored gateway + + if($set->{assignment_type} eq 'proctored_gateway'){ + my $proctor_id = "set_id:".$set->{set_id}; + ## if the proctor doesn't exist as a user in the db, create it. + if(! $db->existsUser($proctor_id)){ + my $proctor = $db->newUser(); + $proctor->user_id($proctor_id); + $proctor->last_name("Proctor"); + $proctor->first_name("Login"); + $proctor->student_id("loginproctor"); + $proctor->status($ce->status_name_to_abbrevs('Proctor')); + $db->addUser($proctor); + + ## add a permission level to the database. + my $procPerm = $db->newPermissionLevel; + $procPerm->user_id($proctor_id); + $procPerm->permission($ce->{userRoles}->{login_proctor}); + $db->addPermissionLevel($procPerm); + $set_from_db->restricted_login_proctor('Yes'); + } + + if($set->{pg_password} ne '******') { + my $dbPass = $db->getPassword($proctor_id); + if(! defined($dbPass)){ + $dbPass = $db->newPassword($proctor_id); + $dbPass->user_id($proctor_id); + } + my $clearPassword = $set->{pg_password}; + $dbPass->password(cryptPassword($set->{pg_password})); + $db->putPassword($dbPass); + $set->{pg_password}=($clearPassword eq '')? '' : '******'; + } + + } + + return $db->putGlobalSet($set_from_db); +} + +### +# +# The gets a userSet (mergedSet) with given $user_id and $set_id +# +### + +sub getUserSet{ + my ($db,$user_id,$set_id) = @_; + + my $mergedSet = $db->getMergedSet($user_id,$set_id); + + $mergedSet->{_id} = $mergedSet->{set_id} . ":" . $mergedSet->{user_id}; + + return convertObjectToHash($mergedSet,\@boolean_set_props); + +} + +### +# +# This puts/updates the user set with properties in the hash ref $set Update only the values that +# differ from the global set properties +# +### + + +sub putUserSet { + my ($db,$set) = @_; + + # get the global problem set to determine if the value has changed + my $globalSet = $db->getGlobalSet($set->{set_id}); + my $userSet = $db->getUserSet($set->{user_id},$set->{set_id}); + + $set = convertBooleans($set,\@boolean_set_props); + for my $key (@user_set_props) { + my $globalValue = $globalSet->{$key} || ""; + # check to see if the value differs from the global value. If so, set it else delete it. + $userSet->{$key} = $set->{$key} if defined($set->{$key}); + delete $userSet->{$key} if $globalValue eq $userSet->{$key} && $key ne "set_id"; + + } + $db->putUserSet($userSet); + + return getUserSet($db,$set->{user_id},$set->{set_id}); +} + +#### +# +# This puts/updates the problem properties for the given problem. Only properties that differ from the global problem +# are updated. +# +#### + +sub putUserProblem { + my ($db,$problem) = @_; + + # get the global problem to determine if the value has changed + my $globalProblem = $db->getGlobalProblem($problem->{set_id},$problem->{problem_id}); + my $userProblem = $db->getUserProblem($problem->{user_id},$problem->{set_id},$problem->{problem_id}); + + for my $key (@user_problem_props){ + my $globalValue = $globalProblem->{$key} || ""; + $userProblem->{$key} = $problem->{$key} if defined($problem->{$key}); + delete $userProblem->{$key} if $globalValue eq $userProblem->{$key} + && $key ne "problem_id" && $key ne "set_id" && $key ne 'user_id'; + } + + $db->putUserProblem($userProblem); + return $userProblem; +} - my @oldProblems = vars->{db}->getAllGlobalProblems(params->{set_id}); - for my $p (@{params->{problems}}){ - my $problem = first { $_->{source_file} eq $p->{source_file} } @oldProblems; - if (vars->{db}->existsGlobalProblem(params->{set_id},$p->{problem_id})){ - $problem->problem_id($p->{problem_id}); - vars->{db}->putGlobalProblem($problem); +sub reorderProblems { + my ($db,$setID,$new_problems,$assigned_users) = @_; + + + for my $i (0..(scalar(@$new_problems)-1)){ + + my $prob; + if($db->existsGlobalProblem($setID,$new_problems->[$i]->{problem_id})){ + $prob = $db->getGlobalProblem($setID,$new_problems->[$i]->{problem_id}); } else { - # delete the problem with the old problem_id and create a new one - vars->{db}->deleteGlobalProblem(params->{set_id},$problem->{problem_id}); - $problem->problem_id($p->{problem_id}); - vars->{db}->addGlobalProblem($problem); - - for my $user (@{params->{assigned_users}}){ - my $userProblem = vars->{db}->newUserProblem; - $userProblem->set_id(params->{set_id}); - $userProblem->user_id($user); - $userProblem->problem_id($p->{problem_id}); - debug $userProblem; - vars->{db}->addUserProblem($userProblem); + $prob = $db->newGlobalProblem();##$setID,$new_problems->[$i]->{problem_id}); + $prob->{set_id} = $setID; + $prob->{problem_id} = $new_problems->[$i]->{problem_id}; + } + + for my $key (@problem_props){ + $prob->{$key} = $new_problems->[$i]->{$key}; + } + if($db->existsGlobalProblem($setID,$new_problems->[$i]->{problem_id})){ + $db->putGlobalProblem($prob); + } else { + $db->addGlobalProblem($prob); + } + } + + ## update the user problems + + for my $user_id (@$assigned_users){ + my $user_probs = [$db->getAllUserProblems($user_id,$setID)]; + for my $i (0..(scalar(@$new_problems)-1)){ + + my $user_prob = first {$_->{problem_id} eq $new_problems->[$i]->{_old_problem_id} } @$user_probs; + + ## need to make a new User Problem. Reusing the old one results in a problem. + my $newUserProblem = createNewUserProblem($user_id,$setID,$new_problems->[$i]->{problem_id}); + for my $prop (@user_problem_props) { + $newUserProblem->{$prop} = $user_prob->{$prop}; } + $db->putUserProblem($newUserProblem); } } - - ## take care of the userProblems now - +} +#### +# +# This takes the problems in the array ref $problems and updates the global problems for course $setID +# +### - return vars->{db}->getAllGlobalProblems(params->{set_id}); +sub updateProblems { + my ($db,$setID,$problems) = @_; + for my $prob_to_update (@$problems){ + my $prob = $db->getGlobalProblem($setID,$prob_to_update->{problem_id}); + for my $attr (@problem_props){ + $prob->{$attr} = $prob_to_update->{$attr} if $prob_to_update->{$attr}; + } + $db->putGlobalProblem($prob); + } } ### @@ -100,23 +287,23 @@ sub createNewUserProblem { sub addGlobalProblems { my ($setID,$problems)=@_; - debug "in addGlobalProblems"; my @oldProblems = vars->{db}->getAllGlobalProblems($setID); for my $p (@{$problems}){ - my $problem = first { $_->{source_file} eq $p->{source_file} } @oldProblems; - - debug $problem; if(! vars->{db}->existsGlobalProblem($setID,$p->{problem_id})){ + my $prob = vars->{db}->newGlobalProblem(); + $prob->{problem_id} = $p->{problem_id}; $prob->{source_file} = $p->{source_file}; $prob->{value} = $p->{value}; $prob->{max_attempts} = $p->{max_attempts}; $prob->{set_id} = $setID; - vars->{db}->addGlobalProblem($prob) unless vars->{db}->existsGlobalProblem($setID,$prob->{problem_id}) + $prob->{_id} = $prob->{set_id} . ":" . $prob->{problem_id}; # this helps backbone on the client side + vars->{db}->addGlobalProblem($prob) unless vars->{db}->existsGlobalProblem($setID,$prob->{problem_id}); } - } + } + return vars->{db}->getAllGlobalProblems($setID); } @@ -133,11 +320,11 @@ sub addGlobalProblems { ##### sub addUserProblems { - my ($setID, $problems,$users) = @_; - + my ($db,$setID, $problems,$users) = @_; + for my $p (@{$problems}){ for my $userID (@{$users}){ - vars->{db}->addUserProblem(createNewUserProblem($userID,$setID,$p->{problem_id})) + $db->addUserProblem(createNewUserProblem($userID,$setID,$p->{problem_id})) unless vars->{db}->existsUserProblem($userID,$setID,$p->{problem_id}); } } @@ -149,23 +336,68 @@ sub addUserProblems { # This deletes a problem. The variable $problems is a reference to an array of problems and # the subroutine checks if any of the given problems are not in the database # +# Note: the calls to $db->deleteGlobalProblem also deletes any user problem associated with it. +# ## -### @oldProblems = [1,2,3,4,5]; -### $problems = [1,2,4,5]; - sub deleteProblems { - my ($setID,$problems)=@_; + my ($db,$setID,$problems,$assigned_users,$problem_id_to_delete)=@_; + + $db->deleteGlobalProblem($setID,$problem_id_to_delete); + + # renumber_problems($db,$setID,$assigned_users); - my @oldProblems = vars->{db}->getAllGlobalProblems($setID); - for my $p (@oldProblems){ - my $problem = first { $_->{problem_id} eq $p->{problem_id} } @{$problems}; - if(! defined($problem)){ - vars->{db}->deleteGlobalProblem($setID,$p->{problem_id}); + return $db->getAllGlobalProblems($setID); +} + +### +# +# The following renumbers problems. If they come in as 2,4,9,11,13 they leave as 1,2,3,4,5 +# +# pstaab: It appears that there is a lot of overlap between this and reorder_problems at the top +# of this file. They should be combined or clarified how. +### + +sub renumber_problems { + my ($db,$setID,$assigned_users) = @_; + + my @probs = $db->getAllGlobalProblems($setID); + + my @prob_ids = (); + my %userprobs = (); + my $j=1; + for my $prob (@probs) { + push(@prob_ids, $prob->{problem_id}); + $prob->{problem_id} = $j++; + } + + for my $user_id (@{$assigned_users}){ + $j=1; + my $userproblems = [$db->getAllUserProblems($user_id,$setID)]; + for my $prob (@$userproblems) { + $prob->{problem_id} = $j++; } + $userprobs{$user_id} = $userproblems; } - - return vars->{db}->getAllGlobalProblems($setID); + + ## delete all old problems; + + for my $prob_id (@prob_ids){ + $db->deleteGlobalProblem($setID,$prob_id); + } + + ## add in all of the global and user problems: + for my $prob (@probs) { + $db->addGlobalProblem($prob); + } + + for my $user_id (@{$assigned_users}){ + for my $user_problem (@{$userprobs{$user_id}}){ + $db->addUserProblem($user_problem); + } + } + + return; } @@ -176,13 +408,19 @@ sub deleteProblems { ### sub addUserSet { - my ($user_id,$set_id) = @_; - my $userSet = vars->{db}->newUserSet; + my ($db,$user_id,$set_id) = @_; + + my $userSet = $db->newUserSet; $userSet->set_id($set_id); $userSet->user_id($user_id); - my $result = vars->{db}->addUserSet($userSet); - - return $result; + + $db->addUserSet($userSet); + + ## create the user problems now + my @users = ("$user_id"); + my @globalProblems = $db->getAllGlobalProblems($set_id); + addUserProblems($db,$set_id,\@globalProblems,\@users); + } @@ -326,7 +564,6 @@ sub record_results { $pureProblem->num_incorrect ); - debug "here!"; } else { if (before($renderParams->{set}->{open_date}) or after($renderParams->{set}->{due_date})) { $scoreRecordedMessage = "Your score was not recorded because this homework set is closed."; @@ -344,3 +581,7 @@ sub record_results { return $scoreRecordedMessage; } + + + +1; \ No newline at end of file diff --git a/webwork3/lib/WeBWorK/Authen.pm b/webwork3/lib/WeBWorK/Authen.pm deleted file mode 100644 index 3111b51b47..0000000000 --- a/webwork3/lib/WeBWorK/Authen.pm +++ /dev/null @@ -1,708 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/Authen.pm,v 1.63 2012/06/06 22:03:15 wheeler Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# Free Software Foundation; either version 2, or (at your option) any later -# version, or (b) the "Artistic License" 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. -################################################################################ - -package WeBWorK::Authen; - -use strict; -use warnings; -use Dancer; -use Data::Dumper; -use WeBWorK::Debug; -use WeBWorK::GeneralUtils qw/writeCourseLog runtime_use/; -use Scalar::Util qw(weaken); -use Carp qw/croak/; - -#use vars qw($GENERIC_ERROR_MESSAGE); -our $GENERIC_ERROR_MESSAGE = ""; # define in new - -################################################################################ -# Public API -################################################################################ - -=head1 FACTORY - -=over - -=item class($ce, $type) - -This subroutine consults the given WeBWorK::CourseEnvironment object to -determine which WeBWorK::Authen subclass should be used. $type can be any key -given in the %authen hash in the course environment. If the type is not found in -the %authen hash, an exception is thrown. - -=cut - -sub class { - my ($ce, $type) = @_; - - if (exists $ce->{authen}{$type}) { - if (ref $ce->{authen}{$type} eq "ARRAY") { - my $authen_type = shift @{$ce ->{authen}{$type}}; - #debug("ref of authen_type = |" . ref($authen_type) . "|"); - if (ref ($authen_type) eq "HASH") { - if (exists $authen_type->{$ce->{dbLayoutName}}) { - return $authen_type->{$ce->{dbLayoutName}}; - } elsif (exists $authen_type->{"*"}) { - return $authen_type->{"*"}; - } else { - die "authentication type '$type' in the course environment has no entry for db layout '", $ce->{dbLayoutName}, "' and no default entry (*)"; - } - } else { - return $authen_type; - } - } elsif (ref $ce->{authen}{$type} eq "HASH") { - if (exists $ce->{authen}{$type}{$ce->{dbLayoutName}}) { - return $ce->{authen}{$type}{$ce->{dbLayoutName}}; - } elsif (exists $ce->{authen}{$type}{"*"}) { - return $ce->{authen}{$type}{"*"}; - } else { - die "authentication type '$type' in the course environment has no entry for db layout '", $ce->{dbLayoutName}, "' and no default entry (*)"; - } - } else { - return $ce->{authen}{$type}; - } - } else { - die "authentication type '$type' not found in course environment"; - } -} - -sub call_next_authen_method { - my ($self,$ce) = shift; - - my $user_authen_module = WeBWorK::Authen::class($ce, "user_module"); - #debug("user_authen_module = |$user_authen_module|"); - if (!defined($user_authen_module) or ($user_authen_module eq "")) { - $self->{error} = "No authentication method found for your request. " - . "If this recurs, please speak with your instructor."; - $self->{log_error} .= "None of the specified authentication modules could handle the request."; - return(0); - } else { - - # not sure what to do here without a Request Object - - #runtime_use $user_authen_module; - # my $authen = $user_authen_module->new($r); - #debug("Using user_authen_module $user_authen_module: $authen\n"); - # $r->authen($authen); - - return; - } -} - - -=back - -=cut - -=head1 CONSTRUCTOR - -=over - -=item new($r) - -Instantiates a new WeBWorK::Authen object for the given WeBWorK::CourseEnvironment ($ce). - -=cut - -sub new { - my ($invocant,$ce) = @_; - my $class = ref($invocant) || $invocant; - my $self = { - ce => $ce, - db => new WeBWorK::DB($ce->{dbLayout}), - params => {} - }; - # weaken $self -> {r}; - #initialize - # $GENERIC_ERROR_MESSAGE = $r->maketext("Invalid user ID or password."); - bless $self, $class; - return $self; -} - - -# 0 == required data was present, but authentication failed -# -1 == required data was not present (i.e. password missing) -sub authenticate { - my $self = shift; - # my $r = $self->{r}; - - my $user_id = $self->{params}->{user}; - my $password = $self->{params}->{password}; - - if (defined $password) { - return $self->checkPassword($user_id, $password); - } else { - return -1; - } -} - -sub set_params { - my ($self,$params) = @_; - $self->{params} = $params; -} - -################################################################################ -# Password management -################################################################################ - -sub checkPassword { - my ($self, $userID, $possibleClearPassword) = @_; - - debug $userID; - debug $possibleClearPassword; - - my $Password = $self->{db}->getPassword($userID); # checked - if (defined $Password) { - # check against WW password database - my $possibleCryptPassword = crypt $possibleClearPassword, $Password->password; - if ($possibleCryptPassword eq $Password->password) { - $self->write_log_entry("AUTH WWDB: password accepted"); - return 1; - } else { - if ($self->can("site_checkPassword")) { - $self->write_log_entry("AUTH WWDB: password rejected, deferring to site_checkPassword"); - return $self->site_checkPassword($userID, $possibleClearPassword); - } else { - $self->write_log_entry("AUTH WWDB: password rejected"); - return 0; - } - } - } else { - $self->write_log_entry("AUTH WWDB: user has no password record"); - return 0; - } -} - -sub request_has_data_for_this_verification_module { - #debug("Authen::request_has_data_for_this_verification_module will return a 1"); - return(1); -} - - -sub verify { - debug("BEGIN VERIFY"); - my $self = shift; - - if (! ($self-> request_has_data_for_this_verification_module)) { - return ( $self -> call_next_authen_method()); - } - - my $result = $self->do_verify; - my $error = $self->{error}; - my $log_error = $self->{log_error}; - - $self->{was_verified} = $result ? 1 : 0; - - if ($self->can("site_fixup")) { - $self->site_fixup; - } - - if ($result) { - $self->write_log_entry("LOGIN OK") if $self->{initial_login}; - #$self->maybe_send_cookie; - #$self->set_params; - } else { - if (defined $log_error) { - $self->write_log_entry("LOGIN FAILED $log_error"); - } - if (defined($error) and $error=~/\S/) { # if error message has a least one non-space character. - - #if (defined($r->param("user")) or defined($r->param("user_id"))) { - if(defined($self->{params}->{user_id})){ - $error = "Your authentication failed. Please try again." - . " Please speak with your instructor if you need help."; - } - - } - - $self->maybe_kill_cookie; - if (defined($error) and $error=~/\S/) { # if error message has a least one non-space character. - return $error; - # MP2 ? $r->notes->set(authen_error => $error) : $r->notes("authen_error" => $error); - } - } - - debug Dumper($self->{params}); - - for my $key (qw/user_id session_key user key error log_error/){ - debug $self->{$key}; - } - - - debug("END VERIFY"); - return $result; -} - -=item was_verified() - -Returns true if verify() returned true the last time it was called. - -=cut - -sub was_verified { - my ($self) = @_; - return 1 if exists $self->{was_verified} and $self->{was_verified}; - return 0; -} - -=item forget_verification() - -Future calls to was_verified() will return false, until verify() is called again and succeeds. - -=cut - -sub forget_verification { - my $self = shift; - $self->{was_verified} = 0; -} - -=back - -=cut - -################################################################################ -# Helper functions (called by verify) -################################################################################ - -sub do_verify { - my $self = shift; - my $ce = $self->{ce}; - my $db = $self->{db}; - - return 0 unless $db; - - return 0 unless $self->get_credentials; - - -# return 0 unless $self->check_user; - - my $practiceUserPrefix = $ce->{practiceUserPrefix}; - if (defined($self->{login_type}) && $self->{login_type} eq "guest"){ - return $self->verify_practice_user; - } else { - return $self->verify_normal_user; - } -} - -## pass all of the parameters as a reference to a has - - -sub get_credentials { - my $self = shift; - my $ce = $self->{ce}; - my $db = $self->{db}; - - - # allow guest login: if the "Guest Login" button was clicked, we find an unused - # practice user and create a session for it. - if ($self->{params}->{login_practice_user}) { - my $practiceUserPrefix = $ce->{practiceUserPrefix}; - # DBFIX search should happen in database - my @guestUserIDs = grep m/^$practiceUserPrefix/, $db->listUsers; - my @GuestUsers = $db->getUsers(@guestUserIDs); - my @allowedGuestUsers = grep { $ce->status_abbrev_has_behavior($_->status, "allow_course_access") } @GuestUsers; - my @allowedGuestUserIDs = map { $_->user_id } @allowedGuestUsers; - - foreach my $userID (@allowedGuestUserIDs) { - if (not $self->unexpired_session_exists($userID)) { - my $newKey = $self->create_session($userID); - $self->{initial_login} = 1; - $self->{user_id} = $userID; - $self->{session_key} = $newKey; - $self->{login_type} = "guest"; - $self->{credential_source} = "none"; - debug("guest user '", $userID. "' key '", $newKey. "'"); - return 1; - } - } - - $self->{log_error} = "no guest logins are available"; - $self->{error} = "No guest logins are available. Please try again in a few minutes."; - return 0; - } - - # my ($cookieUser, $cookieKey, $cookieTimeStamp) = $self->fetchCookie; - - # if (defined $cookieUser and defined $r->param("user") ) { - # if ($cookieUser ne $r->param("user")) { - # croak ("cookieUser = $cookieUser and paramUser = ". $r->param("user") . " are different."); - # } -# I don't understand this next segment. -# If both key and $cookieKey exist then why not just ignore the cookieKey? - -# if (defined $cookieKey and defined $r->param("key")) { -# $self -> {user_id} = $cookieUser; -# $self -> {password} = $r->param("passwd"); -# $self -> {login_type} = "normal"; -# $self -> {credential_source} = "params_and_cookie"; -# $self -> {session_key} = $cookieKey; -# $self -> {cookie_timestamp} = $cookieTimeStamp; -# if ($cookieKey ne $r->param("key")) { -# warn ("cookieKey = $cookieKey and param key = " . $r -> param("key") . " are different, perhaps" -# ." because you opened several windows for the same site and then backed up from a newer one to an older one." -# ." Avoid doing so."); -# $self -> {credential_source} = "conflicting_params_and_cookie"; -# } -# debug("params and cookie user '", $self->{user_id}, "' credential_source = '", $self->{credential_source}, -# "' params and cookie session key = '", $self->{session_key}, "' cookie_timestamp '", $self->{cookieTimeStamp}, "'"); -# return 1; -# } els - -# Use session key for verification -# else use cookieKey for verification -# else use cookie user name but use password provided by request. - - if (defined $self->{params}->{key}) { - $self->{user_id} = $self->{params}->{user}; - $self->{session_key} = $self->{params}->{key}; - $self->{password} = $self->{params}->{password}; - $self->{login_type} = "normal"; - $self->{credential_source} = "params"; - debug("params user '", $self->{user_id}, "' password '", $self->{password}, "' key '", $self->{session_key}, "'"); - return 1; - } - ## don't deal with cookie keys right now. - - # elsif (defined $cookieKey) { - # $self->{user_id} = $cookieUser; - # $self->{session_key} = $cookieKey; - # $self->{cookie_timestamp} = $cookieTimeStamp; - # $self->{login_type} = "normal"; - # $self->{credential_source} = "cookie"; - # debug("cookie user '", $self->{user_id}, "' key '", $self->{session_key}, "' cookie_timestamp '", $self->{cookieTimeStamp}, "'"); - # return 1; - # } else { - # $self -> {user_id} = $cookieUser; - # $self -> {session_key} = $cookieKey; # will be undefined - # $self -> {password} = $r->param("passwd"); - # $self -> {cookie_timestamp} = $cookieTimeStamp; - # $self -> {login_type} = "normal"; - # $self -> {credential_source} = "params_and_cookie"; - # debug("params and cookie user '", $self->{user_id}, "' params and cookie session key = '", - # $self->{session_key}, "' cookie_timestamp '", $self->{cookieTimeStamp}, "'"); - # return 1; - # } - #} - # at least the user ID is available in request parameters - if (defined $self->{params}->{user}) { - $self->{user_id} = $self->{params}->{user}; - $self->{session_key} = $self->{params}->{key}; - $self->{password} = $self->{params}->{password}; - $self->{login_type} = "normal"; - $self->{credential_source} = "params"; - debug Dumper($self->{params}); - debug("params user '", $self->{user_id}, "' password '", $self->{password}, "' key '", $self->{session_key}, "'"); - return 1; - } - - # if (defined $cookieUser) { - # $self->{user_id} = $cookieUser; - # $self->{session_key} = $cookieKey; - # $self->{cookie_timestamp} = $cookieTimeStamp; - # $self->{login_type} = "normal"; - # $self->{credential_source} = "cookie"; - # debug("cookie user '", $self->{user_id}, "' key '", $self->{session_key}, "' cookie_timestamp '", $self->{cookieTimeStamp}, "'"); - # return 1; - # } - - - -} - -# sub check_user { -# my $self = shift; -# my $r = $self->{r}; -# my $ce = $r->{ce}; -# my $db = $r->{db}; -# my $authz = $r->authz; - -# my $user_id = $self->{user_id}; - -# if (defined $user_id and $user_id eq "") { -# $self->{log_error} = "no user id specified"; -# $self->{error} .= $r->maketext("You must specify a user ID."); -# return 0; -# } - -# my $User = $db->getUser($user_id); - -# unless ($User) { -# $self->{log_error} = "user unknown"; -# $self->{error} = $GENERIC_ERROR_MESSAGE; -# return 0; -# } - -# # FIXME "fix invalid status values" used to be here, but it needs to move to $db->getUser - -# unless ($ce->status_abbrev_has_behavior($User->status, "allow_course_access")) { -# $self->{log_error} = "user not allowed course access"; -# $self->{error} = $GENERIC_ERROR_MESSAGE; -# return 0; -# } - -# unless ($authz->hasPermissions($user_id, "login")) { -# $self->{log_error} = "user not permitted to login"; -# $self->{error} = $GENERIC_ERROR_MESSAGE; -# return 0; -# } - -# return 1; -# } - -sub verify_practice_user { - my $self = shift; - my $r = $self->{r}; - my $ce = $r->ce; - - my $user_id = $self->{user_id}; - my $session_key = $self->{session_key}; - - for my $key (qw/user_id session_key/){ - debug $self->{$key}; - } - - my ($sessionExists, $keyMatches, $timestampValid) = $self->check_session($user_id, $session_key, 1); - debug("sessionExists='", $sessionExists, "' keyMatches='", $keyMatches, "' timestampValid='", $timestampValid, "'"); - - if ($sessionExists) { - if ($keyMatches) { - if ($timestampValid) { - return 1; - } else { - $self->{session_key} = $self->create_session($user_id); - $self->{initial_login} = 1; - return 1; - } - } else { - if ($timestampValid) { - my $debugPracticeUser = $ce->{debugPracticeUser}; - if (defined $debugPracticeUser and $user_id eq $debugPracticeUser) { - $self->{session_key} = $self->create_session($user_id); - $self->{initial_login} = 1; - return 1; - } else { - $self->{log_error} = "guest account in use"; - $self->{error} = "That guest account is in use."; - return 0; - } - } else { - $self->{session_key} = $self->create_session($user_id); - $self->{initial_login} = 1; - return 1; - } - } - } else { - $self->{session_key} = $self->create_session($user_id); - $self->{initial_login} = 1; - return 1; - } -} - -sub verify_normal_user { - my $self = shift; - #my $r = $self->{r}; - - my $user_id = $self->{user_id}; - my $session_key = $self->{session_key}; - - for my $key (qw/user user_id session_key key/){ - debug "$key: " . ($self->{$key} || "") - } - - - - my ($sessionExists, $keyMatches, $timestampValid) = $self->check_session($user_id, $session_key, 1); - debug("sessionExists='", $sessionExists, "' keyMatches='", $keyMatches, "' timestampValid='", $timestampValid, "'"); - - if ($sessionExists and $keyMatches and $timestampValid) { - return 1; - } else { - my $auth_result = $self->authenticate; - - if ($auth_result > 0) { - $self->{session_key} = $self->create_session($user_id); - $self->{initial_login} = 1; - return 1; - } elsif ($auth_result == 0) { - $self->{log_error} = "authentication failed"; - $self->{error} = $GENERIC_ERROR_MESSAGE; - return 0; - } else { # ($auth_result < 0) => required data was not present - if ($keyMatches and not $timestampValid) { - $self->{log_error} = "inactivity timeout"; - $self->{error} .= "Your session has timed out due to inactivity. Please log in again."; - } - return 0; - } - } -} - - -sub maybe_send_cookie { - my $self = shift; - my $r = $self->{r}; - my $ce = $r -> {ce}; - - my ($cookie_user, $cookie_key, $cookie_timestamp) = $self->fetchCookie; - - # we send a cookie if any of these conditions are met: - - # (a) a cookie was used for authentication - my $used_cookie = ($self->{credential_source} eq "cookie"); - - # (b) a cookie was sent but not used for authentication, and the - # credentials used for authentication were the same as those in - # the cookie - my $unused_valid_cookie = ($self->{credential_source} ne "cookie" - and defined $cookie_user and $self->{user_id} eq $cookie_user - and defined $cookie_key and $self->{session_key} eq $cookie_key); - - # (c) the user asked to have a cookie sent and is not a guest user. - my $user_requests_cookie = ($self->{login_type} ne "guest" - and $r->param("send_cookie")); - - # (d) session management is done via cookies. - my $session_management_via_cookies = - $ce -> {session_management_via} eq "session_cookie"; - - debug("used_cookie='", $used_cookie, "' unused_valid_cookie='", $unused_valid_cookie, "' user_requests_cookie='", $user_requests_cookie, - "' session_management_via_cookies ='", $session_management_via_cookies, "'"); - - if ($used_cookie or $unused_valid_cookie or $user_requests_cookie or $session_management_via_cookies) { - #debug("Authen::maybe_send_cookie is sending a cookie"); - $self->sendCookie($self->{user_id}, $self->{session_key}); - } else { - $self->killCookie; - } -} - -sub maybe_kill_cookie { - my $self = shift; - #$self->killCookie(@_); -} - - -################################################################################ -# Session key management -################################################################################ - -sub unexpired_session_exists { - my ($self, $userID) = @_; - my $ce = $self->{r}->ce; - my $db = $self->{r}->db; - - my $Key = $db->getKey($userID); # checked - return 0 unless defined $Key; - if (time <= $Key->timestamp()+$ce->{sessionKeyTimeout}) { - # unexpired, but leave timestamp alone - return 1; - } else { - # expired -- delete key - # NEW: no longer delete the key here -- a user re-visiting with a formerly-valid key should - # always get a "session expired" message. formerly, if they i.e. reload the login screen - # the message disappears, which is confusing (i claim ;) - #$db->deleteKey($userID); - return 0; - } -} - -# clobbers any existing session for this $userID -# if $newKey is not specified, a random key is generated -# the key is returned -sub create_session { - my ($self, $userID, $newKey) = @_; - my $ce = $self->{ce}; - my $db = $self->{db}; - - my $timestamp = time; - unless ($newKey) { - my @chars = @{ $ce->{sessionKeyChars} }; - my $length = $ce->{sessionKeyLength}; - - srand; - $newKey = join ("", @chars[map rand(@chars), 1 .. $length]); - } - - my $Key = $db->newKey(user_id=>$userID, key=>$newKey, timestamp=>$timestamp); - # DBFIXME this should be a REPLACE - eval { $db->deleteKey($userID) }; - $db->addKey($Key); - - #if ($ce -> {session_management_via} eq "session_cookie"), - # then the subroutine maybe_send_cookie should send a cookie. - - return $newKey; -} - -# returns ($sessionExists, $keyMatches, $timestampValid) -# if $updateTimestamp is true, the timestamp on a valid session is updated -sub check_session { - my ($self, $userID, $possibleKey, $updateTimestamp) = @_; - my $ce = $self->{ce}; - my $db = $self->{db}; - - my $Key = $db->getKey($userID); # checked - return 0 unless defined $Key; - my $keyMatches = (defined $possibleKey and $possibleKey eq $Key->key); - - my $timestampValid=0; - if ($ce -> {session_management_via} eq "session_cookie" and defined($self->{cookie_timestamp})) { - $timestampValid = (time <= $self -> {cookie_timestamp} + $ce->{sessionKeyTimeout}); - } else { - $timestampValid = (time <= $Key->timestamp()+$ce->{sessionKeyTimeout}); - if ($keyMatches and $timestampValid and $updateTimestamp) { - $Key->timestamp(time); - $db->putKey($Key); - } - } - return (1, $keyMatches, $timestampValid); -} - -sub killSession { - my $self = shift; - - $self -> forget_verification; - if ($self->{ce} -> {session_management_via} eq "session_cookie") { - $self -> killCookie(); - } - - my $userID = $self->{user_id}; - if (defined($userID)) { - $self->{db} -> deleteKey($userID); - } -} - - -################################################################################ -# Utilities -################################################################################ - -sub write_log_entry { - my ($self, $message) = @_; - - my $user_id = defined $self->{user_id} ? $self->{user_id} : ""; - my $login_type = defined $self->{login_type} ? $self->{login_type} : ""; - my $credential_source = defined $self->{credential_source} ? $self->{credential_source} : ""; - - my ($remote_host, $remote_port) = ('',''); - - my $log_msg = "$message user_id=$user_id login_type=$login_type credential_source=$credential_source host=$remote_host port=$remote_port"; - debug("Writing to login log: '$log_msg'.\n"); - writeCourseLog($self->{ce}, "login_log", $log_msg); -} - -1; - diff --git a/webwork3/lib/WeBWorK/GeneralUtils.pm b/webwork3/lib/WeBWorK/GeneralUtils.pm deleted file mode 100644 index 6d93ff196b..0000000000 --- a/webwork3/lib/WeBWorK/GeneralUtils.pm +++ /dev/null @@ -1,447 +0,0 @@ -package WeBWorK::GeneralUtils; -use base qw(Exporter); - -### this is a number of subrotines from the webwork2 version of WeBWorK::Utils - - -use strict; -use warnings; -use DateTime; -use DateTime::TimeZone; -use Date::Parse; -use Date::Format; - - -our @EXPORT = (); -our @EXPORT_OK = qw(parseDateTime decodeAnswers encodeAnswers writeCourseLog writeLog writeTimingLogEntry readDirectory - readFile runtime_use cryptPassword); - - - -################################################################################ -# Date/time processing -################################################################################ - -=head2 Date/time processing - -=over - -=item $dateTime = parseDateTime($string, $display_tz) - -Parses $string as a datetime. If $display_tz is given, $string is assumed to be -in that timezone. Otherwise, the server's timezone is used. The result, -$dateTime, is an integer UNIX datetime (epoch) in the server's timezone. - -=cut - -# This is a modified version of the subroutine of the same name from WeBWorK -# 1.9.05's scripts/FILE.pl (v1.13). It has been modified to understand time -# zones. The time zone specification must appear at the end of the string and be -# preceded by whitespace. The return value is a list consisting of the following -# elements: -# -# ($second, $minute, $hour, $day, $month, $year, $zone) -# -# $second, $minute, $hour, $day, and $month are zero-indexed. $year is the -# number of years since 1900. $zone is a string (hopefully) representing the -# time zone. -# -# Error handling has also been improved. Exceptions are now thrown for errors, -# and more information is given about the nature of errors. -# -sub unformatDateAndTime { - my ($string) = @_; - my $orgString = $string; - - $string =~ s|^\s+||; - $string =~ s|\s+$||; - $string =~ s|at| at |i; ## OK if forget to enter spaces or use wrong case - $string =~ s|AM| AM|i; ## OK if forget to enter spaces or use wrong case - $string =~ s|PM| PM|i; ## OK if forget to enter spaces or use wrong case - $string =~ s|,| at |; ## start translating old form of date/time to new form - - # case where the at is missing: MM/DD/YYYY at HH:MM AMPM ZONE - unformatDateAndTime_error($orgString, "The 'at' appears to be missing.") - if $string =~ m|^\s*[\/\d]+\s+[:\d]+|; - - my ($date, $at, $time, $AMPM, $TZ) = split /\s+/, $string; - - unformatDateAndTime_error($orgString, "The date and/or time appear to be missing.", $date, $time, $AMPM, $TZ) - unless defined $date and defined $at and defined $time; - - # deal with military time - unless ($time =~ /:/) { - { ##bare block for 'case" structure - $time =~ /(\d\d)(\d\d)/; - my $tmp_hour = $1; - my $tmp_min = $2; - if ($tmp_hour eq '00') {$time = "12:$tmp_min"; $AMPM = 'AM';last;} - if ($tmp_hour eq '12') {$time = "12:$tmp_min"; $AMPM = 'PM';last;} - if ($tmp_hour < 12) {$time = "$tmp_hour:$tmp_min"; $AMPM = 'AM';last;} - if ($tmp_hour < 24) { - $tmp_hour = $tmp_hour - 12; - $time = "$tmp_hour:$tmp_min"; - $AMPM = 'PM'; - } - } ##end of bare block for 'case" structure - - } - - # default value for $AMPM - $AMPM = "AM" unless defined $AMPM; - - my ($mday, $mon, $year, $wday, $yday, $sec, $pm, $min, $hour); - $sec=0; - $time =~ /^([0-9]+)\s*\:\s*([0-9]*)/; - $min=$2; - $hour = $1; - unformatDateAndTime_error($orgString, "Hour must be in the range [1,12].", $date, $time, $AMPM, $TZ) - if $hour < 1 or $hour > 12; - unformatDateAndTime_error($orgString, "Minute must be in the range [0-59].", $date, $time, $AMPM, $TZ) - if $min < 0 or $min > 59; - $pm = 0; - $pm = 12 if ($AMPM =~/PM/ and $hour < 12); - $hour += $pm; - $hour = 0 if ($AMPM =~/AM/ and $hour == 12); - $date =~ m|([0-9]+)\s*/\s*([0-9]+)/\s*([0-9]+)|; - $mday =$2; - $mon=($1-1); - unformatDateAndTime_error($orgString, "Day must be in the range [1,31].", $date, $time, $AMPM, $TZ) - if $mday < 1 or $mday > 31; - unformatDateAndTime_error($orgString, "Month must be in the range [1,12].", $date, $time, $AMPM, $TZ) - if $mon < 0 or $mon > 11; - $year=$3; - $wday=""; - $yday=""; - return ($sec, $min, $hour, $mday, $mon, $year, $TZ); -} - -sub unformatDateAndTime_error { - - if (@_ > 2) { - my ($orgString, $error, $date, $time, $AMPM, $TZ) = @_; - $date = "(undefined)" unless defined $date; - $time = "(undefined)" unless defined $time; - $AMPM = "(undefined)" unless defined $AMPM; - $TZ = "(undefined)" unless defined $TZ; - die "Incorrect date/time format \"$orgString\": $error\n", - "Correct format is MM/DD/YY at HH:MM AMPM ZONE\n", - "\tdate = $date\n", - "\ttime = $time\n", - "\tampm = $AMPM\n", - "\tzone = $TZ\n"; - } else { - my ($orgString, $error) = @_; - die "Incorrect date/time format \"$orgString\": $error\n", - "Correct format is MM/DD/YY at HH:MM AMPM ZONE\n"; - } -} - -sub parseDateTime($;$) { - my ($string, $display_tz) = @_; - warn "time zone not defined".caller() unless defined($display_tz); - $display_tz ||= "local"; - $display_tz = verify_timezone($display_tz); - - - # use WeBWorK 1 date parsing routine - my ($second, $minute, $hour, $day, $month, $year, $zone) = unformatDateAndTime($string); - my $zone_str = defined $zone ? $zone : "UNDEF"; - #warn "\tunformatDateAndTime: $second $minute $hour $day $month $year $zone_str\n"; - - # DateTime expects month 1-12, not 0-11 - $month++; - - # Do what Time::Local does to ambiguous years - { - my $ThisYear = (localtime())[5]; # FIXME: should be relative to $string's timezone - my $Breakpoint = ($ThisYear + 50) % 100; - my $NextCentury = $ThisYear - $ThisYear % 100; - $NextCentury += 100 if $Breakpoint < 50; - my $Century = $NextCentury - 100; - my $SecOff = 0; - - if ($year >= 1000) { - # leave alone - } elsif ($year < 100 and $year >= 0) { - $year += ($year > $Breakpoint) ? $Century : $NextCentury; - $year += 1900; - } else { - $year += 1900; - } - } - - my $epoch; - - if (defined $zone and $zone ne "") { - if (DateTime::TimeZone->is_valid_name($zone)) { - #warn "\t\$zone is valid according to DateTime::TimeZone\n"; - - my $dt = new DateTime( - year => $year, - month => $month, - day => $day, - hour => $hour, - minute => $minute, - second => $second, - time_zone => $zone, - ); - #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n"; - - $epoch = $dt->epoch; - #warn "\t\$dt->epoch = $epoch\n"; - } else { - #warn "\t\$zone is invalid according to DateTime::TimeZone, so we ask Time::Zone\n"; - - # treat the date/time as UTC - my $dt = new DateTime( - year => $year, - month => $month, - day => $day, - hour => $hour, - minute => $minute, - second => $second, - time_zone => "UTC", - ); - #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n"; - - # convert to an epoch value - my $utc_epoch = $dt->epoch - or die "Date/time '$string' not representable as an epoch. Get more bits!\n"; - #warn "\t\$utc_epoch = $utc_epoch\n"; - - # get offset for supplied timezone and utc_epoch - my $offset = tz_offset($zone, $utc_epoch) or die "Time zone '$zone' not recognized.\n"; - #warn "\t\$zone is valid according to Time::Zone (\$offset = $offset)\n"; - - #$epoch = $utc_epoch + $offset; - ##warn "\t\$epoch = \$utc_epoch + \$offset = $epoch\n"; - - $dt->subtract(seconds => $offset); - #warn "\t\$dt - \$offset = ", $dt->strftime(DATE_FORMAT), "\n"; - - $epoch = $dt->epoch; - #warn "\t\$epoch = $epoch\n"; - } - } else { - #warn "\t\$zone not supplied, using \$display_tz\n"; - - my $dt = new DateTime( - year => $year, - month => $month, - day => $day, - hour => $hour, - minute => $minute, - second => $second, - time_zone => $display_tz, - ); - #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n"; - - $epoch = $dt->epoch; - #warn "\t\$epoch = $epoch\n"; - } - - return $epoch; -} - - -our $BASE64_ENCODED = 'base64_encoded:'; -# use constant BASE64_ENCODED = 'base64_encoded; -# was not evaluated in the matching and substitution -# statements - - -sub decodeAnswers($) { - my $serialized = shift; - return unless defined $serialized and $serialized; - my $array_ref = eval{ Storable::thaw($serialized) }; - if ($@) { - # My hope is that this next warning is no longer needed since there are few legacy base64 days and the fix seems transparent. - # warn "problem fetching answers -- possibly left over from base64 days. Not to worry -- press preview or submit and this will go away permanently for this question. $@"; - return (); - } else { - return @{$array_ref}; - } -} - -sub encodeAnswers(\%\@) { - my %hash = %{shift()}; - my @order = @{shift()}; - my @ordered_hash = (); - foreach my $key (@order) { - push @ordered_hash, $key, $hash{$key}; - } - return Storable::nfreeze( \@ordered_hash); - -} - - -################################################################################ -# Logging -################################################################################ - -sub writeLog($$@) { - my ($ce, $facility, @message) = @_; - unless ($ce->{webworkFiles}->{logs}->{$facility}) { - warn "There is no log file for the $facility facility defined.\n"; - return; - } - my $logFile = $ce->{webworkFiles}->{logs}->{$facility}; - surePathToFile($ce->{webworkDirs}->{root}, $logFile); - local *LOG; - if (open LOG, ">>", $logFile) { - print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n"; - close LOG; - } else { - warn "failed to open $logFile for writing: $!"; - } -} - -sub writeCourseLog($$@) { - my ($ce, $facility, @message) = @_; - unless ($ce->{courseFiles}->{logs}->{$facility}) { - warn "There is no course log file for the $facility facility defined.\n"; - return; - } - my $logFile = $ce->{courseFiles}->{logs}->{$facility}; - surePathToFile($ce->{courseDirs}->{root}, $logFile); - local *LOG; - if (open LOG, ">>", $logFile) { - print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n"; - close LOG; - } else { - warn "failed to open $logFile for writing: $!"; - } -} - -# $ce - a WeBWork::CourseEnvironment object -# $function - fully qualified function name -# $details - any information, do not use the characters '[' or ']' -# $beginEnd - the string "begin", "intermediate", or "end" -# use the intermediate step begun or completed for INTERMEDIATE -# use an empty string for $details when calling for END -# Information printed in format: -# [formatted date & time ] processID unixTime BeginEnd $function $details -sub writeTimingLogEntry($$$$) { - my ($ce, $function, $details, $beginEnd) = @_; - $beginEnd = ($beginEnd eq "begin") ? ">" : ($beginEnd eq "end") ? "<" : "-"; - writeLog($ce, "timing", "$$ ".time." $beginEnd $function [$details]"); -} - - -sub cryptPassword($) { - my ($clearPassword) = @_; - my $salt = join("", ('.','/','0'..'9','A'..'Z','a'..'z')[rand 64, rand 64]); - my $cryptPassword = crypt($clearPassword, $salt); - return $cryptPassword; -} - - -################################################################################ -# Filesystem interaction -################################################################################ - -=head2 Filesystem interaction - -=over - -=cut - -# Convert Windows and Mac (classic) line endings to UNIX line endings in a string. -# Windows uses CRLF, Mac uses CR, UNIX uses LF. (CR is ASCII 15, LF if ASCII 12) -sub force_eoln($) { - my ($string) = @_; - $string =~ s/\015\012?/\012/g; - return $string; -} - -sub readFile($) { - my $fileName = shift; - local $/ = undef; # slurp the whole thing into one string - open my $dh, "<", $fileName - or die "failed to read file $fileName: $!"; - my $result = <$dh>; - close $dh; - return force_eoln($result); -} - -sub readDirectory($) { - my $dirName = shift; - opendir my $dh, $dirName - or die "Failed to read directory $dirName: $!"; - my @result = readdir $dh; - close $dh; - return @result; -} - -# A very useful macro for making sure that all of the directories to a file have -# been constructed. -sub surePathToFile($$) { - # constructs intermediate directories enroute to the file - # the input path must be the path relative to this starting directory - my $start_directory = shift; - my $path = shift; - my $delim = "/"; - unless ($start_directory and $path ) { - warn "missing directory
surePathToFile start_directory path "; - return ''; - } - # use the permissions/group on the start directory itself as a template - my ($perms, $groupID) = (stat $start_directory)[2,5]; - # warn "&urePathToTmpFile: perms=$perms groupID=$groupID\n"; - - # if the path starts with $start_directory (which is permitted but optional) remove this initial segment - $path =~ s|^$start_directory|| if $path =~ m|^$start_directory|; - - - # find the nodes on the given path - my @nodes = split("$delim",$path); - - # create new path - $path = $start_directory; #convertPath("$tmpDirectory"); - - while (@nodes>1) { # the last node is the file name - $path = $path . shift (@nodes) . "/"; #convertPath($path . shift (@nodes) . "/"); - #FIXME this make directory command may not be fool proof. - unless (-e $path) { - mkdir($path, $perms) - or warn "Failed to create directory $path with start directory $start_directory "; - } - - } - - $path = $path . shift(@nodes); #convertPath($path . shift(@nodes)); - return $path; -} - - -################################################################################ -# Lowlevel thingies -################################################################################ - -# This is like use, except it happens at runtime. You have to quote the module name and put a -# comma after it if you're specifying an import list. Also, to specify an empty import list (as -# opposed to no import list) use an empty arrayref instead of an empty array. -# -# use Xyzzy; => runtime_use "Xyzzy"; -# use Foo qw/pine elm/; => runtime_use "Foo", qw/pine elm/; -# use Foo::Bar (); => runtime_use "Foo::Bar", []; - -sub runtime_use($;@) { - my ($module, @import_list) = @_; - my $package = (caller)[0]; # import into caller's namespace - - my $import_string; - if (@import_list == 1 and ref $import_list[0] eq "ARRAY" and @{$import_list[0]} == 0) { - $import_string = ""; - } else { - # \Q = quote metachars \E = end quoting - $import_string = "import $module " . join(",", map { qq|"\Q$_\E"| } @import_list); - } - eval "package $package; require $module; $import_string"; - die $@ if $@; -} - - - -1; \ No newline at end of file diff --git a/webwork3/lib/WeBWorK3.pm b/webwork3/lib/WeBWorK3.pm new file mode 100755 index 0000000000..ddfa6d87d0 --- /dev/null +++ b/webwork3/lib/WeBWorK3.pm @@ -0,0 +1,179 @@ +#!/usr/bin/env perl + +package WeBWorK3; +use Dancer ':syntax'; +use Dancer::Plugin::Database; +use Data::Dump qw/dd/; +use Path::Class; +use File::Find::Rule; + +set serializer => 'JSON'; + +BEGIN{ + $ENV{WEBWORK_ROOT} = config->{webwork_dir}; +} + + +# link to WeBWorK code libraries +use lib config->{webwork_dir}.'/lib'; +use lib config->{pg_dir}.'/lib'; + +use WeBWorK::CourseEnvironment; +use WeBWorK::DB; +use WeBWorK3::Authen; +# +### note: Routes::Authenication must be passed first +use Utils::Authentication qw/buildSession setCourseEnvironment setCookie/; +use Utils::Convert qw/convertObjectToHash convertArrayOfObjectsToHash/; +use Utils::LibraryUtils qw//; +use Utils::ProblemSets qw/record_results/; +use WeBWorK::DB::Utils qw(global2user); +use WeBWorK::Utils::Tasks qw(fake_user fake_set fake_problem); +use WeBWorK::PG::Local; +use WeBWorK::Constants; + +our $PERMISSION_ERROR = "You don't have the necessary permissions."; + +## the following routes is matched for any URL starting with /courses. It is used to load the +# CourseEnvironment +# +# Note: for this to match before others, make sure this package is loaded before others. +# + + + +any ['get','put','post','delete'] => '/courses/*/**' => sub { + + my ($courseID) = splat; + setCourseEnvironment($courseID); + pass; +}; + +any ['get','post'] => '/renderer/courses/*/**' => sub { + my ($courseID) = splat; + setCourseEnvironment($courseID); + pass; +}; + + + +load 'Routes/Course.pm'; +load 'Routes/Library.pm'; +load 'Routes/ProblemSets.pm'; +load 'Routes/User.pm'; +load 'Routes/Settings.pm'; +load 'Routes/PastAnswers.pm'; + + + + + + +# +#hook 'before' => sub { +# +# for my $key (keys(%{request->params})){ +# my $value = defined(params->{$key}) ? params->{$key} : ''; +# debug($key . " : " . to_dumper($value)); +# } +# +#}; + + +post '/courses/:course_id/login' => sub { + + my $authen = new WeBWorK3::Authen(vars->{ce}); + + $authen->set_params({ + user => params->{user}, + password => params->{password}, + key => params->{session_key} + }); + + my $result = $authen->verify(); + if($result){ + my $key = $authen->create_session(params->{user}); + + session user => params->{user}; + session key => $key; + + my $permission = vars->{db}->getPermissionLevel(session->{user}); + session permission => $permission->{permission}; + session timestamp => time(); + + setCookie(); + + return {session_key=>$key, user=>params->{user},logged_in=>1}; + + } else { + return {logged_in=>0}; + } +}; + + +post '/courses/:course_id/logout' => sub { + + my $deleteKey = vars->{db}->deleteKey(session 'user'); + my $sessionDestroy = session->destroy; + + my $hostname = vars->{ce}->{server_root_url}; + $hostname =~ s/https?:\/\///; + + if ($hostname ne "localhost" && $hostname ne "127.0.0.1") { + cookie "WeBWorKCourseAuthen." . params->{course_id} => "", domain=>$hostname, expires => "-1 hour"; + } else { + cookie "WeBWorKCourseAuthen." . params->{course_id} => "", expires => "-1 hour"; + } + + return {logged_in=>0}; +}; + + + + +get '/app-info' => sub { + return { + appname => config->{appname}, + environment=>config->{environment}, + port=>config->{port}, + content_type=>config->{content_type}, + startup_info=>config->{startup_info}, + server=>config->{server}, + appdir=>config->{appdir}, + template=>config->{template}, + logger=>config->{logger}, + session=>config->{session}, + session_expires=>config->{session_expires}, + session_name=>config->{session_name}, + session_secure=>config->{session_secure}, + session_is_http_only=>config->{session_is_http_only}, + }; +}; + +get '/courses/:course_id/info' => sub { + + setCourseEnvironment(params->{course_id}); + + return { + course_id => params->{course_id}, + webwork_dir => vars->{ce}->{webwork_dir}, + webworkURLs => vars->{ce}->{webworkURLs}, + webworkDirs => vars->{ce}->{webworkDirs} + }; + +}; + + +sub checkCourse { + if (! defined(session->{course})) { + if (defined(params->{course_id})) { + session->{course} = params->{course_id}; + } else { + send_error("The course has not been defined. You may need to authenticate again",401); + } + + } + + var ce => WeBWorK::CourseEnvironment->new({webwork_dir => config->{webwork_dir}, courseName=> session->{course}}); + +} diff --git a/webwork3/lib/WeBWorK3/Authen.pm b/webwork3/lib/WeBWorK3/Authen.pm new file mode 100644 index 0000000000..5cb6c52013 --- /dev/null +++ b/webwork3/lib/WeBWorK3/Authen.pm @@ -0,0 +1,475 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: webwork2/lib/WeBWorK/Authen.pm,v 1.63 2012/06/06 22:03:15 wheeler Exp $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" 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. +################################################################################ + +package WeBWorK3::Authen; + +use base qw (WeBWorK::Authen); +use strict; +use warnings; +#use Carp::Always; +use Dancer ':syntax'; + +use WeBWorK::Utils qw/writeCourseLog runtime_use/; + +our $GENERIC_ERROR_MESSAGE = ""; # define in new + +# +################################################################################# +## Public API +################################################################################# +# +#=head1 FACTORY +# +#=over +# +#=item class($ce, $type) +# +#This is a subclass of the WebWork::Authen class. It overrides methods necessary to run Dancer. +# +#=cut +# +#sub class { +# my ($ce, $type) = @_; +# +# if (exists $ce->{authen}{$type}) { +# if (ref $ce->{authen}{$type} eq "ARRAY") { +# my $authen_type = shift @{$ce ->{authen}{$type}}; +# #debug("ref of authen_type = |" . ref($authen_type) . "|"); +# if (ref ($authen_type) eq "HASH") { +# if (exists $authen_type->{$ce->{dbLayoutName}}) { +# return $authen_type->{$ce->{dbLayoutName}}; +# } elsif (exists $authen_type->{"*"}) { +# return $authen_type->{"*"}; +# } else { +# die "authentication type '$type' in the course environment has no entry for db layout '", $ce->{dbLayoutName}, "' and no default entry (*)"; +# } +# } else { +# return $authen_type; +# } +# } elsif (ref $ce->{authen}{$type} eq "HASH") { +# if (exists $ce->{authen}{$type}{$ce->{dbLayoutName}}) { +# return $ce->{authen}{$type}{$ce->{dbLayoutName}}; +# } elsif (exists $ce->{authen}{$type}{"*"}) { +# return $ce->{authen}{$type}{"*"}; +# } else { +# die "authentication type '$type' in the course environment has no entry for db layout '", $ce->{dbLayoutName}, "' and no default entry (*)"; +# } +# } else { +# return $ce->{authen}{$type}; +# } +# } else { +# die "authentication type '$type' not found in course environment"; +# } +#} +# +#sub call_next_authen_method { +# my ($self,$ce) = shift; +# +# my $user_authen_module = WeBWorK::Authen::class($ce, "user_module"); +# #debug("user_authen_module = |$user_authen_module|"); +# if (!defined($user_authen_module) or ($user_authen_module eq "")) { +# $self->{error} = "No authentication method found for your request. " +# . "If this recurs, please speak with your instructor."; +# $self->{log_error} .= "None of the specified authentication modules could handle the request."; +# return(0); +# } else { +# +# # not sure what to do here without a Request Object +# +# #runtime_use $user_authen_module; +# # my $authen = $user_authen_module->new($r); +# #debug("Using user_authen_module $user_authen_module: $authen\n"); +# # $r->authen($authen); +# +# return; +# } +#} +# +# +#=back +# +#=cut +# +#=head1 CONSTRUCTOR +# +#=over +# +#=item new($r) +# +#Instantiates a new WeBWorK::Authen object for the given WeBWorK::CourseEnvironment ($ce). +# +#=cut +# +sub new { + my ($invocant,$ce) = @_; + my $class = ref($invocant) || $invocant; + my $self = { + ce => $ce, + db => new WeBWorK::DB($ce->{dbLayout}), + params => {} + }; + # weaken $self -> {r}; + #initialize + $GENERIC_ERROR_MESSAGE = "Invalid user ID or password."; + bless $self, $class; + return $self; +} +# +# +## 0 == required data was present, but authentication failed +## -1 == required data was not present (i.e. password missing) +#sub authenticate { +# my $self = shift; +# # my $r = $self->{r}; +# +# my $user_id = $self->{params}->{user}; +# my $password = $self->{params}->{password}; +# +# if (defined $password) { +# return $self->checkPassword($user_id, $password); +# } else { +# return -1; +# } +#} +# +sub set_params { + my ($self,$params) = @_; + $self->{params} = $params; +} +# +################################################################################# +## Password management +################################################################################# +# +sub checkPassword { + my ($self, $userID, $possibleClearPassword) = @_; + + my $Password = $self->{db}->getPassword($userID); # checked + if (defined $Password) { + # check against WW password database + my $possibleCryptPassword = crypt $possibleClearPassword, $Password->password; + if ($possibleCryptPassword eq $Password->password) { + $self->write_log_entry("AUTH WWDB: password accepted"); + return 1; + } else { + if ($self->can("site_checkPassword")) { + $self->write_log_entry("AUTH WWDB: password rejected, deferring to site_checkPassword"); + return $self->site_checkPassword($userID, $possibleClearPassword); + } else { + $self->write_log_entry("AUTH WWDB: password rejected"); + return 0; + } + } + } else { + $self->write_log_entry("AUTH WWDB: user has no password record"); + return 0; + } +} + +sub verify { + + my $self = shift; + + + + if (! ($self-> request_has_data_for_this_verification_module)) { + return ( $self -> call_next_authen_method()); + } + + my $result = $self->do_verify; + my $error = $self->{error}; + my $log_error = $self->{log_error}; + + $self->{was_verified} = $result ? 1 : 0; + + + + if ($self->can("site_fixup")) { + $self->site_fixup; + } + + if ($result) { + $self->write_log_entry("LOGIN OK") if $self->{initial_login}; + } else { + if (defined $log_error) { + $self->write_log_entry("LOGIN FAILED $log_error"); + } + if (defined($error) and $error=~/\S/) { # if error message has a least one non-space character. + + #if (defined($r->param("user")) or defined($r->param("user_id"))) { + if(defined($self->{params}->{user_id})){ + $error = "Your authentication failed. Please try again." + . " Please speak with your instructor if you need help."; + } + + } + + $self->maybe_kill_cookie; + if (defined($error) and $error=~/\S/) { # if error message has a least one non-space character. + return $error; + # MP2 ? $r->notes->set(authen_error => $error) : $r->notes("authen_error" => $error); + } + } + return $result; +} + +# +################################################################################# +## Helper functions (called by verify) +################################################################################# +# +sub do_verify { + my $self = shift; + my $ce = $self->{ce}; + my $db = $self->{db}; + + return 0 unless $db; + + return 0 unless $self->get_credentials; + + +# return 0 unless $self->check_user; + + my $practiceUserPrefix = $ce->{practiceUserPrefix}; + if (defined($self->{login_type}) && $self->{login_type} eq "guest"){ + return $self->verify_practice_user; + } else { + return $self->verify_normal_user; + } + +} + +sub verify_practice_user { + my $self = shift; + my $ce = $self->{ce}; + + my $user_id = $self->{user_id}; + my $session_key = $self->{session_key}; + + my ($sessionExists, $keyMatches, $timestampValid) = $self->check_session($user_id, $session_key, 1); + #debug("sessionExists='". $sessionExists. "' keyMatches='". $keyMatches. "' timestampValid='". $timestampValid. "'"); + + if ($sessionExists) { + if ($keyMatches) { + if ($timestampValid) { + return 1; + } else { + $self->{session_key} = $self->create_session($user_id); + $self->{initial_login} = 1; + return 1; + } + } else { + if ($timestampValid) { + my $debugPracticeUser = $ce->{debugPracticeUser}; + if (defined $debugPracticeUser and $user_id eq $debugPracticeUser) { + $self->{session_key} = $self->create_session($user_id); + $self->{initial_login} = 1; + return 1; + } else { + $self->{log_error} = "guest account in use"; + $self->{error} = "That guest account is in use."; + return 0; + } + } else { + $self->{session_key} = $self->create_session($user_id); + $self->{initial_login} = 1; + return 1; + } + } + } else { + $self->{session_key} = $self->create_session($user_id); + $self->{initial_login} = 1; + return 1; + } +} + +sub verify_normal_user { + my $self = shift; + + my $user_id = $self->{user_id}; + my $session_key = $self->{session_key}; + my ($sessionExists, $keyMatches, $timestampValid) = $self->check_session($user_id, $session_key, 1); + #debug("sessionExists='". $sessionExists. "' keyMatches='".$keyMatches. "' timestampValid='". $timestampValid. "'"); + + if ($sessionExists and $keyMatches and $timestampValid) { + return 1; + } else { + my $auth_result = $self->authenticate; + + if ($auth_result > 0) { + $self->{session_key} = $self->create_session($user_id); + $self->{initial_login} = 1; + return 1; + } elsif ($auth_result == 0) { + $self->{log_error} = "authentication failed"; + $self->{error} = $GENERIC_ERROR_MESSAGE; + return 0; + } else { # ($auth_result < 0) => required data was not present + if ($keyMatches and not $timestampValid) { + $self->{log_error} = "inactivity timeout"; + $self->{error} .= "Your session has timed out due to inactivity. Please log in again."; + } + return 0; + } + } +} + +# +### pass all of the parameters as a reference to a has +# +# +sub get_credentials { + my $self = shift; + my $ce = $self->{ce}; + my $db = $self->{db}; + + + # allow guest login: if the "Guest Login" button was clicked, we find an unused + # practice user and create a session for it. + if ($self->{params}->{login_practice_user}) { + my $practiceUserPrefix = $ce->{practiceUserPrefix}; + # DBFIX search should happen in database + my @guestUserIDs = grep m/^$practiceUserPrefix/, $db->listUsers; + my @GuestUsers = $db->getUsers(@guestUserIDs); + my @allowedGuestUsers = grep { $ce->status_abbrev_has_behavior($_->status, "allow_course_access") } @GuestUsers; + my @allowedGuestUserIDs = map { $_->user_id } @allowedGuestUsers; + + foreach my $userID (@allowedGuestUserIDs) { + if (not $self->unexpired_session_exists($userID)) { + my $newKey = $self->create_session($userID); + $self->{initial_login} = 1; + $self->{user_id} = $userID; + $self->{session_key} = $newKey; + $self->{login_type} = "guest"; + $self->{credential_source} = "none"; + debug("guest user '", $userID. "' key '", $newKey. "'"); + return 1; + } + } + + $self->{log_error} = "no guest logins are available"; + $self->{error} = "No guest logins are available. Please try again in a few minutes."; + return 0; + } + + + if (defined $self->{params}->{key}) { + $self->{user_id} = $self->{params}->{user}; + $self->{session_key} = $self->{params}->{key}; + $self->{password} = $self->{params}->{password}; + $self->{login_type} = "normal"; + $self->{credential_source} = "params"; + debug("params user '", $self->{user_id}, "' password '", $self->{password}, "' key '", $self->{session_key}, "'"); + return 1; + } + + if (defined $self->{params}->{user}) { + $self->{user_id} = $self->{params}->{user}; + $self->{session_key} = $self->{params}->{key}; + $self->{password} = $self->{params}->{password}; + $self->{login_type} = "normal"; + $self->{credential_source} = "params"; + return 1; + } + +} +# +# +################################################################################# +## Session key management +################################################################################# +# +# +## clobbers any existing session for this $userID +## if $newKey is not specified, a random key is generated +## the key is returned +sub create_session { + my ($self, $userID, $newKey) = @_; + my $ce = $self->{ce}; + my $db = $self->{db}; + + my $timestamp = time; + unless ($newKey) { + my @chars = @{ $ce->{sessionKeyChars} }; + my $length = $ce->{sessionKeyLength}; + + srand; + $newKey = join ("", @chars[map rand(@chars), 1 .. $length]); + } + + my $Key = $db->newKey(user_id=>$userID, key=>$newKey, timestamp=>$timestamp); + # DBFIXME this should be a REPLACE + eval { $db->deleteKey($userID) }; + $db->addKey($Key); + + #if ($ce -> {session_management_via} eq "session_cookie"), + # then the subroutine maybe_send_cookie should send a cookie. + + return $newKey; +} + +## returns ($sessionExists, $keyMatches, $timestampValid) +## if $updateTimestamp is true, the timestamp on a valid session is updated +sub check_session { + my ($self, $userID, $possibleKey, $updateTimestamp) = @_; + my $ce = $self->{ce}; + my $db = $self->{db}; + my $Key = $db->getKey($userID); # checked + + return 0 unless defined $Key; + my $keyMatches = (defined $possibleKey and $possibleKey eq $Key->key); + + my $timestampValid=0; + if ($ce -> {session_management_via} eq "session_cookie" and defined($self->{cookie_timestamp})) { + $timestampValid = (time <= $self -> {cookie_timestamp} + $ce->{sessionKeyTimeout}); + } else { + $timestampValid = (time <= $Key->timestamp()+$ce->{sessionKeyTimeout}); + if ($keyMatches and $timestampValid and $updateTimestamp) { + $Key->timestamp(time); + $db->putKey($Key); + } + } + return (1, $keyMatches, $timestampValid); +} + +sub maybe_kill_cookie { + my $self = shift; + #$self->killCookie(@_); +} + + +# +# +################################################################################# +## Utilities +################################################################################# +# +sub write_log_entry { + my ($self, $message) = @_; + + my $user_id = defined $self->{user_id} ? $self->{user_id} : ""; + my $login_type = defined $self->{login_type} ? $self->{login_type} : ""; + my $credential_source = defined $self->{credential_source} ? $self->{credential_source} : ""; + + my ($remote_host, $remote_port) = ('',''); + + my $log_msg = "$message user_id=$user_id login_type=$login_type credential_source=$credential_source host=$remote_host port=$remote_port"; + + writeCourseLog($self->{ce}, "login_log", $log_msg); +} + +1; + diff --git a/webwork3/lib/WeBWorK/PG/Local.pm b/webwork3/lib/WeBWorK3/PG/Local.pm similarity index 94% rename from webwork3/lib/WeBWorK/PG/Local.pm rename to webwork3/lib/WeBWorK3/PG/Local.pm index e177d6a009..735bc01149 100644 --- a/webwork3/lib/WeBWorK/PG/Local.pm +++ b/webwork3/lib/WeBWorK3/PG/Local.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ # $CVSHeader: webwork2/lib/WeBWorK/PG/Local.pm,v 1.28 2009/10/17 15:50:33 apizer Exp $ # # This program is free software; you can redistribute it and/or modify it under @@ -14,8 +14,8 @@ # Artistic License for more details. ################################################################################ -package WeBWorK::PG::Local; -use base qw(WeBWorK::PG); +package WeBWorK3::PG::Local; +use base qw(WeBWorK::PG::Local); =head1 NAME @@ -38,41 +38,12 @@ the WeBWorK::PG module for information about the API. use strict; use warnings; use WeBWorK::Constants; -use Dancer; +#use Dancer; use File::Path qw(rmtree); use WeBWorK::PG::Translator; -use WeBWorK::GeneralUtils qw(readFile writeTimingLogEntry); +use WeBWorK::Utils qw(readFile writeTimingLogEntry); use WeBWorK::Debug; - -# Problem processing will time out after this number of seconds. -use constant TIMEOUT => $WeBWorK::PG::Local::TIMEOUT || 10; - -BEGIN { - # This safe compartment is used to read the large macro files such as - # PG.pl, PGbasicmacros.pl and PGanswermacros and cache the results so that - # future calls have preloaded versions of these large files. This saves a - # significant amount of time. - $WeBWorK::PG::Local::safeCache = new WWSafe; -} - -sub alarm_handler { - my $msg = "Timeout after processing this problem for ". TIMEOUT. " seconds. Check for infinite loops in problem source.\n"; - warn $msg; - CORE::die $msg; -} - -sub new { - my $invocant = shift; - local $SIG{ALRM} = \&alarm_handler; - alarm TIMEOUT; - my $result = eval { $invocant->new_helper(@_) }; - alarm 0; - die $@ if $@; - return $result; -} - - sub new_helper { my $invocant = shift; my $class = ref($invocant) || $invocant; @@ -90,7 +61,7 @@ sub new_helper { ) = @_; - debug("in local.pm"); + # write timing log entry # writeTimingLogEntry($ce, "WeBWorK::PG::new", @@ -286,7 +257,7 @@ sub new_helper { head_text => "", post_header_text => "", body_text => < {}, diff --git a/webwork3/public/css/course-manager.css b/webwork3/public/css/course-manager.css index 3a198640c7..8802a6d0ee 100644 --- a/webwork3/public/css/course-manager.css +++ b/webwork3/public/css/course-manager.css @@ -11,7 +11,9 @@ a#view-header i {margin-left:1ex;} #page-header h2 {font-size: 24px; line-height: 28px; margin: 0px; display: inline-block;} +/* check mark for problem lists */ +.mark-correct-btn { color: darkgreen } .help-hidden {display: none;} @@ -76,7 +78,7 @@ label.checklist {display: inline; margin-left: 8px;} /* #probSetList {margin:0;padding:0px; height:100%; overflow-y: auto;} */ /*.prob-set-container {overflow-y: scroll; width: 100%;}*/ -.prob-value {width: 20px;} +.prob-value, .max-attempts {width: 30px;} .loading {background-color: orange;} @@ -101,6 +103,27 @@ border-radius: 5px;} border: 1px dashed black; background: lightgray; } +/* Problem Set Deatils styles */ + +.border-around-table { + border: 1px lightgray solid; + border-radius: 4px; + padding: 4px; + margin-bottom: 10px; +} + +#customize-problem-set-controls { + margin-bottom: 4px +} + +.rounded-border { + border: 1px lightgray solid; + border-radius: 4px; + margin-top: 10px; + padding: 5px; +} + + /* Classlist Manager styles */ #man_student_table {border: 1px solid black; border-collapse: collapse; width: 95%; } @@ -142,7 +165,9 @@ table#sTable select {width: 125px;} #users_table th { background: #E5E5E5; text-align: left; font-size: 90%; padding: 5px;} #users_table td {font-size: 90%;} -label {font-weight: bold; display: inline; margin-right: 20px;} +/* This needs to be changed. + +label {font-weight: bold; display: inline; margin-right: 20px;} */ #mainActionMenu {width: 200px;} #testButton {margin-left: 25px; margin-right: 25px;} #selectalldiv {display: inline; margin-left: 25px; margin-right: 25px;} diff --git a/webwork3/public/css/webwork.css b/webwork3/public/css/webwork.css index 00595bffd8..8c2a5dcb22 100644 --- a/webwork3/public/css/webwork.css +++ b/webwork3/public/css/webwork.css @@ -86,6 +86,7 @@ border-top: solid 1px rgb(220, 220, 220);} #calendar-table {background: lightgray; min-width: 90%;} #calendar-table td {border: 1px solid black; width: 13%; vertical-align: top;} +#calendar-table th {text-align: center} .calendar-day {z-index: 5; height: 100px;} diff --git a/webwork3/public/js/apps/CourseManager/CourseManager.js b/webwork3/public/js/apps/CourseManager/CourseManager.js index cca5f69b38..49383b8232 100644 --- a/webwork3/public/js/apps/CourseManager/CourseManager.js +++ b/webwork3/public/js/apps/CourseManager/CourseManager.js @@ -83,17 +83,14 @@ var CourseManager = WebPage.extend({ startManager: function () { var self = this; this.navigationBar.setLoginName(this.session.user); - - // put all of the dates in the problem sets in a better data structure for calendar rendering. - this.buildAssignmentDates(); - this.setMainViewList(new MainViewList({settings: this.settings, users: this.users, - problemSets: this.problemSets, eventDispatcher: this.eventDispatcher, parent: this})); + + this.setMainViewList(new MainViewList({settings: this.settings, users: this.users, + problemSets: this.problemSets, eventDispatcher: this.eventDispatcher})); // set up some of the main views with additional information. - this.mainViewList.getView("calendar") - .set({assignmentDates: this.assignmentDateList, viewType: "instructor", calendarType: "month"}) + this.mainViewList.getView("calendar").set({viewType: "instructor", calendarType: "month"}) .on("calendar-change",self.updateCalendar); this.mainViewList.getView("problemSetsManager").set({assignmentDates: this.assignmentDateList}); @@ -105,16 +102,6 @@ var CourseManager = WebPage.extend({ // not sure why this is needed. //config.timezone = this.settings.find(function(v) { return v.get("var")==="timezone"}).get("value"); - - // this will automatically save (sync) any change made to a problem set. - this.problemSets.on("change",function(_set){ - _set.save(); - }) - - // The following is useful in many different views, so is defined here. - // It adjusts dates to ensure that they aren't illegal. - - this.problemSets.on("change:due_date change:reduced_scoring_date change:open_date change:answer_date",this.setDates); this.navigationBar.on({ "stop-acting": this.stopActing, @@ -173,23 +160,6 @@ var CourseManager = WebPage.extend({ } }); - }, - // This travels through all of the assignments and determines the days that assignment dates fall - buildAssignmentDates: function () { - var self = this; - this.assignmentDateList = new AssignmentDateList(); - this.problemSets.each(function(_set){ - self.assignmentDateList.add(new AssignmentDate({type: "open", problemSet: _set, - date: moment.unix(_set.get("open_date")).format("YYYY-MM-DD")})); - self.assignmentDateList.add(new AssignmentDate({type: "due", problemSet: _set, - date: moment.unix(_set.get("due_date")).format("YYYY-MM-DD")})); - self.assignmentDateList.add(new AssignmentDate({type: "answer", problemSet: _set, - date: moment.unix(_set.get("answer_date")).format("YYYY-MM-DD")})); - if(parseInt(_set.get("reduced_scoring_date"))>0) { - self.assignmentDateList.add(new AssignmentDate({type: "reduced-scoring", problemSet: _set, - date: moment.unix(_set.get("reduced_scoring_date")).format("YYYY-MM-DD")}) ); - } - }); } }); diff --git a/webwork3/public/js/apps/CourseManager/config.json b/webwork3/public/js/apps/CourseManager/config.json index 619d9d1508..04d9318458 100644 --- a/webwork3/public/js/apps/CourseManager/config.json +++ b/webwork3/public/js/apps/CourseManager/config.json @@ -2,7 +2,7 @@ "main_views": [ { "name": "Calendar", - "path": "main-views/AssignmentCalendar", + "path": "main-views/CourseCalendar", "icon": "fa fa-calendar fa-lg", "default_sidebar": "problemSets", "other_sidebars": ["problemSets", "userlist"], diff --git a/webwork3/public/js/apps/CourseManager/main-views/ClasslistView.js b/webwork3/public/js/apps/CourseManager/main-views/ClasslistView.js index 15b6782466..676f81b26b 100644 --- a/webwork3/public/js/apps/CourseManager/main-views/ClasslistView.js +++ b/webwork3/public/js/apps/CourseManager/main-views/ClasslistView.js @@ -23,9 +23,10 @@ var ClasslistView = MainView.extend({ self.state.set("man_user_modal_open",false); self.render(); // for some reason the checkboxes don't stay checked. }) + this.addStudentFileView.on("modal-closed",this.render); + this.tableSetup(); - this.users.on({"add": this.addUser,"change": this.changeUser,"sync": this.syncUserMessage, "remove": this.removeUser}); @@ -293,7 +294,8 @@ var ClasslistView = MainView.extend({ stickit_options: {events: ['blur']}}, {name: "Permission", key: "permission", classname: "permission", datatype: "string", search_value: function(model){ - return _(config.permissions).findWhere({value: ""+model.get("permission")}).label; // the ""+ is needed to stringify the permission level + var obj = _(config.permissions).findWhere({value: ""+model.get("permission")}); + return obj.label || ""; // the ""+ is needed to stringify the permission level }, stickit_options: { selectOptions: { collection: config.permissions }} }]; @@ -324,7 +326,9 @@ var ClasslistView = MainView.extend({ success: function(data){ _(data).each(function(st){ var user = self.users.findWhere({user_id: st.user_id}); - user.set("logged_in",st.logged_in); + if(user){ + user.set("logged_in",st.logged_in); + } }) }}); @@ -518,9 +522,7 @@ var AddStudentFileView = ModalView.extend({ //var str = util.CSVToHTMLTable(content,headers); //var arr = util.CSVToHTMLTable(content,headers); var arr = $.csv.toArrays(content); - - console.log(arr); - + var tmpl = _.template($("#imported-from-file-table").html()); $("#studentTable").html(tmpl({array: arr, headers: headers})); @@ -655,7 +657,7 @@ var AddStudentFileView = ModalView.extend({ this.$("input.selRow:checked").map(function(i,v) { return $(v).closest("tr").children(".column" + colNumber);}).each(function(i,cell){ var value = $(cell).html().trim(), errorMessage = self.model.preValidate(changedProperty,value); - if ((errorMessage !== "") && (errorMessage !== false)) { + if (!_.isEmpty(errorMessage)) { self.$(".error-pane").removeClass("hidden") self.$(".error-pane-text").html("Error for the " + headerName + " with value " + value + ": " + errorMessage) self.$(".error-pane").show("slow"); diff --git a/webwork3/public/js/apps/CourseManager/main-views/CourseCalendar.js b/webwork3/public/js/apps/CourseManager/main-views/CourseCalendar.js new file mode 100644 index 0000000000..73ef75dbb6 --- /dev/null +++ b/webwork3/public/js/apps/CourseManager/main-views/CourseCalendar.js @@ -0,0 +1,52 @@ +define(['backbone','views/MainView','views/AssignmentCalendar','moment'], + function(Backbone,MainView,AssignmentCalendar,moment){ +var CourseCalendar = MainView.extend({ + initialize: function (options) { + var self = this; + _(this).bindAll("render"); + MainView.prototype.initialize.call(this,options); + this.calendar = new AssignmentCalendar(_.extend({},options,this.state.attributes)); + this.state.on("change:reduced_scoring_date change:answer_date change:due_date change:open_date", + this.calendar.showHideAssigns); + this.state.on("change",this.render); + this.calendar.state.on("change",function (){ + self.state.set(self.calendar.state.changed); + }); + }, + render: function(){ + this.calendar.setElement(this.$el).render(); + // set up the calendar to scroll correctly + var navbarHeight = $(".navbar-fixed-top").outerHeight(true); + var footerHeight = $(".navbar-fixed-bottom").outerHeight(true); + var buttonRow = $(".calendar-button-row").outerHeight(true); + this.$(".calendar-container").height($(window).height()-navbarHeight - buttonRow-footerHeight); + }, + getDefaultState: function () { + var firstOfMonth = moment(this.date||moment()).date(1) + , firstDay = moment(firstOfMonth).subtract(firstOfMonth.date(1).day(),"days"); + return { + answer_date: true, + due_date: true, + reduced_scoring_date: true, + open_date: true, + first_day: firstDay.format("YYYY-MM-DD"), + calendar_type: "month" + }; + }, + set: function(options) { + this.calendar.set(options); + return this; + }, + events: function(){ + if(this.calendar){ + return this.calendar.events(); + } + } + + + + +}); + +return CourseCalendar; +}); \ No newline at end of file diff --git a/webwork3/public/js/apps/CourseManager/main-views/LibraryBrowser.js b/webwork3/public/js/apps/CourseManager/main-views/LibraryBrowser.js index 9da5a12e51..cda3676ad1 100644 --- a/webwork3/public/js/apps/CourseManager/main-views/LibraryBrowser.js +++ b/webwork3/public/js/apps/CourseManager/main-views/LibraryBrowser.js @@ -29,7 +29,7 @@ function(Backbone, _,TabbedMainView,LibrarySubjectView,LibraryDirectoryView, Lib }; options.views.setDefinition.tabName = "Set Defn. files"; TabbedMainView.prototype.initialize.call(this,options); - }, + }, changeTab: function(options){ TabbedMainView.prototype.changeTab.apply(this,[options]); if(this.sidebar){ diff --git a/webwork3/public/js/apps/CourseManager/main-views/ProblemSetDetailView.js b/webwork3/public/js/apps/CourseManager/main-views/ProblemSetDetailView.js index 21b7108b62..0060193581 100644 --- a/webwork3/public/js/apps/CourseManager/main-views/ProblemSetDetailView.js +++ b/webwork3/public/js/apps/CourseManager/main-views/ProblemSetDetailView.js @@ -2,29 +2,34 @@ * This is the ProblemSetDetailView. The view contains the interface to all of the * details of a given homework set including the changing of HWSet properties and assigning of users. * - * One must pass a ProblemSet as a model to this. + * The ProblemSetDeatilsView is a TabbedMainView and contains the other TabViews (DetailsView, ShowProblemsView, AssignUsersView, + * CustomizeUsersView). * **/ -define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/TabView','views/ProblemSetView', - 'models/ProblemList','views/CollectionTableView','models/ProblemSet','models/UserSetList','sidebars/ProblemListOptionsSidebar', - 'config','moment','bootstrap'], +define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/TabView', + 'views/ProblemSetView', 'models/ProblemList','views/CollectionTableView','models/ProblemSet', + 'models/UserSetList','sidebars/ProblemListOptionsSidebar','views/AssignmentCalendar', + 'models/ProblemSetList','models/SetHeader','models/Problem', + 'apps/util','config','moment','bootstrap'], function(Backbone, _,TabbedMainView,MainView,TabView,ProblemSetView,ProblemList,CollectionTableView,ProblemSet, - UserSetList,ProblemListOptionsSidebar, config,moment){ + UserSetList,ProblemListOptionsSidebar, AssignmentCalendar,ProblemSetList,SetHeader,Problem,util,config,moment){ var ProblemSetDetailsView = TabbedMainView.extend({ className: "set-detail-view", messageTemplate: _.template($("#problem-sets-manager-messages-template").html()), initialize: function(options){ var self = this; - var opts = _(options).pick("users","settings","eventDispatcher"); + var opts = _(options).pick("users","settings","eventDispatcher","problemSets"); this.views = options.views = { propertiesView : new DetailsView(opts), - problemsView : new ShowProblemsView(_.extend({messageTemplate: this.messageTemplate, parent: this},opts)), + problemsView : new ShowProblemsView(_.extend({messageTemplate: this.messageTemplate, + parent: this},opts)), usersAssignedView : new AssignUsersView(opts), - customizeUserAssignView : new CustomizeUserAssignView(opts) + customizeUserAssignView : new CustomizeUserAssignView(opts), + setHeaderView: new SetHeadersView(opts) }; this.views.problemsView.on("page-changed",function(num){ self.eventDispatcher.trigger("save-state"); @@ -35,8 +40,8 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ TabbedMainView.prototype.initialize.call(this,options); this.state.on("change:set_id", function() { self.changeProblemSet(self.state.get("set_id")); - }) - + }); + }, bindings: { ".problem-set-name": {observe: "set_id", selectOptions: { @@ -49,9 +54,6 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ render: function(){ TabbedMainView.prototype.render.call(this); this.stickit(this.state,this.bindings); - if(this.state.get("set_id")){ - this.changeProblemSet(this.state.get("set_id")); - } }, getHelpTemplate: function () { switch(this.state.get("tab_name")){ @@ -79,19 +81,28 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ "undo-problem-delete": function(){ this.views.problemsView.problemSetView.undoDelete(); this.views.problemsView.problemSetView.updateNumProblems() - if(this.views.problemsView.problemSetView.undoStack.length==0 && - this.sidebar instanceof ProblemListOptionsSidebar){ - this.sidebar.$(".undo-delete-button").attr("disabled","disabled"); - } + }, + "add-prob-from-group": function(group_name) { + this.problemSet.addProblem(new Problem({source_file: "group:" + group_name})); } }, + getDefaultState: function () { + return _.extend({set_id: ""}, TabbedMainView.prototype.getDefaultState.apply(this)); + }, changeProblemSet: function (setName) { var self = this; + if(_.isUndefined(setName) || setName == ""){ + this.views.propertiesView.setProblemSet(); + return; + } this.state.set("set_id",setName); this.problemSet = this.problemSets.findWhere({set_id: setName}); _(this.views).chain().keys().each(function(view){ - self.views[view].setProblemSet(self.problemSet); + self.views[view].unstickit(); + if(! _.isUndefined(self.problemSet)){ + self.views[view].setProblemSet(self.problemSet); + } }); this.views.problemsView.currentPage = 0; // make sure that the problems start on a new page. this.loadProblems(); @@ -100,6 +111,9 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ }, loadProblems: function () { var self = this; + if(_.isUndefined(this.problemSet)){ + return; + } if(this.problemSet.get("problems")){ // have the problems been fetched yet? this.views.problemsView.set({problems: this.problemSet.get("problems"), problemSet: this.problemSet}); @@ -119,10 +133,11 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ _userSetList.on( { change: function(_userSet){ + console.log(_userSet.changed); _userSet.changingAttributes=_.pick(_userSet._previousAttributes,_.keys(_userSet.changed)); _userSet.save(); }, - sync: function(_userSet){ // note: this was just copied from HomeworkManager.js perhaps a common place for this + sync: function(_userSet){ // note: this was just copied from ProblemSetsManager.js perhaps a common place for this _(_userSet.changingAttributes||{}).chain().keys().each(function(key){ var _old = key.match(/date$/) ? moment.unix(_userSet.changingAttributes[key]).format("MM/DD/YYYY [at] hh:mmA") @@ -132,8 +147,10 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ : _userSet.get(key); self.eventDispatcher.trigger("add-message",{type: "success", short: self.messageTemplate({type:"set_saved",opts:{setname:_userSet.get("set_id")}}), - text: self.messageTemplate({type:"set_saved_details",opts:{setname:_userSet.get("set_id"),key: key, - oldValue: _old, newValue: _new}})}); + text: self.messageTemplate({type:"set_saved_details", + opts:{setname:_userSet.get("set_id"),key: key, + oldValue: _old, newValue: _new}})}); + _set.changingAttributes = _(_set.changingAttributes).omit(key); }); } }); // _userSetList.on @@ -145,46 +162,84 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ tabName: "Set Details", initialize: function (options) { var self = this; - _.bindAll(this,'render','setProblemSet',"showHideReducedScoringDate"); - this.users = options.users; - this.settings = options.settings; + _.bindAll(this,'render','setProblemSet',"showHideReducedScoringDate","showHideGateway"); + _(this).extend(_(options).pick("users","settings","problemSets")); TabView.prototype.initialize.apply(this,[options]); + this.model = this.problemSets.findWhere({set_id: this.tabState.get("set_id")}); this.tabState.on("change:show_time",function (val){ self.showTime(self.tabState.get("show_time")); - self.stickit(); + if(self.model){ + self.stickit(); + } // gets rid of the line break for showing the time in this view. - $('span.time-span').children('br').attr("hidden",true) - - }); + self.$('span.time-span').children('br').attr("hidden",true) + }).on("change:show_calendar",function(){ + self.showCalendar(self.tabState.get("show_calendar")); + }) + // this sets up a problem set list containing only the current ProblemSet and builds a calendar. + this.calendarProblemSets = new ProblemSetList([],{dateSettings: util.pluckDateSettings(this.settings)}); + this.calendar = new AssignmentCalendar({users: this.users,settings: this.settings, + problemSets: this.calendarProblemSets}); + this.calendar.on("calendar-change",function() { + self.tabState.set({first_day: self.calendar.state.get("first_day")}); + self.showHideReducedScoringDate(); + }) }, render: function(){ if(this.model){ + if(this.model.get("assignment_type") == "jitar"){ + this.$el.html($("#assign-type-not-supported").html()); + return; + } this.$el.html($("#set-properties-tab-template").html()); - this.showHideReducedScoringDate(); this.showTime(this.tabState.get("show_time")); - this.$(".show-time-toggle").prop("checked",this.tabState.get("show_time")); + this.showCalendar(this.tabState.get("show_calendar")); + this.showHideGateway(); + util.changeClass({state: this.tabState.get("show_calendar"), add_class: "hidden",els: this.$(".hideable")}); + util.changeClass({state: this.tabState.get("show_calendar"), remove_class: "hidden", els: this.$(".calendar-row")}); + + this.showHideReducedScoringDate(); this.stickit(); // gets rid of the line break for showing the time in this view. $('span.time-span').children('br').attr("hidden",true) - + this.model.on("change:assignment_type",this.showHideGateway); + } else { + this.$el.html(""); } + return this; }, events: { "click .assign-all-users": "assignAllUsers", - "change .show-time-toggle": function(evt){ - this.tabState.set("show_time",$(evt.target).prop("checked")); + "click .show-time-toggle": function(evt){ + this.tabState.set("show_time",!this.tabState.get("show_time")); + }, + "click .show-calendar-toggle": function(evt){ + this.tabState.set("show_calendar",!this.tabState.get("show_calendar")); + }, + "keyup .input-blur": function(evt){ + if(evt.keyCode == 13) { $(evt.target).blur()} }, }, assignAllUsers: function(){ this.model.set({assigned_users: this.users.pluck("user_id")}); }, setProblemSet: function(_set) { + if(_.isUndefined(_set)){ + this.model = undefined; + return; + } var self = this; - this.model = _set; + this.model = _set; + this.tabState.set("set_id",this.model.get("set_id")); if(this.model){ this.model.on("change:enable_reduced_scoring",this.render); } + this.model.on("sync",function(){ // pstaab: can we integrate this into the stickit handler code in config.js ? + // gets rid of the line break for showing the time in this view. + self.$('span.time-span').children('br').attr("hidden",true); + _.delay(self.showHideReducedScoringDate,100); // hack to get reduced scoring to be hidden. + }); return this; }, bindings: { @@ -195,43 +250,227 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ ".prob-set-visible": "visible", ".reduced-scoring": "enable_reduced_scoring", ".reduced-scoring-date": "reduced_scoring_date", + ".hide-hint": "hide_hint", + ".num-problems": { observe: "problems", onGet:function(value,options) { + return value.length; + }}, + "#set-type": {observe: "assignment_type", selectOptions: { + collection: [{label: "Homework", value: "default"}, + {label: "Gateway/Quiz",value: "gateway"}, + {label: "Proctored Gateway/Quiz", value: "proctored_gateway"}]}}, ".users-assigned": { observe: "assigned_users", onGet: function(value, options){ return value.length + "/" +this.users.size();} - } + }, + ".version-time-limit": "version_time_limit", + ".time-limit-cap": "time_limit_cap", + ".attempts-per-version": "attempts_per_version", + ".time-interval": "time_interval", + ".version-per-interval": "version_per_interval", + ".problem-random-order": "problem_randorder", + ".problems-per-page": "problems_per_page", + ".pg-password": "pg_password", + // I18N + ".hide-score": {observe: ["hide_score","hide_score_by_problem"], selectOptions: { + collection: [{label: "Yes", value: "N:"},{label: "No", value: "Y:N"}, + {label: "Only After Set Answer Date", value: "BeforeAnswerDate:N"}, + {label: "Totals only (not problem scores)", value: "Y:Y"}, + {label: "Totals only, only after answer date", value: "BeforeAnswerDate:Y"}]}, + onGet: function(values){ return values.join(":"); }, + onSet: function(val) { return val.split(":");} + }, + ".hide-work": {observe: "hide_work", selectOptions: { + // are these labels correct? + collection: [{label:"Yes",value: "N"},{label: "No", value: "Y"}, + {label: "Only After Set Answer Date", value: "BeforeAnswerDate"}]}} + }, + showHideGateway: function () { + var type = this.model.get("assignment_type"); + util.changeClass({state: type =="gateway" || type == "proctored_gateway", + els: this.$(".gateway-row"),remove_class: "hidden"}); + util.changeClass({state: type=="gateway" || type == "default", + els: this.$(".pg-row"),add_class:"hidden"}); }, showHideReducedScoringDate: function(){ - if(this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}") && - this.model.get("enable_reduced_scoring")) { // show reduced credit field - this.$(".reduced-scoring-date").closest("tr").removeClass("hidden"); + if(typeof(this.model)==="undefined"){ return;} + util.changeClass({state: this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}"), + remove_class: "hidden", + els: this.$(".reduced-scoring-date,.reduced-scoring").closest("tr")}); + util.changeClass({ state: this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}") && + this.model.get("enable_reduced_scoring"), els: this.$(".reduced-scoring-date").closest("tr"), + remove_class: "hidden"}); + if(this.tabState.get("show_calendar")){ + util.changeClass({state: true, els: this.$(".reduced-scoring-date").closest("tr"), + add_class: "hidden"}); + util.changeClass({state: this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}") && + this.model.get("enable_reduced_scoring"), els: this.$(".assign-reduced-scoring"), + remove_class: "hidden"}); + } + if(this.model.get("enable_reduced_scoring")){ // fill in a reduced_scoring_date if the field is empty or 0. + // I think this should go into the ProblemSet model upon either parsing or creation. if(this.model.get("reduced_scoring_date")=="" || this.model.get("reduced_scoring_date")==0){ var rcDate = moment.unix(this.model.get("due_date")) .subtract(this.settings.getSettingValue("pg{ansEvalDefaults}{reducedScoringPeriod}"),"minutes"); this.model.set({reduced_scoring_date: rcDate.unix()}); } - } else { - this.$(".reduced-scoring-date").closest("tr").addClass("hidden"); - } - if(this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}")){ - this.$(".reduced-scoring").closest("tr").removeClass("hidden") - } else { - this.$(".reduced-scoring").closest("tr").addClass("hidden") - } + } }, showTime: function(_show){ - if(_show){ - this.$(".open-date,.due-date,.reduced-scoring-date,.answer-date") - .addClass("edit-datetime-showtime").removeClass("edit-datetime"); - } else { - this.$(".open-date,.due-date,.reduced-scoring-date,.answer-date") - .removeClass("edit-datetime-showtime").addClass("edit-datetime"); + this.tabState.set("show_time",_show); + // hide or show the date rows in the table + util.changeClass({state: _show, remove_class: "edit-datetime", add_class: "edit-datetime-showtime", + els: this.$(".open-date,.due-date,.reduced-scoring-date,.answer-date")}); + // change the button text + this.$(".show-time-toggle").button(_show?"hide":"reset"); + + }, + showCalendar: function(_show){ + var self = this; + this.tabState.set("show_calendar",_show); + // change the button text + this.$(".show-calendar-toggle").button(_show?"hide":"reset"); + util.changeClass({state: this.tabState.get("show_calendar"), add_class: "hidden",els: this.$(".hideable")}); + util.changeClass({state: this.tabState.get("show_calendar"), remove_class: "hidden", els: this.$(".calendar-row")}); + this.showHideReducedScoringDate(); + if(! _show) return; + if(typeof(this.model)==="undefined") return; + this.calendarProblemSets.reset(this.problemSets.where({set_id: this.model.get("set_id")})); + var assignmentDateList = util.buildAssignmentDates(this.calendarProblemSets); + var first_day = this.tabState.get("first_day"); + if(! moment(this.tabState.get("first_day")).isValid()){ + var open_date = moment.unix(this.model.get("open_date")) + first_day = open_date.subtract(open_date.day(),"days"); } + this.calendar.set({assignmentDates: assignmentDateList,first_day: first_day}) + .setElement(this.$(".calendar-cell")).render(); + this.problemSets.on("change",function(m){ + self.calendarProblemSets.findWhere({set_id: m.get("set_id")}).set(m.changed); + self.calendar.render(); + }); + }, - getDefaultState: function () { return {set_id: "", show_time: false};} + getDefaultState: function () { return {set_id: "", show_time: false, show_calendar: false, first_day: ""};} }); + + var SetHeadersView = TabView.extend({ + tabName: "Set Headers", + initialize: function(opts){ + TabView.prototype.initialize.apply(this,[opts]); + this.headerFiles = void 0; + this.setHeader = void 0; + + }, + render: function(){ + var self = this; + var tmpl = _.template($("#set-headers-template").html()); + if(this.model && this.model.get("assignment_type") == "jitar"){ + this.$el.html($("#assign-type-not-supported").html()); + return this; + } + this.$el.html(tmpl(this.tabState.attributes)); + if(this.headerFiles && this.setHeader){ + this.showSetHeaders(); + this.stickit(); + } else if (_.isUndefined(this.headerFiles)){ + $.get(config.urlPrefix + "courses/" + config.courseSettings.course_id + "/headers", function( data ) { + self.headerFiles = _(data).map(function(f){ return {label: f, value: f};}); + self.headerFiles.unshift({label: "Use Default Header File", value: "defaultHeader"}); // I18N + self.render(); + }); + } else if(_.isUndefined(this.setHeader)) { + + this.setHeader = new SetHeader({set_id: this.model.get("set_id")}); + this.setHeader.on("change", function(model){ + model.save(model.changed,{success: function () { self.showSetHeaders();}}); + self.showSetHeaders(); + }).on("change:set_header_content", function(){ + self.editing = "setheader"; + }).on("change:hardcopy_header_content",function(){ + self.editing = "hardcopyheader"; + }).on("sync",function(){ + switch(self.editing){ + case "setheader": + $("#view-header-button").parent().button("toggle"); break; + case "hardcopyheader": + $("#view-hardcopy-button").parent().button("toggle"); break; + } + self.editing = ""; + }).fetch({success: function (){ + self.render(); + }}); + + } + }, + showSetHeaders: function (){ + var output = ""; + this.$("#hardcopy-header,#set-header").parent().removeClass("has-error"); + switch($(".view-options input:checked").attr("id")){ + case "view-header-button": + output = this.setHeader.get("set_header_html"); + this.$(".header-output").addClass("rounded-border"); + break; + case "view-hardcopy-button": + output = this.setHeader.get("hardcopy_header_html"); + this.$(".header-output").addClass("rounded-border"); + break; + case "edit-header-button": + if(this.setHeader.get("set_header") == "defaultHeader") { + // I18N + output = "Please select a header file to edit"; + this.$("#set-header").parent().addClass("has-error"); + } else { + output = $("#edit-header-template").html() + } + + break; + case "edit-hardcopy-button": + if(this.setHeader.get("hardcopy_header") == "defaultHeader") { + // I18N + output = "Please select a header file to edit"; + this.$("#hardcopy-header").parent().addClass("has-error"); + } else { + output = $("#edit-hardcopy-template").html() + } + + break; + } + this.$(".header-output").html(output); + this.stickit(this.setHeader,this.headerBindings); + }, + events: { + "change .view-options input": function () { + this.showSetHeaders(); + } + }, + bindings: { + '#set-description': {observe: 'description', events: ['blur']}, + '#set-header': { observe: "set_header", selectOptions: {collection: 'this.headerFiles'}}, + '#hardcopy-header': { observe: "hardcopy_header", selectOptions: {collection: 'this.headerFiles'}}, + + }, + headerBindings: { + '#edit-header-textarea': {observe: "set_header_content", events: ['blur']}, + '#edit-hardcopy-textarea': {observe: "hardcopy_header_content", events: ['blur']}, + }, + setProblemSet: function(_set){ + var self = this; + this.tabState.set({set_id: _set.get("set_id")}); + this.model = _set; + this.model.on("change:set_header change:hardcopy_header",function (model) { + if(self.setHeader){ + self.setHeader.set(model.changed); + } + }); + return this; + }, + getDefaultState: function () { + return {set_id: ""}; + } + + }); var ShowProblemsView = TabView.extend({ tabName: "Problems", @@ -239,7 +478,8 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ var self = this; _(this).bindAll("setProblemSet"); this.parent = options.parent; - this.problemSetView = new ProblemSetView({settings: options.settings, messageTemplate: options.messageTemplate}); + this.problemSetView + = new ProblemSetView(_(options).pick("settings","messageTemplate","eventDispatcher")); TabView.prototype.initialize.apply(this,[options]); this.tabState.on("change:show_path",function(){ self.problemSetView.showPath(self.tabState.get("show_path")); @@ -248,19 +488,26 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ }); }, render: function (){ + var self = this; + if(this.problemSetView.problemSet && this.problemSetView.problemSet.get("assignment_type") == "jitar"){ + this.$el.html($("#assign-type-not-supported").html()); + return; + } + this.problemSetView.setElement(this.$el); this.problemSetView.render(); + // disable the ability to drag problems when the set is open. + + this.problemSetView.on("rendered",function(){ + util.changeClass({els: $(".reorder-handle"), state: self.problemSetView.problemSet.isOpen(), + add_class:"disabled",remove_class:""}) + }); }, setProblemSet: function(_set){ - var self = this; this.problemSetView.setProblemSet(_set); - if(this.problemSetView.problemSet){ - this.problemSetView.problemSet.on("problem-deleted",function(p){ - self.parent.sidebar.$(".undo-delete-button").removeAttr("disabled"); - }) - } return this; }, + // set a parameter. set: function(options){ this.problemSetView.set(_.extend({},options,this.tabState.pick("show_path","show_tags"))); return this; @@ -269,7 +516,8 @@ define(['backbone','underscore','views/TabbedMainView','views/MainView', 'views/ this.problemSetView.changeDisplayMode(evt); }, getDefaultState: function () { - return {set_id: "", library_path: "", page_num: 0, rendered: false, page_size: 10, show_path: false, show_tags: false}; + return {set_id: "", library_path: "", page_num: 0, rendered: false, page_size: 10, + show_path: false, show_tags: false}; }, }); @@ -281,6 +529,10 @@ var AssignUsersView = Backbone.View.extend({ TabView.prototype.initialize.apply(this,[options]); }, render: function() { + if(this.problemSet && this.problemSet.get("assignment_type") == "jitar"){ + this.$el.html($("#assign-type-not-supported").html()); + return; + } this.$el.html($("#assign-users-template").html()); this.update(); return this; @@ -341,47 +593,54 @@ var AssignUsersView = Backbone.View.extend({ var CustomizeUserAssignView = TabView.extend({ tabName: "Student Overrides", initialize: function(options){ - _.bindAll(this,"render","saveChanges","buildCollection","setProblemSet"); + _.bindAll(this,"render","saveChanges","buildCollection","setProblemSet","update"); var self = this; // this.model is a clone of the parent ProblemSet. It is used to save properties for multiple students. this.model = options.problemSet ? new ProblemSet(options.problemSet.attributes): null; + + if(options.problemSet){ + options.problemSet.on("change",function(_model){ + self.model.set(_model.changed); + }); + } + _.extend(this,_(options).pick("users","settings","eventDispatcher")); TabView.prototype.initialize.apply(this,[options]); - this.tabState.on({ - "change:filter_string": function(){ + this.tabState.on("change:filter_string", function(){ self.userSetTable.set(self.tabState.pick("filter_string")).updateTable(); self.update(); - }, - "change:show_section change:show_recitation change:show_time": function(){ - self.update();} - }); + }).on("change:show_section change:show_recitation change:show_time", this.update); }, render: function () { var self = this; if(! this.model){ return; } + if(this.problemSet && this.problemSet.get("assignment_type") == "jitar"){ + this.$el.html($("#assign-type-not-supported").html()); + return; + } this.tableSetup(); this.$el.html($("#loading-usersets-template").html()); if (this.collection.size()>0){ this.$el.html($("#customize-assignment-template").html()); - (this.userSetTable = new CollectionTableView({columnInfo: this.cols, collection: this.collection, - paginator: {showPaginator: false}, tablename: ".users-table", page_size: -1, - row_id_field: "user_id", table_classes: "table table-bordered table-condensed"})).render(); + (this.userSetTable = new CollectionTableView({columnInfo: this.cols, + collection: this.collection, + paginator: {showPaginator: false}, + tablename: ".users-table", page_size: -1, + row_id_field: "user_id", + table_classes: "table table-bordered table-condensed"})); + // The following is needed to make sure that the reduced-scoring date shows up in the student overrides table. + this.userSetTable.collection.each(function(model) { model.show_reduced_scoring = true;}); + this.userSetTable.render(); this.userSetTable.set(this.tabState.pick("selected_rows")) - .on({ - "selected-row-changed": function(rowIDs){ + .on("selected-row-changed", function(rowIDs){ self.tabState.set({selected_rows: rowIDs}); - }, - "table-sorted": function (){ - self.update(); - } - }) - .updateTable(); + }).on("table-sorted table-changed",this.update).updateTable(); + this.$el.append(this.userSetTable.el); this.update(); - this.stickit(); this.stickit(this.tabState,{ ".filter-text": "filter_string", ".show-section": "show_section", @@ -414,15 +673,27 @@ var AssignUsersView = Backbone.View.extend({ }); }, setProblemSet: function(_set) { + var self = this; this.problemSet = _set; // this is the globalSet if(_set){ - this.model = new ProblemSet(_set.attributes); // this is used to pull properties for the userSets. We don't want to overwrite the properties in this.problemSet + // this is used to pull properties for the userSets. We don't want to overwrite the properties in this.problemSet + this.model = new ProblemSet(_set.attributes); + this.model.show_reduced_scoring = true; this.userSetList = new UserSetList([],{problemSet: this.model,type: "users"}); - this.userSetList.on("change:due_date change:answer_date change:reduced_scoring_date change:open_date" - , function(model){ model.save(); + this.userSetList.on("change:due_date change:answer_date change:reduced_scoring_date " + + "change:open_date", function(model){ + model.adjustDates(); + model.save(); }); } - + if(this.problemSet){ + this.problemSet.on("change",function(_m){ + self.collection = new Backbone.Collection(); // reset the collection so data is refetched. + }).on("change",function(_model){ + self.model.set(_model.changed); + }); + } + // make a new collection that merges the UserSetListOfUsers and the userList this.collection = new Backbone.Collection(); return this; @@ -430,26 +701,50 @@ var AssignUsersView = Backbone.View.extend({ buildCollection: function(){ // since the collection needs to contain a mixture of userSet and user properties, we merge them. var self = this; this.collection.reset(this.userSetList.models); + //this.collection is a collection of models based on user sets. The following will also pick information + // from the users that is useful for this view. this.collection.each(function(model){ - model.set(self.users.findWhere({user_id: model.get("user_id")}).pick("section","recitation","first_name","last_name")); + model.set(self.users.findWhere({user_id: model.get("user_id")}) + .pick("section","recitation","first_name","last_name"),{silent: true}); }); this.collection.on({change: function(model){ self.userSetList.findWhere({user_id: model.get("user_id")}) - .set(model.pick("open_date","due_date","answer_date","enable_reduced_scoring")).save(); + .set(model.pick("open_date","due_date","answer_date","reduced_scoring_date")).save(); }}); this.setMessages(); return this; }, update: function () { + if(typeof(this.problemSet) === "undefined"){ + return; + } + util.changeClass({state: this.tabState.get("show_section"), els: this.$(".section"), remove_class: "hidden"}) util.changeClass({state: this.tabState.get("show_recitation"), els: this.$(".recitation"), remove_class: "hidden"}) - util.changeClass({state: this.problemSet.get("enable_reduced_scoring") && this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}"), + util.changeClass({state: this.problemSet.get("enable_reduced_scoring") + && this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}"), els: this.$(".reduced-scoring-date,.reduced-scoring-header"), remove_class: "hidden"}); util.changeClass({state: this.tabState.get("show_time"), remove_class: "edit-datetime", add_class: "edit-datetime-showtime", els: this.$(".open-date,.due-date,.reduced-scoring-date,.answer-date")}) - this.userSetTable.refreshTable(); - this.stickit(); + if(this.userSetTable && this.model){ + this.userSetTable.refreshTable(); + this.stickit(); + } else { + this.render(); + return; + } + // color the changed dates blue + _([".open-date",".due-date",".reduced-scoring-date",".answer-date"]).each(function(date){ + var val = $("#customize-problem-set-controls " + date + " .wwdate").val() + $(date +" .wwdate").filter(function(i,v) {return $(v).val()!=val;}).css("color","blue"); + }); + var h = $(window).height()-($(".navbar-fixed-top").outerHeight(true) + + $(".header-set-name").outerHeight(true) + + $("#customize-problem-set-controls").parent().outerHeight() + + $("#footer").outerHeight()); + $("#student-override-container").height(h); + }, tableSetup: function () { var self = this; @@ -467,12 +762,12 @@ var AssignUsersView = Backbone.View.extend({ editable: false, datatype: "integer", use_contenteditable: false}, {name: "Section", key: "section", classname: "section", editable: false, datatype: "string"}, {name: "Recitation", key: "recitation", classname: "recitation", editable: false,datatype: "string"}, - {name: "Enable Reduced Scoring", key: "enable_reduced_scoring", classname: "enable-reduced-scoring", - editable: false, datatype: "boolean", show_column: false} + {name: "Enab RS", key: "enable_reduced_scoring", classname: "enable_reduced_scoring", editable: false, + datatype: "boolean", show_column: false} ]; }, - messageTemplate: _.template($("#customize-users-messages-template").html()), + messageTemplate: _.template($("#problem-set-messages").html()), setMessages: function(){ var self = this; this.userSetList.on({ @@ -491,6 +786,7 @@ var AssignUsersView = Backbone.View.extend({ {type:"set_saved",opts:{set_id:_set.get("set_id"), user_id: _set.get("user_id")}}), text: self.messageTemplate({type:"set_saved_details",opts:{setname:_set.get("set_id"), key: key, user_id: _set.get("user_id"),oldValue: _old, newValue: _new}})}); + _set.changingAttributes = _(_set.changingAttributes).omit(key); } }); } diff --git a/webwork3/public/js/apps/CourseManager/main-views/ProblemSetsManager.js b/webwork3/public/js/apps/CourseManager/main-views/ProblemSetsManager.js index 82ba66d7e8..4fb8723601 100644 --- a/webwork3/public/js/apps/CourseManager/main-views/ProblemSetsManager.js +++ b/webwork3/public/js/apps/CourseManager/main-views/ProblemSetsManager.js @@ -11,7 +11,7 @@ function(Backbone, _,MainView,CollectionTableView,config,util,ModalView,ProblemS var ProblemSetsManager = MainView.extend({ initialize: function (options) { MainView.prototype.initialize.call(this,options); - _.bindAll(this, 'render','addProblemSet','clearFilterText','deleteSets','update','syncProblemEvent'); // include all functions that need the this object + _.bindAll(this, 'render','addProblemSet','clearFilterText','deleteSets','update'); // include all functions that need the this object var self = this; this.state.on({ @@ -46,7 +46,6 @@ var ProblemSetsManager = MainView.extend({ "table-changed": function(){ // I18N self.$(".num-sets").html(self.problemSetTable.getRowCount() + " of " + self.problemSets.length + " sets shown."); - self.update(); } }); @@ -58,11 +57,19 @@ var ProblemSetsManager = MainView.extend({ self.render(); // for some reason the checkboxes don't stay checked. }) + // builds the "change:set_id ... " + var changeableFields = _(this.problemSets.at(0).defaults).chain().keys().map(function(key){ + return "change:" + key}).value().join(" "); + + this.problemSets.on(changeableFields,function(_set){ + _set.save(_set.changed); + }); + this.problemSets.on({ "add": this.update, "remove": this.update, "change:enable_reduced_scoring":this.update - }); + }) this.setMessages(); }, events: { @@ -105,9 +112,12 @@ var ProblemSetsManager = MainView.extend({ return this; }, update: function (){ - util.changeClass({state: this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}"), remove_class: "hidden", - els: this.$("td:has(input.enable-reduced-scoring),td.reduced-scoring-date,th.enable-reduced-scoring,th.reduced-scoring-date")}) - this.problemSetTable.refreshTable(); + this.problemSetTable.updateTable(); + util.changeClass({state: this.settings.getSettingValue("pg{ansEvalDefaults}{enableReducedScoring}"), + remove_class: "hidden", + els: this.$("td:has(input.enable-reduced-scoring),td.reduced-scoring-date," + + "th.enable-reduced-scoring,th.reduced-scoring-date")}) + return this; }, bindings: { @@ -230,11 +240,8 @@ var ProblemSetsManager = MainView.extend({ this.problemSets.on({ add: function (_set){ _set.save(); - _set.problems.on({ - "change:value": function(prob){ self.changeProblemValueEvent(prob,_set)}, - add: function(prob){ self.addProblemEvent(prob,_set)}, - sync: function(prob){ self.syncProblemEvent(prob,_set)}, - }); + _set.problems.on("change:value change:max_attempts change:source_file", function(prob){ + self.changeProblemValueEvent(prob,_set)}) _set._network={add: ""}; }, remove: function(_set){ @@ -243,26 +250,10 @@ var ProblemSetsManager = MainView.extend({ short: self.messageTemplate({type:"set_removed",opts:{setname: _set.get("set_id")}}), text: self.messageTemplate({type: "set_removed_details",opts:{setname: _set.get("set_id")}})}); - // update the assignmentDates to delete the proper assignments - - self.assignmentDates.remove(self.assignmentDates.filter(function(assign) { - return assign.get("problemSet").get("set_id")===_set.get("set_id");})); self.problemSetTable.updateTable(); self.update(); }}); }, - "change:due_date change:open_date change:answer_date change:reduced_scoring_date": function(_set){ - _set.adjustDates(); - self.assignmentDates.chain().filter(function(assign) { - return assign.get("problemSet").get("set_id")===_set.get("set_id");}) - .each(function(assign){ - assign.set("date",moment.unix(assign.get("problemSet").get(assign.get("type").replace("-","_")+"_date")) - .format("YYYY-MM-DD")); - }); - }, - "change:problems": function(_set){ - _set.save(); - }, "set_date_error": function(_opts, model){ self.eventDispatcher.trigger("add-message",{type: "danger", short: self.messageTemplate({type: "date_set_error", opts: _opts}), @@ -273,7 +264,9 @@ var ProblemSetsManager = MainView.extend({ }) }, change: function(_set){ - _set.changingAttributes=_.pick(_set._previousAttributes,_.keys(_set.changed)); + var keys = _(_set.changed).keys(); + _set.changingAttributes= _(keys).intersection(["_delete_problem_id","_reorder","_add_problem"]).length>0 ? + _set.changed : _(_set.previousAttributes()).pick(keys); }, sync: function(_set){ _(_set.changingAttributes||{}).chain().keys().each(function(key){ @@ -283,90 +276,81 @@ var ProblemSetsManager = MainView.extend({ short: self.messageTemplate({type:"set_added",opts:{setname: _set.get("set_id")}}), text: attr.msg}); break; - case "problem_added": + case "_add_problem": self.eventDispatcher.trigger("add-message",{type: "success", short: self.messageTemplate({type:"problem_added",opts:{setname: _set.get("set_id")}}), - text: self.messageTemplate({type:"problem_added_details",opts:{setname: _set.get("set_id")}})}); + text: self.messageTemplate({type:"problem_added_details", + opts:{setname: _set.get("set_id")}})}); + _set.changingAttributes = _(_set.changingAttributes).omit("_add_problem"); break; - case "problems_reordered": + case "_reorder": self.eventDispatcher.trigger("add-message",{type: "success", short: self.messageTemplate({type:"problems_reordered",opts:{setname: _set.get("set_id")}}), - text: self.messageTemplate({type:"problems_reordered_details",opts:{setname: _set.get("set_id")}})}); + text: self.messageTemplate({type:"problems_reordered_details", + opts:{setname: _set.get("set_id")}})}); + _set.changingAttributes = _(_set.changingAttributes).omit("_reorder"); break; - case "problem_deleted": + case "_delete_problem_id": self.eventDispatcher.trigger("add-message",{type: "success", short: self.messageTemplate({type:"problem_deleted",opts:{setname: _set.get("set_id")}}), - text: self.messageTemplate({type: "problem_deleted_details", opts: _set.changingAttributes[key]})}); + text: self.messageTemplate({type: "problem_deleted_details", + opts: {setname: _set.get("set_id"), + problem_id: _set.changingAttributes["_delete_problem_id"]}})}); + _set.changingAttributes = _(_set.changingAttributes).omit("_delete_problem_id"); break; case "assigned_users": self.eventDispatcher.trigger("add-message",{type: "success", short: self.messageTemplate({type:"set_saved",opts:{setname:_set.get("set_id")}}), text: self.messageTemplate({type:"set_assigned_users_saved",opts:{setname:_set.get("set_id")}})}); + _set.changingAttributes = _(_set.changingAttributes).omit(key); + break; + case "problem_changed": + self.eventDispatcher.trigger("add-message",{type: "success", + short: self.messageTemplate({type:"set_saved",opts:{setname: _set.get("set_id")}}), + text: self.messageTemplate({type: "problems_values_details", + opts: _.extend({set_id:_set.get("set_id")},_set.changingAttributes[key])})}); + _set.changingAttributes = _(_set.changingAttributes).omit("problem_changed"); break; - default: var _old = key.match(/date$/) ? moment.unix(_set.changingAttributes[key]).format("MM/DD/YYYY [at] hh:mmA") : _set.changingAttributes[key]; var _new = key.match(/date$/) ? moment.unix(_set.get(key)).format("MM/DD/YYYY [at] hh:mmA") : _set.get(key); self.eventDispatcher.trigger("add-message",{type: "success", short: self.messageTemplate({type:"set_saved",opts:{setname:_set.get("set_id")}}), - text: self.messageTemplate({type:"set_saved_details",opts:{setname:_set.get("set_id"),key: key, - oldValue: _old, newValue: _new}})}); + text: self.messageTemplate({type:"set_saved_details", + opts:{setname:_set.get("set_id"), + key: key, + oldValue: _old, + newValue: _new}})}); + _set.changingAttributes = _(_set.changingAttributes).omit(key); } // switch - }); // .each + }); + _(_set._network).chain().keys().each(function(key){ switch(key){ case "add": self.eventDispatcher.trigger("add-message",{type: "success", short: self.messageTemplate({type:"set_added",opts:{setname: _set.get("set_id")}}), text: self.messageTemplate({type: "set_added_details",opts:{setname: _set.get("set_id")}})}); - self.assignmentDates.add(new AssignmentDate({type: "open", problemSet: _set, - date: moment.unix(_set.get("open_date")).format("YYYY-MM-DD")})); - self.assignmentDates.add(new AssignmentDate({type: "due", problemSet: _set, - date: moment.unix(_set.get("due_date")).format("YYYY-MM-DD")})); - self.assignmentDates.add(new AssignmentDate({type: "answer", problemSet: _set, - date: moment.unix(_set.get("answer_date")).format("YYYY-MM-DD")})); - self.assignmentDates.add(new AssignmentDate({type: "reduced-scoring", problemSet: _set, - date: moment.unix(_set.get("reduced_scoring_date")).format("YYYY-MM-DD")})); - self.problemSetTable.set({filter_string: self.state.get("filter_string")}).updateTable(); - delete _set._network; - break; - } - }); + }}); } // sync }); // this.problemSets.on /* This sets the events for the problems (of type ProblemList) in each problem Set */ this.problemSets.each(function(_set) { - _set.problems.on({ - "change:value": function(prob){ self.changeProblemValueEvent(prob,_set)}, - add: function(prob){ self.addProblemEvent(prob,_set)}, - sync: function(prob){ self.syncProblemEvent(prob,_set)}, - }); + _set.get("problems") + .on("change:value change:max_attempts change:source_file",function(prob){ self.changeProblemValueEvent(_set,prob);}); }); }, // setMessages - changeProblemValueEvent: function (prob,_set){ // not sure this is actually working. - _set.changingAttributes={"value_changed": {oldValue: prob._previousAttributes.value, - newValue: prob.get("value"), name: _set.get("set_id"), problem_id: prob.get("problem_id")}}; + changeProblemValueEvent: function (_set,prob){ + var attr = _(prob.changed).keys()[0]; + _set.changingAttributes={ + "problem_changed": { attribute: attr, + oldValue: prob._previousAttributes[attr], + newValue: prob.get(attr), + problem_id: prob.get("problem_id")}}; - }, - addProblemEvent: function(prob,_set){ - _set.changingAttributes={"problem_added": ""}; - }, - syncProblemEvent: function(prob,_set){ - var self = this; - _(_set.changingAttributes||{}).chain().keys().each(function(key){ - switch(key){ - case "value_changed": - self.eventDispatcher.trigger("add-message",{type: "success", - short: self.messageTemplate({type:"set_saved",opts:{setname: _set.get("set_id")}}), - text: self.messageTemplate({type: "problems_values_details", - opts: _.extend({set_id:_set.get("set_id")},_set.changingAttributes[key])})}); - break; - - } - }); } }); diff --git a/webwork3/public/js/apps/CourseManager/sidebars/ProblemListOptionsSidebar.js b/webwork3/public/js/apps/CourseManager/sidebars/ProblemListOptionsSidebar.js index 61f46ff8e7..dc5861c5cd 100644 --- a/webwork3/public/js/apps/CourseManager/sidebars/ProblemListOptionsSidebar.js +++ b/webwork3/public/js/apps/CourseManager/sidebars/ProblemListOptionsSidebar.js @@ -6,11 +6,16 @@ define(['backbone','views/Sidebar', 'config'],function(Backbone,Sidebar,config){ this.problemSets = options.problemSets; this.settings = options.settings; this.state.set({display_option: this.settings.getSettingValue("pg{options}{displayMode}"), - show_path: false, show_tags: false},{silent: true}) + show_path: false, show_tags: false, problem_group: null},{silent: true}) .on("change:show_path",function(){ self.trigger("show-hide-path",self.state.get("show_path")) }).on("change:show_tags",function(){ self.trigger("show-hide-tags",self.state.get("show_tags")); + }).on("change:problem_group", function(){ + if(self.state.get("problem_group")){ + self.trigger("add-prob-from-group", self.state.get("problem_group")); + self.state.set("problem_group",null); + } }); _.extend(this,Backbone.Events); @@ -18,11 +23,24 @@ define(['backbone','views/Sidebar', 'config'],function(Backbone,Sidebar,config){ render: function(){ this.$el.html($("#problem-list-options-template").html()); this.stickit(this.state,this.bindings); + this.stopListening(); + var problems = this.mainView.views.problemsView.problemSetView.deletedProblems; + if(problems.length >0) { + this.$(".undo-delete-button").removeAttr("disabled"); + } + this.listenTo(problems,"add", function(){ + this.$(".undo-delete-button").removeAttr("disabled"); + }).listenTo(this.mainView.views.problemsView.problemSetView.deletedProblems,"remove", function() { + if(problems.length == 0 ){ + this.$(".undo-delete-button").attr("disabled","disabled"); + } + }); + return this; }, bindings: {".problem-display-option": {observe: "display_option", selectOptions: { collection: function () { - var modes = this.settings.getSettingValue("pg{displayModes}").slice(); + var modes = this.settings.getSettingValue("pg{displayModes}").slice(); // make a copy of the pg{displayModes} modes.push("None"); return modes; } @@ -32,8 +50,12 @@ define(['backbone','views/Sidebar', 'config'],function(Backbone,Sidebar,config){ }}, ".show-hide-path-button": {observe: "show_path", update: function($el, val, model, options){ $el.text(val?"Hide Path":"Show Path"); + }}, + "select#add-prob-group": {observe: "problem_group", selectOptions: { + collection: function (){ + return this.problemSets.map(function(_set) { return _set.get("set_id");}); + }, defaultOption: {label: "Add a Problem From a Group", value: null} }} - }, events: { "click .undo-delete-button": function(){ diff --git a/webwork3/public/js/apps/CourseManager/sidebars/ProblemSetListView.js b/webwork3/public/js/apps/CourseManager/sidebars/ProblemSetListView.js index 41fb33edf6..4e2dcfe58e 100644 --- a/webwork3/public/js/apps/CourseManager/sidebars/ProblemSetListView.js +++ b/webwork3/public/js/apps/CourseManager/sidebars/ProblemSetListView.js @@ -9,8 +9,8 @@ */ define(['backbone', 'underscore','models/ProblemSetList','models/ProblemSet','config','views/Sidebar', - 'main-views/AssignmentCalendar', 'views/ModalView','main-views/LibraryBrowser'], -function(Backbone, _,ProblemSetList,ProblemSet,config,Sidebar,AssignmentCalendar,ModalView,LibraryBrowser){ + 'main-views/CourseCalendar', 'views/ModalView','main-views/LibraryBrowser'], +function(Backbone, _,ProblemSetList,ProblemSet,config,Sidebar,CourseCalendar,ModalView,LibraryBrowser){ var ProblemSetListView = Sidebar.extend({ @@ -50,7 +50,7 @@ function(Backbone, _,ProblemSetList,ProblemSet,config,Sidebar,AssignmentCalendar var self = this; // The following allows a problem set (on the sidebar to be dragged onto the Calendar) - if(this.mainView instanceof AssignmentCalendar){ + if(this.mainView instanceof CourseCalendar){ this.$(".sidebar-problem-set").draggable({ disabled: false, revert: true, diff --git a/webwork3/public/js/apps/config.js b/webwork3/public/js/apps/config.js index e833186c5b..da425d5c8c 100644 --- a/webwork3/public/js/apps/config.js +++ b/webwork3/public/js/apps/config.js @@ -38,8 +38,10 @@ define(['backbone','underscore','moment','backbone-validation','stickit','jquery ], - permissions : [{value: "-5", label: "guest"},{value: "0", label: "student"},{value: "2", label: "login proctor"}, - {value: "3", label: "T.A."},{value: "10", label: "professor"}, {value: "20", label: "administrator"}], + permissions : [{value: "-5", label: "guest"},{value: "0", label: "student"},{value: "2", label: "login proctor"}, + {value: "3", label: "grade proctor"},{value: "5", label: "T.A."}, + {value: "10", label: "professor"}, {value: "20", label: "administrator"}, + {value: "99999999", label: "nobody"}], enrollment_statuses: [ {value: "A", label: "Audit", abbrs: ["A","a","audit"]}, @@ -69,7 +71,7 @@ define(['backbone','underscore','moment','backbone-validation','stickit','jquery var theDate = moment.unix(evt.data.model.get(evt.data.options.observe)); var newDate = moment(time,"hh:mmA"); theDate.hours(newDate.hours()).minutes(newDate.minutes()); - evt.data.model.set(evt.data.options.observe,""+theDate.unix()); + evt.data.model.set(evt.data.options.observe,theDate.unix()); evt.data.$el.popover("destroy"); evt.data.$el.removeAttr("style"); } else { @@ -77,7 +79,7 @@ define(['backbone','underscore','moment','backbone-validation','stickit','jquery var errorMessage = config.messageTemplate({type: "time_error"}) evt.data.$el.popover({title: "Error", content: errorMessage, placement: "left"}).popover("show"); } - + console.log(theDate); }, sortIcons: { "string1": "fa fa-sort-alpha-asc", @@ -167,14 +169,15 @@ define(['backbone','underscore','moment','backbone-validation','stickit','jquery Backbone.Stickit.addHandler({ selector: '.edit-datetime', - update: function($el, val, model, options){ + update: function($el, val,model, options){ + // hide this for sets in which the reduced_scoring date should not be shown. if(options.observe==="reduced_scoring_date" && ! model.get("enable_reduced_scoring") - && ! model.show_reduced_scoring){ + && ! model.show_reduced_scoring){ $el.html(""); } else { var tmpl = _.template($("#edit-date-time-template").html()); - $el.html(tmpl({date: moment.unix(val).format("MM/DD/YYYY")})); + $el.html(tmpl({date: moment.unix(model.get(options.observe)).format("MM/DD/YYYY")})); } var tmpl = _.template($("#time-popover-template").html()); @@ -185,7 +188,6 @@ define(['backbone','underscore','moment','backbone-validation','stickit','jquery timeIcon.parent().delegate(".save-time-button","click",{$el:$el.closest(".edit-datetime"), model: model, options: options}, function (evt) { - timeIcon.popover("hide"); config.setTime(evt,$(this).siblings(".wwtime").val()); }); timeIcon.parent().delegate(".cancel-time-button","click",{},function(){timeIcon.popover("hide");}); @@ -318,7 +320,18 @@ define(['backbone','underscore','moment','backbone-validation','stickit','jquery return val==="yes"; } }) - + + Backbone.Stickit.addHandler({ + selector: ".input-blur", + events: ["blur"], + + }); + + Backbone.Stickit.addHandler({ + selector: ".integer-input", + onSet: function(value) { + return parseInt(value); + }}); return config; }); \ No newline at end of file diff --git a/webwork3/public/js/apps/util.js b/webwork3/public/js/apps/util.js index 51b133e956..bc84f7c5b9 100644 --- a/webwork3/public/js/apps/util.js +++ b/webwork3/public/js/apps/util.js @@ -4,9 +4,8 @@ * */ -define(['underscore','config'], function(_,config){ -var util = { - +define(['underscore','config','models/AssignmentDateList','models/AssignmentDate','moment'], function(_,config,AssignmentDateList,AssignmentDate,moment){ +var util = { // as of 2015-01-02, this function is no longer used in lieu of a library. To delete after some testing. CSVToHTMLTable: function( strData,headers, strDelimiter ){ strDelimiter = (strDelimiter || ","); @@ -118,13 +117,36 @@ var util = { _.extend(obj,_.object(fields,values)); return obj; }, + // this returns the object for a Backbone.Stickit bindings object. This is useful for error reporting. + invBindings: function(bindings){ + var keys = _(bindings).keys() + var vals = _(bindings).chain().values().map(function(x) { return _.isObject(x)? x.observe : x;}).value(); + return _.object(vals,keys); + }, + // This travels through all of the assignments and determines the days that assignment dates fall + buildAssignmentDates: function (problemSets) { + var assignmentDateList = new AssignmentDateList(); + problemSets.each(function(_set){ + assignmentDateList.add(new AssignmentDate({type: "open", problemSet: _set, + date: moment.unix(_set.get("open_date")).format("YYYY-MM-DD")})); + assignmentDateList.add(new AssignmentDate({type: "due", problemSet: _set, + date: moment.unix(_set.get("due_date")).format("YYYY-MM-DD")})); + assignmentDateList.add(new AssignmentDate({type: "answer", problemSet: _set, + date: moment.unix(_set.get("answer_date")).format("YYYY-MM-DD")})); + if(parseInt(_set.get("reduced_scoring_date"))>0) { + assignmentDateList.add(new AssignmentDate({type: "reduced-scoring", problemSet: _set, + date: moment.unix(_set.get("reduced_scoring_date")).format("YYYY-MM-DD")}) ); + } + }); + return assignmentDateList; + }, changeClass:function(opts){ if(opts.state){ opts.els.removeClass(opts.remove_class).addClass(opts.add_class) } else { opts.els.addClass(opts.remove_class).removeClass(opts.add_class) } - } + }, } diff --git a/webwork3/public/js/models/Problem.js b/webwork3/public/js/models/Problem.js index 5ff2074c0b..542f4e373a 100644 --- a/webwork3/public/js/models/Problem.js +++ b/webwork3/public/js/models/Problem.js @@ -1,4 +1,4 @@ -define(['backbone', 'underscore', 'config'], function(Backbone, _, config){ +define(['backbone', 'underscore', 'config', 'apps/util'], function(Backbone, _, config,util){ /** * * This defines a single webwork Problem (Global Problem) @@ -14,9 +14,20 @@ define(['backbone', 'underscore', 'config'], function(Backbone, _, config){ max_attempts: -1, set_id: "", flags: "", - problem_seed: 1 + problem_seed: 1, + showMeAnotherCount: 0, + showMeAnother: -1 }, - idAttribute: "source_file", + integerFields: ["problem_id","value","max_attempts","problem_seed","showMeAnotherCount"], + validation: { + // need to put the validation message in a template + value: {pattern: /^[1-9]\d*$/, msg: "The value must be a positive whole number." }, + max_attempts: {pattern: /^(-1|\d*)$/, msg: "The value must be a whole number or -1 for unlimited attempts." } + }, + parse: function(response){ + return util.parseAsIntegers(response,this.integerFields); + }, + idAttribute: "_id", url: function () { // need to determine if this is a problem in a problem set or a problem from a library browser if(typeof(this.collection.problemSet)!=="undefined") { // the problem comes from a problem set diff --git a/webwork3/public/js/models/ProblemSet.js b/webwork3/public/js/models/ProblemSet.js index 23e09c7a47..bc579e01d5 100644 --- a/webwork3/public/js/models/ProblemSet.js +++ b/webwork3/public/js/models/ProblemSet.js @@ -18,24 +18,27 @@ var ProblemSet = Backbone.Model.extend({ reduced_scoring_date: "", visible: false, enable_reduced_scoring: false, - assignment_type: "", + assignment_type: "default", attempts_per_version: -1, time_interval: 0, versions_per_interval: 0, version_time_limit: 0, version_creation_time: 0, - problem_randorder: 0, + problem_randorder: false, version_last_attempt_time: 0, problems_per_page: 0, hide_score: "N", hide_score_by_problem: "N", hide_work: "N", - time_limit_cap: "0", + hide_hint: false, + time_limit_cap: false, restrict_ip: "No", relax_restrict_ip: "No", restricted_login_proctor: "No", assigned_users: [], - problems: null + problems: null, + description: "", + pg_password: "", }, validation: { open_date: "checkDates", @@ -47,24 +50,24 @@ var ProblemSet = Backbone.Model.extend({ } }, integerFields: ["open_date","reduced_scoring_date","due_date","answer_date", - "problem_randorder","attempts_per_version","version_creation_time","version_time_limit", + "attempts_per_version","version_creation_time","version_time_limit", "problems_per_page","versions_per_interval","version_last_attempt_time","time_interval"], idAttribute: "_id", initialize: function (opts,dateSettings) { _.bindAll(this,"addProblem"); this.dateSettings = dateSettings; - _(this.attributes).extend(_(util.parseAsIntegers(opts,this.integerFields)).pick(this.integerFields)); - var pbs = (opts && opts.problems) ? opts.problems : []; - this.problems = new ProblemList(pbs); - this.attributes.problems = this.problems; - this.saveProblems = []; // holds added problems temporarily if the problems haven't been loaded. - + opts.problems = opts.problems || []; + this.set(this.parse(opts),{silent: true}); }, parse: function (response) { if (response.problems){ - this.problems.set(response.problems); + if (typeof(this.problems)=="undefined"){ + this.problems = new ProblemList(); + } + this.problems.set(response.problems,{silent: true}); this.attributes.problems = this.problems; } + response.assignment_type = response.assignment_type || "default"; response = util.parseAsIntegers(response,this.integerFields); return _.omit(response, 'problems'); }, @@ -90,12 +93,22 @@ var ProblemSet = Backbone.Model.extend({ }, addProblem: function (prob) { var self = this; - var newProblem = new Problem(prob.attributes); var lastProblem = this.get("problems").last(); - newProblem.set("problem_id",lastProblem ? parseInt(lastProblem.get("problem_id"))+1:1); - this.get("problems").add(newProblem); - this.trigger("change:problems",this); // + //var prob = new Problem(); + var attrs = _.extend({},prob.attributes, + { problem_id: lastProblem ? parseInt(lastProblem.get("problem_id"))+1:1}); + this.get("problems").add(_(attrs).omit("_id"));; + this.set("_add_problem",true); this.save(); + this.unset("_add_problem",{silent: true}); + }, + // delete the problem _prob and if successfull remove the view _view + deleteProblem: function(_prob,_view){ + var self = this; + this.set("_delete_problem_id",_prob.get("problem_id")); + this.save(); + this.unset("_delete_problem_id",{silent: true}); + this.get("problems").remove(_prob); }, setDate: function(attr,_date){ // sets the date of open_date, answer_date or due_date without changing the time var currentDate = moment.unix(this.get(attr)) @@ -140,6 +153,13 @@ var ProblemSet = Backbone.Model.extend({ } } }, + // this checks if the problem set is open. Using current time to determine this. + isOpen: function(){ + var openDate = moment.unix(this.get("open_date")) + , dueDate = moment.unix(this.get("due_date")) + , now = moment(); + return now.isBefore(dueDate) && now.isAfter(openDate); + }, // this adjusts all of the dates to make sure that they don't trigger an error. adjustDates: function (){ var self = this; @@ -150,12 +170,15 @@ var ProblemSet = Backbone.Model.extend({ } var prevAttr = _.object([[_(this.changed).keys()[0],moment.unix(this._previousAttributes[_(this.changed).keys()[0]])]]) + + var prevDates = _(util.parseAsIntegers(this._previousAttributes,this.integerFields)) + .pick("answer_date","due_date","reduced_scoring_date","open_date"); // convert all of the dates to Moment objects. - var prevDates = _(this._previousAttributes).pick("answer_date","due_date","reduced_scoring_date","open_date") - var dates1 = _(prevDates).chain() - .pairs().map(function(date){ return [date[0],moment.unix(date[1])];}).object().value(); + // dates1 is the moment objects of the previous dates + // dates2 is the moment objects of the new dates. + var dates1 = _(prevDates).mapObject(function(val,key) { return moment.unix(val);}); var dates2 = _(this.pick("answer_date","due_date","reduced_scoring_date","open_date")).chain() - .pairs().map(function(date){ return [date[0],moment.unix(date[1])];}).object().value(); + .mapObject(function(val,key) { return moment.unix(val);}).value(); var mins_a_d = dates1.answer_date.diff(dates1.due_date,'minutes'); var mins_d_r = dates1.due_date.diff(dates1.reduced_scoring_date,'minutes'); @@ -205,10 +228,13 @@ var ProblemSet = Backbone.Model.extend({ dates2.answer_date = moment(dates2.due_date).add(mins_a_d,"minutes"); } } - - // convert the moments back to unix time - var newUnixDates = _(dates2).chain().pairs().map(function(date) { - return [date[0],date[1].unix()]}).object().value(); + + var changedKeys = _(dates1).chain().keys().filter(function(key) + {return !dates1[key].isSame(dates2[key]);}).value(); + // get the unix dates of the dates that have changed. + var newUnixDates = _(dates2).chain().pick(changedKeys) + .mapObject(function(val,key) { return val.unix();}).value(); + this.set(newUnixDates); } diff --git a/webwork3/public/js/models/SetHeader.js b/webwork3/public/js/models/SetHeader.js new file mode 100644 index 0000000000..78be2381bb --- /dev/null +++ b/webwork3/public/js/models/SetHeader.js @@ -0,0 +1,26 @@ +/** + * + * This defines a set Header object. + * + */ + +define(['backbone', 'underscore', 'config'], function(Backbone, _, config){ + +var SetHeader = Backbone.Model.extend({ + defaults: { + set_id : "", + set_header_html: "", + hardcopy_header_html: "", + set_header: "", + hardcopy_header: "", + set_header_content: "", + hardcopy_header_content: "" + }, + idAttribute: "_id", + url: function () { + return config.urlPrefix + "courses/" + config.courseSettings.course_id + "/sets/" + this.get("set_id") + "/setheader" ; + }, +}); + + return SetHeader; +}) \ No newline at end of file diff --git a/webwork3/public/js/models/UserProblemList.js b/webwork3/public/js/models/UserProblemList.js index ba4cf991dc..bee4f2791f 100644 --- a/webwork3/public/js/models/UserProblemList.js +++ b/webwork3/public/js/models/UserProblemList.js @@ -1,3 +1,9 @@ +/** + * This is a list of UserProblems + * + */ + + define(['backbone', 'models/ProblemList','models/UserProblem','config'], function(Backbone, ProblemList,UserProblem,config){ var UserProblemList = ProblemList.extend({ initialize: function(models,options){ @@ -6,6 +12,10 @@ define(['backbone', 'models/ProblemList','models/UserProblem','config'], functio ProblemList.prototype.initialize.apply(this,arguments); }, model: UserProblem, + //parse: function(data){ + //this.models = _(data).map(function(prob) { return new UserProblem(prob);}); + // return data; + //}, url: function(){ return config.urlPrefix + "courses/" + config.courseSettings.course_id + "/sets/" + this.set_id + "/users/" + this.user_id + "/problems"; diff --git a/webwork3/public/js/models/UserSet.js b/webwork3/public/js/models/UserSet.js index d2f5a58a26..a07b19930c 100644 --- a/webwork3/public/js/models/UserSet.js +++ b/webwork3/public/js/models/UserSet.js @@ -9,46 +9,12 @@ define(['backbone', 'underscore','config','models/ProblemSet','models/UserProblemList','apps/util'], function(Backbone, _,config,ProblemSet,UserProblemList,util){ - var UserSet = Backbone.Model.extend({ - defaults: { - user_id: "", - set_id: "", - psvn: "", - set_header: "defaultHeader", - hardcopy_header: "", - open_date: "", - due_date: "", - answer_date: "", - visible: false, - enable_reduced_scoring: false, - assignment_type: "", - description: "", - restricted_release: "", - restricted_status: "", - attempts_per_version: "", - time_interval: "", - versions_per_interval: "", - version_time_limit: "", - version_creation_time: "", - problem_randorder: "", - version_last_attempt_time: "", - problems_per_page: "", - hide_score: "", - hide_score_by_problem: "", - hide_work: "", - time_limit_cap: "", - restrict_ip: "", - relax_restrict_ip: "", - restricted_login_proctor: "", - hide_hint:"" - }, - integerFields: ["open_date","reduced_scoring_date","due_date","answer_date", - "problem_randorder","attempts_per_version","version_creation_time","version_time_limit", - "problems_per_page","versions_per_interval","version_last_attempt_time","time_interval"], + var UserSet = ProblemSet.extend({ idAttribute: "_id", - initialize: function(opts){ + initialize: function(opts,dateSettings){ if(_.isObject(opts)){ - _(this.attributes).extend(_(util.parseAsIntegers(opts,this.integerFields)).pick(this.integerFields)); + _(this.attributes).extend(_(util.parseAsIntegers(opts,this.integerFields)) + .pick(this.integerFields)); var pbs = (opts && opts.problems) ? opts.problems : []; if(pbs instanceof UserProblemList){ this.problems = pbs; @@ -56,18 +22,11 @@ define(['backbone', 'underscore','config','models/ProblemSet','models/UserProble this.problems = new UserProblemList(pbs,{user_id: this.get("user_id")}); } this.attributes.problems = this.problems; - } + } }, url: function () { return config.urlPrefix + "courses/" + config.courseSettings.course_id + "/users/" + this.get("user_id") + "/sets/" + this.get("set_id"); - }, - parse: function(response){ - if(response.problems && _.isArray(response.problems)){ - response.problems = new UserProblemList(response.problems,{user_id: response.user_id,set_id: response.set_id}); - } - response = util.parseAsIntegers(response,this.integerFields); - return response; } }); diff --git a/webwork3/public/js/models/UserSetList.js b/webwork3/public/js/models/UserSetList.js index 77204f9cac..60e67c4f0b 100644 --- a/webwork3/public/js/models/UserSetList.js +++ b/webwork3/public/js/models/UserSetList.js @@ -27,9 +27,11 @@ define(['backbone','models/UserSet','config'], function(Backbone, UserSet,config console.error("UserSetList error. The user field must be defined."); } if(this.loadProblems){ - return config.urlPrefix + "courses/" + config.courseSettings.course_id + "/users/" + this.user + "/sets/all/problems"; + return config.urlPrefix + "courses/" + config.courseSettings.course_id + "/users/" + + this.user + "/sets/all/problems"; } else { - return config.urlPrefix + "courses/" + config.courseSettings.course_id + "/users/" + this.user + "/sets"; + return config.urlPrefix + "courses/" + config.courseSettings.course_id + "/users/" + + this.user + "/sets"; } case "users": if(typeof(this.problemSet)==="undefined"){ diff --git a/webwork3/public/js/apps/CourseManager/main-views/AssignmentCalendar.js b/webwork3/public/js/views/AssignmentCalendar.js similarity index 68% rename from webwork3/public/js/apps/CourseManager/main-views/AssignmentCalendar.js rename to webwork3/public/js/views/AssignmentCalendar.js index 953052c89b..655a1bad62 100644 --- a/webwork3/public/js/apps/CourseManager/main-views/AssignmentCalendar.js +++ b/webwork3/public/js/views/AssignmentCalendar.js @@ -4,45 +4,72 @@ */ -define(['backbone', 'underscore', 'moment','views/MainView', 'views/CalendarView','apps/util','config'], - function(Backbone, _, moment,MainView, CalendarView,util,config) { +define(['backbone', 'underscore', 'moment','views/MainView', 'views/CalendarView', + 'models/AssignmentDate','models/AssignmentDateList','config','apps/util'], + function(Backbone, _, moment,MainView, CalendarView,AssignmentDate,AssignmentDateList,config,util) { var AssignmentCalendar = CalendarView.extend({ template: this.$("#calendar-date-bar").html(), popupTemplate: _.template(this.$("#calendar-date-popup-bar").html()), - headerInfo: {template: "#calendar-header", events: - { "click .previous-week": "viewPreviousWeek", - "click .next-week": "viewNextWeek", - "click .view-week": "showWeekView", - "click .view-month": "showMonthView"} - }, initialize: function (options) { var self = this; CalendarView.prototype.initialize.call(this,options); _.bindAll(this,"render","renderDay","update","showHideAssigns"); - - this.problemSets.on({sync: this.render}); - this.state.on("change:reduced_scoring_date change:answer_date change:due_date change:open_date", - this.showHideAssigns); - this.state.on("change",this.render); + _(this).extend(_(options).pick("problemSets","settings","users","eventDispatcher")); + + this.assignmentDates = util.buildAssignmentDates(this.problemSets); + this.problemSets.on({sync: self.render, + remove: function(_set){ + // update the assignmentDates to delete the proper assignments + + self.assignmentDates.remove(self.assignmentDates.filter(function(assign) { + return assign.get("problemSet").get("set_id")===_set.get("set_id");})); + }}).on("change:due_date change:open_date change:answer_date change:reduced_scoring_date", + function(_set){ + _set.adjustDates(); + self.assignmentDates.chain().filter(function(assign) { + return assign.get("problemSet").get("set_id")===_set.get("set_id");}) + .each(function(assign){ + assign.set("date",moment.unix(assign.get("problemSet").get(assign.get("type") + .replace("-","_")+"_date")) + .format("YYYY-MM-DD")); + }) + }).on("sync",function(_set) { + _(_set._network).chain().keys().each(function(key){ + switch(key){ + case "add": + self.assignmentDates.add(new AssignmentDate({type: "open", problemSet: _set, + date: moment.unix(_set.get("open_date")).format("YYYY-MM-DD")})); + self.assignmentDates.add(new AssignmentDate({type: "due", problemSet: _set, + date: moment.unix(_set.get("due_date")).format("YYYY-MM-DD")})); + self.assignmentDates.add(new AssignmentDate({type: "answer", problemSet: _set, + date: moment.unix(_set.get("answer_date")).format("YYYY-MM-DD")})); + self.assignmentDates.add(new AssignmentDate({type: "reduced-scoring", problemSet: _set, + date: moment.unix(_set.get("reduced_scoring_date")).format("YYYY-MM-DD")})); + delete _set._network; + break; + } + }); + }); return this; }, render: function (){ CalendarView.prototype.render.apply(this); - this.update(); + MainView.prototype.render.apply(this); + + // remove any popups that exist already. + this.$(".show-set-popup-info").popover("destroy") this.$(".assign").popover({html: true}); - // set up the calendar to scroll correctly - this.$(".calendar-container").height($(window).height()-160); $('.show-date-types input, .show-date-types label').click(function(e) { e.stopPropagation(); }); - MainView.prototype.render.apply(this); + // hides any popover clicked outside. $('body').on('click', function (e) { @@ -55,9 +82,10 @@ define(['backbone', 'underscore', 'moment','views/MainView', 'views/CalendarView } }); }); - - this.stickit(this.state,this.bindings); - this.showHideAssigns(this.state); + this.update(); + //this.stickit(this.state,this.bindings); + //this.showHideAssigns(this.state); + return this; }, bindings: { @@ -66,6 +94,9 @@ define(['backbone', 'underscore', 'moment','views/MainView', 'views/CalendarView ".show-reduced-scoring-date": "reduced_scoring_date", ".show-answer-date": "answer_date" }, + additionalEvents: function() { + return CalendarView.prototype.events.call(this); + }, renderDay: function (day){ var self = this; var assignments = this.assignmentDates.where({date: day.model.format("YYYY-MM-DD")}); @@ -77,6 +108,10 @@ define(['backbone', 'underscore', 'moment','views/MainView', 'views/CalendarView }); }, + set: function(opts){ + if(opts.assignmentDates)this.assignmentDates = opts.assignmentDates; + return CalendarView.prototype.set.apply(this,[opts]); + }, getHelpTemplate: function (){ return $("#calendar-help-template").html(); }, @@ -101,7 +136,7 @@ define(['backbone', 'underscore', 'moment','views/MainView', 'views/CalendarView } else if ($(ui.draggable).hasClass("assign-reduced-scoring")){ self.setDate($(ui.draggable).data("setname"),$(this).data("date"),"reduced_scoring_date"); } - + self.trigger("calendar-change"); } }); diff --git a/webwork3/public/js/views/CalendarView.js b/webwork3/public/js/views/CalendarView.js index e979afb0f2..1b053b34ff 100644 --- a/webwork3/public/js/views/CalendarView.js +++ b/webwork3/public/js/views/CalendarView.js @@ -8,16 +8,21 @@ define(['backbone', 'underscore','views/MainView', 'moment','jquery-truncate','bootstrap'], function(Backbone, _,MainView, moment) { - var CalendarView = MainView.extend({ + var CalendarView = Backbone.View.extend({ className: "calendar", initialize: function (options){ - MainView.prototype.initialize.call(this,options); - //this.constructor.__super__.constructor.__super__.initialize.apply(this, options); - _.bindAll(this, 'render','showWeekView','showMonthView','viewPreviousWeek','viewNextWeek'); // include all functions that need the this object - + var self = this; + _.bindAll(this, 'render','showWeekView','showMonthView','viewPreviousWeek','viewNextWeek'); + + var defaults = {num_of_weeks: 6, first_day: ""}; + this.state = new Backbone.Model(_.extend({},defaults,_(options).pick("num_of_weeks","first_day"))); + if (! this.date){ this.date = moment(); // today! } + this.state.on("change:first_day",function(){ + self.trigger("calendar-change"); + }); this.weekViews = []; // array of CalendarWeekViews return this; @@ -25,12 +30,8 @@ define(['backbone', 'underscore','views/MainView', 'moment','jquery-truncate','b render: function () { var self = this; - // remove any popups that exist already. - this.$(".show-set-popup-info").popover("destroy") - - var numberOfWeeks = this.state.get("calendar_type")==="month"? 6 : 2; this.weekViews = []; - for(var i = 0; i this.pageSize)? - this.pageSize : this.problems.length; + // start with showing 10 (page_size) problems + this.maxProblemIndex = (this.problems.length > this.page_size)? + this.page_size : this.problems.length; + if(this.page_size <0) { + this.maxProblemIndex = this.problems.length; + } this.pageRange = _.range(this.maxProblemIndex); this.problemViews = []; return this; @@ -73,7 +73,8 @@ define(['backbone', 'underscore', 'views/ProblemView','config','models/ProblemLi var self = this; var ul = this.$(".prob-list").empty(); _(this.pageRange).each(function(i){ - ul.append((self.problemViews[i] = new ProblemView({model: self.problems.at(i), + ul.append((self.problemViews[i] = new ProblemView({model: self.problems.at(i), + problem_set_view: self.problem_set_view, libraryView: self.libraryView, viewAttrs: self.viewAttrs})).render().el); }); @@ -83,6 +84,26 @@ define(['backbone', 'underscore', 'views/ProblemView','config','models/ProblemLi placeholder: "sortable-placeholder",axis: "y", stop: this.reorder}); } + // check if all of the problems are rendered. When they are, trigger an event + // + // I think this needs work. It appears that MathJax fires lots of "Math End" signals, + // although why not just one. + // + // this may also be part of the many calls to render throughout the app. + // (Note: after further work on another branch, this may not be necessary) + + _(this.problemViews).each(function(pv){ + if(pv && pv.model){ + pv.model.on("rendered", function () { + if(_(self.problemViews).chain().map(function(pv){ + if(pv) { + return pv.state.get("rendered");} + }).every().value()){ + self.trigger("rendered"); + } + }); + } + }) this.showPath(this.show_path); this.showTags(this.show_tags); this.updatePaginator(); @@ -104,7 +125,7 @@ define(['backbone', 'underscore', 'views/ProblemView','config','models/ProblemLi updatePaginator: function() { // render the paginator - this.maxPages = Math.ceil(this.problems.length / this.pageSize); + this.maxPages = Math.ceil(this.problems.length / this.page_size); var start =0, stop = this.maxPages; if(this.maxPages>8){ @@ -137,12 +158,12 @@ define(['backbone', 'underscore', 'views/ProblemView','config','models/ProblemLi }, showPath: function(_show){ this.show_path = _show; - _(this.problemViews).each(function(pv){ pv.set({show_path: _show})}); + _(this.problemViews).each(function(pv){ if(pv){pv.set({show_path: _show})}}); return this; }, showTags: function (_show) { this.show_tags = _show; - _(this.problemViews).each(function(pv){ pv.set({show_tags: _show})}); + _(this.problemViews).each(function(pv){ if(pv){pv.set({show_tags: _show})}}); return this; }, firstPage: function() { this.gotoPage(0);}, @@ -151,9 +172,9 @@ define(['backbone', 'underscore', 'views/ProblemView','config','models/ProblemLi lastPage: function() {this.gotoPage(this.maxPages-1);}, gotoPage: function(arg){ this.currentPage = /^\d+$/.test(arg) ? parseInt(arg,10) : parseInt($(arg.target).text(),10)-1; - this.pageRange = _.range(this.currentPage*this.pageSize, - (this.currentPage+1)*this.pageSize>this.problems.size()? this.problems.size():(this.currentPage+1)*this.pageSize); - + this.pageRange = this.page_size >0 ? _.range(this.currentPage*this.page_size, + (this.currentPage+1)*this.page_size>this.problems.size()? this.problems.size():(this.currentPage+1)*this.page_size) + : _.range(this.problems.length); this.updatePaginator(); this.renderProblems(); this.$(".problem-paginator button").removeClass("current-page"); @@ -165,30 +186,6 @@ define(['backbone', 'underscore', 'views/ProblemView','config','models/ProblemLi openSimpleEditor: function(){ console.log("opening the simple editor."); }, - reorder: function (event,ui) { - var self = this; - if(typeof(self.problems.problemSet) == "undefined"){ - return; - } - this.problems.problemSet.changingAttributes = {"problems_reordered":""}; - this.$(".problem").each(function (i) { - self.problems.findWhere({source_file: $(this).data("path")}) - .set({problem_id: i+1}, {silent: true}); // set the new order of the problems. - }); - this.problems.problemSet.save(); - }, - undoDelete: function(){ - if (this.undoStack.length>0){ - var prob = this.undoStack.pop(); - if(this.problems.findWhere({problem_id: prob.get("problem_id")})){ - prob.set("problem_id",parseInt(this.problems.last().get("problem_id"))+1); - } - this.problems.add(prob); - this.updatePaginator(); - this.gotoPage(this.currentPage); - this.problemSet.trigger("change:problems",this.problemSet); - } - }, setProblemSet: function(_set) { this.model = _set; if(this.model){ @@ -197,24 +194,18 @@ define(['backbone', 'underscore', 'views/ProblemView','config','models/ProblemLi return this; }, addProblemView: function (prob){ - var probView = new ProblemView({model: prob, type: this.type, viewAttrs: this.viewAttrs}); - this.$("#prob-list").append(probView.el); - probView.render(); - this.trigger("update-num-problems", - {number_shown: this.$(".prob-list li").length, total: this.problems.size()}); - + if(this.pageRange.length < this.page_size){ + var probView = new ProblemView({model: prob, problem_set_view: this.problem_set_view, + type: this.type, viewAttrs: this.viewAttrs}); + var numViews = this.problemViews.length; + probView.render().$el.data("id",this.model.get("set_id")+":"+(numViews+1)); + probView.model.set("_id", this.model.get("set_id")+":"+(numViews+1)); + this.$(".prob-list").append(probView.el); + this.problemViews.push(probView); + this.pageRange.push(_(this.pageRange).last() +1); + } + this.updateNumProblems(); }, - // this is called when the problem has been removed from the problemList - deleteProblem: function (problem){ - var self = this; - this.problemSet.changingAttributes = - {"problem_deleted": {setname: this.problemSet.get("set_id"), problem_id: problem.get("problem_id")}}; - this.problemSet.trigger("change:problems",this.problemSet); - this.problemSet.trigger("problem-deleted",problem); - this.undoStack.push(problem); - this.gotoPage(this.currentPage); - this.$(".undo-delete-button").removeAttr("disabled"); - } }); return ProblemListView; }); diff --git a/webwork3/public/js/views/ProblemSetView.js b/webwork3/public/js/views/ProblemSetView.js index 75ab57970f..e30a8f9500 100644 --- a/webwork3/public/js/views/ProblemSetView.js +++ b/webwork3/public/js/views/ProblemSetView.js @@ -1,21 +1,125 @@ -define(['backbone', 'views/ProblemListView'], - function(Backbone, ProblemListView) { - var ProblemSetView = ProblemListView.extend({ - viewName: "Problems", - initialize: function (options) { - this.viewAttrs = {reorderable: true, showPoints: true, showAddTool: false, - showEditTool: true, problem_seed: 1, showRefreshTool: true, - showViewTool: true, showHideTool: false, deletable: true, - draggable: false, show_undo: true}; - this.problemSet = options.problemSet; - options.type = "problem_set"; - ProblemListView.prototype.initialize.apply(this,[options]); - }, - render: function () { - ProblemListView.prototype.render.apply(this); - this.$(".prob-list-container").height($(window).height()-((this.maxPages==1) ? 200: 250)); +define(['backbone', 'views/ProblemListView', 'models/UserProblemList', 'models/ProblemList','moment'], +function (Backbone, ProblemListView, UserProblemList, ProblemList, moment) { + var ProblemSetView = ProblemListView.extend({ + viewName: "Problems", + initialize: function (options) { + this.viewAttrs = {reorderable: true, showPoints: true, showAddTool: false, showMaxAttempts: true, + showEditTool: false, problem_seed: 1, showRefreshTool: true, showTools: true, + showViewTool: false, showHideTool: false, deletable: true, + draggable: false, show_undo: true, markCorrect: true}; + _(this).extend(_(options).pick("problemSet","eventDispatcher")); + _(this).bindAll("reorder","deleteProblem"); + options.type = "problem_set"; + ProblemListView.prototype.initialize.apply(this,[options]); + + // this is where problems are placed upon delete, so the delete can be undone. + this.deletedProblems = new ProblemList(); + + this.set({problem_set_view: this, page_size: -1}); + }, + render: function () { + ProblemListView.prototype.render.apply(this); + this.$(".prob-list-container").height($(window).height()-((this.maxPages==1) ? 200: 250)); + }, + // this is a method that will mark the problem with id problem_id as correct (status=1) + //for all assigned users. + // + // (note: alternatively, we can make a method on the backend to handle all this) + + markAllCorrect: function(_prob_id){ + var self = this; + var prob = this.problemSet.problems.findWhere({problem_id: parseInt(_prob_id)}); + this.problemsToUpdate = _(this.problemSet.get("assigned_users")).map(function(_user_id){ + var upl = new UserProblemList([],{user_id: _user_id, + set_id: self.problemSet.get("set_id")}); + return upl.fetch({success: function(){ self.markProblemCorrect(upl,_prob_id)}}); + + }); + }, + markProblemCorrect: function(_prob_list,_prob_id){ + var self = this; + var prob = _prob_list.findWhere({problem_id: parseInt(_prob_id)}); + //var prob = _(_prob_list.models).find(function(prob) {return prob.get("problem_id")==_prob_id;}) + prob.set({status: 1}).save(prob.attributes,{success: function () { + var msg = {type: "problem_updated", + opts: {set_id: _prob_list.set_id, + user_id: _prob_list.user_id, + problem_id: _prob_id}}; + self.eventDispatcher.trigger("add-message",{type: "success", + short: self.messageTemplate(msg), + text:self.messageTemplate(msg)}); + //console.log(self); + + }}); + }, + // this removes the problem _prob from the problemSet. + deleteProblem: function (_prob){ + var self = this; + if(moment.unix(this.problemSet.get("open_date")).isBefore(new moment())){ + var conf = confirm(this.messageTemplate({type: "problem_deleted_warning"})); + + if(! conf){ + return; + } } - }); + + this.problemSet.changingAttributes = + {"problem_deleted": {setname: this.problemSet.get("set_id"), + problem_id: _prob.get("problem_id")}}; + this.deletedProblems.push(_(_prob.attributes).omit("_id")); + + var index = _(this.problemViews).findIndex(function(pv){ + return pv.model.get("problem_id") == _prob.get("problem_id")}); + var viewToRemove = this.problemViews.splice(index,1); + this.problemSet.deleteProblem(_prob); + viewToRemove[0].remove(); + /*_(this.problemViews).each(function(pv,i){ + pv.model.set({problem_id: (i+1), _id: self.problemSet.get("set_id")+":"+(i+1)}); + pv.$el.data("id",self.problemSet.get("set_id")+":"+(i+1)); + }); */ + }, + undoDelete: function(){ + if (this.deletedProblems.length>0){ + this.problemSet.addProblem(this.deletedProblems.pop()); + } + }, + setProblemSet: function(_set){ + var self = this; + this.problemSet = _set; + this.problemSet.problems.on("add",function(_prob){ + self.addProblemView(_prob); + }); + + ProblemListView.prototype.setProblemSet.call(this,_set); + return this; + }, + reorder: function (event,ui) { + var self = this; + if(typeof(this.problemSet) == "undefined"){ + return; + } + var oldProblems = this.problemSet.problems.map(function(p) { return _.clone(p.attributes); }); + var newProblemViews = []; + + this.$(".problem").each(function (i) { + // this determines which model the ith one is. + var id = $(this).data("id").split(":")[1]; // id is the ith problem_id in the reshuffled list. + var prob = _(oldProblems).find(function(p) {return p.problem_id == id; }); + var attrs = _.extend({},prob,{problem_id: (i+1), + _id: self.model.get("set_id") + ":" + (i+1), + _old_problem_id: id}); + newProblemViews[i] = _(self.problemViews).find(function(pv) { return pv.model.get("problem_id")==id}); + newProblemViews[i].model.set(attrs,{silent: true}); + newProblemViews[i].$el.data("id",self.problemSet.get("set_id")+":"+(i+1)); + }); + this.problemViews = newProblemViews; + + this.problemSet.problems.reset(_(this.problemViews).map(function(pv) { return pv.model.attributes;})); + + this.problemSet.save({_reorder: true}); + this.problemSet.unset("_reorder"); + }, + }); - return ProblemSetView; + return ProblemSetView; }); diff --git a/webwork3/public/js/views/ProblemView.js b/webwork3/public/js/views/ProblemView.js index 1c17902a89..6ef4580276 100644 --- a/webwork3/public/js/views/ProblemView.js +++ b/webwork3/public/js/views/ProblemView.js @@ -1,4 +1,5 @@ -define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl'], function(Backbone, _,config,Problem){ +define(['backbone', 'underscore','config','models/Problem','apps/util','imagesloaded','knowl','bootstrap'], + function(Backbone, _,config,Problem,util){ //##The problem View //A view defined for the browser app for the webwork Problem model. @@ -8,6 +9,7 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl reorderable (boolean): whether the reorder arrow should be shown showPoints (boolean): whether the # of points should be shown + showMaxAttemptes (boolean): whether the maximum number of attempts is shown. showAddTool (boolean): whether the + should be shown to be added to a problemSet showEditTool (boolean): whether the edit button should be shown showViewTool (boolean): whether the show button should be shown (to be taken to the userSetView ) @@ -15,7 +17,7 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl showHideTool (boolean): whether the hide button (X) should be shown to hide the problem deletable (boolean): is the problem deletable from the list its in. draggable (boolean): can the problem be dragged and show the drag arrow (for library problems) - displayMode (boolean): the PG display mode for the problem (images, MathJax, none) + displayMode (string): the PG display mode for the problem (images, MathJax, none) "tags_loaded", "tags_shown", "path_shown", @@ -30,7 +32,7 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl initialize:function (options) { var self = this; _.bindAll(this,"render","removeProblem","showPath","showTags"); - this.libraryView = options.libraryView; + _(this).extend(_(options).pick("libraryView","problem_set_view")); if(typeof(this.model)==="undefined"){ this.model = new Problem(); } @@ -48,17 +50,22 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl self.showPath(self.state.get("show_path")); }); - this.model.on('change:value', function () { - if(self.model.get("value").match(/^\d+$/)) { - self.model.save(); - } - }); + this.model.on('change:value change:max_attempts change:source_file', function () { + var isValid = self.model.isValid(_(self.model.changed).keys()); + if(isValid){ + self.problem_set_view.model.trigger("change:problems",self.problem_set_view.model,self.model); + }}).on('change:source_file', function(){ + self.model.set("data",""); + self.render(); + }); + this.invBindings = util.invBindings(this.bindings); }, render:function () { var self = this; + var group_name; if(this.model.get('data') || this.state.get("displayMode")=="None"){ - + if(this.state.get("displayMode")=="None"){ this.model.attributes.data=""; } @@ -84,6 +91,7 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl this.el.id = this.model.cid; // why do we need this? this.$el.attr('data-path', this.model.get('source_file')); + this.$el.attr('data-id', this.model.get('set_id')+":"+this.model.get("problem_id")); this.$el.attr('data-source', this.state.get("type")); if (this.state.get("displayMode")==="MathJax"){ MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.el]); @@ -91,8 +99,31 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl this.showPath(this.state.get("show_path")); this.stickit(); - this.model.trigger("rendered",this); - this.state.set("rendered",true); + Backbone.Validation.bind(this,{ + valid: function(view,attr){ + view.$(self.invBindings[attr]).popover("hide").popover("destroy"); + }, + invalid: function(view,attr,error){ + view.$(self.invBindings[attr]).popover({title: "Error", content: error,container: view.$el}).popover("show"); + } + }); + + + // send rendered signal after MathJax + if(MathJax){ + MathJax.Hub.Register.MessageHook("End Math", function (message) { + self.model.trigger("rendered",this); + self.state.set("rendered",true); + }) + } else { + this.model.trigger("rendered",this); + this.state.set("rendered",true); + } + } else if(group_name = this.model.get("source_file").match(/group:([\w._]+)$/)){ + if(! this.state.get("rendered")){ + this.model.set("data","This problem is selected from the group: " + group_name[1]); + this.render(); + } } else { this.state.set("rendered",false); this.$el.html($("#problem-loading-template").html()); @@ -115,9 +146,26 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl "click .add-problem": "addProblem", "click .seed-button": "toggleSeed", "click .path-button": function () {this.state.set("show_path",!this.state.get("show_path"))}, - "click .tags-button": function () {this.state.set("show_tags",!this.state.get("show_tags"))} + "click .tags-button": function () {this.state.set("show_tags",!this.state.get("show_tags"))}, + "click .mark-correct-btn": "markCorrect", + "keyup .prob-value,.max-attempts": function (evt){ + if(evt.keyCode == 13){ $(evt.target).blur() } + }, + "blur .max-attempts": function(evt){ + if($(evt.target).val()==-1){ + //I18N + $(evt.target).val("unlimited"); + } + } }, - bindings: {".prob-value": "value", + bindings: { + ".prob-value": {observe: "value", events: ['blur']}, + ".max-attempts": {observe: "max_attempts", events: ['blur'] , onSet: function(val) { + return (val=="unlimited")?-1:val; + }, onGet: function(val){ + return (val==-1)?"unlimited":val; + } + }, ".mlt-tag": "morelt", ".level-tag": "level", ".keyword-tag": "keyword", @@ -129,7 +177,7 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl ".DBsubject-tag": "subject", ".DBchapter-tag": "chapter", ".DBsection-tag": "section", - ".problem-path": "source_file", + ".problem-path": {observe: "source_file", events: ["blur"]}, ".seed": "problem_seed" }, reloadWithRandomSeed: function (){ @@ -139,32 +187,28 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl this.render(); }, showPath: function (_show){ - if(_show){ - this.$(".path-row").removeClass("hidden"); - } else { - this.$(".path-row").addClass("hidden"); - } + util.changeClass({els: this.$(".path-row"), state: _show, remove_class: "hidden"}); }, showTags: function (_show){ var self = this; - if(_show){ - if(this.state.get("tags_loaded")){ - this.$(".tag-row").removeClass("hidden"); - } else { - this.$(".loading-row").removeClass("hidden"); - this.model.loadTags({success: function (){ + if(_show && ! this.state.get("tags_loaded")){ + this.model.loadTags({success: function (){ self.$(".loading-row").addClass("hidden"); self.$(".tag-row").removeClass("hidden"); self.state.set('tags_loaded',true); }}); - } - } else { - this.$(".tag-row").addClass("hidden"); } + util.changeClass({els:this.$(".tag-row"),state: _show, remove_class: "hidden"}); }, toggleSeed: function () { this.$(".problem-seed").toggleClass("hidden"); }, + markCorrect: function () { + var conf = confirm(this.problem_set_view.messageTemplate({type:"mark_all_correct"})); + if(conf){ + this.problem_set_view.markAllCorrect(this.model.get("problem_id")); + } + }, addProblem: function (evt){ if(this.libraryView){ this.libraryView.addProblem(this.model); @@ -183,11 +227,11 @@ define(['backbone', 'underscore','config','models/Problem','imagesloaded','knowl this.$el.addClass("hidden"); }, removeProblem: function(){ - this.model.collection.remove(this.model); - this.remove(); // remove the view + this.problem_set_view.deleteProblem(this.model); + }, set: function(opts){ - this.state.set(_(opts).pick("show_path","show_tags","tags_loaded")) + this.state.set(_(opts).pick("show_path","show_tags","tags_loaded")); } }); diff --git a/webwork3/public/js/views/TabbedMainView.js b/webwork3/public/js/views/TabbedMainView.js index 09eaec4e21..32bf6b8e92 100644 --- a/webwork3/public/js/views/TabbedMainView.js +++ b/webwork3/public/js/views/TabbedMainView.js @@ -70,12 +70,13 @@ define(['backbone','underscore','views/MainView'], }, setState: function(_state){ var self = this; - MainView.prototype.setState.apply(this,[_state]); + if(_state){ _(_state.tab_states).chain().keys().each(function(st){ - self.views[st].tabState.set(_state.tab_states[st],{silent: true}); + self.views[st].tabState.set(_state.tab_states[st]); }); } + MainView.prototype.setState.apply(this,[_state]); return this; }, getDefaultState: function () { diff --git a/webwork3/public/js/views/WebPage.js b/webwork3/public/js/views/WebPage.js index ec43c3e11b..5398578bd5 100644 --- a/webwork3/public/js/views/WebPage.js +++ b/webwork3/public/js/views/WebPage.js @@ -52,7 +52,7 @@ function(Backbone,MessageListView,ModalView,config,NavigationBar,Sidebar){ ul.append(menuItemTemplate({name: _view.info.name, id: _view.info.id,icon: _view.info.icon})); }); - // this ensures that the rerender call on resizing the window only occurs once every 500 ms. + // this ensures that the rerender call on resizing the window only occurs once every 250 ms. var renderMainPane = _.debounce(function(evt){ self.currentView.render(); @@ -101,7 +101,7 @@ function(Backbone,MessageListView,ModalView,config,NavigationBar,Sidebar){ if(! this.currentSidebar){ var otherSidebars = this.mainViewList.getOtherSidebars(this.currentView.info.id); if(otherSidebars[0]){ - this.changeSidebar([0]); + this.changeSidebar(otherSidebars[0]); } else { this.changeSidebar("help",{is_open: true}); } @@ -159,11 +159,10 @@ function(Backbone,MessageListView,ModalView,config,NavigationBar,Sidebar){ _(this.currentView.sidebarEvents).chain().keys().each(function(event){ self.currentView.listenTo(self.currentSidebar,event,self.currentView.sidebarEvents[event]); }); + this.currentSidebar.mainView = this.currentView; - - // set up the possible options and render the sidebar this.$(".sidebar-menu .sidebar-name").text(this.currentSidebar.info.name); diff --git a/webwork3/public/js/views/library-views/LibraryProblemsView.js b/webwork3/public/js/views/library-views/LibraryProblemsView.js index 7dfa3b670a..1e7886d806 100644 --- a/webwork3/public/js/views/library-views/LibraryProblemsView.js +++ b/webwork3/public/js/views/library-views/LibraryProblemsView.js @@ -3,8 +3,10 @@ define(['backbone', 'views/ProblemListView','config'], var LibraryProblemsView = ProblemListView.extend({ initialize: function (options) { _(this).bindAll("highlightCommonProblems"); - this.viewAttrs = {reorderable: false, showPoints: false, showAddTool: true, showEditTool: true, problem_seed: 1, - showRefreshTool: true, showViewTool: true, showHideTool: true, deletable: false, draggable: true}; + this.viewAttrs = { + reorderable: false, showPoints: false, showAddTool: true, showEditTool: true, + problem_seed: 1, showMaxAttempts: false, showRefreshTool: true, showViewTool: true, + showHideTool: true, deletable: false, draggable: true, markCorrect: false}; _.extend(this,_(options).pick("problemSets","libraryView","settings","type")); ProblemListView.prototype.initialize.apply(this,[options]); }, @@ -30,7 +32,7 @@ define(['backbone', 'views/ProblemListView','config'], pv.highlight(_(pathsInCommon).contains(pathsInLibrary[i])); } else { pv.model.once("rendered", function(v) { - v.highlight(_(pathsInCommon).contains(pathsInLibrary[i])); + pv.highlight(_(pathsInCommon).contains(pathsInLibrary[i])); }); } }); diff --git a/webwork3/t/001_base.t b/webwork3/t/001_base.t index 19348d7a3a..93a37dabc0 100644 --- a/webwork3/t/001_base.t +++ b/webwork3/t/001_base.t @@ -1,5 +1,39 @@ -use Test::More tests => 1; +use Test::More tests => 5; use strict; use warnings; -use_ok 'webwork3'; +use Data::Dump qw/dd/; +use JSON qw/from_json/; + +BEGIN {$ENV{MOD_PERL_API_VERSION}=2} + +use WeBWorK3; +Dancer::set logger => 'console'; +use Dancer::Test; + + +# +#my $settings = Dancer::Config::settings(); +#dd $settings; + +response_status_is [GET => '/app-info'], 200, "GET /webwork3/app-info is found"; +route_exists [GET => '/app-info'], "GET /webwork3/app-info is handled"; + +my $resp = dancer_response GET => '/app-info'; +my $obj = from_json $resp->{content}; +is( $obj->{appname}, "webwork3", "The webapp returned as 'webwork3'" ); + +route_exists [GET=> '/courses'] , "GET /webwork3/courses is handled."; + +#route_exists [GET => '/webwork3/courses/test' ], "GET /webwork3/courses/test is handled."; + +$resp = dancer_response(GET=>'/courses'); +my @courses = from_json($resp->{content}); +my $type; +for my $item (@courses){ +$type = ref($item); +} + +is($type,"ARRAY", "The method GET /webwork3/courses returns an array"); + + diff --git a/webwork3/t/002_create_course.t b/webwork3/t/002_create_course.t new file mode 100644 index 0000000000..95e238ad85 --- /dev/null +++ b/webwork3/t/002_create_course.t @@ -0,0 +1,42 @@ +use Test::More tests => 4; +use strict; +use warnings; +use JSON qw/from_json/; + +BEGIN {$ENV{MOD_PERL_API_VERSION}=2} + +# the order is important +use WeBWorK3; +Dancer::set logger => 'console'; +use Dancer::Test; + + +## login to the admin course +my $new_course_name = "newcourseXYZ"; +my $resp = dancer_response(POST=>'/courses/admin/login',{params=>{user=>'admin',password=>'admin'}}); +my $obj = from_json $resp->{content}; +is($obj->{logged_in},1,"You successfully logged in to the admin course"); + +## see if the course $new_course_name already exists. +route_exists [GET => '/courses/'. $new_course_name ], "GET /webwork3/courses/" . $new_course_name . " is handled"; + + +## check that that course create URL exists. +route_exists [POST => '/courses/'. $new_course_name ], "POST /webwork3/courses/" . $new_course_name . " is handled"; + +## check if the course $new_course_name exists. +$resp = dancer_response(GET=>'/courses/'. $new_course_name, {headers=>HTTP::Headers->new( 'X-Requested-With' => 'XMLHttpRequest')}); +$obj = from_json $resp->{content}; + +my $course_exists = ($obj->{message} ||"") eq "Course exists."; + +if($course_exists) { + is($obj->{message}, "Course exists.", "The course " .$new_course_name . " exists."); +} else { + ## create a new course called $new_course_name + $resp = dancer_response(POST=>'/courses/'. $new_course_name, + {params=>{user=>'admin',password=>'admin',new_userID => 'profa'}}); + $obj = from_json $resp->{content}; + + is($obj->{message},"Course created successfully.","The course " . $new_course_name . " was created sucessfully."); +} \ No newline at end of file diff --git a/webwork3/t/002_index_route.t b/webwork3/t/002_index_route.t deleted file mode 100644 index 81319d4387..0000000000 --- a/webwork3/t/002_index_route.t +++ /dev/null @@ -1,10 +0,0 @@ -use Test::More tests => 2; -use strict; -use warnings; - -# the order is important -use webwork3; -use Dancer::Test; - -route_exists [GET => '/'], 'a route handler is defined for /'; -response_status_is ['GET' => '/'], 200, 'response status is 200 for /'; diff --git a/webwork3/t/003_delete_course.t b/webwork3/t/003_delete_course.t new file mode 100644 index 0000000000..a826605000 --- /dev/null +++ b/webwork3/t/003_delete_course.t @@ -0,0 +1,43 @@ +use Test::More tests => 3; +use strict; +use warnings; +use JSON qw/from_json/; +use Data::Dump qw/dd/; + +BEGIN {$ENV{MOD_PERL_API_VERSION}=2} + +# the order is important +use WeBWorK3; +Dancer::set logger => 'console'; +#Dancer::set log => 'core'; +use Dancer::Test; + + +## login to the admin course +my $new_course_name = "newcourseXYZ"; +my $resp = dancer_response(POST=>'/courses/admin/login',{params=>{user=>'admin',password=>'admin'}}); +my $obj = from_json $resp->{content}; +is($obj->{logged_in},1,"You successfully logged in to the admin course"); + +## see if the course $new_course_name already exists. +route_exists [GET => '/courses/'. $new_course_name ], "GET /webwork3/courses/" . $new_course_name . " is handled"; + + +## check that that course create URL exists. +#route_exists [DELETE => '/courses/'. $new_course_name ], "DELETE /webwork3/courses/" . $new_course_name . " is handled"; + +## check if the course $new_course_name exists. +$resp = dancer_response(DELETE=>'/courses/'. $new_course_name, {headers=>HTTP::Headers->new( 'X-Requested-With' => 'XMLHttpRequest')}); +$obj = from_json $resp->{content}; + +my $course_exists = $obj->{message} eq "Course exists."; +dd $course_exists; +if($course_exists) { + +## create a new course called $new_course_name + $resp = dancer_response(DEL=>'/courses/'. $new_course_name, + {params=>{user=>'admin',password=>'admin'}}); + $obj = from_json $resp->{content}; + + is($obj->{message},"Course deleted.","The course " . $new_course_name . " was created sucessfully."); +} \ No newline at end of file diff --git a/webwork3/t/004_problem_sets.t b/webwork3/t/004_problem_sets.t new file mode 100644 index 0000000000..647b3d4f9e --- /dev/null +++ b/webwork3/t/004_problem_sets.t @@ -0,0 +1,85 @@ +use Test::More tests => 4; +use Test::Deep qw/cmp_deeply/; +use strict; +use warnings; +use JSON qw/from_json/; +use Data::Dump qw/dd/; +use DateTime; +use Utils::Convert qw/convertBooleans/; +use Utils::ProblemSets qw/@boolean_set_props/; + +BEGIN {$ENV{MOD_PERL_API_VERSION}=2} + +# the order is important +use WeBWorK3; +Dancer::set logger => 'console'; +#Dancer::set log => 'core'; +use Dancer::Test; + + +## login to the admin course +my $new_course_name = "newcourseXYZ"; +my $resp = dancer_response(POST=>'/courses/admin/login',{params=>{user=>'admin',password=>'admin'}}); +my $obj = from_json $resp->{content}; +is($obj->{logged_in},1,"You successfully logged in to the admin course"); + +## see if the course $new_course_name already exists. +route_exists [GET => '/courses/'. $new_course_name ], "GET /webwork3/courses/" . $new_course_name . " is handled"; + +## check if the course $new_course_name exists. +$resp = dancer_response(GET=>'/courses/'. $new_course_name, {headers=>HTTP::Headers->new( 'X-Requested-With' => 'XMLHttpRequest')}); +$obj = from_json $resp->{content}; + +my $course_exists = ($obj->{message} ||"") eq "Course exists."; + +if($course_exists) { + is($obj->{message}, "Course exists.", "The course " .$new_course_name . " exists."); +} else { + ## create a new course called $new_course_name + $resp = dancer_response(POST=>'/courses/'. $new_course_name, + {params=>{user=>'admin',password=>'admin',new_userID => 'profa'}}); + $obj = from_json $resp->{content}; + + is($obj->{message},"Course created successfully.","The course " . $new_course_name . " was created sucessfully."); +} + +## login to the course $new_course_name as profa + +$resp = dancer_response(POST=>"/courses/$new_course_name/login",{params=>{user=>'profa',password=>'profa'}}); +$obj = from_json $resp->{content}; +is($obj->{logged_in},1,"You successfully logged in to the $new_course_name course"); + +## Create a new Problem set that is open today at 10am, has a reduced scoring date 1 week later, a due date 2 days after that +## and a answer_date 3 after that. + +my $now = DateTime->today(time_zone=>"America/New_York"); +my $open_date = DateTime->new(year=>$now->year(),month=>$now->month(),day=>$now->day(), + hour=>10,minute=>0,second=>0,time_zone=>"America/New_York"); +my $reduced_scoring_date = $open_date->clone()->add(days=>7); +my $due_date = $reduced_scoring_date->clone()->add(days=>2); +my $answer_date = $due_date->clone()->add(days=>3); + +my $set = { set_id => "set1", open_date => $open_date->epoch(), reduced_scoring_date => $reduced_scoring_date->epoch(), + due_date => $due_date->epoch(), answer_date => $answer_date->epoch(), assigned_users => [], problems => [], + hide_hint => 0, problems_per_page => '', versions_per_interval => '', time_interval => '', hide_score => '', + attempts_per_version => '',restricted_login_proctor => '', version_creation_time => '', _id => "set1", + set_header => 'defaultHeader', hardcopy_header => 'defaultHeader', restrict_ip => '', hide_score_by_problem => '', + problem_randorder => 0, description=>'', hide_work => '', restricted_status => '', + version_time_limit => '', relax_restrict_ip => '', restricted_release => '', version_last_attempt_time => '', + visible => 0, enable_reduced_scoring => 0, time_limit_cap => 0, assignment_type => "default"}; + + +$resp = dancer_response(POST=>"/courses/$new_course_name/sets/set1",{params=>$set}); +$obj = from_json $resp->{content}; +my $obj2 = convertBooleans($obj,\@boolean_set_props); + +cmp_deeply $obj2, $set, "yippee!"; + + +sub toString { + my $obj = shift; + for my $key (keys(%$obj)){ + $obj->{$key} = '' . $obj->{$key}; + } + return $obj; +} \ No newline at end of file diff --git a/webwork3/testing/dir-search b/webwork3/testing/dir-search new file mode 100644 index 0000000000..05f594ab96 --- /dev/null +++ b/webwork3/testing/dir-search @@ -0,0 +1,19 @@ +#!/usr/bin/env perl + +#use strict; +use warnings; + +use File::Find::Rule; +use Data::Dumper; + +my $dir = "/Library/WebServer/Documents/webwork/courses/test"; + +my $inclRE = qr/header.*\.pg$/i; +my @skip = ('set40','Library'); +my $rule = File::Find::Rule->new; +$rule->or($rule->new->directory->name(@skip)->prune->discard,$rule->new); +my @files = $rule->file()->name($inclRE)->in($dir); + +my @x = map { my @dirs = split("/templates/",$_); $dirs[1];} @files; + +print Dumper(@x); \ No newline at end of file diff --git a/webwork3/testing/test3.pl b/webwork3/testing/test3.pl index a34770bccd..368900f8d7 100644 --- a/webwork3/testing/test3.pl +++ b/webwork3/testing/test3.pl @@ -1,14 +1,31 @@ #!/local/bin/perl -my $string = "/Library/subjects/Calculus - single variable/chapters/Applications of differentiation/sections/Increasing/decreasing functions and local extrema/problems"; -my $re = m/\/Library\/subjects\/(.+)\/chapters\/(.+)\/sections\/(.+)\/problems/; +use DateTime; +use DateTime::TimeZone; +use feature qw/say/; +use Data::Dump; -my $re2 = m/\/Library\/subjects/; -my $string2 = "/Library/subjects"; +use Utils::GeneralUtils qw/timeToUTC/; -if ($string =~ $re) { - print "yeah!"; +#my $dt = DateTime->new(year=>2015,month=>7,day=>17,hour=>9,minute=>45,second=>0,time_zone=>"America/New_York"); + +my $dt = DateTime->now; +$dt->time_zone("UTC"); +#my $start_time = 1430020140; + +my $time = timeToUTC($dt->epoch,"America/New_York"); + +#my $new_time = DateTime->from_epoch(epoch=>$start_time,time_zone=>"America/New_York"); +# +#say $new_time->mdy("/") . " " . $new_time->hms; +# +##dd($new_time); +# +#say $otime = DateTime->from_epoch(epoch=>$start_time); +#say $otime->mdy("/") . " " . $otime->hms; +#say $new_time->epoch; + + +my $utc = DateTime::TimeZone->new( name => "UTC"); +dd $utc; -} else { - print "nae!"; -} \ No newline at end of file diff --git a/webwork3/views/course_manager.tt b/webwork3/views/course_manager.tt index 211170415b..090ef72f22 100644 --- a/webwork3/views/course_manager.tt +++ b/webwork3/views/course_manager.tt @@ -141,101 +141,6 @@ main_view_paths.unshift("module","backbone"); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - \ No newline at end of file + + + diff --git a/webwork3/views/main/problem_set_manager.tt b/webwork3/views/main/problem_set_manager.tt index f5f71b82cf..32a204361a 100644 --- a/webwork3/views/main/problem_set_manager.tt +++ b/webwork3/views/main/problem_set_manager.tt @@ -112,12 +112,14 @@ var messages = { set_error_details: function(opt){ return "The set " + opt.setname + " had an error."}, set_assigned_users_saved: function(opt){ return "The assigned users for set " + opt.setname + " was updated."}, + mark_all_correct: function (opt) {return "Would you like to mark this problem correct for all assigned students? This can not be undone.";}, problem_added: function(opt){ return "A problem was added to " + opt.setname}, problem_added_details: function(opt){ return "A problem was added to " + opt.setname}, problems_reordered: function(opt){ return "The problems were reordered in " + opt.setname}, problems_reordered_details: function(opt){ return "The problems were reordered in " + opt.setname}, - problems_values_details: function(opt) { return "The value for problem " + opt.problem_id + " of set " + opt.set_id + - " was changed from "+ opt.oldValue + " to " + opt.newValue + "."}, + problems_values_details: function(opt) { return "The attribute " + opt.attribute + " for problem " + + opt.problem_id + " of set " + opt.set_id + + " was changed from "+ opt.oldValue + " to " + opt.newValue + "."}, problem_deleted: function(opt) { return "A problem was deleted from set " + opt.setname}, problem_deleted_details: function(opt) { return "The problem with problem_id " + opt.problem_id + " was deleted from set " + opt.setname}, @@ -125,7 +127,11 @@ var messages = { setting_saved: function(opt){ return "The setting " + opt.varname + " was saved.";}, setting_saved_details: function(opt) { return "The setting " + opt.varname + " has changed from " + opt.oldValue + " to " + opt.newValue + "."}, - empty_selected_sets_error: function () { return "You must select at least one set.";} + empty_selected_sets_error: function () { return "You must select at least one set.";}, + problem_updated: function(opt) { return "Problem # " + opt.problem_id + " for set " + opt.set_id + + " and user " + opt.user_id + " has been updated.";}, + problem_deleted_warning: function () { return "This set has been opened. It is not advised to delete a " + + "problem that has been open. Click OK to continue deleting this problem or Cancel.";} } print(messages[type](typeof(opts)=="undefined"?{}:opts)) %>