From c1132f6e71a09bfcdc72fb7751867c0c6b9dbee1 Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Thu, 16 Nov 2023 15:47:42 +1300 Subject: [PATCH 1/9] Upgrade to support Moodle 3.11 and other features like exporting, grading via ajax. --- actions/ajax/datatable/grading.php | 39 + actions/ajax/deadline_extension/edit.php | 32 + actions/ajax/deadline_extension/new.php | 32 + actions/ajax/deadline_extension/submit.php | 33 + actions/feedbacks/create.php | 7 + actions/feedbacks/edit.php | 2 + actions/feedbacks/new.php | 4 +- actions/feedbacks/show.php | 3 + actions/feedbacks/update.php | 11 + actions/personal_deadline.php | 36 +- actions/upload_allocations.php | 3 + backup/moodle2/backup_coursework_stepslib.php | 1 + .../moodle2/restore_coursework_stepslib.php | 1 + classes/ability.php | 2 +- classes/allocation/auto_allocator.php | 2 + classes/allocation/table/cell/builder.php | 50 +- classes/allocation/table/row/builder.php | 33 +- classes/allocation/upload.php | 16 +- classes/auto_grader/auto_grader.php | 2 +- classes/auto_grader/average_grade.php | 159 + classes/auto_grader/none.php | 2 +- classes/auto_grader/percentage_distance.php | 4 +- .../deadline_extensions_controller.php | 228 + classes/controllers/feedback_controller.php | 259 +- classes/controllers/grading_controller.php | 33 + .../personal_deadlines_controller.php | 89 + .../controllers/submissions_controller.php | 44 + classes/cron.php | 5 +- classes/export/csv.php | 41 +- classes/export/csv/cells/agreedgrade_cell.php | 8 +- .../export/csv/cells/assessorgrade_cell.php | 2 +- classes/export/csv/cells/email_cell.php | 37 + classes/export/csv/cells/idnumber_cell.php | 37 + classes/export/csv/cells/name_cell.php | 5 +- .../export/csv/cells/otherassessors_cell.php | 30 +- classes/export/csv/cells/username_cell.php | 5 +- classes/export/grading_sheet.php | 4 +- classes/export/import.php | 90 +- classes/forms/advance_plugins_form.php | 67 + classes/forms/assessor_feedback_mform.php | 97 +- .../choose_student_for_submission_mform.php | 2 +- classes/forms/student_submission_form.php | 9 +- classes/framework/decorator.php | 2 +- classes/framework/table_base.php | 54 +- classes/grade_judge.php | 11 +- classes/grades/gradeitems.php | 61 + classes/grading_report.php | 128 +- classes/grading_table_row_base.php | 107 +- classes/mailer.php | 168 +- classes/models/allocation.php | 67 +- classes/models/assessment_set_membership.php | 60 + classes/models/course_module.php | 61 + classes/models/coursework.php | 605 +- classes/models/deadline_extension.php | 74 +- classes/models/feedback.php | 114 +- classes/models/group.php | 32 + classes/models/moderation.php | 2 +- classes/models/module.php | 61 + classes/models/personal_deadline.php | 60 +- classes/models/plagiarism_flag.php | 69 +- classes/models/submission.php | 446 +- classes/models/user.php | 81 +- classes/observer.php | 19 + .../personal_deadline/table/row/builder.php | 53 + classes/plagiarism_helpers/turnitin.php | 4 +- classes/privacy/provider.php | 134 +- .../grading_report/cells/_user_cell.php | 113 + .../grading_report/cells/cell_base.php | 27 +- .../grading_report/cells/email_cell.php | 50 + .../grading_report/cells/first_name_cell.php | 50 + .../cells/first_name_letter_cell.php | 58 + .../grading_report/cells/group_cell.php | 2 +- .../grading_report/cells/idnumber_cell.php | 66 + .../grading_report/cells/last_name_cell.php | 50 + .../cells/last_name_letter_cell.php | 58 + .../cells/multiple_agreed_grade_cell.php | 31 +- .../cells/personal_deadline_cell.php | 42 +- .../cells/single_assessor_feedback_cell.php | 17 +- .../grading_report/cells/submission_cell.php | 3 +- .../cells/time_submitted_cell.php | 80 +- .../grading_report/cells/user_cell.php | 47 +- .../multi_marker_feedback_sub_rows.php | 69 +- classes/router.php | 26 + classes/stages/base.php | 265 +- classes/traits/allocatable_functions.php | 63 +- classes/user_row.php | 4 + classes/utils/cs_editor.php | 109 + classes/warnings.php | 18 + datatables/css/buttons.datatables.min.css | 1 + datatables/css/datatables.bootstrap.min.css | 1 + datatables/css/jquery.datetimepicker.css | 568 + datatables/css/responsive.bootstrap.min.css | 1 + datatables/css/searchpanes.datatables.min.css | 1 + datatables/css/select.bootstrap.min.css | 1 + datatables/css/select.dataTables.min.css | 1 + datatables/images/sort_asc.png | Bin 0 -> 160 bytes datatables/images/sort_asc_disabled.png | Bin 0 -> 148 bytes datatables/images/sort_both.png | Bin 0 -> 201 bytes datatables/images/sort_desc.png | Bin 0 -> 158 bytes datatables/images/sort_desc_disabled.png | Bin 0 -> 146 bytes datatables/js/allocation_datatables.js | 472 + .../js/bulkplagiarismflag_datatables.js | 239 + datatables/js/datatables.buttons.js | 2104 +++ datatables/js/datatables.js | 337 + datatables/js/datatables.responsive.min.js | 45 + datatables/js/datatables.searchpanes.js | 2534 +++ datatables/js/datatables.select.js | 1206 ++ datatables/js/edit_datatables.js | 1001 + datatables/js/jquery-3.3.1.min.js | 2 + datatables/js/jquery.datatables.js | 15354 ++++++++++++++++ datatables/js/jquery.datetimepicker.js | 2718 +++ datatables/js/jquery.mousewheel.js | 238 + datatables/js/moment.js | 5668 ++++++ datatables/js/php-date-formatter.min.js | 13 + db/events.php | 8 + db/install.xml | 3 + db/upgrade.php | 75 +- index.php | 56 +- lang/en/coursework.php | 64 +- lib.php | 143 +- mod_form.php | 129 +- module.js | 25 + pix/calendar-icon.png | Bin 0 -> 447 bytes pix/details_close.png | Bin 0 -> 686 bytes pix/details_open.png | Bin 0 -> 709 bytes pix/loadding.gif | Bin 0 -> 3208 bytes renderers/grading_report_renderer.php | 132 +- renderers/object_renderer.php | 379 +- renderers/page_renderer.php | 536 +- settings.php | 8 + styles.css | 520 +- version.php | 6 +- view.php | 93 +- 133 files changed, 38673 insertions(+), 1296 deletions(-) create mode 100644 actions/ajax/datatable/grading.php create mode 100644 actions/ajax/deadline_extension/edit.php create mode 100644 actions/ajax/deadline_extension/new.php create mode 100644 actions/ajax/deadline_extension/submit.php create mode 100644 classes/auto_grader/average_grade.php create mode 100644 classes/controllers/grading_controller.php create mode 100644 classes/export/csv/cells/email_cell.php create mode 100644 classes/export/csv/cells/idnumber_cell.php create mode 100644 classes/forms/advance_plugins_form.php create mode 100644 classes/grades/gradeitems.php create mode 100644 classes/models/course_module.php create mode 100644 classes/models/module.php create mode 100644 classes/render_helpers/grading_report/cells/_user_cell.php create mode 100644 classes/render_helpers/grading_report/cells/email_cell.php create mode 100644 classes/render_helpers/grading_report/cells/first_name_cell.php create mode 100644 classes/render_helpers/grading_report/cells/first_name_letter_cell.php create mode 100644 classes/render_helpers/grading_report/cells/idnumber_cell.php create mode 100644 classes/render_helpers/grading_report/cells/last_name_cell.php create mode 100644 classes/render_helpers/grading_report/cells/last_name_letter_cell.php create mode 100644 classes/utils/cs_editor.php create mode 100644 datatables/css/buttons.datatables.min.css create mode 100644 datatables/css/datatables.bootstrap.min.css create mode 100644 datatables/css/jquery.datetimepicker.css create mode 100644 datatables/css/responsive.bootstrap.min.css create mode 100644 datatables/css/searchpanes.datatables.min.css create mode 100644 datatables/css/select.bootstrap.min.css create mode 100644 datatables/css/select.dataTables.min.css create mode 100644 datatables/images/sort_asc.png create mode 100644 datatables/images/sort_asc_disabled.png create mode 100644 datatables/images/sort_both.png create mode 100644 datatables/images/sort_desc.png create mode 100644 datatables/images/sort_desc_disabled.png create mode 100644 datatables/js/allocation_datatables.js create mode 100644 datatables/js/bulkplagiarismflag_datatables.js create mode 100644 datatables/js/datatables.buttons.js create mode 100644 datatables/js/datatables.js create mode 100644 datatables/js/datatables.responsive.min.js create mode 100644 datatables/js/datatables.searchpanes.js create mode 100644 datatables/js/datatables.select.js create mode 100644 datatables/js/edit_datatables.js create mode 100644 datatables/js/jquery-3.3.1.min.js create mode 100644 datatables/js/jquery.datatables.js create mode 100644 datatables/js/jquery.datetimepicker.js create mode 100644 datatables/js/jquery.mousewheel.js create mode 100644 datatables/js/moment.js create mode 100644 datatables/js/php-date-formatter.min.js create mode 100644 pix/calendar-icon.png create mode 100644 pix/details_close.png create mode 100644 pix/details_open.png create mode 100644 pix/loadding.gif diff --git a/actions/ajax/datatable/grading.php b/actions/ajax/datatable/grading.php new file mode 100644 index 0000000..a3875c1 --- /dev/null +++ b/actions/ajax/datatable/grading.php @@ -0,0 +1,39 @@ +set_context(context_system::instance()); + +$group = required_param('group', PARAM_INT); +$perpage = required_param('perpage', PARAM_INT); +$sortby = required_param('sortby', PARAM_ALPHA); +$sorthow = required_param('sorthow', PARAM_ALPHA); +$courseworkid = required_param('courseworkid', PARAM_INT); +$unallocated = optional_param('unallocated', false, PARAM_BOOL); + +// Grading report display options. +$report_options = array(); +if ($unallocated) { + $report_options['unallocated'] = true; +} + +$report_options['group'] = $group; +$report_options['perpage'] = $perpage; +$report_options['sortby'] = $sortby; +$report_options['sorthow'] = $sorthow; +$report_options['showsubmissiongrade'] = false; +$report_options['showgradinggrade'] = false; +$report_options['courseworkid'] = $courseworkid; +$report_options['mode'] = \mod_coursework\grading_report::$MODE_GET_REMAIN_RECORDS; + +//$controller = new mod_coursework\controllers\grading_controller(['courseworkid' => $report_options, 'allocatableid' => $USER->id, 'allocatabletype' => $USER->id]); +$controller = new mod_coursework\controllers\grading_controller([]); +sleep(10); +$table_html = $controller->get_remain_rows_grading_table($report_options); +if (ob_get_contents()) { + ob_end_clean(); +} + +echo $table_html; diff --git a/actions/ajax/deadline_extension/edit.php b/actions/ajax/deadline_extension/edit.php new file mode 100644 index 0000000..34a70bb --- /dev/null +++ b/actions/ajax/deadline_extension/edit.php @@ -0,0 +1,32 @@ +id, PARAM_INT); +$allocatabletype = optional_param('allocatabletype', $USER->id, PARAM_ALPHANUMEXT); +$requesttype = optional_param('requesttype', 'new', PARAM_ALPHANUMEXT); +$id = optional_param('id', 0, PARAM_INT); +$extra_information_text = optional_param('text', '', PARAM_RAW); +$extra_information_format = optional_param('format', 1, PARAM_ALPHANUMEXT); +$extended_deadline = optional_param('extended_deadline', 0, PARAM_ALPHANUMEXT); +$pre_defined_reason = optional_param('pre_defined_reason', null, PARAM_ALPHANUMEXT); +$submissionid = optional_param('submissionid', 0, PARAM_INT); +$name = optional_param('name', 0, PARAM_ALPHANUMEXT); +$params = array( + 'courseworkid' => $courseworkid, + 'allocatableid' => $allocatableid, + 'allocatabletype' => $allocatabletype, +); +$controller = new mod_coursework\controllers\deadline_extensions_controller($params); + +$params['id'] = $id; +$params['extra_information_text'] = $extra_information_text; +$params['extra_information_format'] = $extra_information_format; +$params['extended_deadline'] = strtotime($extended_deadline); +$params['pre_defined_reason'] = $pre_defined_reason; +$params['submissionid'] = $submissionid; +$params['name'] = $name; + +$controller->ajax_edit_mitigation($params); \ No newline at end of file diff --git a/actions/ajax/deadline_extension/new.php b/actions/ajax/deadline_extension/new.php new file mode 100644 index 0000000..59b9ac9 --- /dev/null +++ b/actions/ajax/deadline_extension/new.php @@ -0,0 +1,32 @@ +id, PARAM_INT); +$allocatabletype = optional_param('allocatabletype', $USER->id, PARAM_ALPHANUMEXT); +$requesttype = optional_param('requesttype', 'new', PARAM_ALPHANUMEXT); +$id = optional_param('id', 0, PARAM_INT); +$extra_information_text = optional_param('text', '', PARAM_RAW); +$extra_information_format = optional_param('format', 1, PARAM_ALPHANUMEXT); +$extended_deadline = optional_param('extended_deadline', 0, PARAM_ALPHANUMEXT); +$pre_defined_reason = optional_param('pre_defined_reason', null, PARAM_ALPHANUMEXT); +$submissionid = optional_param('submissionid', 0, PARAM_INT); +$name = optional_param('name', 0, PARAM_ALPHANUMEXT); +$params = array( + 'courseworkid' => $courseworkid, + 'allocatableid' => $allocatableid, + 'allocatabletype' => $allocatabletype, +); +$controller = new mod_coursework\controllers\deadline_extensions_controller($params); + +$params['id'] = $id; +$params['extra_information_text'] = $extra_information_text; +$params['extra_information_format'] = $extra_information_format; +$params['extended_deadline'] = strtotime($extended_deadline); +$params['pre_defined_reason'] = $pre_defined_reason; +$params['submissionid'] = $submissionid; +$params['name'] = $name; + +$controller->ajax_new_mitigation($params); \ No newline at end of file diff --git a/actions/ajax/deadline_extension/submit.php b/actions/ajax/deadline_extension/submit.php new file mode 100644 index 0000000..2e46b34 --- /dev/null +++ b/actions/ajax/deadline_extension/submit.php @@ -0,0 +1,33 @@ +id, PARAM_INT); +$allocatabletype = optional_param('allocatabletype', $USER->id, PARAM_ALPHANUMEXT); +$requesttype = optional_param('requesttype', 'new', PARAM_ALPHANUMEXT); +$id = optional_param('id', 0, PARAM_INT); +$extra_information_text = optional_param('text', '', PARAM_RAW); +$extra_information_format = optional_param('format', 1, PARAM_ALPHANUMEXT); +$extended_deadline = optional_param('extended_deadline', 0, PARAM_ALPHANUMEXT); +$pre_defined_reason = optional_param('pre_defined_reason', null, PARAM_ALPHANUMEXT); +$submissionid = optional_param('submissionid', 0, PARAM_INT); +$name = optional_param('name', 0, PARAM_ALPHANUMEXT); +$params = array( + 'courseworkid' => $courseworkid, + 'allocatableid' => $allocatableid, + 'allocatabletype' => $allocatabletype, +); +$controller = new mod_coursework\controllers\deadline_extensions_controller($params); + +$params['id'] = $id; +$params['extra_information_text'] = $extra_information_text; +$params['extra_information_format'] = $extra_information_format; +$params['extended_deadline'] = strtotime($extended_deadline); +$params['pre_defined_reason'] = $pre_defined_reason; +$params['submissionid'] = $submissionid; +$params['name'] = $name; + +$controller->ajax_submit_mitigation($params); + diff --git a/actions/feedbacks/create.php b/actions/feedbacks/create.php index 303f572..a7ab4ff 100644 --- a/actions/feedbacks/create.php +++ b/actions/feedbacks/create.php @@ -14,6 +14,7 @@ $assessorid = optional_param('assessorid', $USER->id, PARAM_INT); $stage_identifier = optional_param('stage_identifier', '', PARAM_ALPHANUMEXT); $finalised = !!optional_param('submitbutton', 0, PARAM_TEXT); +$ajax = optional_param('ajax', 0, PARAM_INT); $params = array( 'submissionid' => $submissionid, @@ -21,6 +22,12 @@ 'assessorid' => $assessorid, 'stage_identifier' => $stage_identifier, 'finalised' => $finalised, + 'ajax' => $ajax, ); + +if ($ajax) { + $params['cell_type'] = required_param('cell_type', PARAM_TEXT); +} + $controller = new mod_coursework\controllers\feedback_controller($params); $controller->create_feedback(); \ No newline at end of file diff --git a/actions/feedbacks/edit.php b/actions/feedbacks/edit.php index a16b1a8..ee01b23 100644 --- a/actions/feedbacks/edit.php +++ b/actions/feedbacks/edit.php @@ -27,9 +27,11 @@ $feedbackid = optional_param('feedbackid', 0, PARAM_INT); +$ajax = optional_param('ajax', 0, PARAM_INT); $params = array( 'feedbackid' => $feedbackid, + 'ajax' => $ajax ); $controller = new mod_coursework\controllers\feedback_controller($params); $controller->edit_feedback(); \ No newline at end of file diff --git a/actions/feedbacks/new.php b/actions/feedbacks/new.php index d0baad5..d68afdf 100644 --- a/actions/feedbacks/new.php +++ b/actions/feedbacks/new.php @@ -30,6 +30,7 @@ $feedbackid = optional_param('feedbackid', 0, PARAM_INT); $assessorid = optional_param('assessorid', $USER->id, PARAM_INT); $stage_identifier = optional_param('stage_identifier', 'uh-oh', PARAM_RAW); +$ajax = optional_param('ajax', 0, PARAM_INT); $params = array( 'submissionid' => $submissionid, @@ -37,6 +38,7 @@ 'feedbackid' => $feedbackid, 'assessorid' => $assessorid, 'stage_identifier' => $stage_identifier, + 'ajax' => $ajax, ); $controller = new mod_coursework\controllers\feedback_controller($params); -$controller->new_feedback(); \ No newline at end of file +$controller->new_feedback(); diff --git a/actions/feedbacks/show.php b/actions/feedbacks/show.php index 7eea80d..47d822c 100644 --- a/actions/feedbacks/show.php +++ b/actions/feedbacks/show.php @@ -26,9 +26,12 @@ global $CFG, $USER; $feedbackid = optional_param('feedbackid', 0, PARAM_INT); +$ajax = optional_param('ajax', 0, PARAM_INT); $params = array( 'feedbackid' => $feedbackid, + 'ajax' => $ajax + ); $controller = new mod_coursework\controllers\feedback_controller($params); $controller->show_feedback(); \ No newline at end of file diff --git a/actions/feedbacks/update.php b/actions/feedbacks/update.php index 95e15ba..d0fe764 100644 --- a/actions/feedbacks/update.php +++ b/actions/feedbacks/update.php @@ -11,10 +11,21 @@ $feedbackid = required_param('feedbackid', PARAM_INT); $finalised = !!optional_param('submitbutton', 0, PARAM_TEXT); +$ajax = optional_param('ajax', 0, PARAM_INT); +$remove = !!optional_param('removefeedbackbutton', 0, PARAM_TEXT); +$confirm = optional_param('confirm', 0, PARAM_INT); $params = array( 'feedbackid' => $feedbackid, 'finalised' => $finalised, + 'remove' => $remove, + 'confirm' => $confirm, + 'ajax' => $ajax ); + +if ($ajax) { + $params['cell_type'] = required_param('cell_type', PARAM_TEXT); +} + $controller = new mod_coursework\controllers\feedback_controller($params); $controller->update_feedback(); \ No newline at end of file diff --git a/actions/personal_deadline.php b/actions/personal_deadline.php index c8b5850..baf5be4 100644 --- a/actions/personal_deadline.php +++ b/actions/personal_deadline.php @@ -2,7 +2,9 @@ require_once(dirname(__FILE__) . '/../../../config.php'); -global $CFG, $USER; +global $CFG, $USER, $PAGE, $DB; + + $courseworkid = required_param('courseworkid', PARAM_INT); $allocatableid_arr = optional_param_array('allocatableid_arr', false, PARAM_RAW); @@ -10,10 +12,18 @@ $allocatabletype = optional_param('allocatabletype', $USER->id, PARAM_ALPHANUMEXT); $setpersonaldeadlinespage = optional_param('setpersonaldeadlinespage', 0, PARAM_INT); $multipleuserdeadlines = optional_param('multipleuserdeadlines', 0, PARAM_INT); - +$selectedtype = optional_param('selectedtype','date', PARAM_RAW); +$personal_deadline_time = optional_param('personal_deadline_time',null,PARAM_RAW); $allocatableid = (!empty($allocatableid_arr)) ? $allocatableid_arr : $allocatableid ; + +$coursework_db = $DB->get_record('coursework',array('id'=>$courseworkid)); + +$coursework = \mod_coursework\models\coursework::find($coursework_db); + +require_login($coursework->get_course(),false,$coursework->get_course_module()); + $params = array( 'courseworkid' => $courseworkid, 'allocatableid' => $allocatableid, @@ -21,5 +31,23 @@ 'setpersonaldeadlinespage' => $setpersonaldeadlinespage, 'multipleuserdeadlines' => $multipleuserdeadlines ); -$controller = new mod_coursework\controllers\personal_deadlines_controller($params); -$controller->new_personal_deadline(); + +if ($selectedtype != 'unfinalise') { + $controller = new mod_coursework\controllers\personal_deadlines_controller($params); + + if(!empty($personal_deadline_time)) { + $result = $controller->insert_update($personal_deadline_time); + echo json_encode($result); + } else { + $controller->new_personal_deadline(); + } +} else { + + if (!has_capability('mod/coursework:revertfinalised', $PAGE->context)) { + $message = 'You do not have permission to revert submissions'; + redirect($url, $message); + } + + $controller = new mod_coursework\controllers\submissions_controller($params); + $controller->unfinalise_submission(); +} diff --git a/actions/upload_allocations.php b/actions/upload_allocations.php index 5893cff..145c34f 100644 --- a/actions/upload_allocations.php +++ b/actions/upload_allocations.php @@ -81,6 +81,9 @@ $page_renderer = $PAGE->get_renderer('mod_coursework', 'page'); echo $page_renderer->process_csv_upload($procsessingresults, $content, $csvtype); + $cache = \cache::make('mod_coursework', 'courseworkdata', ['id' => $coursework->id]); + $cache->purge(); + } else { $page_renderer = $PAGE->get_renderer('mod_coursework', 'page'); echo $page_renderer->csv_upload($allocationsuploadform, $csvtype); diff --git a/backup/moodle2/backup_coursework_stepslib.php b/backup/moodle2/backup_coursework_stepslib.php index 44c6126..9c37f42 100644 --- a/backup/moodle2/backup_coursework_stepslib.php +++ b/backup/moodle2/backup_coursework_stepslib.php @@ -72,6 +72,7 @@ protected function define_structure() 'automaticagreement', 'automaticagreementrange', 'automaticagreementstrategy', + 'averagerounding', 'feedbackreleaseemail', 'gradeeditingtime', 'markingdeadlineenabled', diff --git a/backup/moodle2/restore_coursework_stepslib.php b/backup/moodle2/restore_coursework_stepslib.php index 08bf3ea..0a5d6ec 100644 --- a/backup/moodle2/restore_coursework_stepslib.php +++ b/backup/moodle2/restore_coursework_stepslib.php @@ -448,6 +448,7 @@ protected function process_coursework($data) 'automaticagreement'=>0, 'automaticagreementrange'=>0, 'automaticagreementstrategy'=>'', + 'averagerounding'=>'mid', 'feedbackreleaseemail'=>0, 'gradeeditingtime'=>0, 'markingdeadlineenabled'=>0, diff --git a/classes/ability.php b/classes/ability.php index 169fd1b..b45aa04 100644 --- a/classes/ability.php +++ b/classes/ability.php @@ -532,7 +532,7 @@ protected function prevent_revert_submission_when_deadline_passed_and_no_extensi $this->prevent('revert', 'mod_coursework\models\submission', function (submission $submission) { - return $submission->get_coursework()->deadline_has_passed() && !$submission->has_extension() + return $submission->get_coursework()->deadline_has_passed() && (!$submission->has_extension() || $submission->extension_deadline() < time()) && !$submission->get_coursework()->allow_late_submissions(); }); } diff --git a/classes/allocation/auto_allocator.php b/classes/allocation/auto_allocator.php index d6e197c..4caa89a 100644 --- a/classes/allocation/auto_allocator.php +++ b/classes/allocation/auto_allocator.php @@ -42,6 +42,8 @@ public function process_allocations() { $this->process_marking_stage($stage); } } + allocation::remove_cache($this->coursework->id); + allocation::fill_pool_coursework($this->coursework->id); } /** diff --git a/classes/allocation/table/cell/builder.php b/classes/allocation/table/cell/builder.php index 8ec4a36..cd86737 100644 --- a/classes/allocation/table/cell/builder.php +++ b/classes/allocation/table/cell/builder.php @@ -3,6 +3,7 @@ namespace mod_coursework\allocation\table\cell; use mod_coursework\models\allocation; use mod_coursework\models\coursework; +use mod_coursework\models\feedback; use mod_coursework\stages\base as stage_base; use mod_coursework\allocation\allocatable; use mod_coursework\models\submission; @@ -422,32 +423,37 @@ private function get_automatically_in_sample_label() { /** * @return bool */ - private function has_final_feedback(){ - global $DB; - - $sql = "SELECT * - FROM {coursework_submissions} s - JOIN {coursework_feedbacks} f - ON f.submissionid = s.id - WHERE s.courseworkid = :courseworkid - AND s.allocatableid = :allocatableid - AND s.allocatabletype = :allocatabletype - AND f.stage_identifier = 'final_agreed_1'"; - - return $DB->record_exists_sql($sql, array('courseworkid'=>$this->coursework->id, - 'allocatableid'=>$this->allocatable->id(), - 'allocatabletype'=>$this->allocatable->type())); + private function has_final_feedback() { + submission::fill_pool_coursework($this->coursework->id); + feedback::fill_pool_coursework($this->coursework->id); + $submission = submission::get_object( + $this->coursework->id, + 'allocatableid-allocatabletype', + [$this->allocatable->id(), $this->allocatable->type()] + ); + if($submission){ + $feedbacks = isset(feedback::$pool[$this->coursework->id]['submissionid'][$submission->id]) ? + feedback::$pool[$this->coursework->id]['submissionid'][$submission->id] : []; + + foreach ($feedbacks as $feedback) { + if ($feedback->stage_identifier == 'final_agreed_1') { + return true; + } + } + } + return false; } /** * @return static */ - private function get_submission(){ - - $submission_params = array('courseworkid' => $this->coursework->id, - 'allocatableid' => $this->allocatable->id(), - 'allocatabletype' => $this->allocatable->type()); - - return submission::find($submission_params); + private function get_submission() { + submission::fill_pool_coursework($this->coursework->id); + $submission = submission::get_object( + $this->coursework->id, + 'allocatableid-allocatabletype', + [$this->allocatable->id(), $this->allocatable->type()] + ); + return $submission; } } \ No newline at end of file diff --git a/classes/allocation/table/row/builder.php b/classes/allocation/table/row/builder.php index 6c21e1e..35db042 100644 --- a/classes/allocation/table/row/builder.php +++ b/classes/allocation/table/row/builder.php @@ -140,5 +140,36 @@ public function get_student_lastname() { return $this->get_allocatable()->lastname; } - + /** + * @return string + */ + public function get_idnumber() { + + global $DB; + + $allocatable = $this->get_allocatable(); + if (empty($allocatable->idnumber)) { + $this->allocatable = user::find($allocatable); + } + + return $this->get_allocatable()->idnumber; + } + + + /** + * @return string + */ + public function get_email() { + + global $DB; + + $allocatable = $this->get_allocatable(); + if (empty($allocatable->email)) { + $this->allocatable = user::find($allocatable); + } + + return $this->get_allocatable()->email; + } + + } diff --git a/classes/allocation/upload.php b/classes/allocation/upload.php index 4042090..d2b03c8 100644 --- a/classes/allocation/upload.php +++ b/classes/allocation/upload.php @@ -47,7 +47,9 @@ public function __construct($coursework) { * @throws \moodle_exception */ public function validate_csv($content,$encoding,$delimeter){ - global $DB; + global $CFG, $DB; + + $assessor_identifier = $CFG->coursework_allocation_identifier; $iid = \csv_import_reader::get_new_iid('courseworkallocationsdata'); $csvreader = new \csv_import_reader($iid, 'courseworkallocationsdata'); @@ -102,7 +104,7 @@ public function validate_csv($content,$encoding,$delimeter){ if ($allocatabletype == 'user'){ // get user id - $suballocatable = $DB->get_record('user', array('username'=>$value)); + $suballocatable = $DB->get_record('user', array($assessor_identifier=>$value)); $allocatable = ($suballocatable)? \mod_coursework\models\user::find($suballocatable->id): ''; } else { // get group id @@ -125,7 +127,7 @@ public function validate_csv($content,$encoding,$delimeter){ // skip empty assessors fields if(empty($value)){ continue;} - $assessor = $DB->get_record('user', array('username'=>$value)); + $assessor = $DB->get_record('user', array($assessor_identifier=>$value)); if(!$assessor ||!in_array($assessor->id, $assessors)){$errors[$s] = get_string('assessornotincoursework', 'coursework', $keynum ); continue;} @@ -159,7 +161,9 @@ public function validate_csv($content,$encoding,$delimeter){ */ public function process_csv($content, $encoding, $delimiter, $processingresults){ - global $DB, $PAGE; + global $CFG, $DB, $PAGE; + + $assessor_identifier = $CFG->coursework_allocation_identifier; $iid = \csv_import_reader::get_new_iid('courseworkallocationsdata'); $csvreader = new \csv_import_reader($iid, 'courseworkallocationsdata'); @@ -211,7 +215,7 @@ public function process_csv($content, $encoding, $delimiter, $processingresults) if ($cells[$keynum] == 'allocatable') { if ($allocatabletype == 'user'){ // get user id - $suballocatable = $DB->get_record('user', array('username'=>$value)); + $suballocatable = $DB->get_record('user', array($assessor_identifier=>$value)); $allocatable = ($suballocatable)? \mod_coursework\models\user::find($suballocatable->id): ''; } else { // get group id @@ -222,7 +226,7 @@ public function process_csv($content, $encoding, $delimiter, $processingresults) } if (substr($cells[$keynum],0,8) == 'assessor' && !(empty($value))){ - $assessor = $DB->get_record('user', array('username'=>$value)); + $assessor = $DB->get_record('user', array($assessor_identifier=>$value)); $params = array('courseworkid' => $this->coursework->id, 'allocatableid' => $allocatable->id, diff --git a/classes/auto_grader/auto_grader.php b/classes/auto_grader/auto_grader.php index 5038c15..08f2a84 100644 --- a/classes/auto_grader/auto_grader.php +++ b/classes/auto_grader/auto_grader.php @@ -7,7 +7,7 @@ */ interface auto_grader { - public function __construct($coursework, $allocatable, $percentage); + public function __construct($coursework, $allocatable); public function create_auto_grade_if_rules_match(); } \ No newline at end of file diff --git a/classes/auto_grader/average_grade.php b/classes/auto_grader/average_grade.php new file mode 100644 index 0000000..e74faae --- /dev/null +++ b/classes/auto_grader/average_grade.php @@ -0,0 +1,159 @@ +coursework = $coursework; + $this->roundingrule = $this->coursework->roundingrule; + $this->allocatable = $allocatable; + } + + /** + * This will test whether there is a grade already present, test whether the rules for this class match the + * state of the initial assessor grades and make an automatic grade if they do. + * + */ + public function create_auto_grade_if_rules_match(){ + + // bounce out if conditions are not right/ + if (!$this->get_allocatable()->has_all_initial_feedbacks($this->get_coursework())) { + return; + } + if ($this->get_coursework()->numberofmarkers == 1) { + return; + } + + + + if (!$this->get_allocatable()->has_agreed_feedback($this->get_coursework())) { + $this->create_final_feedback(); + } else { + // update only if AgreedGrade has been automatic + $agreed_feedback = $this->get_allocatable()->get_agreed_feedback($this->get_coursework()); + if ($agreed_feedback->timecreated == $agreed_feedback->timemodified || $agreed_feedback->lasteditedbyuser == 0) { + $this->update_final_feedback($agreed_feedback); + } + } + + + + // trigger events? + + } + + /** + * @return coursework + */ + private function get_coursework() + { + return $this->coursework; + } + + /** + * @return int + */ + private function automatic_grade(){ + + $grades = $this->grades_as_percentages(); + + // calculate average + $avggrade = array_sum($grades) / count($grades); + + // round it according to the chosen rule + switch ($this->roundingrule) { + case 'mid': + $avggrade = round($avggrade); + break; + case 'up': + $avggrade = ceil($avggrade); + break; + case 'down': + $avggrade = floor($avggrade); + break; + } + + + return $avggrade; + } + + + /** + * @return allocatable + */ + private function get_allocatable(){ + return $this->allocatable; + } + + /** + * + */ + private function create_final_feedback() { + feedback::create(array( + 'stage_identifier' => 'final_agreed_1', + 'submissionid' => $this->get_allocatable()->get_submission($this->get_coursework())->id(), + 'grade' => $this->automatic_grade() + + )); + } + + + /** + * + */ + private function update_final_feedback($feedback){ + global $DB; + + $updated_feedback = new \stdClass(); + $updated_feedback->id = $feedback->id; + $updated_feedback->grade = $this->automatic_grade(); + $updated_feedback->lasteditedbyuser = 0; + + $DB->update_record('coursework_feedbacks', $updated_feedback); + + } + + + /** + * @return array + */ + private function grades_as_percentages() { + $initial_feedbacks = $this->get_allocatable()->get_initial_feedbacks($this->get_coursework()); + $grades = array_map(function ($feedback) { + return ($feedback->get_grade() / $this->get_coursework()->get_max_grade()) * 100; + }, + $initial_feedbacks); + return $grades; + } +} \ No newline at end of file diff --git a/classes/auto_grader/none.php b/classes/auto_grader/none.php index e94e6c6..13383bd 100644 --- a/classes/auto_grader/none.php +++ b/classes/auto_grader/none.php @@ -14,7 +14,7 @@ class none implements auto_grader { * @param $allocatable * @param $percentage */ - public function __construct($coursework, $allocatable, $percentage) { + public function __construct($coursework, $allocatable) { } public function create_auto_grade_if_rules_match() { diff --git a/classes/auto_grader/percentage_distance.php b/classes/auto_grader/percentage_distance.php index 750fa29..055880d 100644 --- a/classes/auto_grader/percentage_distance.php +++ b/classes/auto_grader/percentage_distance.php @@ -34,9 +34,9 @@ class percentage_distance implements auto_grader { * @param allocatable $allocatable * @param int $percentage */ - public function __construct($coursework, $allocatable, $percentage) { + public function __construct($coursework, $allocatable) { $this->coursework = $coursework; - $this->percentage = (int)$percentage; + $this->percentage = (int)$this->coursework->automaticagreementrange; $this->allocatable = $allocatable; } diff --git a/classes/controllers/deadline_extensions_controller.php b/classes/controllers/deadline_extensions_controller.php index 2bf788d..7fbc230 100644 --- a/classes/controllers/deadline_extensions_controller.php +++ b/classes/controllers/deadline_extensions_controller.php @@ -3,8 +3,11 @@ namespace mod_coursework\controllers; use mod_coursework\ability; use mod_coursework\allocation\allocatable; +use mod_coursework\decorators\coursework_groups_decorator; use mod_coursework\forms\deadline_extension_form; +use mod_coursework\models\coursework; use mod_coursework\models\deadline_extension; +use mod_coursework\models\group; use mod_coursework\models\user; /** @@ -155,4 +158,229 @@ protected function set_default_current_deadline() } + + /** + * Begin Ajax functions + */ + public function ajax_submit_mitigation($data_params) + { + global $USER, $OUTPUT; + $extended_deadline = false; + $response = []; + $this->coursework = coursework::find(['id' => $this->params['courseworkid']]); + require_login($this->coursework->course); + $params = $this->set_default_current_deadline(); + $ability = new ability(user::find($USER), $this->coursework); + $errors = $this->validation($data_params); + $data = (object) $data_params; + if (!$errors) { + if ($data->id > 0) { + $this->deadline_extension = deadline_extension::find(['id' => $data->id]); + $ability->require_can('edit', $this->deadline_extension); + $data->createdbyid = $USER->id; + $this->deadline_extension->update_attributes($data); + } else { + $this->deadline_extension = deadline_extension::build($data); + $ability->require_can('new', $this->deadline_extension); + $this->deadline_extension->save(); + $data_params['id'] = $this->deadline_extension->id; + } + + $content = $this->table_cell_response($data_params); + + $response = [ + 'error' => 0, + 'data' => $data_params, + 'content' => $content + ]; + echo json_encode($response); + } else { + $response = [ + 'error' => 1, + 'messages' => $errors + ]; + + echo json_encode($response); + } + } + + public function validation($data){ + global $CFG; + $max_deadline = $CFG->coursework_max_extension_deadline; + + + if ($this->coursework->personaldeadlineenabled && $personal_deadline = $this->personal_deadline()) { + $deadline = $personal_deadline->personal_deadline; + } else { + $deadline = $this->coursework->deadline; + } + + + if ( $data['extended_deadline'] <= $deadline) { + return $errors = 'The new deadline must be later than the current deadline'; + } + + return false; + } + + public function submission_exists($data){ + global $DB; + + return $DB->record_exists('coursework_submissions', array( + 'courseworkid' => $data['courseworkid'], + 'allocatableid' => $data['allocatableid'], + 'allocatabletype' => $data['allocatabletype'] + )); + } + + public function personal_deadline(){ + global $DB; + + $extensionid = optional_param('id', 0, PARAM_INT); + + if ($extensionid != 0) { + $ext = $DB->get_record('coursework_extensions', array('id' => $extensionid)); + $allocatableid = $ext->allocatableid; + $allocatabletype = $ext->allocatabletype; + $courseworkid = $ext->courseworkid; + } else { + + $allocatableid = required_param('allocatableid', PARAM_INT); + $allocatabletype = required_param('allocatabletype', PARAM_ALPHANUMEXT); + $courseworkid = required_param('courseworkid', PARAM_INT); + } + + $params = array( + 'allocatableid' => $allocatableid ?? 0, + 'allocatabletype' => $allocatabletype ?? 0, + 'courseworkid' => $courseworkid ?? 0, + ); + + return $personal_deadline = $DB->get_record('coursework_person_deadlines', $params); + } + + public function ajax_edit_mitigation($data_params) { + global $USER; + $response = []; + if ($data_params['id'] > 0) { + $this->coursework = coursework::find(['id' => $this->params['courseworkid']]); + require_login($this->coursework->course); + + $ability = new ability(user::find($USER), $this->coursework); + $deadline_extension = deadline_extension::find(['id' => $data_params['id']]); + if (empty($deadline_extension)) { + $response = [ + 'error' => 1, + 'message' => 'This Deadline Extension does not exist!', + ]; + } else { + $ability->require_can('edit', $deadline_extension); + $time_content = ''; + $time = ''; + if ($this->coursework->personaldeadlineenabled && $personal_deadline = $this->personal_deadline()) { + $time_content = 'Personal deadline: ' . userdate($personal_deadline->personal_deadline); + $time = date('d-m-Y H:i', $personal_deadline->personal_deadline); + } else if ($this->coursework->deadline) { + // Current deadline for comparison + $time_content = 'Default deadline: ' . userdate($this->coursework->deadline); + $time = date('d-m-Y H:i', $this->coursework->deadline); + } + + if(!empty($deadline_extension->extended_deadline) && $deadline_extension->extended_deadline > 0) { + $time = date('d-m-Y H:i', $deadline_extension->extended_deadline); + } + + $deadline_extension_transform = [ + 'time_content' => $time_content, + 'time' => $time, + 'text' => $deadline_extension->extra_information_text, + 'allocatableid' => $deadline_extension->allocatableid, + 'allocatabletype' => $deadline_extension->allocatabletype, + 'courseworkid' => $deadline_extension->courseworkid, + 'id' => $deadline_extension->id, + 'pre_defined_reason' => $deadline_extension->pre_defined_reason, + ]; + $response = [ + 'error' => 0, + 'data' => $deadline_extension_transform + ]; + } + } else { + $response = [ + 'error' => 1, + 'message' => 'ID can not be lower than 1!' + ]; + } + echo json_encode($response); + } + + public function ajax_new_mitigation($data_params){ + global $USER, $DB; + $response = []; + $this->coursework = coursework::find(['id' => $this->params['courseworkid']]); + require_login($this->coursework->course); + + $params = array( + 'allocatableid' => $this->params['allocatableid'], + 'allocatabletype' => $this->params['allocatabletype'], + 'courseworkid' => $this->params['courseworkid'], + ); + + $ability = new ability(user::find($USER), $this->coursework); + $deadline_extension= deadline_extension::build($params); + $ability->require_can('new', $deadline_extension); + $time_content = ''; + $time = ''; + if ($this->coursework->deadline){ + $personal_deadline = $DB->get_record('coursework_person_deadlines', $params); + if ($personal_deadline) { + $time_content = 'Personal deadline: ' . userdate($personal_deadline->personal_deadline); + // $this->coursework->deadline = $personal_deadline->personal_deadline; + $time = date('d-m-Y H:i', $personal_deadline->personal_deadline); + } else { + $time_content = 'Default deadline: ' . userdate($this->coursework->deadline); + $time = date('d-m-Y H:i', $this->coursework->deadline); + } + } + + $deadline_extension_transform = [ + 'time_content' => $time_content, + 'time' => $time, + ]; + + $response = [ + 'error' => 0, + 'data' => $deadline_extension_transform + ]; + + echo json_encode($response); + } + + + /** + * function table_cell_response + * @param array $data_params + * + * Generate html cell base on time_submitted_cell + * @return html_string $content + * + */ + + public function table_cell_response($data_params) { + $participant = ($data_params['allocatabletype'] && $data_params['allocatabletype'] == 'group') ? group::find($data_params['allocatableid']) : user::find($data_params['allocatableid']); + $coursework = ($this->coursework instanceof coursework_groups_decorator) ? $this->coursework->wrapped_object() : $this->coursework; + if ($this->coursework->has_multiple_markers()) { + $row_object = new \mod_coursework\grading_table_row_multi($coursework, $participant); + } else { + $row_object = new \mod_coursework\grading_table_row_single($coursework, $participant); + } + + $time_submitted_cell = new \mod_coursework\render_helpers\grading_report\cells\time_submitted_cell(array('coursework'=>$this->coursework)); + + $content = $time_submitted_cell->prepare_content_cell($row_object); + + return $content; + } + + } \ No newline at end of file diff --git a/classes/controllers/feedback_controller.php b/classes/controllers/feedback_controller.php index 8d77c03..54cd504 100644 --- a/classes/controllers/feedback_controller.php +++ b/classes/controllers/feedback_controller.php @@ -10,6 +10,17 @@ use mod_coursework\models\feedback; use mod_coursework\models\submission; use mod_coursework\models\user; +use mod_coursework\models\group; +use mod_coursework\models\coursework; +use mod_coursework\assessor_feedback_row; +use mod_coursework\decorators\coursework_groups_decorator; +use mod_coursework\grading_table_row_multi; +use mod_coursework\grading_table_row_single; +use mod_coursework\render_helpers\grading_report\cells\grade_for_gradebook_cell; +use mod_coursework\render_helpers\grading_report\cells\multiple_agreed_grade_cell; +use mod_coursework\render_helpers\grading_report\cells\single_assessor_feedback_cell; +use mod_coursework\render_helpers\grading_report\sub_rows\multi_marker_feedback_sub_rows; +use mod_coursework\stages\assessor; use moodle_exception; use stdClass; @@ -39,6 +50,7 @@ protected function show_feedback() { $urlparams = array('feedbackid' => $this->params['feedbackid']); $PAGE->set_url('/mod/coursework/actions/feedbacks/show.php', $urlparams); + $ajax = (isset($this->params['ajax'])) ? $this->params['ajax'] : 0; $teacherfeedback = new feedback($this->params['feedbackid']); @@ -46,7 +58,15 @@ protected function show_feedback() { $ability->require_can('show', $teacherfeedback); $renderer = $this->get_page_renderer(); - $renderer->show_feedback_page($teacherfeedback); + $html = $renderer->show_feedback_page($teacherfeedback,$ajax); + + + + if (empty($ajax)) { + echo $html; + } else { + echo json_encode(['success' => true, 'formhtml' => $html]); + } } /** @@ -88,7 +108,7 @@ protected function new_feedback() { $PAGE->set_url('/mod/coursework/actions/feedbacks/new.php', $urlparams); $renderer = $this->get_page_renderer(); - $renderer->new_feedback_page($teacherfeedback); + $renderer->new_feedback_page($teacherfeedback, $this->params['ajax']); } @@ -119,7 +139,7 @@ protected function edit_feedback() { } $renderer = $this->get_page_renderer(); - $renderer->edit_feedback_page($teacherfeedback, $assessor, $editor); + $renderer->edit_feedback_page($teacherfeedback, $assessor, $editor, $this->params['ajax']); } /** @@ -145,6 +165,7 @@ protected function create_feedback() { 'submission' => $submission, 'assessor' => \core_user::get_user($this->params['assessorid']), 'stage' => $teacherfeedback->get_stage(), + ); $url = $this->get_router()->get_path('new feedback', $path_params, true); $PAGE->set_url($url); @@ -172,9 +193,10 @@ protected function create_feedback() { redirect($coursework_page_url); } + $ajax = !empty($this->params['ajax']); $data = $form->get_data(); - if ($data) { + if ($data && $form->validate_grade($data)) { $teacherfeedback->save(); // Need an id so we can save the advanced grading here. $teacherfeedback = $form->process_data($teacherfeedback); @@ -199,11 +221,87 @@ protected function create_feedback() { if (empty($gradeeditingtime) || time() > $teacherfeedback->timecreated + $gradeeditingtime) { $this->try_auto_feedback_creation($teacherfeedback->get_submission()); } + if ($ajax) { + $coursework = $teacherfeedback->get_coursework(); + $coursework->clear_stage($teacherfeedback->stage_identifier); + if ($coursework instanceof coursework_groups_decorator) { + $coursework = $coursework->wrapped_object(); + } + //feedback::$pool[$coursework->id] = null; + $participant = $submission->get_allocatable(); + $cell_class = $this->params['cell_type']; + $stage = new assessor($coursework, $teacherfeedback->stage_identifier); + $provisional = new grade_for_gradebook_cell(array('coursework'=>$coursework)); - redirect($coursework_page_url); + $jsonarray = array('success' => true); + + if (strpos($cell_class, 'multi_marker_feedback_sub_rows') !== false) { + $feedback_row = new assessor_feedback_row($stage, $participant, $this->coursework); + $cell_object = new $cell_class($coursework, $participant); + $html = $cell_object->get_grade_cell_content($feedback_row, $this->coursework); + + if ($teacherfeedback->stage_identifier == 'assessor_1' || $teacherfeedback->stage_identifier == 'assessor_2') { + + $jsonarray['assessorname'] = (empty($feedback_row->get_assessor()->id()) && $coursework->allocation_enabled()) ? + get_string('assessornotallocated','mod_coursework') : $cell_object->profile_link($feedback_row); + $jsonarray['assessdate'] = $cell_object->date_for_column($feedback_row); + + if ($teacherfeedback->stage_identifier == 'assessor_1') { + $ability = new ability(user::find($USER, false), $coursework); + $stage = new assessor($coursework, 'assessor_2'); + $assessor_feedback_row = new assessor_feedback_row($stage, $feedback_row->get_allocatable(), $coursework); + + $assessortwocell = $cell_object->get_grade_cell_content($assessor_feedback_row,$coursework,$ability); + //$jsonarray['assessortwo'] =$assessortwocell; + if (strpos($assessortwocell, 'new_feedback') !== false) $jsonarray['assessortwo'] = $assessortwocell; + + } + + $finalfeedback = $feedback_row->get_submission()->get_final_feedback(); + $finalsubmission = $feedback_row->get_submission(); + + + if ($coursework->automaticagreementrange != 'none' && !empty($finalfeedback) && $finalsubmission->all_inital_graded()) { + $finalstage = new assessor($coursework, "final_agreed_1"); + $finalfeedback_row = new assessor_feedback_row($finalstage, $participant, $coursework); + $agreed_grade_object = new multiple_agreed_grade_cell(array('coursework'=>$coursework,'stage'=>$finalstage)); + $jsonarray['finalhtml'] = $agreed_grade_object->get_table_cell($finalfeedback_row); + $jsonarray['allocatableid'] = $submission->get_allocatable()->id(); + } + + } else { + + $jsonarray['extrahtml'] = $provisional->get_table_cell($feedback_row); + + } + + } else { + $row_class = $coursework->has_multiple_markers() ? + '\\mod_coursework\\grading_table_row_multi' : '\\mod_coursework\\grading_table_row_single'; + $row_object = new $row_class($coursework, $participant); + $cell_object = new $cell_class(['coursework' => $coursework, 'stage' => $stage]); + $html = $cell_object->get_content($row_object); + $jsonarray['extrahtml'] = $provisional->get_table_cell($row_object); + + + + + + } + + $jsonarray['html'] = $html; + + echo json_encode($jsonarray); + } else { + redirect($coursework_page_url); + } } else { - $renderer = $this->get_page_renderer(); - $renderer->new_feedback_page($teacherfeedback); + if ($ajax) { + echo json_encode(['success' => false, 'message' => get_string('guidenotcompleted', 'gradingform_guide')]); + } else { + $renderer = $this->get_page_renderer(); + $renderer->new_feedback_page($teacherfeedback); + } } @@ -222,6 +320,75 @@ protected function update_feedback() { $ability = new ability(user::find($USER), $this->coursework); $ability->require_can('update', $teacherfeedback); + $coursework_page_url = $this->get_path('coursework', array('coursework' => $teacherfeedback->get_coursework())); + + // remove feedback comments and associated feedback files if 'Remove feedback' button pressed + if($this->params['remove']){ + if (!$this->params['confirm']) { + + $urlparams = array('confirm'=>$this->params['confirm'], + 'remove'=>$this->params['remove'],'feedbackid'=>$this->params['feedbackid'],'finalised'=>$this->params['finalised']); + + $PAGE->set_url('/mod/coursework/actions/feedbacks/edit.php', $urlparams); + + // Ask the user for confirmation. + $confirmurl = new \moodle_url('/mod/coursework/actions/feedbacks/update.php'); + $confirmurl->param('confirm', 1); + $confirmurl->param('removefeedbackbutton', 1); + $confirmurl->param('feedbackid',$this->params['feedbackid']); + $confirmurl->param('finalised',$this->params['finalised']); + + $cancelurl = clone $PAGE->url; + $cancelurl->param('removefeedbackbutton', 0); + $cancelurl->param('feedbackid',$this->params['feedbackid']); + $cancelurl->param('finalised',$this->params['finalised']); + $renderer = $this->get_page_renderer(); + return $renderer->confirm_feedback_removal_page($teacherfeedback,$confirmurl,$cancelurl); + + //$OUTPUT->confirm(get_string('confirmremovefeedback', 'mod_coursework'), $confirmurl, $PAGE->url); + + } else { + $teacherfeedback->destroy(); + //remove associated files + $fs = get_file_storage(); + $fs->delete_area_files($teacherfeedback->get_coursework()->get_context()->id, 'mod_coursework', 'feedback', $teacherfeedback->id()); + + + $ajax = !empty($this->params['ajax']); + if ($ajax) { + + $coursework = $teacherfeedback->get_coursework(); + $coursework->clear_stage($teacherfeedback->stage_identifier); + if ($coursework instanceof coursework_groups_decorator) { + $coursework = $coursework->wrapped_object(); + } + //feedback::$pool[$coursework->id] = null; + $submission = $teacherfeedback->get_submission(); + $participant = $submission->get_allocatable(); + $cell_class = $this->params['cell_type']; + $stage = new assessor($coursework, $teacherfeedback->stage_identifier); + if (strpos($cell_class, 'multi_marker_feedback_sub_rows') !== false) { + $feedback_row = new assessor_feedback_row($stage, $participant, $coursework); + $cell_object = new $cell_class($coursework, $participant); + $html = $cell_object->get_grade_cell_content($feedback_row, $coursework); + } else { + $row_class = $coursework->has_multiple_markers() ? + '\\mod_coursework\\grading_table_row_multi' : '\\mod_coursework\\grading_table_row_single'; + $row_object = new $row_class($coursework, $participant); + $cell_object = new $cell_class(['coursework' => $coursework, 'stage' => $stage]); + $html = $cell_object->get_content($row_object); + + $finalfeedback = $row_object->get_submission()->get_final_feedback(); + + } + + echo json_encode(['success' => true, 'html' => $html]); + exit; + } else { + redirect($coursework_page_url); + } + } + } $this->check_stage_permissions($teacherfeedback->stage_identifier); @@ -235,12 +402,9 @@ protected function update_feedback() { $teacherfeedback = $form->process_data($teacherfeedback); $teacherfeedback->save(); - $form->save_feedback_files($teacherfeedback); - //only implement auto feedback (automatic agreement) if the settings is set to disabled otherwise - //we will do this in the cron - $gradeeditingtime = $teacherfeedback->get_coursework()->get_grade_editing_time(); + if (empty($gradeeditingtime) || time() > $teacherfeedback->timecreated + $gradeeditingtime) { $this->try_auto_feedback_creation($teacherfeedback->get_submission()); @@ -251,7 +415,73 @@ protected function update_feedback() { $teacherfeedback->get_submission()->publish(); } - redirect($coursework_page_url); + $ajax = !empty($this->params['ajax']); + if ($ajax) { + $coursework = $teacherfeedback->get_coursework(); + $coursework->clear_stage($teacherfeedback->stage_identifier); + if ($coursework instanceof coursework_groups_decorator) { + $coursework = $coursework->wrapped_object(); + } + //feedback::$pool[$coursework->id] = null; + $submission = $teacherfeedback->get_submission(); + $participant = $submission->get_allocatable(); + $cell_class = $this->params['cell_type']; + $stage = new assessor($coursework, $teacherfeedback->stage_identifier); + $provisional = new grade_for_gradebook_cell(array('coursework'=>$coursework)); + $jsonarray = array('success' => true); + + if (strpos($cell_class, 'multi_marker_feedback_sub_rows') !== false) { + $feedback_row = new assessor_feedback_row($stage, $participant, $coursework); + $cell_object = new $cell_class($coursework, $participant); + $html = $cell_object->get_grade_cell_content($feedback_row, $coursework); + + if ($teacherfeedback->stage_identifier == 'assessor_1' || $teacherfeedback->stage_identifier == 'assessor_2') { + $jsonarray['assessorname'] = (empty($feedback_row->get_assessor()->id()) && $coursework->allocation_enabled()) ? + get_string('assessornotallocated','mod_coursework') : $cell_object->profile_link($feedback_row); + $jsonarray['assessdate'] = $cell_object->date_for_column($feedback_row); + + if ($teacherfeedback->stage_identifier == 'assessor_1') { + $ability = new ability(user::find($USER, false), $coursework); + $stage = new assessor($coursework, 'assessor_2'); + $assessor_feedback_row = new assessor_feedback_row($stage, $feedback_row->get_allocatable(), $coursework); + + $assessortwocell = $cell_object->get_grade_cell_content($assessor_feedback_row,$coursework,$ability); + //$jsonarray['assessortwo'] =$assessortwocell; + if (strpos($assessortwocell, 'new_feedback') !== false) $jsonarray['assessortwo'] = $assessortwocell; + + } + + $finalfeedback = $submission->get_final_feedback(); + + if ($coursework->automaticagreementrange != 'none' && !empty($finalfeedback)) { + $finalstage = new assessor($coursework, "final_agreed_1"); + + $finalfeedbackrow_object = new \mod_coursework\grading_table_row_multi($coursework, $participant); + + $agreed_grade_cell = new multiple_agreed_grade_cell(['coursework' => $coursework, 'stage' => $finalstage]); + $jsonarray['finalhtml'] = $agreed_grade_cell->get_content($finalfeedbackrow_object); + $jsonarray['allocatableid'] = $submission->get_allocatable()->id(); + } + + } else { + $jsonarray['extrahtml'] = strip_tags($provisional->get_table_cell($feedback_row)); + } + + } else { + $row_class = $coursework->has_multiple_markers() ? + '\\mod_coursework\\grading_table_row_multi' : '\\mod_coursework\\grading_table_row_single'; + $row_object = new $row_class($coursework, $participant); + $cell_object = new $cell_class(['coursework' => $coursework, 'stage' => $stage]); + $html = $cell_object->get_content($row_object); + $jsonarray['extrahtml'] = strip_tags($provisional->get_table_cell($row_object)); + } + + $jsonarray['html'] = $html; + + echo json_encode($jsonarray); + } else { + redirect($coursework_page_url); + } } /** @@ -361,8 +591,7 @@ protected function try_auto_feedback_creation($submission) { * @var auto_grader $auto_grader */ $auto_grader = new $auto_feedback_classname($this->coursework, - $submission->get_allocatable(), - $this->coursework->automaticagreementrange); + $submission->get_allocatable()); $auto_grader->create_auto_grade_if_rules_match(); } -} \ No newline at end of file +} diff --git a/classes/controllers/grading_controller.php b/classes/controllers/grading_controller.php new file mode 100644 index 0000000..1881ce1 --- /dev/null +++ b/classes/controllers/grading_controller.php @@ -0,0 +1,33 @@ +get_record('coursework', array('id' => $options['courseworkid']), '*', MUST_EXIST); + //$coursework = mod_coursework\models\coursework::find($coursework_record); + $coursework = coursework::find($coursework_record, false); + require_login($coursework->course); + + $coursework->coursemodule = get_coursemodule_from_instance('coursework', $coursework->id, $coursework->course, false, MUST_EXIST); + $grading_report = $coursework->renderable_grading_report_factory($options); + + $tablerows = $grading_report->get_table_rows_for_page(); + $cell_helpers = $grading_report->get_cells_helpers(); + $sub_row_helper = $grading_report->get_sub_row_helper(); + + $grading_report_renderer = $PAGE->get_renderer('mod_coursework', 'grading_report'); + $table_html = $grading_report_renderer->make_rows($tablerows, $cell_helpers, $sub_row_helper, $coursework->has_multiple_markers()); + return $table_html; + } + +} \ No newline at end of file diff --git a/classes/controllers/personal_deadlines_controller.php b/classes/controllers/personal_deadlines_controller.php index a0946c0..160e05f 100644 --- a/classes/controllers/personal_deadlines_controller.php +++ b/classes/controllers/personal_deadlines_controller.php @@ -4,6 +4,7 @@ use mod_coursework\ability; use mod_coursework\allocation\allocatable; use mod_coursework\forms\personal_deadline_form; +use mod_coursework\models\coursework; use mod_coursework\models\personal_deadline; use mod_coursework\models\user; @@ -158,5 +159,93 @@ protected function get_personal_deadline(){ return $DB->get_record('coursework_person_deadlines', $params); } + + /** + * @param $time + * @return array + * @throws \coding_exception + * @throws \mod_coursework\exceptions\access_denied + * @throws \moodle_exception + * @throws \require_login_exception + */ + public function insert_update($time){ + global $USER; + if (!$this->validated($time)) { + return [ + 'error' => 1, + 'message' => 'The new deadline you chose has already passed. Please select appropriate deadline' + ]; + } + $this->coursework = coursework::find(['id'=>$this->params['courseworkid']]); + require_login($this->coursework->course); + $params = $this->set_default_current_deadline(); + + $ability = new ability(user::find($USER), $this->coursework); + $ability->require_can('edit', $this->personal_deadline); + $params['allocatableid'] = (!is_array($params['allocatableid'])) ? $params['allocatableid'] + : serialize($params['allocatableid']) ; + + $data = (object) $this->params; + if (empty($data->multipleuserdeadlines)) { + if (!$this->get_personal_deadline()) { // personal deadline doesnt exist + // add new + $data->createdbyid = $USER->id; + $data->personal_deadline = strtotime($time); + $this->personal_deadline = personal_deadline::build($data); + $this->personal_deadline->save(); + } else { + // update + $data->lastmodifiedbyid = $USER->id; + $data->personal_deadline = strtotime($time); + $data->timemodified = time(); + $this->personal_deadline->update_attributes($data); + } + } else { + $allocatables = unserialize($data->allocatableid); + + foreach ($allocatables as $allocatableid) { + $data->allocatableid = $allocatableid; + $data->id = ''; + //$data->id = ''; + $findparams = array( + 'allocatableid' => $allocatableid, + 'allocatabletype' => $data->allocatabletype, + 'courseworkid' => $data->courseworkid, + ); + $this->personal_deadline = personal_deadline::find_or_build($findparams); + + if (empty($this->personal_deadline->personal_deadline)) { // personal deadline doesnt exist + // add new + $data->createdbyid = $USER->id; + $this->personal_deadline = personal_deadline::build($data); + $this->personal_deadline->save(); + } else { + // update + $data->id = $this->personal_deadline->id; + $data->lastmodifiedbyid = $USER->id; + $data->timemodified = time(); + $this->personal_deadline->update_attributes($data); + } + } + } + $timestamp = $this->personal_deadline->personal_deadline; + $date = userdate($timestamp, '%a, %d %b %Y, %H:%M'); + return [ + 'error' => 0, + 'time' => $date, + 'timestamp' => $timestamp + ]; + } + + /** + * @param $time + * @return bool + */ + protected function validated($time) { + if(strtotime($time) <= time()) { + return false; + } + return true; + } } diff --git a/classes/controllers/submissions_controller.php b/classes/controllers/submissions_controller.php index 0c3b545..14e44b2 100644 --- a/classes/controllers/submissions_controller.php +++ b/classes/controllers/submissions_controller.php @@ -308,6 +308,50 @@ protected function finalise_submission() { redirect($coursework_page_url); } + + protected function unfinalise_submission() { + + global $USER, $DB; + + + + $allocatableids = (!is_array($this->params['allocatableid'])) ? array($this->params['allocatableid']) : $this->params['allocatableid'] ; + + $personaldeadline_page_url = new \moodle_url('/mod/coursework/actions/personal_deadline.php', + array('id'=>$this->coursework->get_coursemodule_id(),'multipleuserdeadlines'=>1,'setpersonaldeadlinespage'=>1, + 'courseworkid'=>$this->params['courseworkid'],'allocatabletype'=>$this->params['allocatabletype'])); + + $changedeadlines = false; + + foreach($allocatableids as $aid) { + + $submission_db = $DB->get_record('coursework_submissions', + array('courseworkid' => $this->params['courseworkid'], 'allocatableid' => $aid, 'allocatabletype' => $this->params['allocatabletype'])); + if (!empty($submission_db)) { + $submission = \mod_coursework\models\submission::find($submission_db); + + if ($submission->can_be_unfinalised()) { + $submission->finalised = 0; + $submission->save(); + $personaldeadline_page_url->param("allocatableid_arr[$aid]",$aid); + $changedeadlines = true; + } + } + + } + + + + if (!empty($changedeadlines)) { + redirect($personaldeadline_page_url, get_string('unfinalisedchangesubmissiondate', 'mod_coursework')); + } else { + $setpersonaldeadline_page_url = new \moodle_url('/mod/coursework/actions/set_personal_deadlines.php', + array('id'=>$this->coursework->get_coursemodule_id())); + redirect($setpersonaldeadline_page_url); + } + + } + protected function prepare_environment() { if (!empty($this->params['submissionid'])) { diff --git a/classes/cron.php b/classes/cron.php index c870dd1..6f35aef 100644 --- a/classes/cron.php +++ b/classes/cron.php @@ -62,7 +62,10 @@ private static function send_reminders_to_students() { * @var coursework $coursework */ $coursework = coursework::find($raw_coursework); - + if (!$coursework || !$coursework->is_coursework_visible()) {// check if coursework exists and is not hidden + continue; + } + // if cw doesn't have personal deadlines and deadline passed and cw doesnt have any individual extensions if (!$coursework->personal_deadlines_enabled() && (!$coursework->has_deadline() || $coursework->deadline_has_passed() && !$coursework->extension_exists())) { diff --git a/classes/export/csv.php b/classes/export/csv.php index d191a17..d1fa51d 100644 --- a/classes/export/csv.php +++ b/classes/export/csv.php @@ -210,16 +210,24 @@ private function add_data_to_csv($csv_data) { } /** + * @param null $groupid + * @param string $selected_submission_ids * @return array * @throws \coding_exception */ - public function get_submissions(){ - - $params = array( - 'courseworkid' => $this->coursework->id - ); - return submission::find_all($params); - + public function get_submissions($groupid = null, $selected_submission_ids = '') { + + $submissions = submission::$pool[$this->coursework->id]['id'] ?? submission::find_all(['courseworkid' => $this->coursework->id]); + if ($selected_submission_ids && $selected_submission_ids = json_decode($selected_submission_ids)) { + $result = array_flip($selected_submission_ids); + foreach ($submissions as $submission) { + if (array_key_exists($submission->id, $result)) { + $result[$submission->id] = $submission; + } + } + $submissions = $result; + } + return $submissions; } /** @@ -243,10 +251,25 @@ public function add_csv_data($submission){ public function other_assessors_cells(){ $cells = 0; - for ($i = 1; $i < $this->coursework->get_max_markers() ; $i++) { - $cells = $cells + 2; // one for grade, one for feedback + if ($this->coursework->is_using_rubric()) { + $criterias = $this->coursework->get_rubric_criteria(); + //we will increment by the number of criterias plus 1 for feedback + $increment = (count($criterias) * 2) +1; + + } else { + $increment = 2; } + + for ($i = 1; $i < $this->coursework->get_max_markers(); $i++) { + $cells = $cells + $increment; // one for grade, one for feedback + } + + + + + + return $cells; diff --git a/classes/export/csv/cells/agreedgrade_cell.php b/classes/export/csv/cells/agreedgrade_cell.php index 2e73e9f..2cc45c1 100644 --- a/classes/export/csv/cells/agreedgrade_cell.php +++ b/classes/export/csv/cells/agreedgrade_cell.php @@ -22,7 +22,7 @@ class agreedgrade_cell extends cell_base{ public function get_cell($submission, $student, $stage_identifier){ $agreedgrade = $submission->get_agreed_grade(); - if($this->coursework->is_using_rubric()){ + if($this->coursework->is_using_rubric() && $this->coursework->finalstagegrading != 1){ $gradedata = array(); $this->get_rubric_scores_gradedata($agreedgrade, $gradedata); // multiple parts are handled here } else { @@ -39,7 +39,7 @@ public function get_cell($submission, $student, $stage_identifier){ */ public function get_header($stage){ - if ($this->coursework->is_using_rubric()) { + if ($this->coursework->is_using_rubric() && $this->coursework->finalstagegrading != 1) { $strings = array(); $criterias = $this->coursework->get_rubric_criteria(); foreach ($criterias as $criteria) { // rubrics can have multiple parts, so let's create header for each of it @@ -68,7 +68,7 @@ public function validate_cell($value,$submissionid,$stage_identifier='', $upload $errormsg = ''; - if (!$this->coursework->is_using_rubric()) { + if (!$this->coursework->is_using_rubric() || ($this->coursework->is_using_rubric() && $this->coursework->finalstagegrading == 1)) { $gradejudge = new grade_judge($this->coursework); if (!$gradejudge->grade_in_scale($value)){ $errormsg = get_string('valuenotincourseworkscale', 'coursework'); @@ -224,7 +224,7 @@ function value_in_rubric($criteria, $value) { function get_rubrics($coursework,$csv_cells) { - if ($coursework->is_using_rubric()) { + if ($coursework->is_using_rubric() && $this->coursework->finalstagegrading != 1) { $rubricheaders = array(); diff --git a/classes/export/csv/cells/assessorgrade_cell.php b/classes/export/csv/cells/assessorgrade_cell.php index d3fd196..8abf1a2 100644 --- a/classes/export/csv/cells/assessorgrade_cell.php +++ b/classes/export/csv/cells/assessorgrade_cell.php @@ -69,7 +69,7 @@ public function get_cell($submission, $student, $stage_identifier){ */ public function get_header($stage){ - if ($this->coursework->is_using_rubric()) { + if ($this->coursework->is_using_rubric() ) { $strings = array(); $criterias = $this->coursework->get_rubric_criteria(); foreach ($criterias as $criteria) { // rubrics can have multiple parts, so let's create header for each of it diff --git a/classes/export/csv/cells/email_cell.php b/classes/export/csv/cells/email_cell.php new file mode 100644 index 0000000..d073488 --- /dev/null +++ b/classes/export/csv/cells/email_cell.php @@ -0,0 +1,37 @@ +can_view_hidden() || $submission->is_published()){ + $name = $student->email; + } else { + $name = get_string('hidden', 'coursework'); + } + + return $name; + } + + /** + * @param $stage + * @return string + * @throws \coding_exception + */ + public function get_header($stage){ + return get_string('email'); + } +} \ No newline at end of file diff --git a/classes/export/csv/cells/idnumber_cell.php b/classes/export/csv/cells/idnumber_cell.php new file mode 100644 index 0000000..ac72c00 --- /dev/null +++ b/classes/export/csv/cells/idnumber_cell.php @@ -0,0 +1,37 @@ +can_view_hidden() || $submission->is_published()){ + $name = $student->idnumber; + } else { + $name = get_string('hidden', 'coursework'); + } + + return $name; + } + + /** + * @param $stage + * @return string + * @throws \coding_exception + */ + public function get_header($stage){ + return get_string('idnumber'); + } +} \ No newline at end of file diff --git a/classes/export/csv/cells/name_cell.php b/classes/export/csv/cells/name_cell.php index b1e2f6b..da45f97 100644 --- a/classes/export/csv/cells/name_cell.php +++ b/classes/export/csv/cells/name_cell.php @@ -1,6 +1,7 @@ can_view_hidden()){ + if ($this->can_view_hidden() || $submission->is_published()){ $name = $student->lastname . ' ' . $student->firstname; } else { $name = get_string('hidden', 'coursework'); diff --git a/classes/export/csv/cells/otherassessors_cell.php b/classes/export/csv/cells/otherassessors_cell.php index e9c84cd..2594d06 100644 --- a/classes/export/csv/cells/otherassessors_cell.php +++ b/classes/export/csv/cells/otherassessors_cell.php @@ -48,10 +48,10 @@ public function get_cell($submission, $student, $stage_identifier){ if ($grade){ // skip if you are allocated but someone else graded it $allocation = $submission->get_assessor_allocation_by_stage($feedback->stage_identifier); - if ($allocation->assessorid == $USER->id) continue; + if ($allocation && $allocation->assessorid == $USER->id) continue; $ability = new ability(user::find($USER), $this->coursework); if ((($ability->can('show', $feedback) || has_capability('mod/coursework:addallocatedagreedgrade', $submission->get_coursework()->get_context())) && - !$submission->any_editable_feedback_exists()) || is_siteadmin($USER->id)) { + (!$submission->any_editable_feedback_exists() && count($submission->get_assessor_feedbacks()) <= $submission->max_number_of_feedbacks())) || is_siteadmin($USER->id)) { if($this->coursework->is_using_rubric()){ $this->get_rubric_scores_gradedata($grade, $gradedata); // multiple parts are handled here @@ -83,6 +83,28 @@ public function get_cell($submission, $student, $stage_identifier){ } + $numothereassessorfeedbacks = $submission->max_number_of_feedbacks() -1; + + if ($numothereassessorfeedbacks - count($feedbacks) != 0 ) { + + $blankcolumns = $numothereassessorfeedbacks - count($feedbacks); + + for($i = 0; $i < $blankcolumns; $i++) { + if ($this->coursework->is_using_rubric()) { + $criterias = $this->coursework->get_rubric_criteria(); + foreach ($criterias as $criteria) { // rubrics can have multiple parts, so let's create header for each of it + $gradedata['assessor' . $stage_identifier.$i. '_' . $criteria['id']] = ''; + $gradedata['assessor' . $stage_identifier.$i. '_' . $criteria['id'] . 'comment'] = ''; + } + } else { + $gradedata[] = ''; + } + $gradedata[] = ''; + + } + + } + return $gradedata; } @@ -99,8 +121,8 @@ public function get_header($stage){ if ($this->coursework->is_using_rubric()) { $criterias = $this->coursework->get_rubric_criteria(); foreach ($criterias as $criteria) { // rubrics can have multiple parts, so let's create header for each of it - $fields['otherassessorgrade'.$stage.'_'.$criteria['id']] = 'Other assessor ('.$i.') - '.$criteria['description']; - $fields['otherassessorgrade'.$stage.'_'.$criteria['id'] . 'comment'] = 'Comment for: Other assessor ('.$i.') - '.$criteria['description']; + $fields['otherassessorgrade'.$i.$stage.'_'.$criteria['id']] = 'Other assessor ('.$i.') - '.$criteria['description']; + $fields['otherassessorgrade'.$i.$stage.'_'.$criteria['id'] . 'comment'] = 'Comment for: Other assessor ('.$i.') - '.$criteria['description']; } } else { $fields['otherassessorgrade' . $i] = get_string('otherassessorgrade', 'coursework', $i); diff --git a/classes/export/csv/cells/username_cell.php b/classes/export/csv/cells/username_cell.php index 925245a..9869cd4 100644 --- a/classes/export/csv/cells/username_cell.php +++ b/classes/export/csv/cells/username_cell.php @@ -1,6 +1,7 @@ can_view_hidden()){ + if ($this->can_view_hidden() || $submission->is_published()){ $username = $student->username; } else { $username = get_string('hidden', 'coursework'); diff --git a/classes/export/grading_sheet.php b/classes/export/grading_sheet.php index bfce0af..d520d2c 100644 --- a/classes/export/grading_sheet.php +++ b/classes/export/grading_sheet.php @@ -26,7 +26,7 @@ class grading_sheet extends csv{ - public function get_submissions(){ + public function get_submissions($groupid = null, $selected_submission_ids = ''){ global $PAGE, $USER; $params = array( 'courseworkid' => $this->coursework->id @@ -157,6 +157,8 @@ public static function cells_array($coursework){ } else { $csv_cells[] = 'name'; $csv_cells[] = 'username'; + $csv_cells[] = 'idnumber'; + $csv_cells[] = 'email'; } $csv_cells[] = 'submissiontime'; diff --git a/classes/export/import.php b/classes/export/import.php index a81d577..b79aeb6 100644 --- a/classes/export/import.php +++ b/classes/export/import.php @@ -115,7 +115,8 @@ public function validate_csv($content,$encoding,$delimeter,$csv_cells) { //offsets the position of that we extract the data from $line based on data that has been extracted before - if (($cells[$i] == "singlegrade" || $cells[$i] == "assessorgrade" || $cells[$i] == "agreedgrade") && $this->coursework->is_using_rubric()) { + if (($cells[$i] == "singlegrade" || $cells[$i] == "assessorgrade" || $cells[$i] == "agreedgrade") + && $this->coursework->is_using_rubric() && !($cells[$i] == "agreedgrade" && $this->coursework->finalstagegrading == 1)) { //get the headers that would contain the rubric grade data $rubricheaders = $cell->get_header(null); @@ -159,7 +160,9 @@ function rubric_count_correct($csvheader,$linefromimportedcsv) { // get criteria of rubrics and match it to grade cells if ($this->coursework->is_using_rubric()) { - $types = array("singlegrade","assessorgrade","agreedgrade"); + $types = array("singlegrade","assessorgrade"); + + if ($this->coursework->finalstagegrading == 0 ) $types[] = "agreedgrade"; foreach($types as $type) { @@ -185,7 +188,7 @@ function rubric_count_correct($csvheader,$linefromimportedcsv) { if(!empty($typefound)) { - //this var is need to provide an offset so the positions in the array we are looking for + //this var is needed to provide an offset so the positions in the array we are looking for //are correct even after a splice and add is carried out $offset = 0; @@ -195,13 +198,17 @@ function rubric_count_correct($csvheader,$linefromimportedcsv) { $cell = new $class($this->coursework); $headers = $cell->get_header(null); - unset($csvheader[$position+$offset]); - unset($linefromimportedcsv[$position+$offset]); - array_splice($csvheader, $position+$offset, 0, array_keys($headers)); - array_splice($linefromimportedcsv, $position+$offset, 0, array('')); - $offset = $offset + count($headers)-1; - $expectedsize = (int)sizeof($csvheader); - $actualsize = (int)sizeof($linefromimportedcsv); + + unset($csvheader[$position + $offset]); + unset($linefromimportedcsv[$position + $offset]); +// if ($type == 'agreedgrade' && $this->coursework->finalstagegrading == 0) { + array_splice($csvheader, $position + $offset, 0, array_keys($headers)); + array_splice($linefromimportedcsv, $position + $offset, 0, array('')); +// } + $offset = $offset + count($headers) - 1; + $expectedsize = (int)sizeof($csvheader); + $actualsize = (int)sizeof($linefromimportedcsv); + } @@ -400,14 +407,12 @@ public function process_csv($content, $encoding, $delimiter, $csv_cells, $proces } //we need to carry out a further check to see if the coursework is using advanced grades. - //if yes then we may need to genenrate the grade for the grade pointer as - // dont have grades - + //if yes then we may need to generate the grade for the grade pointer as + //they dont have grades - if ($coursework->is_using_rubric()) { - + if ($coursework->is_using_rubric() && !($stage == 'final_agreed_1' && $this->coursework->finalstagegrading == 1)) { //array that will hold the advanced grade data @@ -434,10 +439,18 @@ public function process_csv($content, $encoding, $delimiter, $csv_cells, $proces if ($coursework->allocation_enabled()) $rubricoffset += 1; $rubricdata = array_slice($line, $rubricoffset, $numberofrubrics); + $feedbackdata = array_slice($line, $rubricoffset+$numberofrubrics, 1); + + $csvline[$feedbackpointer] = $feedbackdata[0]; + } else { $rubricdata = array_slice($line, $rubricoffset, $numberofrubrics); + $feedbackdata = array_slice($line, $rubricoffset+$numberofrubrics, 1); + + $csvline[$feedbackpointer] = $feedbackdata[0]; + $rubricoffset = $rubricoffset + $numberofrubrics + 1; } @@ -483,17 +496,47 @@ public function process_csv($content, $encoding, $delimiter, $csv_cells, $proces + } else if ($coursework->is_using_rubric() && ($stage == 'final_agreed_1' && $this->coursework->finalstagegrading == 1)) { + + + if (!isset($numberofrubrics)) { + + $criterias = $this->coursework->get_rubric_criteria(); + + $numberofrubrics = count($criterias) * 2; + + } + + $stagemultiplier = $numberofstages -1; + + //the calculation below finds the position of the agreed grades in the uploaded csv + + + $rubricoffset = $rubricoffsetstart + $stagemultiplier + ($numberofrubrics * $stagemultiplier); + + + if ($coursework->allocation_enabled()) $rubricoffset += 1; + + $gradearrvalue = array_slice($line, $rubricoffset, 2); + + $csvline[$gradepointer] = $gradearrvalue[0]; + $csvline[$feedbackpointer] = $gradearrvalue[1]; + } // don't create/update feedback if grade is empty if (!empty($csvline[$gradepointer])) { + + $stageusesrubric = ($this->coursework->is_using_rubric() + && !($stage == 'final_agreed_1' && $this->coursework->finalstagegrading)) ? true : false; + if (empty($grade)) { - $cwfeedbackid = $this->add_grade($csvline['submissionid'], $csvline[$k], $csvline[$feedbackpointer], $stage,$this->coursework->is_using_rubric()); + $cwfeedbackid = $this->add_grade($csvline['submissionid'], $csvline[$k], $csvline[$feedbackpointer], $stage,$stageusesrubric); } else { $cwfeedbackid = $this->get_coursework_feedback_id($csvline['submissionid'], $stage); - $this->edit_grade($cwfeedbackid, $csvline[$k], $csvline[$feedbackpointer], $this->coursework->is_using_rubric()); + $this->edit_grade($cwfeedbackid, $csvline[$k], $csvline[$feedbackpointer], $stageusesrubric); } // if feedback created and coursework has automatic grading enabled update agreedgrade if ($cwfeedbackid && $this->coursework->automaticagreement_enabled()) { @@ -803,8 +846,19 @@ public function remove_other_assessors_grade($csv_cells, &$line){ $key = array_search('otherassessors', $csv_cells); unset($csv_cells[$key]); $othercells = $this->other_assessors_cells(); + if ($this->coursework->is_using_rubric()) { + + $singlegradeposition = array_search('singlegrade', $csv_cells); + + $criterias = $this->coursework->get_rubric_criteria(); + + $startposition = $singlegradeposition+ ((count($criterias) *2) +1); + + } else { + $startposition = array_search('otherassessors', $csv_cells); + } - for ($i = $key; $i < $key+$othercells ; $i++) { + for ($i = $startposition; $i < $startposition+$othercells ; $i++) { unset($line[$i]); } $csv_cells =array_values($csv_cells); diff --git a/classes/forms/advance_plugins_form.php b/classes/forms/advance_plugins_form.php new file mode 100644 index 0000000..919519c --- /dev/null +++ b/classes/forms/advance_plugins_form.php @@ -0,0 +1,67 @@ +. + +/** + * Creates an mform for final grade + * + * @package mod + * @subpackage coursework + * @copyright 2012 University of London Computer Centre {@link ulcc.ac.uk} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_coursework\forms; + +global $CFG; + +use coding_exception; +use gradingform_rubric_instance; +use mod_coursework\models\feedback; +use moodleform; +use stdClass; + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Simple form providing a grade and comment area that will feed straight into the feedback table so + * that the final comment for the gradebook can be added. + */ +class advance_plugins_form extends moodleform { + + /** + * Makes the form elements. + */ + public function definition() { + + $mform =& $this->_form; + + $mform->addElement('editor', 'text_element', get_string('comment', 'mod_coursework'), array()); + $mform->setType('editor', PARAM_RAW); + + $file_manager_options = array( + 'subdirs' => false, + 'accepted_types' => '*', + 'return_types' => FILE_INTERNAL + ); + $this->_form->addElement('filemanager', + 'file_element', + '', + null, + $file_manager_options); + + } +} + diff --git a/classes/forms/assessor_feedback_mform.php b/classes/forms/assessor_feedback_mform.php index 9184a8c..4fd1b88 100644 --- a/classes/forms/assessor_feedback_mform.php +++ b/classes/forms/assessor_feedback_mform.php @@ -30,7 +30,9 @@ use coding_exception; use gradingform_rubric_instance; use mod_coursework\models\feedback; +use mod_coursework\utils\cs_editor; use moodleform; +use stdClass; require_once($CFG->libdir.'/formslib.php'); @@ -50,6 +52,10 @@ class assessor_feedback_mform extends moodleform { */ public $assessorid; + private $_grading_controller; + + private $_grading_instance; + /** * Makes the form elements. */ @@ -63,34 +69,34 @@ public function definition() { $feedback = $this->_customdata['feedback']; $coursework = $feedback->get_coursework(); - $mform->addElement('hidden', 'submissionid', $feedback->submissionid); + $mform->addElement('hidden', 'submissionid', $feedback->submissionid ?? 0); $mform->setType('submissionid', PARAM_INT); - $mform->addElement('hidden', 'isfinalgrade', $feedback->isfinalgrade); + $mform->addElement('hidden', 'isfinalgrade', $feedback->isfinalgrade ?? 0); $mform->setType('isfinalgrade', PARAM_INT); - $mform->addElement('hidden', 'ismoderation', $feedback->ismoderation); + $mform->addElement('hidden', 'ismoderation', $feedback->ismoderation ?? 0); $mform->setType('ismoderation', PARAM_INT); - $mform->addElement('hidden', 'assessorid', $feedback->assessorid); + $mform->addElement('hidden', 'assessorid', $feedback->assessorid ?? 0); $mform->setType('assessorid', PARAM_INT); - $mform->addElement('hidden', 'feedbackid', $feedback->id); + $mform->addElement('hidden', 'feedbackid', $feedback->id ?? 0); $mform->setType('feedbackid', PARAM_INT); - $mform->addElement('hidden', 'stage_identifier', $feedback->stage_identifier); + $mform->addElement('hidden', 'stage_identifier', $feedback->stage_identifier ?? ''); $mform->setType('stage_identifier', PARAM_ALPHANUMEXT); $grademenu = make_grades_menu($coursework->grade); - if ($coursework->is_using_advanced_grading()) { - $controller = $coursework->get_advanced_grading_active_controller(); - $gradinginstance = $controller->get_or_create_instance(0, $feedback->assessorid, $feedback->id); - $mform->addElement('grading', 'advancedgrading', get_string('grade'), array('gradinginstance' => $gradinginstance)); + if (($coursework->is_using_advanced_grading() && $coursework->finalstagegrading ==0 ) || ($coursework->is_using_advanced_grading() && $coursework->finalstagegrading == 1 && $feedback->stage_identifier != 'final_agreed_1')) { + $this->_grading_controller = $coursework->get_advanced_grading_active_controller(); + $this->_grading_instance = $this->_grading_controller->get_or_create_instance(0, $feedback->assessorid, $feedback->id); + $mform->addElement('grading', 'advancedgrading', get_string('grade', 'mod_coursework'), array('gradinginstance' => $this->_grading_instance)); } else { $mform->addElement('select', 'grade', - get_string('grade'), + get_string('grade', 'mod_coursework'), $grademenu, array('id' => 'feedback_grade')); } @@ -114,14 +120,22 @@ public function definition() { $file_manager_options); - $this->add_submit_buttons($coursework->draft_feedback_enabled()); + $this->add_submit_buttons($coursework->draft_feedback_enabled(), $feedback->id); } + /** + * + * @return mixed + */ + public function get_grading_controller() { + return $this->_grading_controller; + } + /** * @param $draftenabled */ - public function add_submit_buttons($draftenabled){ + public function add_submit_buttons($draftenabled, $feedbackid){ $button_array = array(); @@ -133,12 +147,33 @@ public function add_submit_buttons($draftenabled){ $button_array[] = $this->_form->createElement('submit', 'submitbutton', get_string('saveandfinalise', 'coursework')); + $feedback = $this->_customdata['feedback']; + + + $is_published = $feedback->get_submission()->is_published(); + + if ($feedbackid && !$is_published) { + $button_array[] = $this->_form->createElement('submit', 'removefeedbackbutton', get_string('removefeedback', 'coursework')); + } $button_array[] = $this->_form->createElement('cancel'); $this->_form->addGroup($button_array, 'buttonar', '', array(' '), false); $this->_form->closeHeaderBefore('buttonar'); } + /** + * + * @param $data + * @return bool + */ + public function validate_grade($data){ + $result = true; + if (!empty($this->_grading_instance) && property_exists($data, 'advancedgrading')) { + $result = $this->_grading_instance->validate_grading_element($data->advancedgrading); + } + return $result; + } + /** * This is just to grab the data and add it to the feedback object. * @@ -148,9 +183,9 @@ public function add_submit_buttons($draftenabled){ public function process_data(feedback $feedback) { $formdata = $this->get_data(); + $coursework = $feedback->get_coursework(); - if ($feedback->get_coursework()->is_using_advanced_grading()) { - $coursework = $feedback->get_coursework(); + if (($coursework->is_using_advanced_grading() && $coursework->finalstagegrading == 0 ) || ($coursework->is_using_advanced_grading() && $coursework->finalstagegrading == 1 && $feedback->stage_identifier != 'final_agreed_1')) { $controller = $coursework->get_advanced_grading_active_controller(); $gradinginstance = $controller->get_or_create_instance(0, $feedback->assessorid, $feedback->id); /** @@ -190,6 +225,38 @@ public function save_feedback_files(feedback $feedback) { $feedback->id); } + /** + * + * @return stdClass|null + */ + public function get_file_options() { + global $PAGE, $CFG; + require_once("$CFG->dirroot/lib/form/filemanager.php"); + $options = null; + $filemanager = $this->_form->getElement('feedback_manager'); + if ($filemanager) { + $params = (object) [ + 'maxfiles' => $filemanager->getMaxfiles(), + 'subdirs' => $filemanager->getSubdirs(), + 'areamaxbytes' => $filemanager->getAreamaxbytes(), + 'target' => 'id_' . $filemanager->getName(), + 'context' => $PAGE->context, + 'itemid' => $filemanager->getValue() + ]; + $fm = new \form_filemanager($params); + $options = $fm->options; + } + return $options; + } + /** + * + * @return stdClass|null + */ + public function get_editor_options() { + $editor = new cs_editor(); + $options = $editor->get_options('feedback_comment'); + return $options; + } } diff --git a/classes/forms/choose_student_for_submission_mform.php b/classes/forms/choose_student_for_submission_mform.php index 9f31b8f..ee7855d 100644 --- a/classes/forms/choose_student_for_submission_mform.php +++ b/classes/forms/choose_student_for_submission_mform.php @@ -55,7 +55,7 @@ protected function definition() { } $options = array(); - $allnames = get_all_user_name_fields(); + $allnames = \core_user\fields::get_name_fields(); foreach ($students as $student) { diff --git a/classes/forms/student_submission_form.php b/classes/forms/student_submission_form.php index 8c6dc04..8c34db3 100644 --- a/classes/forms/student_submission_form.php +++ b/classes/forms/student_submission_form.php @@ -370,7 +370,7 @@ protected function add_instructions_to_form() { $file_manager_options = $this->get_file_manager_options(); $usernamehash = $this->get_coursework()->get_username_hash($this->get_submission()->userid); - $filerenamestring = get_string('file_rename', 'coursework', $usernamehash); + $filerenamestring = ($this->get_coursework()->renamefiles == 1)? get_string('file_rename', 'coursework', $usernamehash) : ""; $filerenamestring .= $this->make_plagiarism_instructions(); $filerenamestring .= html_writer::empty_tag('br'); if ($file_manager_options['accepted_types'] != '*') { @@ -387,11 +387,12 @@ protected function add_instructions_to_form() { */ protected function add_header_to_form() { $file_manager_options = $this->get_file_manager_options(); - - $files_string = ($file_manager_options['maxfiles'] == 1) ? 'yoursubmissionfile_upload' + $files_string = ($file_manager_options['maxfiles'] == 1) ? 'yoursubmissionfile' : 'yoursubmissionfiles'; + $renamed = ($this->get_coursework()->renamefiles == 1)?get_string('yoursubmissionfile_renamed', 'coursework') : ""; + + $this->_form->addElement('header', 'submitform', get_string($files_string, 'coursework'). $renamed); - $this->_form->addElement('header', 'submitform', get_string($files_string, 'coursework')); } /** diff --git a/classes/framework/decorator.php b/classes/framework/decorator.php index 1115235..57d988a 100644 --- a/classes/framework/decorator.php +++ b/classes/framework/decorator.php @@ -61,6 +61,6 @@ public function __set($name, $value) { * @return mixed */ public function wrapped_object() { - return $this->wrapped_object(); + return $this->wrapped_object; } } \ No newline at end of file diff --git a/classes/framework/table_base.php b/classes/framework/table_base.php index 5a1d2c8..cdf0520 100644 --- a/classes/framework/table_base.php +++ b/classes/framework/table_base.php @@ -57,9 +57,12 @@ abstract class table_base { * Makes a new instance. Can be overridden to provide a factory * * @param \stdClass|int|array $db_record - * @return static + * @param bool $reload + * @return bool + * @throws \coding_exception + * @throws \dml_exception */ - public static function find($db_record) { + public static function find($db_record, $reload = true) { global $DB; @@ -91,7 +94,9 @@ public static function find($db_record) { if ($db_record) { $record = new $klass($db_record); - $record->reload(); + if ($reload) { + $record->reload(); + } return $record; } @@ -610,4 +615,47 @@ public function id() { } return $this->id; } + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * + * @param $coursework_id + * @throws \dml_exception + */ + public static function fill_pool_coursework($coursework_id) { + if (isset(static::$pool[$coursework_id])) { + return; + } + $key = static::$table_name; + $cache = \cache::make('mod_coursework', 'courseworkdata', ['id' => $coursework_id]); + + $data = $cache->get($key); + if ($data === false) { + // no cache found + $data = static::get_cache_array($coursework_id); + $cache->set($key, $data); + } + + static::$pool[$coursework_id] = $data; + } + + /** + * @param $coursework_id + */ + public static function remove_cache($coursework_id) { + global $SESSION; + if (!empty($SESSION->keep_cache_data)) { + return; + } + static::$pool[$coursework_id] = null; + $cache = \cache::make('mod_coursework', 'courseworkdata', ['id' => $coursework_id]); + $cache->delete(static::$table_name); + } + } diff --git a/classes/grade_judge.php b/classes/grade_judge.php index ebbc77a..c218c0c 100644 --- a/classes/grade_judge.php +++ b/classes/grade_judge.php @@ -146,13 +146,12 @@ public function is_feedback_that_is_promoted_to_gradebook(feedback $feedback) { * @param allocatable $allocatable * @return bool */ - private function allocatable_needs_more_than_one_feedback ($allocatable){ + public function allocatable_needs_more_than_one_feedback ($allocatable){ - if ($this->coursework->sampling_enabled()){ - $parameters = array('courseworkid' => $this->coursework->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type()); - return assessment_set_membership::exists($parameters); + if ($this->coursework->sampling_enabled()) { + assessment_set_membership::fill_pool_coursework($this->coursework->id); + $record = assessment_set_membership::get_object($this->coursework->id, 'allocatableid-allocatabletype', [$allocatable->id(), $allocatable->type()]); + return !empty($record); } else { return $this->coursework->has_multiple_markers(); } diff --git a/classes/grades/gradeitems.php b/classes/grades/gradeitems.php new file mode 100644 index 0000000..63e7a50 --- /dev/null +++ b/classes/grades/gradeitems.php @@ -0,0 +1,61 @@ +. + +/** + * Grade item mappings for the activity. + * + * @package mod_coursework + */ + +declare(strict_types = 1); + +namespace mod_coursework\grades; + +use \core_grades\local\gradeitem\itemnumber_mapping; +use \core_grades\local\gradeitem\advancedgrading_mapping; + +/** + * Grade item mappings for the activity. + * + * @package mod_coursework + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gradeitems implements itemnumber_mapping, advancedgrading_mapping { + + /** + * Return the list of grade item mappings for the assign. + * + * @return array + */ + public static function get_itemname_mapping_for_component(): array { + return [ + 0 => 'submissions', + ]; + } + + /** + * Get the list of advanced grading item names for this component. + * + * @return array + */ + public static function get_advancedgrading_itemnames(): array { + return [ + 'submissions', + ]; + } +} + + diff --git a/classes/grading_report.php b/classes/grading_report.php index 78dcbea..ace35c2 100644 --- a/classes/grading_report.php +++ b/classes/grading_report.php @@ -16,7 +16,12 @@ namespace mod_coursework; +use mod_coursework\models\allocation; +use mod_coursework\models\course_module; use mod_coursework\models\coursework; +use mod_coursework\models\feedback; +use mod_coursework\models\module; +use mod_coursework\models\submission; use mod_coursework\models\user; use mod_coursework\render_helpers\grading_report\cells\cell_interface; use mod_coursework\render_helpers\grading_report\sub_rows\sub_rows_interface; @@ -28,6 +33,11 @@ */ class grading_report { + //added static vars to determine in what manner the report is loaded + public static $MODE_GET_ALL = 1; + public static $MODE_GET_FIRST_RECORDS = 2; + public static $MODE_GET_REMAIN_RECORDS = 3; + /** * @var array rendering options */ @@ -49,6 +59,11 @@ class grading_report { */ private $totalrows; + /** + * @var int The real total number of rows the user could see on all pages. + */ + public $realtotalrows; + /** * @var */ @@ -67,8 +82,27 @@ class grading_report { * @param coursework $coursework */ public function __construct(array $options, $coursework) { + + $options['courseworkid'] = $coursework->id; + $this->options = $options; $this->coursework = $coursework; + $this->fill_pool(); + } + + /** + * Cache data for later use + */ + protected function fill_pool() { + global $DB; + + $coursework_id = $this->coursework->id; + submission::fill_pool_coursework($coursework_id); + coursework::fill_pool([$this->coursework]); + course_module::fill_pool([$this->coursework->get_course_module()]); + module::fill_pool($DB->get_records('modules', ['name' => 'coursework'])); + feedback::fill_pool_submissions($coursework_id, array_keys(submission::$pool[$coursework_id]['id'])); + allocation::fill_pool_coursework($coursework_id); } /** @@ -306,7 +340,7 @@ public function get_participant_count() { * * @return grading_table_row_base[] row objects */ - public function get_table_rows_for_page() { + public function get_table_rows_for_page($rowcount=false) { global $USER; @@ -317,70 +351,68 @@ public function get_table_rows_for_page() { // Make tablerow objects so we can use the methods to check permissions and set things. $rows = array(); - foreach ($participants as $participant) { + $row_class = $this->coursework->has_multiple_markers() ? 'mod_coursework\grading_table_row_multi' : 'mod_coursework\grading_table_row_single'; + $ability = new ability(user::find($USER, false), $this->get_coursework()); + + $participantsfound = 0; + + foreach ($participants as $key => $participant) { // handle 'Group mode' - unset groups/individuals thaat are not in the chosen group - if(!empty($options['group']) && $options['group'] != -1){ - if ($this->coursework->is_configured_to_have_group_submissions()){ - if($options['group'] != $participant->id) continue; - } else { - if(!$this->coursework->student_in_group($participant->id, $options['group']))continue; - } - } - - if ($this->coursework->has_multiple_markers()) { - $row = new grading_table_row_multi($this->coursework, $participant); - $rows[$participant->id()] = $row; - } else { - $row = new grading_table_row_single($this->coursework, $participant); - $rows[$participant->id()] = $row; + if(!empty($options['group']) && $options['group'] != -1){ + if ($this->coursework->is_configured_to_have_group_submissions()){ + if($options['group'] != $participant->id) continue; + } else { + if(!$this->coursework->student_in_group($participant->id, $options['group']))continue; + } } - } - // Sort the rows. - $method_name = 'sort_by_' . $options['sortby']; - if (method_exists($this, $method_name)) { - usort($rows, - array($this, - $method_name)); - } + $row = new $row_class($this->coursework, $participant); - $ability = new ability(user::find($USER), $this->get_coursework()); - - // Now, we remove the ones who should not be visible on this page. Must happen AFTER the sort! - // Rather than sort in SQL, we sort here so we can use complex permissions stuff. - // Page starts at 0! - $start = ($options['page']) * $options['perpage']; // Will start at 0. - $end = ($options['page'] + 1) * $options['perpage']; // Take care of overlap: 0-10, 10-20, 20-30. - $counter = 0; // Begin from the first one that the user could see. - foreach ($rows as $allocatable_id => $row) { - /** - * @var grading_table_row_base $row - */ - // Some the user should not even know are there. Important that we only increment the counter after - // this point. - if (!$ability->can('show', $row) && !isset($options['unallocated'])) { - unset($rows[$allocatable_id]); + // Now, we skip the ones who should not be visible on this page. + $can_show = $ability->can('show', $row); + if (!$can_show && !isset($options['unallocated'])) { + unset($participants[$key]); continue; } - - if ($ability->can('show', $row) && isset($options['unallocated'])) { - unset($rows[$allocatable_id]); + if ($can_show && isset($options['unallocated'])) { + unset($participants[$key]); continue; } - $counter++; + $rows[$participant->id()] = $row; + $participantsfound++; + if (!empty($rowcount) && $participantsfound >= $rowcount) break; - if ($counter <= $start || $counter > $end) { // Taking care not to include the same ones in two pages. - unset($rows[$allocatable_id]); - } + } + + // Sort the rows. + $method_name = 'sort_by_' . $options['sortby']; + if (method_exists($this, $method_name)) { + usort($rows, + array($this, + $method_name)); } // Some will have submissions and therefore data fields. Others will have those fields null. /* @var grading_table_row_base[] $tablerows */ + $counter = count($rows); + $this->realtotalrows = $counter; + $mode = empty($this->options['mode']) ? self::$MODE_GET_ALL : $this->options['mode']; + if ($mode != self::$MODE_GET_ALL) { + $perpage = $this->options['perpage'] ?? 10; + if ($counter > $perpage) { + if ($mode == self::$MODE_GET_FIRST_RECORDS) { + $rows = array_slice($rows, 0, $perpage); + } else if ($mode == self::$MODE_GET_REMAIN_RECORDS) { + $rows = array_slice($rows, $perpage); + } + } + } + $this->tablerows = $rows; - $this->totalrows = $counter; + $this->totalrows = count($rows); } return $this->tablerows; diff --git a/classes/grading_table_row_base.php b/classes/grading_table_row_base.php index e95163d..9359326 100644 --- a/classes/grading_table_row_base.php +++ b/classes/grading_table_row_base.php @@ -117,6 +117,42 @@ public function get_user_name($link = false) { } } + /** + * Will return the idnumber if permissions allow, otherwise, an anonymous placeholder. + * + * @throws \coding_exception + * @return string + */ + public function get_idnumber() { + global $DB; + + $viewanonymous = has_capability('mod/coursework:viewanonymous', $this->get_coursework()->get_context()); + if (!$this->get_coursework()->blindmarking || $viewanonymous || $this->is_published()) { + $user = $DB->get_record('user', array('id' => $this->get_allocatable_id())); + return $user->idnumber; + } else { + return get_string('hidden', 'mod_coursework'); + } + } + + /** + * Will return the email if permissions allow, otherwise, an anonymous placeholder. + * + * @throws \coding_exception + * @return string + */ + public function get_email() { + global $DB; + + $viewanonymous = has_capability('mod/coursework:viewanonymous', $this->get_coursework()->get_context()); + if (!$this->get_coursework()->blindmarking || $viewanonymous || $this->is_published()) { + $user = $DB->get_record('user', array('id' => $this->get_allocatable_id())); + return $user->email; + } else { + return ''; + } + } + /** * Returns the id of the student who's submission this is * @@ -202,13 +238,10 @@ public function get_coursework() { public function get_submission() { if (!isset($this->submission)) { - - $params = array( - 'courseworkid' => $this->get_coursework_id(), - 'allocatableid' => $this->get_allocatable()->id(), - 'allocatabletype' => $this->get_allocatable()->type(), - ); - $this->submission = submission::find($params); + $allocatableid = $this->get_allocatable()->id(); + $allocatabletype = $this->get_allocatable()->type(); + $params = [$allocatableid, $allocatabletype]; + $this->submission = submission::get_object($this->get_coursework_id(), 'allocatableid-allocatabletype', $params); } return $this->submission; @@ -361,4 +394,64 @@ public function get_single_feedback(){ return $this->get_submission()->get_assessor_feedback_by_stage('assessor_1'); } + + /** + * Check if the extension is given to this row + * + * @return bool + */ + + public function has_extension() { + + global $DB; + return $DB->record_exists('coursework_extensions', array('courseworkid' => $this->get_coursework()->id, + 'allocatableid' => $this->get_allocatable()->id(), + 'allocatabletype'=> $this->get_allocatable()->type())); + + } + + + /** + * Getter for row extension + * + * @return mixed + + */ + public function get_extension() { + global $DB; + return $DB->get_record('coursework_extensions', array('courseworkid' => $this->get_coursework()->id, + 'allocatableid' => $this->get_allocatable()->id(), + 'allocatabletype'=> $this->get_allocatable()->type())); + } + + public function get_user_firstname() { + /** + * @var user $user + */ + $user = $this->get_allocatable(); + + $viewanonymous = has_capability('mod/coursework:viewanonymous', $this->get_coursework()->get_context()); + if (!$this->get_coursework()->blindmarking || $viewanonymous || $this->is_published() ) { + return $user->firstname; + } + else { + return get_string('hidden', 'mod_coursework'); + } + } + + public function get_user_lastname() { + /** + * @var user $user + */ + $user = $this->get_allocatable(); + + $viewanonymous = has_capability('mod/coursework:viewanonymous', $this->get_coursework()->get_context()); + if (!$this->get_coursework()->blindmarking || $viewanonymous || $this->is_published()) { + return $user->lastname; + } + else { + return get_string('hidden', 'mod_coursework'); + } + } + } diff --git a/classes/mailer.php b/classes/mailer.php index 7e7a840..99ab0ed 100644 --- a/classes/mailer.php +++ b/classes/mailer.php @@ -37,39 +37,42 @@ public function send_submission_receipt($user, $finalised = false) { $submission = $this->coursework->get_user_submission($user); - $email_data = new \stdClass(); - $email_data->name = $user->name(); - $dateformat = '%a, %d %b %Y, %H:%M'; - $email_data->submittedtime = userdate($submission->time_submitted(), $dateformat); - $email_data->coursework_name = $this->coursework->name; - $email_data->submissionid = $submission->id; - if ($finalised) { - $email_data->finalised = get_string('save_email_finalised', 'coursework'); - } else { - $email_data->finalised = ''; - } + if ($this->coursework && $this->coursework->is_coursework_visible()) {// check if coursework exists and is not hidden + + $email_data = new \stdClass(); + $email_data->name = $user->name(); + $dateformat = '%a, %d %b %Y, %H:%M'; + $email_data->submittedtime = userdate($submission->time_submitted(), $dateformat); + $email_data->coursework_name = $this->coursework->name; + $email_data->submissionid = $submission->id; + if ($finalised) { + $email_data->finalised = get_string('save_email_finalised', 'coursework'); + } else { + $email_data->finalised = ''; + } + + $subject = get_string('save_email_subject', 'coursework'); + $text_body = get_string('save_email_text', 'coursework', $email_data); + $html_body = get_string('save_email_html', 'coursework', $email_data); - $subject = get_string('save_email_subject', 'coursework'); - $text_body = get_string('save_email_text', 'coursework', $email_data); - $html_body = get_string('save_email_html', 'coursework', $email_data); - - // New approach. - $eventdata = new \core\message\message(); - $eventdata->component = 'mod_coursework'; - $eventdata->name = 'submission_receipt'; - $eventdata->userfrom = \core_user::get_noreply_user(); - $eventdata->userto = $user->get_raw_record(); - $eventdata->subject = $subject; - $eventdata->fullmessage = $text_body; - $eventdata->fullmessageformat = FORMAT_PLAIN; - $eventdata->fullmessagehtml = $html_body; - $eventdata->smallmessage = $text_body; - $eventdata->notification = 1; - $eventdata->contexturl = $CFG->wwwroot.'/mod/coursework/view.php?id='.$submission->get_coursework()->get_coursemodule_id(); - $eventdata->contexturlname = 'View your submission here'; - $eventdata->courseid = $this->coursework->course; + // New approach. + $eventdata = new \core\message\message(); + $eventdata->component = 'mod_coursework'; + $eventdata->name = 'submission_receipt'; + $eventdata->userfrom = \core_user::get_noreply_user(); + $eventdata->userto = $user->get_raw_record(); + $eventdata->subject = $subject; + $eventdata->fullmessage = $text_body; + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = $html_body; + $eventdata->smallmessage = $text_body; + $eventdata->notification = 1; + $eventdata->contexturl = $CFG->wwwroot . '/mod/coursework/view.php?id=' . $submission->get_coursework()->get_coursemodule_id(); + $eventdata->contexturlname = 'View your submission here'; + $eventdata->courseid = $this->coursework->course; - message_send($eventdata); + message_send($eventdata); + } } @@ -117,37 +120,40 @@ public function send_late_submission_notification($submission) { public function send_feedback_notification($submission) { global $CFG; - $email_data = new \stdClass(); - $email_data->coursework_name = $this->coursework->name; + if ($this->coursework && $this->coursework->is_coursework_visible()) {// check if coursework exists and is not hidden - $subject = get_string('feedback_released_email_subject', 'coursework'); + $email_data = new \stdClass(); + $email_data->coursework_name = $this->coursework->name; - // get a student or all students from a group - $students = $submission->students_for_gradebook(); + $subject = get_string('feedback_released_email_subject', 'coursework'); - foreach ($students as $student) { - $student = \mod_coursework\models\user::find($student); + // get a student or all students from a group + $students = $submission->students_for_gradebook(); - $email_data->name = $student->name(); - $text_body = get_string('feedback_released_email_text', 'coursework', $email_data); - $html_body = get_string('feedback_released_email_html', 'coursework', $email_data); + foreach ($students as $student) { + $student = \mod_coursework\models\user::find($student); - $eventdata = new \core\message\message(); - $eventdata->component = 'mod_coursework'; - $eventdata->name = 'feedback_released'; - $eventdata->userfrom = \core_user::get_noreply_user(); - $eventdata->userto = $student->get_raw_record(); - $eventdata->subject = $subject; - $eventdata->fullmessage = $text_body; - $eventdata->fullmessageformat = FORMAT_PLAIN; - $eventdata->fullmessagehtml = $html_body; - $eventdata->smallmessage = $text_body; - $eventdata->notification = 1; - $eventdata->contexturl = $CFG->wwwroot . '/mod/coursework/view.php?id=' . $submission->get_coursework()->get_coursemodule_id(); - $eventdata->contexturlname = 'View your submission here'; - $eventdata->courseid = $this->coursework->course; + $email_data->name = $student->name(); + $text_body = get_string('feedback_released_email_text', 'coursework', $email_data); + $html_body = get_string('feedback_released_email_html', 'coursework', $email_data); - message_send($eventdata); + $eventdata = new \core\message\message(); + $eventdata->component = 'mod_coursework'; + $eventdata->name = 'feedback_released'; + $eventdata->userfrom = \core_user::get_noreply_user(); + $eventdata->userto = $student->get_raw_record(); + $eventdata->subject = $subject; + $eventdata->fullmessage = $text_body; + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = $html_body; + $eventdata->smallmessage = $text_body; + $eventdata->notification = 1; + $eventdata->contexturl = $CFG->wwwroot . '/mod/coursework/view.php?id=' . $submission->get_coursework()->get_coursemodule_id(); + $eventdata->contexturlname = 'View your submission here'; + $eventdata->courseid = $this->coursework->course; + + message_send($eventdata); + } } } @@ -162,6 +168,9 @@ public function send_student_deadline_reminder($user) { global $CFG; + if (!$this->coursework || !$this->coursework->is_coursework_visible()) {// check if coursework exists and is not hidden + return false; + } $email_data = new \stdClass(); $email_data->coursework_name = $this->coursework->name; $email_data->coursework_name_with_link = \html_writer::link($CFG->wwwroot . '/mod/coursework/view.php?id=' . $this->coursework->get_coursemodule_id(), $this->coursework->name); @@ -210,33 +219,36 @@ public function send_submission_notification($userstonotify) { global $CFG; - $email_data = new \stdClass(); - $email_data->coursework_name = $this->coursework->name; + if ($this->coursework && $this->coursework->is_coursework_visible()) {// check if coursework exists and is not hidden - $subject = get_string('submission_notification_subject', 'coursework',$email_data->coursework_name); + $email_data = new \stdClass(); + $email_data->coursework_name = $this->coursework->name; - $userstonotify = \mod_coursework\models\user::find($userstonotify); + $subject = get_string('submission_notification_subject', 'coursework', $email_data->coursework_name); - $email_data->name = $userstonotify->name(); - $text_body = get_string('submission_notification_text', 'coursework', $email_data); - $html_body = get_string('submission_notification_html', 'coursework', $email_data); + $userstonotify = \mod_coursework\models\user::find($userstonotify); - $eventdata = new \core\message\message(); - $eventdata->component = 'mod_coursework'; - $eventdata->name = 'coursework_submission'; - $eventdata->userfrom = \core_user::get_noreply_user(); - $eventdata->userto = $userstonotify->get_raw_record(); - $eventdata->subject = $subject; - $eventdata->fullmessage = $text_body; - $eventdata->fullmessageformat = FORMAT_PLAIN; - $eventdata->fullmessagehtml = $html_body; - $eventdata->smallmessage = $text_body; - $eventdata->notification = 1; - $eventdata->contexturl = $CFG->wwwroot . '/mod/coursework/view.php?id=' . $this->coursework->id(); - $eventdata->contexturlname = 'coursework submission'; - $eventdata->courseid = $this->coursework->course; + $email_data->name = $userstonotify->name(); + $text_body = get_string('submission_notification_text', 'coursework', $email_data); + $html_body = get_string('submission_notification_html', 'coursework', $email_data); - message_send($eventdata); + $eventdata = new \core\message\message(); + $eventdata->component = 'mod_coursework'; + $eventdata->name = 'coursework_submission'; + $eventdata->userfrom = \core_user::get_noreply_user(); + $eventdata->userto = $userstonotify->get_raw_record(); + $eventdata->subject = $subject; + $eventdata->fullmessage = $text_body; + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = $html_body; + $eventdata->smallmessage = $text_body; + $eventdata->notification = 1; + $eventdata->contexturl = $CFG->wwwroot . '/mod/coursework/view.php?id=' . $this->coursework->id(); + $eventdata->contexturlname = 'coursework submission'; + $eventdata->courseid = $this->coursework->course; + + message_send($eventdata); + } } diff --git a/classes/models/allocation.php b/classes/models/allocation.php index 229674e..73b50b5 100644 --- a/classes/models/allocation.php +++ b/classes/models/allocation.php @@ -100,7 +100,7 @@ public function get_coursework() { * @return user|bool */ public function assessor() { - return user::find($this->assessorid); + return user::get_object($this->assessorid); } /** @@ -141,4 +141,69 @@ public function unpin() { $this->update_attribute('manual', 0); } } + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * + * @param $coursework_id + * @return array + */ + protected static function get_cache_array($coursework_id) { + global $DB; + $records = $DB->get_records(static::$table_name, ['courseworkid' => $coursework_id]); + $result = [ + 'id' => [], + 'stage_identifier' => [], + 'allocatableid-allocatabletype-stage_identifier' => [], + 'allocatableid-allocatabletype-assessorid' => [], + 'assessorid-allocatabletype' => [] + ]; + if ($records) { + foreach ($records as $record) { + $object = new self($record); + $result['id'][$record->id] = $object; + $result['stage_identifier'][$record->stage_identifier][] = $object; + $result['allocatableid-allocatabletype-stage_identifier'][$record->allocatableid . '-' . $record->allocatabletype . '-' . $record->stage_identifier][] = $object; + $result['allocatableid-allocatabletype-assessorid'][$record->allocatableid . '-' . $record->allocatabletype . '-' . $record->assessorid][] = $object; + $result['assessorid-allocatabletype'][$record->assessorid . '-' . $record->allocatabletype][] = $object; + } + } + return $result; + } + + /** + * + * @param $coursework_id + * @param $key + * @param $params + * @return bool + */ + public static function get_object($coursework_id, $key, $params) { + if (!isset(self::$pool[$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + $value_key = implode('-', $params); + return self::$pool[$coursework_id][$key][$value_key][0] ?? false; + } + + /** + * + */ + protected function post_save_hook() { + self::remove_cache($this->courseworkid); + } + + /** + * + */ + protected function after_destroy() { + self::remove_cache($this->courseworkid); + } + } diff --git a/classes/models/assessment_set_membership.php b/classes/models/assessment_set_membership.php index c427660..5482ab9 100644 --- a/classes/models/assessment_set_membership.php +++ b/classes/models/assessment_set_membership.php @@ -20,9 +20,69 @@ */ class assessment_set_membership extends table_base implements moderatable { + /** + * cache array + * + * @var + */ + public static $pool; + /** * @var string */ protected static $table_name = 'coursework_sample_set_mbrs'; + /** + * + * @param $coursework_id + * @return array + */ + protected static function get_cache_array($coursework_id) { + global $DB; + $records = $DB->get_records(self::$table_name, ['courseworkid' => $coursework_id]); + $result = [ + 'allocatableid-allocatabletype' => [], + 'allocatableid-allocatabletype-stage_identifier' => [], + 'allocatableid-stage_identifier-selectiontype' => [] + ]; + if ($records) { + foreach ($records as $record) { + $object = new self($record); + $result['allocatableid-allocatabletype'][$record->allocatableid . '-' . $record->allocatabletype][] = $object; + $result['allocatableid-allocatabletype-stage_identifier'][$record->allocatableid . '-' . $record->allocatabletype . '-' . $record->stage_identifier][] = $object; + $result['allocatableid-stage_identifier-selectiontype'][$record->allocatableid . '-' . $record->stage_identifier . '-' . $record->selectiontype][] = $object; + } + } + return $result; + } + + /** + * + * @param $coursework_id + * @param $key + * @param $params + * @return bool + */ + public static function get_object($coursework_id, $key, $params) { + if (!isset(self::$pool[$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + $value_key = implode('-', $params); + return self::$pool[$coursework_id][$key][$value_key][0] ?? false; + } + + /** + * + */ + protected function post_save_hook() { + self::remove_cache($this->courseworkid); + } + + /** + * + */ + protected function after_destroy() { + self::remove_cache($this->courseworkid); + } + } \ No newline at end of file diff --git a/classes/models/course_module.php b/classes/models/course_module.php new file mode 100644 index 0000000..2674272 --- /dev/null +++ b/classes/models/course_module.php @@ -0,0 +1,61 @@ +. + +namespace mod_coursework\models; + +use mod_coursework\framework\table_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Represents a row in the course_modules table. + */ +class course_module extends table_base { + + /** + * @var string + */ + protected static $table_name = 'course_modules'; + + /** + * @var int + */ + public $id; + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * Fill pool to cache for later use + * + * @param $array + */ + public static function fill_pool($array) { + self::$pool = [ + 'id' => [], + 'course-module-instance' => [] + ]; + foreach ($array as $record) { + $object = new self($record); + self::$pool['id'][$record->id] = $object; + self::$pool['course-module-instance'][$record->course][$record->module][$record->instance] = $object; + } + } +} diff --git a/classes/models/coursework.php b/classes/models/coursework.php index a787737..fe942c5 100644 --- a/classes/models/coursework.php +++ b/classes/models/coursework.php @@ -41,7 +41,11 @@ use mod_coursework\decorators\coursework_groups_decorator; use mod_coursework\grade_judge; use mod_coursework\grading_report; +use mod_coursework\render_helpers\grading_report\cells\first_name_cell; use mod_coursework\render_helpers\grading_report\cells\grade_for_gradebook_cell; +use mod_coursework\render_helpers\grading_report\cells\last_name_cell; +use mod_coursework\render_helpers\grading_report\cells\email_cell; +use mod_coursework\render_helpers\grading_report\cells\idnumber_cell; use mod_coursework\render_helpers\grading_report\cells\moderation_agreement_cell; use mod_coursework\render_helpers\grading_report\cells\single_assessor_feedback_cell; use mod_coursework\render_helpers\grading_report\cells\filename_hash_cell; @@ -147,6 +151,11 @@ class coursework extends table_base { */ public $numberofmarkers; + /** + * @var int + */ + public $finalstagegrading; + /** * @var int 0 or 1 */ @@ -331,6 +340,11 @@ class coursework extends table_base { */ public $maxfiles; + /** + * @var int + */ + public $renamefiles; + /** * @var string */ @@ -398,7 +412,7 @@ class coursework extends table_base { * @param int|\stdClass $db_record * @return mixed|\mod_coursework_coursework */ - public static function find($db_record) { + public static function find($db_record, $reload = true) { $coursework_object = parent::find($db_record); if ($coursework_object && $coursework_object->is_configured_to_have_group_submissions()) { @@ -433,11 +447,14 @@ public function get_course_module() { if (empty($this->id)) { throw new moodle_exception('Trying to get course module for a coursework that has not yet been saved'); } - $module_id = $DB->get_field('modules', 'id', array('name' => 'coursework')); - $this->coursemodule = $DB->get_record('course_modules', - array('course' => $this->get_course_id(), - 'module' => $module_id, - 'instance' => $this->id), '*', MUST_EXIST); + $module_record = module::$pool['name']['coursework'] ?? $DB->get_record('modules', ['name' => 'coursework']); + $module_id = $module_record->id; + $course_id = $this->get_course_id(); + if (!isset(course_module::$pool['course-module-instance'][$course_id][$module_id][$this->id]->id)) { + $this->coursemodule = course_module::$pool['course-module-instance'][$course_id][$module_id][$this->id] = + $DB->get_record('course_modules', ['course' => $course_id, 'module' => $module_id, 'instance' => $this->id], '*', MUST_EXIST); + } + $this->coursemodule = course_module::$pool['course-module-instance'][$course_id][$module_id][$this->id]; } return $this->coursemodule; @@ -478,6 +495,7 @@ public function get_coursemodule_idnumber() { return (int)$coursemodule->idnumber; } + /** * Getter function for the coursework's course object. * @@ -527,17 +545,8 @@ public function set_course_id($courseid) { * @return array */ public function get_all_raw_feedbacks() { - - global $DB; - - $sql = "SELECT f.* - FROM {coursework_feedbacks} f - INNER JOIN {coursework_submissions} s - ON s.id = f.submissionid - WHERE s.courseworkid = :courseworkid "; - - $params['courseworkid'] = $this->id; - $feedbacks = $DB->get_records_sql($sql, $params); + feedback::fill_pool_coursework($this->id); + $feedbacks = feedback::$pool[$this->id]['id']; return $feedbacks; } @@ -553,14 +562,13 @@ public function get_ungraded_assessments_number($can_grade) { return 0; } // Count submitted work that this person has not graded. - $submissions = $DB->get_records('coursework_submissions', - array('courseworkid' => $this->id), '', 'id'); + submission::fill_pool_coursework($this->id); + feedback::fill_pool_coursework($this->id); + $submissions = submission::$pool[$this->id]['id']; $count = 0; foreach ($submissions as $s) { - $feedbacks = $DB->count_records('coursework_feedbacks', - array('submissionid' => $s->id, - 'assessorid' => $USER->id)); - if ($feedbacks < 1) { + $feedback = feedback::get_object($this->id, 'submissionid-assessorid', [$s->id, $USER->id]); + if (empty($feedback)) { $count++; } } @@ -689,7 +697,7 @@ public function get_file_options() { $turnitinenabled = $this->tii_enabled(); // Turn it in only allows one file. - $max_files = $turnitinenabled ? 1 : $this->maxfiles; + $max_files = $this->maxfiles; // Turn it in only likes some file types. /* DOC, DOCX, Corel @@ -969,10 +977,31 @@ public function pack_files() { $files = $fs->get_area_files($context->id, 'mod_coursework', 'submission', $submission->id, "id", false); foreach ($files as $f) { + + $filename = basename($f->get_filename()); + + $foldername = ''; + + if($this->blindmarking == 0 || has_capability('mod/coursework:viewanonymous',$this->get_context())) { + $submissionuser = $submission->get_allocatable(); + if ($this->is_configured_to_have_group_submissions() && $submissionuser->name) { + $foldername = $submissionuser->name . '_'; + } elseif(!$this->is_configured_to_have_group_submissions()) { + $foldername = $submissionuser->firstname . ' ' . $submissionuser->lastname . '_'; + } + } + + $foldername .= $this->get_username_hash($submission->get_allocatable()->id()); + + $filename = $foldername.'/'.$filename; + /* @var $f stored_file */ - $files_for_zipping[$f->get_filename()] = $f; + $files_for_zipping[$filename] = $f; } } + + + // Create path for new zip file. $temp_zip = tempnam($CFG->dataroot.'/temp/', 'ocm_'); // Zip files. @@ -1216,28 +1245,21 @@ public function assessor_has_any_allocation_for_student($allocatable, $userid=fa */ public function assessor_has_allocation_for_student_not_in_current_stage($allocatable, $userid, $stage) { - global $DB, $USER; - - if (!$userid){ + global $USER; + if (!$userid) { $userid = $USER->id; } - $params = array( - 'courseworkid' => $this->id, - 'assessorid' => $userid, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - 'stage' => $stage ); - - $sql = "SELECT * - FROM {coursework_allocation_pairs} - WHERE courseworkid = :courseworkid - AND assessorid = :assessorid - AND allocatableid = :allocatableid - AND allocatabletype = :allocatabletype - AND stage_identifier <> :stage"; + allocation::fill_pool_coursework($this->id); + $records = isset(allocation::$pool[$this->id]['allocatableid-allocatabletype-assessorid'][$allocatable->id() . '-' . $allocatable->type() . "-$userid"]) ? + allocation::$pool[$this->id]['allocatableid-allocatabletype-assessorid'][$allocatable->id() . '-' . $allocatable->type() . "-$userid"] : []; + foreach ($records as $record) { + if ($record->stage_identifier != $stage) { + return true; + } + } - return $DB->record_exists_sql($sql, $params); + return false; } @@ -1268,18 +1290,114 @@ public function current_user_is_moderator_for_student($allocatable) { * @return submission[] */ public function get_all_submissions() { + submission::fill_pool_coursework($this->id); + $submissions = submission::$pool[$this->id]['id']; + + return $submissions; + } + + /** + * Get submissions that need grading, either in initial or final stage + * For multiple marker coursework if final grade is not given it is assumed that submission may need grading in + * either initial, final or both stages + * + * @throws \dml_exception + * @throws \dml_missing_record_exception + * @throws \dml_multiple_records_exception + */ + public function get_submissions_needing_grading(){ + + $needsgrading = array(); + $submissions = $this->get_finalised_submissions(); + + foreach ($submissions as $submission) { + + $stage_identifier = ($this->has_multiple_markers())? 'final_agreed_1': 'assessor_1'; + $submission = submission::find($submission); + if (!$feedback = $submission->get_assessor_feedback_by_stage($stage_identifier)){ + $needsgrading[] = $submission; + } + + } + + + return $needsgrading; + + } + + /** + * Get all graded submissions for the specified marking stage + * + * @param $stage_identifier + * @return array + * @throws \dml_missing_record_exception + * @throws \dml_multiple_records_exception + */ + public function get_graded_submissions_by_stage($stage_identifier){ + + $graded = array(); + $submissions = $this->get_finalised_submissions(); + + foreach ($submissions as $submission) { + $submission = submission::find($submission); + if ($feedback = $submission->get_assessor_feedback_by_stage($stage_identifier)){ + $graded[$submission->id] = $submission; + } + } + return $graded; + } + /** + * Function to get all assessor's graded submissions within the specified coursework + * + * @param $assessorid + * @return array + * @throws \dml_exception + */ + public function get_assessor_graded_submissions($assessorid){ global $DB; - $submissions = $DB->get_records('coursework_submissions', array('courseworkid' => $this->id)); + $graded = array(); + $params = array('courseworkid' => $this->id, 'assessorid' => $assessorid); + $sql = "SELECT cs.id + FROM {coursework_feedbacks} cf + JOIN {coursework_submissions} cs + ON cs.id = cf.submissionid + WHERE cs.courseworkid = :courseworkid + AND assessorid = :assessorid"; + $submissions = $DB->get_records_sql($sql, $params); - foreach ($submissions as &$submission) { + foreach ($submissions as $submission) { $submission = submission::find($submission); + $graded[$submission->id] = $submission; } + return $graded; + } + + + /** + * Get all published submissions in the coursework + * + * @return array + * @throws \dml_exception + */ + public function get_published_submissions(){ + global $DB; + + $sql = "SELECT * + FROM {coursework_submissions} + WHERE courseworkid = :courseworkid + AND firstpublished IS NOT NULL"; + + $submissions = $DB->get_records_sql($sql, array('courseworkid' => $this->id)); + foreach ($submissions as &$submission) { + $submission = submission::find($submission); + } return $submissions; } + /** * Returns a hash of this user's id. Not username as this would be non-unique across * different courseworks, compromising anonymity. @@ -1425,21 +1543,11 @@ public function get_student_feedback_release_date() { */ public function get_unfinalised_students($fields = 'u.id, u.firstname, u.lastname') { - global $DB; - $students = get_enrolled_users(context_course::instance($this->get_course_id()), 'mod/coursework:submit', 0, $fields); - - $sql = " - SELECT submissions.userid - FROM {coursework_submissions} submissions - WHERE submissions.courseworkid = :courseworkid - AND submissions.finalised = 1 - "; - $params = array('courseworkid' => $this->id); - $alreadyfinalised = $DB->get_records_sql($sql, $params); - - foreach ($alreadyfinalised as $studentid => $studentrecord) { - unset ($students[$studentid]); + submission::fill_pool_coursework($this->id); + $alreadyfinalised = isset(submission::$pool[$this->id]['finalised'][1]) ? submission::$pool[$this->id]['finalised'][1] : []; + foreach ($alreadyfinalised as $submission) { + unset ($students[$submission->userid]); } return $students; @@ -1742,7 +1850,11 @@ public function renderable_grading_report_factory($report_options) { if ($this->is_configured_to_have_group_submissions()) { $report->add_cell(new group_cell($cell_items)); } else { + $report->add_cell(new first_name_cell($cell_items)); + $report->add_cell(new last_name_cell($cell_items)); + $report->add_cell(new email_cell($cell_items)); $report->add_cell(new user_cell($cell_items)); + $report->add_cell(new idnumber_cell($cell_items)); } if ($this->personal_deadlines_enabled()) { @@ -2330,6 +2442,10 @@ public function get_finalised_submissions(){ $submissions = $DB->get_records('coursework_submissions', array('courseworkid' => $this->id, 'finalised'=>1), '', 'id'); + foreach ($submissions as &$submission) { + $submission = submission::find($submission); + } + return $submissions; } @@ -2413,10 +2529,11 @@ public function extensions_enabled() { return (bool)$this->extensionsenabled; } - /** Let us know if any extension was granted in the coursework - * @return bool - */ + /* +* @return bool +*/ public function extension_exists(){ + global $DB; return $DB->record_exists('coursework_extensions',array('courseworkid'=>$this->id)); } @@ -2513,33 +2630,38 @@ public function get_allocatables_with_feedback($stage, $random = false) { * */ public function create_automatic_feedback() { + global $SESSION; - global $DB; - - $module = $DB->get_record('modules',array('name'=>'coursework')); - - - //get all submissions that could need automatic agreement - $sql = "SELECT DISTINCT(cs.id) - FROM {coursework} c, - {coursework_submissions} cs, - {coursework_feedbacks} cf - WHERE c.id = cs.courseworkid - AND cs.id = cf.submissionid - AND c.numberofmarkers > 1 - AND cs.finalised = 1 - AND cf.stage_identifier NOT LIKE 'final_agreed%' - AND c.id = :courseworkid - AND c.automaticagreementstrategy != 'null' - AND (c.gradeeditingtime = 0 OR c.gradeeditingtime != 0 AND cf.timecreated + c.gradeeditingtime <= :currenttime) "; - - $submissionids = $DB->get_records_sql($sql,array('courseworkid'=>$this->id,'currenttime'=>time())); - - foreach($submissionids as $s) { - - $submission = submission::find(array('id'=>$s->id)); - //check if any feedback for this submission are editable + if ($this->numberofmarkers <= 1 || $this->automaticagreementstrategy == 'null') { + return; + } + submission::fill_pool_coursework($this->id); + feedback::fill_pool_coursework($this->id); + $submissions = isset(submission::$pool[$this->id]['finalised'][1]) ? submission::$pool[$this->id]['finalised'][1] : []; + if (empty($submissions)) { + return; + } + $current = time(); + $gradeeditingtime = $this->gradeeditingtime; + $SESSION->keep_cache_data = 1; + foreach ($submissions as $submission) { + if ($gradeeditingtime != 0) { + // initial feedbacks - other feedbacks than final + $initial_feedbacks = isset(feedback::$pool[$this->id]['submissionid-stage_identifier_index'][$submission->id . '-others']) ? + feedback::$pool[$this->id]['submissionid-stage_identifier_index'][$submission->id . '-others'] : []; + $valid_fb = false; + foreach ($initial_feedbacks as $feedback) { + if ($feedback->timecreated + $gradeeditingtime <= $current) { + $valid_fb = true; + break; + } + } + if (!$valid_fb) { + continue; + } + } if (!$submission->editable_feedbacks_exist()) { + // this submission needs automatic agreement $auto_feedback_classname = '\mod_coursework\auto_grader\\' . $submission->get_coursework()->automaticagreementstrategy; /** * @var auto_grader $auto_grader @@ -2551,36 +2673,45 @@ public function create_automatic_feedback() { } } + unset($SESSION->keep_cache_data); + feedback::remove_cache($this->id); } + /** Function to check it Turnitin is enabled for the particular coursework * @return bool * @throws \dml_exception */ public function tii_enabled(){ - global $CFG, $DB; - $turnitinenabled = false; - if ($CFG->enableplagiarism) { - $plagiarismsettings = (array)get_config('plagiarism'); - if (!empty($plagiarismsettings['turnitin_use'])) { - $params = array( - 'cm' => $this->get_coursemodule_id(), - 'name' => 'use_turnitin', - 'value' => 1 - ); - if ($DB->record_exists('plagiarism_turnitin_config', $params)) { - $turnitinenabled = true; + if (!isset(self::$pool[$this->id]['tii_enabled'][$this->id])) { + global $CFG, $DB; + $turnitinenabled = false; + if ($CFG->enableplagiarism) { + $plagiarismsettings = (array)get_config('plagiarism'); + if (!empty($plagiarismsettings['turnitin_use'])) { + $params = array( + 'cm' => $this->get_coursemodule_id(), + 'name' => 'use_turnitin', + 'value' => 1 + ); + if ($DB->record_exists('plagiarism_turnitin_config', $params)) { + $turnitinenabled = true; + } } } + self::$pool[$this->id]['tii_enabled'][$this->id] = $turnitinenabled; } - return $turnitinenabled; + return self::$pool[$this->id]['tii_enabled'][$this->id]; } + + + /** * Lets us know if personal deadlines are enabled in the coursework. * @return bool @@ -2631,7 +2762,8 @@ private function get_allocatable_personal_deadline($allocatable) { $allocatable->coursework_id = $this->id; if ($this->personal_deadlines_enabled()) { - $deadlinerecord = $DB->get_record('coursework_person_deadlines', array('courseworkid' => $this->id, 'allocatableid' => $allocatable->id)); + personal_deadline::fill_pool_coursework($this->id); + $deadlinerecord = personal_deadline::get_object($this->id, 'allocatableid-allocatabletype', [$allocatable->id, $allocatable->type()]); if (!empty($deadlinerecord)) { $allocatable->deadline = $deadlinerecord->personal_deadline; @@ -2686,7 +2818,6 @@ public function marking_deadline_enabled(){ return (bool)$this->markingdeadlineenabled ; } - /** * Function to check if a given individual is in the given group * @@ -2696,9 +2827,7 @@ public function marking_deadline_enabled(){ * @throws \dml_exception */ public function student_in_group($studentid, $groupid){ - global $DB; - $sql = "SELECT groups.* FROM {groups} groups INNER JOIN {groups_members} gm @@ -2706,34 +2835,28 @@ public function student_in_group($studentid, $groupid){ WHERE gm.userid = :userid AND groups.courseid = :courseid AND groups.id = :groupid"; - $params = array('userid' => $studentid, - 'courseid' => $this->get_course()->id, - 'groupid' => $groupid); - + 'courseid' => $this->get_course()->id, + 'groupid' => $groupid); return $DB->record_exists_sql($sql, $params); } - /** * Function to retrieve all submissions by coursework * * @return submissions */ - function retrieve_submissions_by_coursework() { + function retrieve_submissions_by_coursework(){ global $DB; - return $DB->get_records('coursework_submissions', ['courseworkid' => $this->id, 'allocatabletype' => 'user']); } - /** * Function to retrieve all submissions submitted by a user * * @param $user_id * @return submissions */ - public function retrieve_submissions_by_user($user_id) { + public function retrieve_submissions_by_user($user_id){ global $DB; - return $DB->get_records('coursework_submissions', ['courseworkid' => $this->id, 'authorid' => $user_id, 'allocatabletype' => 'user']); } @@ -2744,44 +2867,28 @@ public function retrieve_submissions_by_user($user_id) { * @return feedbacks */ public function retrieve_feedbacks_by_submission($submission_id) { - global $DB; - - return $DB->get_records('coursework_feedbacks', ['submissionid' => $submission_id]); + feedback::fill_pool_coursework($this->id); + $result = isset(feedback::$pool[$this->id][submissionid]) ? feedback::$pool[$this->id][submissionid] : []; + return $result; } - /** * Function to remove all submissions submitted by a user * * @param $user_id */ - public function remove_submissions_by_user($user_id) { + public function remove_submissions_by_user($user_id){ global $DB; - $DB->delete_records('coursework_submissions', ['courseworkid' => $this->id, 'authorid' => $user_id, 'allocatabletype' => 'user']); } - /** * Function to remove all submissions by this coursework * * @return submissions */ - public function remove_submissions_by_coursework() { + public function remove_submissions_by_coursework(){ global $DB; - $DB->delete_records('coursework_submissions', ['courseworkid' => $this->id, 'allocatabletype' => 'user']); } - - /** - * Function to Remove all plagiarisms by a submission - * - * @param $submission_id - */ - public function remove_plagiarisms_by_submission($submission_id) { - global $DB; - - $DB->delete_records('coursework_plagiarism_flags', ['submissionid' => $submission_id]); - } - /** * Function to Remove the corresponding file by context, item-id and fielarea * @@ -2789,76 +2896,258 @@ public function remove_plagiarisms_by_submission($submission_id) { * @param $item_id * @param $filearea */ - public function remove_corresponding_file($context_id, $item_id, $filearea) { + public function remove_corresponding_file($context_id, $item_id, $filearea){ global $DB; - - $component = 'mod_coursework'; - + $component = 'mod_coursework'; $fs = get_file_storage(); $fs->delete_area_files($context_id, $component, $filearea, $item_id); } - /** * Function to Remove all feedbacks by a submission * * @param $submission_id */ - public function remove_feedbacks_by_submission($submission_id) { + public function remove_feedbacks_by_submission($submission_id){ global $DB; - $DB->delete_records('coursework_feedbacks', ['submissionid' => $submission_id]); } - /** * Function to Remove all agreements by a feedback * * @param $feedback_id */ - public function remove_agreements_by_feedback($feedback_id) { + public function remove_agreements_by_feedback($feedback_id){ global $DB; - $DB->delete_records('coursework_mod_agreements', ['feedbackid' => $feedback_id]); } - /** * Function to Remove all deadline extensions by user * * @param $user_id */ - public function remove_deadline_extensions_by_user($user_id) { + public function remove_deadline_extensions_by_user($user_id){ global $DB; - $DB->execute('DELETE FROM {coursework_extensions} WHERE allocatabletype = ? AND (allocatableid = ? OR allocatableuser = ? ) ', array('user', $user_id, $user_id)); } + /** + * Function to Remove all personal deadlines by coursework + * + */ + public function remove_personal_deadlines_by_coursework() { + global $DB; + $DB->execute('DELETE FROM {coursework_person_deadlines} WHERE allocatabletype = ? AND courseworkid = ? ', array('user', $this->id)); + personal_deadline::remove_cache($this->id); + } /** - * Function to Remove all personal deadlines by user + * Function to Remove all deadline extensions by coursework * - * @param $user_id */ - public function remove_personal_deadlines_by_user($user_id) { + public function remove_deadline_extensions_by_coursework(){ + global $DB; + $DB->execute('DELETE FROM {coursework_extensions} WHERE allocatabletype = ? AND courseworkid = ? ', array('user', $this->id)); + } + + + /** + * Function to check if Coursework has any final feedback + * + * @return bool + * @throws \dml_exception + */ + public function has_any_final_feedback(){ global $DB; - $DB->execute('DELETE FROM {coursework_person_deadlines} WHERE allocatabletype = ? AND (allocatableid = ? OR allocatableuser = ? ) ', array('user', $user_id, $user_id)); + $sql = "SELECT * + FROM {coursework_feedbacks} cf + JOIN {coursework_submissions} cs ON cs.id = cf.submissionid + WHERE cs.courseworkid = :courseworkid + AND cf.stage_identifier = 'final_agreed_1'"; + + return $DB->record_exists_sql($sql, array('courseworkid' => $this->id)); } /** - * Function to Remove all deadline extensions by coursework + * Tells us the deadline for a specific allocatable. * + * @param int $allocatableid + * @return int + */ + public function get_allocatable_deadline($allocatableid) { + $deadline = $this->deadline; + + if($this->use_groups){ + $allocatable = group::find($allocatableid); + } else { + $allocatable = user::find($allocatableid); + } + + + if ($this->personal_deadlines_enabled()){ + // find personal deadline for a user if this option enabled + $personal = $this->get_allocatable_personal_deadline($allocatable); + if (!empty($personal)) { + $deadline = $personal->deadline; + } + } + + if($this->extensions_enabled()){ // check if coursework allows extensions + // check if extension for this user exists + $extension = $this->get_allocatable_extension($allocatable); + if (!empty($extension)) { + $deadline = $extension; + } + } + + return $deadline; + } + + /** + * * This function returns allocatable extension if given + * @param $allocatable + * @return bool/int */ - public function remove_deadline_extensions_by_coursework() { + private function get_allocatable_extension($allocatable) { + + global $DB; + $extension = false; + + + if ($this->extensions_enabled() ) { + $extensionrecord = $DB->get_record('coursework_extensions', array('courseworkid' => $this->id,'allocatableid' => $allocatable->id)); + + if (!empty($extensionrecord)) { + $extension = $extensionrecord->extended_deadline; + } + } + + return $extension; + } + + + /** + * Function to Remove all plagiarisms by a submission + * + * @param $submission_id + */ + public function remove_plagiarisms_by_submission($submission_id) { global $DB; - $DB->execute('DELETE FROM {coursework_extensions} WHERE allocatabletype = ? AND courseworkid = ? ', array('user', $this->id)); + $DB->delete_records('coursework_plagiarism_flags', ['submissionid' => $submission_id]); } + /** - * Function to Remove all personal deadlines by coursework + * Function to check if Coursework has any submission * + * @return bool + * @throws \dml_exception */ - public function remove_personal_deadlines_by_coursework() { + public function has_any_submission(){ global $DB; - $DB->execute('DELETE FROM {coursework_person_deadlines} WHERE allocatabletype = ? AND courseworkid = ? ', array('user', $this->id)); + $sql = "SELECT * + FROM {coursework_submissions} cs + WHERE cs.courseworkid = :courseworkid"; + + return $DB->record_exists_sql($sql, array('courseworkid' => $this->id)); + } + + + /** + * Function to check if coursework or course that a coursework belongs to is hidden + * + * @return bool + * @throws moodle_exception + */ + public function is_coursework_visible(){ + + $visible = true; + if ($this->get_course_module()->visible == 0 || $this->get_course()->visible == 0){ + $visible = false; + } + return $visible; + + } + + /** + * @param null $stage_index + */ + public function clear_stage($stage_index = null) { + if ($stage_index) { + if (isset($this->stages[$stage_index])) { + unset($this->stages[$stage_index]); + } + } else { + $this->stages = []; + } + } + + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * Fill pool to cache for later use + * + * @param $array + */ + public static function fill_pool($array) { + self::$pool = [ + 'id' => [] + ]; + + foreach ($array as $record) { + $object = new self($record); + self::$pool['id'][$record->id] = $object; + } + } + + /** + * + * @param $coursework_id + * @throws \dml_exception + */ + public static function fill_pool_coursework($coursework_id) { + global $DB; + if (empty(self::$pool['id'][$coursework_id])) { + $courseworks = $DB->get_records('coursework', ['id' => $coursework_id]); + self::fill_pool($courseworks); + } + } + + /** + * + * @param $coursework_id + * @return bool + */ + public static function get_object($coursework_id) { + if (!isset(self::$pool['id'][$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + return self::$pool['id'][$coursework_id] ?? false; + } + + /** + * + */ + public function fill_cache() { + global $DB; + $coursework_id = $this->id; + submission::fill_pool_coursework($coursework_id); + coursework::fill_pool([$this]); + course_module::fill_pool([$this->get_course_module()]); + module::fill_pool($DB->get_records('modules', ['name' => 'coursework'])); + feedback::fill_pool_submissions($coursework_id, array_keys(submission::$pool[$coursework_id]['id'])); + allocation::fill_pool_coursework($coursework_id); + plagiarism_flag::fill_pool_coursework($coursework_id); + assessment_set_membership::fill_pool_coursework($coursework_id); + if (\core_plugin_manager::instance()->get_plugin_info('local_uolw_sits_data_import')) { + user::fill_candidate_number_data($this); + } } } diff --git a/classes/models/deadline_extension.php b/classes/models/deadline_extension.php index e12e127..465d9af 100644 --- a/classes/models/deadline_extension.php +++ b/classes/models/deadline_extension.php @@ -34,11 +34,9 @@ class deadline_extension extends table_base { * @return bool */ public static function allocatable_extension_allows_submission($allocatable, $coursework) { - $params = array('allocatabletype' => $allocatable->type(), - 'allocatableid' => $allocatable->id(), - 'courseworkid' => $coursework->id, - ); - $extension = self::find($params); + self::fill_pool_coursework($coursework->id); + $extension = self::get_object($coursework->id, 'allocatableid-allocatabletype', [$allocatable->id(), $allocatable->type()]); + return !empty($extension) && $extension->extended_deadline > time(); } @@ -54,10 +52,9 @@ public static function get_extension_for_student($student, $coursework) { $allocatable = $student; } if ($allocatable) { - return static::find(array('courseworkid' => $coursework->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - )); + self::fill_pool_coursework($coursework->id); + $extension = self::get_object($coursework->id, 'allocatableid-allocatabletype', [$allocatable->id(), $allocatable->type()]); + return $extension; } } @@ -66,7 +63,7 @@ public static function get_extension_for_student($student, $coursework) { */ public function get_coursework() { if (!isset($this->coursework)) { - $this->coursework = coursework::find($this->courseworkid); + $this->coursework = coursework::get_object($this->courseworkid); } return $this->coursework; @@ -84,4 +81,61 @@ protected function pre_save_hook() { $this->createdbyid = $USER->id; } } + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * + * @param $coursework_id + * @return array + */ + protected static function get_cache_array($coursework_id) { + global $DB; + $records = $DB->get_records(static::$table_name, ['courseworkid' => $coursework_id]); + $result = [ + 'allocatableid-allocatabletype' => [] + ]; + if ($records) { + foreach ($records as $record) { + $object = new self($record); + $result['allocatableid-allocatabletype'][$record->allocatableid . '-' . $record->allocatabletype][] = $object; + } + } + return $result; + } + + /** + * + * @param $coursework_id + * @param $key + * @param $params + * @return bool + */ + public static function get_object($coursework_id, $key, $params) { + if (!isset(self::$pool[$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + $value_key = implode('-', $params); + return self::$pool[$coursework_id][$key][$value_key][0] ?? false; + } + + /** + * + */ + protected function post_save_hook() { + self::remove_cache($this->courseworkid); + } + + /** + * + */ + protected function after_destroy() { + self::remove_cache($this->courseworkid); + } + } \ No newline at end of file diff --git a/classes/models/feedback.php b/classes/models/feedback.php index 723bb4c..89d33b6 100644 --- a/classes/models/feedback.php +++ b/classes/models/feedback.php @@ -208,7 +208,8 @@ public function display_assessor_name(){ public function get_assesor_username() { if (!$this->firstname && !empty($this->lasteditedbyuser)) { - $this->assessor = core_user::get_user($this->lasteditedbyuser); + //$this->assessor = core_user::get_user($this->lasteditedbyuser); + $this->assessor = user::get_object($this->lasteditedbyuser); } return fullname($this->assessor); @@ -472,7 +473,12 @@ protected function get_submission_state() { public function get_submission() { if (!isset($this->submission) && !empty($this->submissionid)) { - $this->submission = submission::find($this->submissionid); + global $DB; + $coursework_id = $this->courseworkid ?? $DB->get_field(submission::$table_name, 'courseworkid', ['id' => $this->submissionid], MUST_EXIST); + if (!isset(submission::$pool[$coursework_id])) { + submission::fill_pool_coursework($coursework_id); + } + $this->submission = submission::$pool[$coursework_id]['id'][$this->submissionid] ?? false; } return $this->submission; @@ -546,7 +552,7 @@ public function is_allowed_to_show_comment() { * @return user */ public function assessor() { - return user::find($this->assessorid); + return user::get_object($this->assessorid); } /** @@ -566,5 +572,107 @@ public function get_allocatable() { public function is_assessor_anonymity_enabled(){ return $this->get_coursework()->assessoranonymity; } + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * + * @param $coursework_id + * @throws \dml_exception + */ + public static function fill_pool_coursework($coursework_id) { + global $DB; + if (isset(self::$pool[$coursework_id])) { + return; + } + if (submission::$pool[$coursework_id]) { + $submission_ids = submission::$pool[$coursework_id]['id']; + } else { + $submission_ids = $DB->get_records(submission::$table_name, ['courseworkid' => $coursework_id], '', 'id'); + } + self::fill_pool_submissions($coursework_id, array_keys($submission_ids)); + } + + /** + * @param $coursework_id + * @param $submission_ids + */ + public static function fill_pool_submissions($coursework_id, $submission_ids) { + global $DB; + + if (isset(self::$pool[$coursework_id])) { + return; + } + + $key = self::$table_name; + $cache = \cache::make('mod_coursework', 'courseworkdata', ['id' => $coursework_id]); + + $data = $cache->get($key); + if ($data === false) { + $data = [ + 'id' => [], + 'submissionid-stage_identifier' => [], + 'submissionid-stage_identifier_index' => [], + 'submissionid-finalised' => [], + 'submissionid-ismoderation-isfinalgrade-stage_identifier' => [], + 'submissionid-assessorid' => [], + 'submissionid' => [] + ]; + if ($submission_ids) { + list($submission_id_sql, $submission_id_params) = $DB->get_in_or_equal($submission_ids, SQL_PARAMS_NAMED); + $feedbacks = $DB->get_records_sql("SELECT * FROM {coursework_feedbacks} WHERE submissionid $submission_id_sql", $submission_id_params); + foreach ($feedbacks as $record) { + $object = new self($record); + $stage_identifier = $record->stage_identifier; + $stage_identifier_index = ($stage_identifier == 'final_agreed_1') ? $stage_identifier : 'others'; + $data['id'][$record->id] = $object; + $data['submissionid-stage_identifier'][$record->submissionid . '-' . $stage_identifier][] = $object; + $data['submissionid-stage_identifier_index'][$record->submissionid . '-' . $stage_identifier_index][] = $object; + $data['submissionid-finalised'][$record->submissionid . '-' . $record->finalised][] = $object; + $data['submissionid-ismoderation-isfinalgrade-stage_identifier'][$record->submissionid . '-' . $record->ismoderation . '-' . $record->isfinalgrade . '-' . $stage_identifier][] = $object; + $data['submissionid-assessorid'][$record->submissionid . '-' . $record->assessorid][] = $object; + $data['submissionid'][$record->submissionid][] = $object; + } + } + $cache->set($key, $data); + } + self::$pool[$coursework_id] = $data; + } + + /** + * + * @param $coursework_id + * @param $key + * @param $params + * @return bool + */ + public static function get_object($coursework_id, $key, $params) { + if (!isset(self::$pool[$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + $value_key = implode('-', $params); + return self::$pool[$coursework_id][$key][$value_key][0] ?? false; + } + + /** + * + */ + protected function post_save_hook() { + $courseworkid = $this->get_submission()->courseworkid; + self::remove_cache($courseworkid); + } + + /** + * + */ + protected function after_destroy() { + $courseworkid = $this->get_submission()->courseworkid; + self::remove_cache($courseworkid); + } } diff --git a/classes/models/group.php b/classes/models/group.php index 745936d..e75fc93 100644 --- a/classes/models/group.php +++ b/classes/models/group.php @@ -91,4 +91,36 @@ public function profile_link($with_picture = false) { public function is_valid_for_course($course) { return $this->courseid == $course->id; } + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * Fill pool to cache for later use + * + * @param $array + */ + public static function fill_pool($array) { + foreach ($array as $record) { + $object = new self($record); + self::$pool['id'][$record->id] = $object; + } + } + + /** + * @param $id + * @return mixed + */ + public static function get_object($id) { + if (!isset(self::$pool['id'][$id])) { + global $DB; + $user = $DB->get_record(self::$table_name, ['id' => $id]); + self::$pool['id'][$id] = new self($user); + } + return self::$pool['id'][$id]; + } } \ No newline at end of file diff --git a/classes/models/moderation.php b/classes/models/moderation.php index 1645c3b..2a9debb 100644 --- a/classes/models/moderation.php +++ b/classes/models/moderation.php @@ -125,7 +125,7 @@ public function get_submission() { * @return user */ public function moderator() { - return user::find($this->moderatorid); + return user::get_object($this->moderatorid); } /** diff --git a/classes/models/module.php b/classes/models/module.php new file mode 100644 index 0000000..57739a7 --- /dev/null +++ b/classes/models/module.php @@ -0,0 +1,61 @@ +. + +namespace mod_coursework\models; + +use mod_coursework\framework\table_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Represents a row in the modules table. + */ +class module extends table_base { + + /** + * @var string + */ + protected static $table_name = 'modules'; + + /** + * @var int + */ + public $id; + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * Fill pool to cache for later use + * + * @param $array + */ + public static function fill_pool($array) { + self::$pool = [ + 'id' => [], + 'name' => [] + ]; + foreach ($array as $record) { + $object = new self($record); + self::$pool['id'][$record->id] = $object; + self::$pool['name'][$record->name] = $object; + } + } +} diff --git a/classes/models/personal_deadline.php b/classes/models/personal_deadline.php index 120bd3f..c8349b1 100644 --- a/classes/models/personal_deadline.php +++ b/classes/models/personal_deadline.php @@ -31,7 +31,8 @@ class personal_deadline extends table_base { */ public function get_coursework() { if (!isset($this->coursework)) { - $this->coursework = coursework::find($this->courseworkid); + coursework::fill_pool_coursework($this->courseworkid); + $this->coursework = coursework::get_object($this->courseworkid); } return $this->coursework; @@ -75,4 +76,61 @@ public static function get_personal_deadline_for_student($student, $coursework) } } + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * + * @param $coursework_id + * @return array + */ + protected static function get_cache_array($coursework_id) { + global $DB; + $records = $DB->get_records(static::$table_name, ['courseworkid' => $coursework_id]); + $result = [ + 'allocatableid-allocatabletype' => [] + ]; + if ($records) { + foreach ($records as $record) { + $object = new self($record); + $result['allocatableid-allocatabletype'][$record->allocatableid . '-' . $record->allocatabletype][] = $object; + } + } + return $result; + } + + /** + * + * @param $coursework_id + * @param $key + * @param $params + * @return bool + */ + public static function get_object($coursework_id, $key, $params) { + if (!isset(self::$pool[$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + $value_key = implode('-', $params); + return self::$pool[$coursework_id][$key][$value_key][0] ?? false; + } + + /** + * + */ + protected function post_save_hook() { + self::remove_cache($this->courseworkid); + } + + /** + * + */ + protected function after_destroy() { + self::remove_cache($this->courseworkid); + } + } \ No newline at end of file diff --git a/classes/models/plagiarism_flag.php b/classes/models/plagiarism_flag.php index 0d3a4ca..d5e5bf1 100644 --- a/classes/models/plagiarism_flag.php +++ b/classes/models/plagiarism_flag.php @@ -39,7 +39,8 @@ class plagiarism_flag extends table_base { */ public function get_coursework() { if (!isset($this->coursework)) { - $this->coursework = coursework::find($this->courseworkid); + coursework::fill_pool_coursework($this->courseworkid); + $this->coursework = coursework::get_object($this->courseworkid); } return $this->coursework; @@ -52,7 +53,9 @@ public function get_coursework() { */ public function get_submission() { if (!isset($this->submission) && !empty($this->submissionid)) { - $this->submission = submission::find($this->submissionid); + submission::fill_pool_coursework($this->courseworkid); + $this->submission = isset(submission::$pool[$this->courseworkid]['id'][$this->submissionid]) ? + submission::$pool[$this->courseworkid]['id'][$this->submissionid] : null; } return $this->submission; @@ -62,8 +65,10 @@ public function get_submission() { * @param $submission * @return static */ - public static function get_plagiarism_flag($submission){ - return static::find(array('submissionid' => $submission->id)); + public static function get_plagiarism_flag($submission) { + self::fill_pool_coursework($submission->courseworkid); + $result = self::get_object($submission->courseworkid, 'submissionid', [$submission->id]); + return $result; } @@ -86,5 +91,61 @@ public function can_release_grades(){ } } + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * + * @param $coursework_id + * @return array + */ + protected static function get_cache_array($coursework_id) { + global $DB; + $records = $DB->get_records(self::$table_name, ['courseworkid' => $coursework_id]); + $result = [ + 'submissionid' => [] + ]; + if ($records) { + foreach ($records as $record) { + $object = new self($record); + $result['submissionid'][$record->submissionid][] = $object; + } + } + return $result; + } + + /** + * + * @param $coursework_id + * @param $key + * @param $params + * @return bool + */ + public static function get_object($coursework_id, $key, $params) { + if (!isset(self::$pool[$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + $value_key = implode('-', $params); + return self::$pool[$coursework_id][$key][$value_key][0] ?? false; + } + + /** + * + */ + protected function post_save_hook() { + self::remove_cache($this->courseworkid); + } + + /** + * + */ + protected function after_destroy() { + self::remove_cache($this->courseworkid); + } + } \ No newline at end of file diff --git a/classes/models/submission.php b/classes/models/submission.php index 2829d47..65785f4 100644 --- a/classes/models/submission.php +++ b/classes/models/submission.php @@ -50,7 +50,7 @@ class submission extends table_base implements \renderable { /** * @var string */ - protected static $table_name = 'coursework_submissions'; + public static $table_name = 'coursework_submissions'; /** * @var int @@ -221,7 +221,7 @@ public function __construct($db_record = null) { // Get the real first and last name from the user table. We use fullname($this), which needs it, // so we can't lazy-load. $user = $DB->get_record('user', array('id' => $this->userid)); - $allnames = get_all_user_name_fields(); + $allnames = \core_user\fields::get_name_fields(); foreach ($allnames as $namefield) { $this->$namefield = $user->$namefield; } @@ -286,7 +286,7 @@ public function has_feedback() { */ public function has_disability() { global $DB; - $user = $DB->get_record('user', array('id' => $this->userid)); + $user = user::get_object($this->userid); return (!empty($user)) ? has_disability($user->username) : false; } @@ -428,16 +428,12 @@ public function get_context() { * @return feedback[] array of raw db records */ public function get_feedbacks() { - - global $DB; - if (!is_array($this->feedbacks)) { - $conditions = array('submissionid' => $this->id); // Sort here is on ID so that if there's any need to get the first one chronologically, we can use reset(). - $this->feedbacks = $DB->get_records('coursework_feedbacks', $conditions, 'id'); - foreach ($this->feedbacks as &$feedback) { - $feedback = new feedback($feedback, $this); - } + + feedback::fill_pool_coursework($this->courseworkid); + $this->feedbacks = isset(feedback::$pool[$this->courseworkid]['submissionid'][$this->id]) ? + feedback::$pool[$this->courseworkid]['submissionid'][$this->id] : []; } return $this->feedbacks; @@ -459,40 +455,18 @@ public function get_first_feedback() { * @return feedback[] */ public function get_assessor_feedbacks() { - - global $DB; - if (!$this->id) { // No submission - empty placeholder. return array(); } - $params = array( - 'submissionid' => $this->id, - 'ismoderation' => 0, - 'isfinalgrade' => 0, - 'stageidentifier' => 'final_agreed_1' - ); - - - $sql = "SELECT * FROM {coursework_feedbacks} - WHERE submissionid = :submissionid - AND ismoderation = :ismoderation - AND isfinalgrade = :isfinalgrade - AND stage_identifier <> :stageidentifier - ORDER BY stage_identifier"; - - $feedbacks = $DB->get_records_sql($sql, $params); - - if (!$feedbacks) { - $feedbacks = array(); // In case of loops, we want to avoid returning false. - } - - foreach ($feedbacks as &$feedback) { - $feedback = new feedback($feedback, $this); + if (!isset(feedback::$pool[$this->courseworkid]['submissionid-stage_identifier_index'])) { + feedback::fill_pool_coursework($this->courseworkid); } - - return $feedbacks; + // Get all other feedbacks whose stage_identifier is not "final_agreed_1" + // In case of loops, we would like empty array instead of false. + $res = feedback::$pool[$this->courseworkid]['submissionid-stage_identifier_index']["$this->id-others"] ?? []; + return $res; } /** @@ -502,26 +476,15 @@ public function get_assessor_feedbacks() { * @throws \dml_multiple_records_exception */ public function get_assessor_feedback_by_stage($stage_identifier) { - - global $DB; - - $params = array( + $params = [ 'submissionid' => $this->id, 'ismoderation' => 0, 'isfinalgrade' => 0, - 'stageidentifier' => $stage_identifier - ); - - - $sql = "SELECT * FROM {coursework_feedbacks} - WHERE submissionid = :submissionid - AND ismoderation = :ismoderation - AND isfinalgrade = :isfinalgrade - AND stage_identifier = :stageidentifier"; - - $feedback = $DB->get_record_sql($sql, $params); + 'stage_identifier' => $stage_identifier + ]; + feedback::fill_pool_coursework($this->courseworkid); + $feedback = feedback::get_object($this->courseworkid, 'submissionid-ismoderation-isfinalgrade-stage_identifier', $params); return $feedback; - } /** @@ -532,23 +495,12 @@ public function get_assessor_feedback_by_stage($stage_identifier) { */ public function get_assessor_allocation_by_stage($stage_identifier) { - global $DB; - - $params = array( - 'courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $this->get_allocatable()->id(), - 'allocatabletype' => $this->get_allocatable()->type(), - 'stageidentifier' => $stage_identifier - ); - - - $sql = "SELECT * FROM {coursework_allocation_pairs} - WHERE courseworkid = :courseworkid - AND allocatableid = :allocatableid - AND allocatabletype = :allocatabletype - AND stage_identifier = :stageidentifier"; - - $allocation = $DB->get_record_sql($sql, $params); + $courseworkid = $this->get_coursework()->id; + allocation::fill_pool_coursework($courseworkid); + $allocation = allocation::get_object( + $courseworkid, + 'allocatableid-allocatabletype-stage_identifier', + [$this->get_allocatable()->id(), $this->get_allocatable()->type(), $stage_identifier]); return $allocation; } @@ -567,26 +519,14 @@ public function get_agreed_grade(){ return array(); } - $params = array( + $params = [ 'submissionid' => $this->id, 'ismoderation' => 0, 'isfinalgrade' => 0, - 'stageidentifier' => 'final_agreed_1' - ); - - - $sql = "SELECT * FROM {coursework_feedbacks} - WHERE submissionid = :submissionid - AND ismoderation = :ismoderation - AND isfinalgrade = :isfinalgrade - AND stage_identifier = :stageidentifier"; - - $feedback = $DB->get_record_sql($sql, $params); - if ($feedback){ - $feedback = new feedback($feedback, $this); - } else { - $feedback = false; - } + 'stage_identifier' => 'final_agreed_1' + ]; + feedback::fill_pool_coursework($this->courseworkid); + $feedback = feedback::get_object($this->courseworkid, 'submissionid-ismoderation-isfinalgrade-stage_identifier', $params); return $feedback; } @@ -741,7 +681,7 @@ public function get_allocatable_name($as_link = false) { * @return mixed */ public function get_last_updated_by_user() { - return user::find($this->lastupdatedby); + return user::get_object($this->lastupdatedby); } /** @@ -772,7 +712,10 @@ public function is_published() { public function get_coursework() { if (empty($this->coursework)) { - $this->coursework = coursework::find($this->courseworkid); + if (!isset(coursework::$pool['id'][$this->courseworkid])) { + coursework::fill_pool_coursework($this->courseworkid); + } + $this->coursework = coursework::$pool['id'][$this->courseworkid]; if (!$this->coursework) { throw new \coding_exception('Could not find the coursework for submission id '. $this->id); } @@ -804,12 +747,16 @@ public function user_has_submitted_feedback($userid = 0) { $userid = $USER->id; } - $params = array('submissionid' => $this->id, - 'assessorid' => $userid, - 'isfinalgrade' => 0, - 'ismoderation' => 0); - return $DB->record_exists('coursework_feedbacks', $params); - + $params = [ + 'submissionid' => $this->id, + 'assessorid' => $userid + ]; + feedback::fill_pool_coursework($this->courseworkid); + $feedback = feedback::get_object($this->courseworkid, 'submissionid-assessorid', $params); + if ($feedback && $feedback->isfinalgrade == 0 && $feedback->ismoderation == 0) { + return true; + } + return false; } @@ -967,6 +914,10 @@ public function all_inital_graded() { return $this->get_state() >= submission::FULLY_GRADED; } + public function is_finalised() { + return $this->get_state() == submission::FINALISED; + } + /** * @return bool @@ -983,7 +934,7 @@ public function get_allocatable() { * @var table_base $classname */ $classname = "\\mod_coursework\\models\\".$this->allocatabletype; - return $classname::find($this->allocatableid); + return $classname::get_object($this->allocatableid); } /** @@ -1013,7 +964,8 @@ public function ready_to_publish() { if ($this->get_coursework()->plagiarism_flagging_enbled()) { // check if not stopped by plagiarism flag - $plagiarism = plagiarism_flag::find(array('submissionid' => $this->id)); + plagiarism_flag::fill_pool_coursework($this->courseworkid); + $plagiarism = plagiarism_flag::get_object($this->courseworkid, 'submissionid', [$this->id]); if ($plagiarism && !$plagiarism->can_release_grades()) { return false; } @@ -1091,7 +1043,7 @@ private function get_grades_to_update() { * @return user|false */ public function get_last_submitter() { - return user::find($this->lastupdatedby); + return user::get_object($this->lastupdatedby); } /** @@ -1121,7 +1073,9 @@ public function save_files($files_id) { $this->id, $this->coursework->get_file_options()); - $this->rename_files(); + if (!empty($this->coursework->renamefiles)) { + $this->rename_files(); + } } /** @@ -1193,7 +1147,7 @@ public function time_submitted() { public function sampled_feedback_exists(){ global $DB; - return $DB->record_exists('coursework_sample_set_mbrs', array('courseworkid' => $this->coursework->id, + return $DB->record_exists('coursework_sample_set_mbrs', array('courseworkid' => $this->courseworkid, 'allocatableid' => $this->get_allocatable()->id(), 'allocatabletype' => $this->get_allocatable()->type())); @@ -1269,12 +1223,11 @@ private function is_submission_on_behalf(){ * @throws \coding_exception */ - public function get_submissions_in_sample(){ - global $DB; - return $DB->get_records('coursework_sample_set_mbrs', array('courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $this->allocatableid, - 'allocatabletype' => $this->allocatabletype)); - + public function get_submissions_in_sample() { + assessment_set_membership::fill_pool_coursework($this->courseworkid); + $records = isset(assessment_set_membership::$pool[$this->courseworkid]['allocatableid-allocatabletype'][$allocatable->id . '-' . $allocatable->type()]) ? + assessment_set_membership::$pool[$this->courseworkid]['allocatableid-allocatabletype'][$allocatable->id . '-' . $allocatable->type()] : []; + return $records; } @@ -1284,14 +1237,14 @@ public function get_submissions_in_sample(){ * @throws \coding_exception */ - public function get_submissions_in_sample_by_stage($stage_identifier){ - global $DB; - return $DB->get_record('coursework_sample_set_mbrs', - array('courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $this->allocatableid, - 'allocatabletype' => $this->allocatabletype, - 'stage_identifier'=> $stage_identifier)); - + public function get_submissions_in_sample_by_stage($stage_identifier) { + assessment_set_membership::fill_pool_coursework($this->courseworkid); + $record = assessment_set_membership::get_object( + $this->courseworkid, + 'allocatableid-allocatabletype-stage_identifier', + [$this->allocatableid, $this->allocatabletype, $stage_identifier] + ); + return $record; } @@ -1301,12 +1254,10 @@ public function get_submissions_in_sample_by_stage($stage_identifier){ * @return bool * @throws \coding_exception */ - public function has_extension(){ - global $DB; - return $DB->record_exists('coursework_extensions', array('courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $this->get_allocatable()->id(), - 'allocatabletype'=> $this->get_allocatable()->type())); - + public function has_extension() { + deadline_extension::fill_pool_coursework($this->courseworkid); + $extension = deadline_extension::get_object($this->courseworkid, 'allocatableid-allocatabletype', [$this->allocatableid, $this->allocatabletype]); + return !empty($extension); } /** @@ -1315,14 +1266,12 @@ public function has_extension(){ * @return mixed * @throws \coding_exception */ - public function submission_extension(){ - global $DB; - return $DB->get_record('coursework_extensions', array('courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $this->get_allocatable()->id(), - 'allocatabletype'=> $this->get_allocatable()->type())); - + public function submission_extension() { + deadline_extension::fill_pool_coursework($this->courseworkid); + $extension = deadline_extension::get_object($this->courseworkid, 'allocatableid-allocatabletype', [$this->allocatableid, $this->allocatabletype]); + return $extension; } - + /** * Retrieve details of submission's personal deadline, if not given, use corsework default * @@ -1330,18 +1279,17 @@ public function submission_extension(){ * @throws \coding_exception */ public function submission_personal_deadline(){ - global $DB; - $personal_deadline = $DB->get_record('coursework_person_deadlines', array('courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $this->get_allocatable()->id(), - 'allocatabletype'=> $this->get_allocatable()->type())); + $allocatableid = $this->get_allocatable()->id(); + $allocatabletype = $this->get_allocatable()->type(); + $personal_deadline = personal_deadline::get_object($this->courseworkid, 'allocatableid-allocatabletype', [$allocatableid, $allocatabletype]); if ($personal_deadline){ - $personal_deadline = $personal_deadline->personal_deadline; + $personal_deadline = $personal_deadline->personal_deadline; } else { $personal_deadline = $this->get_coursework()->deadline; } - return $personal_deadline; + return $personal_deadline; } @@ -1363,25 +1311,22 @@ public function submitted_within_extension(){ public function extension_deadline(){ return $this->submission_extension()->extended_deadline; } - + /** * Has assessor graded in any of the initial stages? */ - public function is_assessor_initial_grader(){ - global $DB, $USER; - - - $params = array('submissionid' => $this->id, - 'assessorid' => $USER->id, - 'stage_identifier' => 'final_agreed_1'); - - $sql = "SELECT id - FROM {coursework_feedbacks} - WHERE submissionid = :submissionid - AND assessorid = :assessorid - AND stage_identifier <> :stage_identifier"; + public function is_assessor_initial_grader() { + global $USER; - return $DB->record_exists_sql($sql, $params); + feedback::fill_pool_coursework($this->courseworkid); + $feedbacks = isset(feedback::$pool[$this->courseworkid]['submissionid-assessorid'][$this->id . '-' . $USER->id]) ? + feedback::$pool[$this->courseworkid]['submissionid-assessorid'][$this->id . '-' . $USER->id] : []; + foreach ($feedbacks as $feedback) { + if ($feedback->stage_identifier != 'final_agreed_1') { + return true; + } + } + return false; } @@ -1394,41 +1339,26 @@ public function is_assessor_initial_grader(){ */ public function editable_feedbacks_exist() { - global $DB; - - $this->get_coursework()->get_grade_editing_time(); - $params = array('submissionid'=>$this->id); - $extrasql = ""; - - - if($this->get_coursework()->get_grade_editing_time() != 0){ - $extrasql = " AND (( cf.timecreated + c.gradeeditingtime > :time - AND cf.finalised = 1) OR cf.finalised = 0)"; - $params['time'] = time(); - } else{ - $extrasql = " AND cf.finalised = 0"; + $editablefeedbacks = []; + $coursework = $this->get_coursework(); + if ($coursework->numberofmarkers > 1 && $this->finalised = 1) { + $this->get_coursework()->get_grade_editing_time(); + $gradeeditingtime = $coursework->get_grade_editing_time(); + + $editablefeedbacks = isset(feedback::$pool[$coursework->id]['submissionid-finalised'][$this->id . '-0']) ? + feedback::$pool[$coursework->id]['submissionid-finalised'][$this->id . '-0'] : []; + if ($gradeeditingtime != 0) { + $time = time(); + $finalized_feedbacks = isset(feedback::$pool[$coursework->id]['submissionid-finalised'][$this->id . '-1']) ? + feedback::$pool[$coursework->id]['submissionid-finalised'][$this->id . '-1']: []; + foreach ($finalized_feedbacks as $feedback) { + if ($feedback->timecreated + $gradeeditingtime > $time) { + $editablefeedbacks[] = $feedback; + } + } + } } - - $sql = " - SELECT * - FROM {coursework} c, - {coursework_submissions} cs, - {coursework_feedbacks} cf - WHERE c.id = cs.courseworkid - AND cs.id = cf.submissionid - AND c.numberofmarkers > 1 - AND cs.finalised = 1 - AND cf.stage_identifier NOT LIKE 'final_agreed%' - AND cs.id = :submissionid - - - $extrasql - - "; - - $editablefeedbacks = $DB->get_records_sql($sql, $params); - return (empty($editablefeedbacks)) ? false : $editablefeedbacks; } @@ -1438,44 +1368,25 @@ public function editable_feedbacks_exist() { * */ public function editable_final_feedback_exist() { - - global $DB; - - $this->get_coursework()->get_grade_editing_time(); - $params = array('submissionid'=>$this->id); - $extrasql = ""; - - - if($this->get_coursework()->get_grade_editing_time() != 0){ - $extrasql = " AND (( cf.timecreated + c.gradeeditingtime > :time - AND cf.finalised = 1) OR cf.finalised = 0)"; - $params['time'] = time(); - } else { - $extrasql = " AND cf.finalised = 0"; - } - if (($this->get_coursework()->has_multiple_markers() && !$this->get_coursework()->sampling_enabled()) - || ($this->get_coursework()->sampling_enabled() && $this->sampled_feedback_exists())){ - $extrasql .= " AND cf.stage_identifier LIKE 'final_agreed%'"; + if (!isset($this->editable_final_feedback)) { + $this->editable_final_feedback = false; + if ($this->finalised == 1) { + + $coursework = $this->get_coursework(); + $final_feedback = feedback::get_object($coursework->id, 'submissionid-stage_identifier', [$this->id, 'final_agreed_1']); + if ($final_feedback) { + $grade_editing_time = $coursework->get_grade_editing_time(); + if ($grade_editing_time) { + if ($final_feedback->timecreated + $grade_editing_time > time()) { + $this->editable_final_feedback = true; + } + } elseif ($final_feedback->finalised == 0) { + $this->editable_final_feedback = true; + } + } + } } - - - $sql = " - SELECT * - FROM {coursework} c, - {coursework_submissions} cs, - {coursework_feedbacks} cf - WHERE c.id = cs.courseworkid - AND cs.id = cf.submissionid - AND cs.finalised = 1 - AND cs.id = :submissionid - - $extrasql - - "; - - $editablefinalfeedback = $DB->get_record_sql($sql, $params); - - return (empty($editablefinalfeedback)) ? false : $editablefinalfeedback; + return $this->editable_final_feedback; } /** @@ -1539,13 +1450,8 @@ function any_editable_feedback_exists(){ * @return bool */ function has_valid_extension(){ - global $DB; - - $valid_extension = false; - $extension = $DB->get_record('coursework_extensions', - array('courseworkid' => $this->courseworkid, - 'allocatableid' => $this->allocatableid, - 'allocatabletype' => $this->allocatabletype)); + deadline_extension::fill_pool_coursework($this->courseworkid); + $extension = deadline_extension::get_object($this->courseworkid, 'allocatableid-allocatabletype', [$this->allocatableid, $this->allocatabletype]); if ($extension) { if ($extension->extended_deadline > time()) { @@ -1554,4 +1460,90 @@ function has_valid_extension(){ } return $valid_extension; } + + + function can_be_unfinalised() { + return ($this->get_state() == submission::FINALISED); + } + + /** + * check if the feedback by provided assessor exists + * + * @param $assessorid + * @return bool|false|mixed|stdClass + * @throws \dml_exception + */ + function has_specific_assessor_feedback($assessorid){ + global $DB; + + $feedback = $DB->get_record('coursework_feedbacks', array('submissionid'=>$this->id, + 'assessorid'=>$assessorid)); + + return (empty($feedback)) ? false : $feedback; + } + + //caching + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * + * @param $coursework_id + * @return array + */ + protected static function get_cache_array($coursework_id) { + global $DB; + $records = $DB->get_records(static::$table_name, ['courseworkid' => $coursework_id]); + $result = [ + 'id' => [], + 'allocatableid' => [], + 'finalised' => [], + 'allocatableid-allocatabletype' => [] + ]; + if ($records) { + foreach ($records as $record) { + $object = new self($record); + $result['id'][$record->id] = $object; + $result['allocatableid'][$record->allocatableid][] = $object; + $result['finalised'][$record->finalised][] = $object; + $result['allocatableid-allocatabletype'][$record->allocatableid . '-' . $record->allocatabletype][] = $object; + } + } + return $result; + } + + /** + * + * @param $coursework_id + * @param $key + * @param $params + * @return bool + */ + public static function get_object($coursework_id, $key, $params) { + if (!isset(self::$pool[$coursework_id])) { + self::fill_pool_coursework($coursework_id); + } + $value_key = implode('-', $params); + return self::$pool[$coursework_id][$key][$value_key][0] ?? false; + } + + /** + * + */ + protected function post_save_hook() { + self::remove_cache($this->courseworkid); + } + + /** + * + */ + protected function after_destroy() { + self::remove_cache($this->courseworkid); + } + } diff --git a/classes/models/user.php b/classes/models/user.php index adb2582..62705e6 100644 --- a/classes/models/user.php +++ b/classes/models/user.php @@ -30,7 +30,7 @@ class user extends table_base implements allocatable, moderatable { * @param bool $data */ public function __construct($data = false) { - $allnames = get_all_user_name_fields(); + $allnames = \core_user\fields::get_name_fields(); foreach($allnames as $namefield) { $this->$namefield = ''; } @@ -112,4 +112,83 @@ public function __toString() { } + /** + * cache array + * + * @var + */ + //public static $pool; + + /** + * + * @param $coursework_id + * @throws \dml_exception + */ + /* + public static function fill_pool_coursework($coursework_id) { + if (isset(static::$pool[$coursework_id])) { + return; + } + $key = static::$table_name; + $cache = \cache::make('mod_coursework', 'courseworkdata', ['id' => $coursework_id]); + + $data = $cache->get($key); + if ($data === false) { + // no cache found + $data = static::get_cache_array($coursework_id); + $cache->set($key, $data); + } + + static::$pool[$coursework_id] = $data; + } + */ + + /** + * @param $coursework_id + */ + /* + public static function remove_cache($coursework_id) { + global $SESSION; + if (!empty($SESSION->keep_cache_data)) { + return; + } + static::$pool[$coursework_id] = null; + $cache = \cache::make('mod_coursework', 'courseworkdata', ['id' => $coursework_id]); + $cache->delete(static::$table_name); + } + */ + + /** + * cache array + * + * @var + */ + public static $pool; + + /** + * Fill pool to cache for later use + * + * @param $array + */ + public static function fill_pool($array) { + foreach ($array as $record) { + $object = new self($record); + self::$pool['id'][$record->id] = $object; + } + } + + /** + * @param $id + * @return mixed + */ + public static function get_object($id) { + if (!isset(self::$pool['id'][$id])) { + global $DB; + $user = $DB->get_record(self::$table_name, ['id' => $id]); + self::$pool['id'][$id] = new self($user); + } + return self::$pool['id'][$id]; + } + + } \ No newline at end of file diff --git a/classes/observer.php b/classes/observer.php index a4287fc..66cecc7 100644 --- a/classes/observer.php +++ b/classes/observer.php @@ -77,4 +77,23 @@ public static function process_allocations_when_group_member_added(\core\event\g public static function process_allocations_when_group_member_removed(\core\event\group_member_removed $event){ course_group_member_removed($event); } + + /** + * @param \core\event\role_assigned $event + */ + public static function add_teacher_to_dropdown_when_enrolled(core\event\role_assigned$event){ + teacher_allocation_cache_purge($event); + + } + + /** + * @param \core\event\role_unassigned $event + */ + public static function remove_teacher_from_dropdown_when_unenrolled(core\event\role_unassigned$event){ + teacher_removed_allocated_not_graded($event); + teacher_allocation_cache_purge($event); + + } + + } \ No newline at end of file diff --git a/classes/personal_deadline/table/row/builder.php b/classes/personal_deadline/table/row/builder.php index 7e0488d..0f9f3a8 100644 --- a/classes/personal_deadline/table/row/builder.php +++ b/classes/personal_deadline/table/row/builder.php @@ -142,6 +142,37 @@ public function get_student_lastname() { return $this->get_allocatable()->lastname; } + /** + * @return string + */ + public function get_idnumber() { + + global $DB; + + $allocatable = $this->get_allocatable(); + if (empty($allocatable->idnumber)) { + $this->allocatable = user::find($allocatable); + } + + return $this->get_allocatable()->idnumber; + } + + + /** + * @return string + */ + public function get_email() { + + global $DB; + + $allocatable = $this->get_allocatable(); + if (empty($allocatable->email)) { + $this->allocatable = user::find($allocatable); + } + + return $this->get_allocatable()->email; + } + /** * Getter for personal deadline time * @@ -168,6 +199,28 @@ public function get_personal_deadlines() { return $personal_deadline; } + + + public function get_submission_status() { + global $DB; + + $submission_db = $DB->get_record('coursework_submissions', + array('courseworkid' => $this->get_coursework()->id, + 'allocatableid' => $this->allocatable->id(), + 'allocatabletype'=> $this->allocatable->type())); + + $submission = \mod_coursework\models\submission::find($submission_db); + + $statustext = get_string('statusnotsubmitted','mod_coursework'); + + if (!empty($submission) && $submission->is_finalised()) { + $statustext = get_string('finalisedsubmission','mod_coursework'); + } else if (!empty($submission)) { + $statustext = $submission->get_status_text(); + } + + return $statustext; + } } diff --git a/classes/plagiarism_helpers/turnitin.php b/classes/plagiarism_helpers/turnitin.php index e206670..ca03191 100644 --- a/classes/plagiarism_helpers/turnitin.php +++ b/classes/plagiarism_helpers/turnitin.php @@ -25,8 +25,8 @@ public function enabled() { global $DB, $CFG; if ($CFG->enableplagiarism) { - $plagiarismsettings = (array)get_config('plagiarism'); - if (!empty($plagiarismsettings['turnitin_use'])) { + $plagiarismsettings = (array)get_config('plagiarism_turnitin'); + if (!empty($plagiarismsettings['enabled'])) { $params = array( 'cm' => $this->get_coursework()->get_course_module()->id, 'name' => 'use_turnitin', diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 20a7837..96537ec 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -13,11 +13,8 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . - namespace mod_coursework\privacy; - defined('MOODLE_INTERNAL') || die(); - use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\writer; @@ -31,13 +28,12 @@ * * @package mod_coursework * @category privacy - * @copyright 2019 Linh Truong Hong (linh.hong@cosector.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements - \core_privacy\local\metadata\provider, - \core_privacy\local\request\plugin\provider, - \core_privacy\local\request\core_userlist_provider { + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider, + \core_privacy\local\request\core_userlist_provider { /** * Provides meta data that is stored about a user with mod_coursework * @@ -89,35 +85,29 @@ public static function get_metadata(collection $collection) : collection { 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'timemodified' ]; - $collection->add_database_table('coursework_feedbacks', $feedbacks, 'privacy:metadata:feedbacks'); $collection->add_database_table('coursework_submissions', $submissions, 'privacy:metadata:submissions'); $collection->add_database_table('coursework_extensions', $extensions, 'privacy:metadata:extensions'); $collection->add_database_table('coursework_person_deadlines', $persondeadlines, 'privacy:metadata:persondeadlines'); $collection->add_database_table('coursework_mod_agreements', $modagreements, 'privacy:metadata:modagreements'); $collection->add_database_table('coursework_plagiarism_flags', $plagiarismflags, 'privacy:metadata:plagiarismflags'); - return $collection; } - /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { - $context = $userlist->get_context(); if ($context->contextlevel != CONTEXT_MODULE) { return; } - $params = [ 'modulename' => 'coursework', 'contextid' => $context->id, 'contextlevel' => CONTEXT_MODULE ]; - $sql = "SELECT cwf.assessorid FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid @@ -127,7 +117,6 @@ public static function get_users_in_context(userlist $userlist) { JOIN {coursework_feedbacks} cwf ON cws.id = cwf.submissionid WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('assessorid', $sql, $params); - $sql = "SELECT cws.userid, cws.authorid FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid @@ -137,7 +126,6 @@ public static function get_users_in_context(userlist $userlist) { WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); $userlist->add_from_sql('authorid', $sql, $params); - $sql = "SELECT cwe.allocatableid, cwe.allocatableuser, cwe.allocatablegroup FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid @@ -148,7 +136,6 @@ public static function get_users_in_context(userlist $userlist) { $userlist->add_from_sql('allocatableid', $sql, $params); $userlist->add_from_sql('allocatableuser', $sql, $params); $userlist->add_from_sql('allocatablegroup', $sql, $params); - $sql = "SELECT cwpd.allocatableid, cwpd.allocatableuser, cwpd.allocatablegroup FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid @@ -159,7 +146,6 @@ public static function get_users_in_context(userlist $userlist) { $userlist->add_from_sql('allocatableid', $sql, $params); $userlist->add_from_sql('allocatableuser', $sql, $params); $userlist->add_from_sql('allocatablegroup', $sql, $params); - $sql = "SELECT cwma.moderatorid FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid @@ -183,7 +169,6 @@ public static function get_users_in_context(userlist $userlist) { } - /** * Returns all of the contexts that has information relating to the userid. * @@ -203,17 +188,14 @@ public static function get_contexts_for_userid(int $userid) : contextlist { 'moderatorid' => $userid, 'createdby' => $userid ]; - $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {coursework} cw ON cm.instance = cw.id JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {coursework_submissions} cws ON cw.id = cws.courseworkid AND cws.authorid = :authorid"; - $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); - $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename @@ -222,7 +204,6 @@ public static function get_contexts_for_userid(int $userid) : contextlist { JOIN {coursework_submissions} cws ON cw.id = cws.courseworkid JOIN {coursework_feedbacks} cwf ON cws.id = cwf.submissionid AND cwf.assessorid = :assessorid"; $contextlist->add_from_sql($sql, $params); - $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename @@ -230,7 +211,6 @@ public static function get_contexts_for_userid(int $userid) : contextlist { JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {coursework_extensions} cwx ON cwx.courseworkid = cw.id AND (cwx.allocatableid = :userid OR cwx.allocatableuser = :allocatableuser OR cwx.allocatablegroup = :allocatablegroup)"; $contextlist->add_from_sql($sql, $params); - $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename @@ -238,7 +218,6 @@ public static function get_contexts_for_userid(int $userid) : contextlist { JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {coursework_person_deadlines} cw_pd ON cw_pd.courseworkid = cw.id AND (cw_pd.allocatableid = :userid OR cw_pd.allocatableuser = :allocatableuser OR cw_pd.allocatablegroup = :allocatablegroup)"; $contextlist->add_from_sql($sql, $params); - $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename @@ -248,7 +227,6 @@ public static function get_contexts_for_userid(int $userid) : contextlist { JOIN {coursework_feedbacks} cwf ON cws.id = cwf.submissionid JOIN {coursework_mod_agreements} cw_ag ON cw_ag.feedbackid = cwf.id AND cw_ag.moderatorid = :moderatorid"; $contextlist->add_from_sql($sql, $params); - $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename @@ -261,194 +239,145 @@ public static function get_contexts_for_userid(int $userid) : contextlist { return $contextlist; } - /** * Write out the user data filtered by contexts. * * @param approved_contextlist $contextlist contexts that we are writing data out from. */ public static function export_user_data(approved_contextlist $contextlist) { - foreach ($contextlist->get_contexts() as $context) { // Check that the context is a module context. if ($context->contextlevel != CONTEXT_MODULE) { continue; } - $user = $contextlist->get_user(); $courseworkdata = helper::get_context_data($context, $user); helper::export_context_files($context, $user); writer::with_context($context)->export_data([], $courseworkdata); - $coursework = self::get_coursework_instance($context); - static::export_coursework_submissions($coursework, $user, $context, []); static::export_coursework_extension($coursework->id, $user->id, $context, []); static::export_person_deadlines($coursework->id, $user->id, $context, []); static::export_plagiarism_flags($coursework->id, $context, []); } } - public static function delete_data_for_all_users_in_context(\context $context) { global $DB; - if ($context->contextlevel == CONTEXT_MODULE) { $cm = get_coursemodule_from_id('coursework', $context->instanceid); if ($cm) { // Get the coursework related to this context. $coursework = self::get_coursework_instance($context); - // Retrieve all submissions by this coursework to remove relating data $submissions = $coursework->retrieve_submissions_by_coursework(); foreach ($submissions as $submission) { - // Remove all plagiarisms of the current submission $coursework->remove_plagiarisms_by_submission($submission->id); - // remove corresponding file of this submission $coursework->remove_corresponding_file($context->id, $submission->id, 'submission'); - // Retrieve all feedbacks for this current submission $feedbacks = $coursework->retrieve_feedbacks_by_submission($submission->id); foreach ($feedbacks as $feedback) { // Remove all agreements for a feedback $coursework->remove_agreements_by_feedback($feedback->id); - // remove corresponding file of this feedback $coursework->remove_corresponding_file($context->id, $feedback->id, 'feedback'); } - // Remove all feedbacks for this submission $coursework->remove_feedbacks_by_submission($submission->id); } - // Remove all submissions by this coursework $coursework->remove_submissions_by_coursework(); - // Remove all deadline extensions by coursework $coursework->remove_deadline_extensions_by_coursework(); - // Remove all personal deadlines by coursework $coursework->remove_personal_deadlines_by_coursework(); } } } - public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; - $user = $contextlist->get_user(); - foreach ($contextlist as $context) { if ($context->contextlevel != CONTEXT_MODULE) { continue; } - // Get the coursework related to this context. $coursework = self::get_coursework_instance($context); - // Retrieve all submissions by user-id to remove relating data $submissions = $coursework->retrieve_submissions_by_user($user->id); foreach ($submissions as $submission) { - // Remove all plagiarisms of the current submission $coursework->remove_plagiarisms_by_submission($submission->id); - // remove corresponding file of this submission $coursework->remove_corresponding_file($context->id, $submission->id, 'submission'); - // Retrieve all feedbacks for this current submission $feedbacks = $coursework->retrieve_feedbacks_by_submission($submission->id); foreach ($feedbacks as $feedback) { // Remove all agreements for a feedback $coursework->remove_agreements_by_feedback($feedback->id); - // remove corresponding file of this feedback $coursework->remove_corresponding_file($context->id, $feedback->id, 'feedback'); } - // Remove all feedbacks for this submission $coursework->remove_feedbacks_by_submission($submission->id); } - // Remove all submissions submitted by this user $coursework->remove_submissions_by_user($user->id); - // Remove all deadline extensions $coursework->remove_deadline_extensions_by_user($user->id); - // Remove all personal deadlines $coursework->remove_personal_deadlines_by_user($user->id); } } - public static function delete_data_for_users(approved_userlist $userlist) { global $DB; - $context = $userlist->get_context(); if ($context->contextlevel != CONTEXT_MODULE) { return; } - // Get the coursework related to this context. $coursework = self::get_coursework_instance($context); - $userids = $userlist->get_userids(); foreach ($userids as $user_id) { - // Get the coursework related to this context. $coursework = self::get_coursework_instance($context); - // Retrieve all submissions by user-id to remove relating data $submissions = $coursework->retrieve_submissions_by_user($user_id); foreach ($submissions as $submission) { - // Remove all plagiarisms of the current submission $coursework->remove_plagiarisms_by_submission($submission->id); - // remove corresponding file of this submission $coursework->remove_corresponding_file($context->id, $submission->id, 'submission'); - // Retrieve all feedbacks for this current submission $feedbacks = $coursework->retrieve_feedbacks_by_submission($submission->id); foreach ($feedbacks as $feedback) { // Remove all agreements for a feedback $coursework->remove_agreements_by_feedback($feedback->id); - // remove corresponding file of this feedback $coursework->remove_corresponding_file($context->id, $feedback->id, 'feedback'); } - // Remove all feedbacks for this submission $coursework->remove_feedbacks_by_submission($submission->id); } - // Remove all submissions submitted by this user $coursework->remove_submissions_by_user($user_id); - // Remove all deadline extensions $coursework->remove_deadline_extensions_by_user($user_id); - // Remove all personal deadlines $coursework->remove_personal_deadlines_by_user($user_id); } } - protected static function get_coursework_instance(\context $context) { global $DB; - $courseId = array('id' => $context->get_course_context()->instanceid); $course = $DB->get_record('course', $courseId, '*', MUST_EXIST); - $modinfo = get_fast_modinfo($course); $coursemodule = $modinfo->get_cm($context->instanceid); - $courseworkId = array('id' => $coursemodule->instance); $coursework = new \mod_coursework\models\coursework($courseworkId); - return $coursework; } - /** * Exports coursework submission data for a user. * @@ -461,14 +390,11 @@ protected static function get_coursework_instance(\context $context) { protected static function export_coursework_submissions($coursework, $user, $context, $path, $exportforteacher = false) { $submissions = self::get_user_submissions($user->id, $coursework->id); $teacher = ($exportforteacher) ? $user : null; - foreach ($submissions as $submission) { self::export_submission_files($context, $submission->id, $path); - if (!isset($teacher)) { self::export_coursework_submission($submission, $context, $path); } - // Export feedbacks for each submission, feedbacks include user's grade $feedbacks = self::get_submission_feedbacks($submission->id); if ($feedbacks) { @@ -476,18 +402,15 @@ protected static function export_coursework_submissions($coursework, $user, $con } } } - protected static function export_submission_files($context, $submissionId, $currentpath) { $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_coursework', 'submission', $submissionId); - if (!empty($files)) { foreach ($files as $file) { writer::with_context($context)->export_file($currentpath, $file); } } } - /** * Gets all the submissions at once for user. * @param $userid @@ -496,14 +419,10 @@ protected static function export_submission_files($context, $submissionId, $curr */ protected static function get_user_submissions($userid, $courseworkid) { global $DB; - $params = array('courseworkid' => $courseworkid, 'authorid' => $userid); - $submissions = $DB->get_records('coursework_submissions', $params); - return $submissions; } - /** * Formats and then exports the user's submission data. * @@ -513,7 +432,6 @@ protected static function get_user_submissions($userid, $courseworkid) { */ protected static function export_coursework_submission(\stdClass $submission, \context $context, array $currentpath) { $status = self::get_submissions_status($submission->id); - $submissionData = (object)[ 'userid' => $submission->userid, 'status' => $status ? $status : '', @@ -523,65 +441,49 @@ protected static function export_coursework_submission(\stdClass $submission, \c 'createdby' => $submission->createdby, 'finalised' => transform::yesno($submission->finalised) ]; - writer::with_context($context) ->export_data(array_merge($currentpath, [get_string('privacy:submissionpath', 'mod_coursework')]), $submissionData); } - protected static function get_submissions_status($submissionid) { $submission = new \mod_coursework\models\submission($submissionid); - $status = $submission->get_status_text(); - if (!empty($status)) { return $status; } } - protected static function get_submission_feedbacks($submissionid) { global $DB; - $params = array('submissionid' => $submissionid); - $feedbacks = $DB->get_records('coursework_feedbacks', $params); - return $feedbacks; } - protected static function export_submissions_feedbacks($feedbacks, $context, $path) { $feedbacksData = []; - foreach ($feedbacks as $feedback) { $feedbackDataFormatted = self::format_submissions_feedback($feedback); if ($feedbackDataFormatted) { array_push($feedbacksData, $feedbackDataFormatted); } - if ($feedback->ismoderation) { self::export_mod_agreements($feedback->id, $context, $path); } - self::export_feedback_files($context, $feedback->id, $path); } - // coursework_feedbacks table contains all grading information if (!empty($feedbacksData)) { writer::with_context($context) ->export_data(array_merge($path, [get_string('privacy:feedbackspath', 'mod_coursework')]), (object) $feedbacksData); } } - protected static function export_feedback_files($context, $feedbackId, $currentpath) { $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_coursework', 'feedback', $feedbackId); - if (!empty($files)) { foreach ($files as $file) { writer::with_context($context)->export_file($currentpath, $file); } } } - /** * Formats the user's submission grade data. * @@ -597,55 +499,41 @@ protected static function format_submissions_feedback(\stdClass $feedback) { 'stage_identifier' => $feedback->stage_identifier, 'finalised' => transform::yesno($feedback->finalised) ]; - return $feedbackData; } - protected static function export_coursework_extension($courseworkId, $userId, $context, $path) { $extension = self::get_coursework_extension($courseworkId, $userId); if ($extension) { self::export_coursework_extension_data($extension, $context, $path); } } - protected static function get_coursework_extension($courseworkId, $userId) { global $DB; - $params = array('courseworkid' => $courseworkId, 'allocatableid' => $userId); - $extension = $DB->get_record('coursework_extensions', $params); - return $extension; } - protected static function export_coursework_extension_data($extension, $context, $path) { $extensionData = [ 'extended_deadline' => transform::datetime($extension->extended_deadline), 'extra_information_text' => $extension->extra_information_text, 'createdbyid' => $extension->createdbyid ]; - writer::with_context($context) - ->export_data(array_merge($path, [get_string('privacy:extensionpath', 'mod_coursework')]), (object) $extensionData); + ->export_data(array_merge($path, [get_string('privacy:extensionpath', 'mod_coursework')]), (object) $extensionData); } - protected static function export_person_deadlines($courseworkId, $userId, $context, $path) { $personDeadline = self::get_person_deadline($courseworkId, $userId); if ($personDeadline) { self::export_person_deadline_data($personDeadline, $context, $path); } } - protected static function get_person_deadline($courseworkId, $userId) { global $DB; - $params = array('courseworkid' => $courseworkId, 'allocatableid' => $userId); - $personDeadline = $DB->get_record('coursework_person_deadlines', $params); - return $personDeadline; } - protected static function export_person_deadline_data($personDeadline, $context, $path) { $personDeadlineData = [ 'personal_deadline' => transform::datetime($personDeadline->personal_deadline), @@ -653,9 +541,8 @@ protected static function export_person_deadline_data($personDeadline, $context, 'timemodified' => transform::datetime($personDeadline->timemodified), 'createdbyid' => $personDeadline->createdbyid ]; - writer::with_context($context) - ->export_data(array_merge($path, [get_string('privacy:person_deadlines', 'mod_coursework')]), (object) $personDeadlineData); + ->export_data(array_merge($path, [get_string('privacy:person_deadlines', 'mod_coursework')]), (object) $personDeadlineData); } protected static function export_plagiarism_flags($courseworkId, $context, $path) { @@ -719,17 +606,12 @@ protected static function export_mod_agreements($feedbackId, $context, $path) { self::export_mod_agreement_data($agreement, $context, $path); } } - protected static function get_mod_agreement($feedbackId) { global $DB; - $param = array('feedbackid' => $feedbackId); - $agreement = $DB->get_record('coursework_mod_agreements', $param); - return $agreement; } - protected static function export_mod_agreement_data($agreement, $context, $path) { $agreementData = [ 'moderatorid' => $agreement->moderatorid, @@ -738,9 +620,7 @@ protected static function export_mod_agreement_data($agreement, $context, $path) 'timecreated' => transform::datetime($agreement->timecreated), 'timemodified' => transform::datetime($agreement->timemodified), ]; - writer::with_context($context) - ->export_data(array_merge($path, [get_string('privacy:moderator', 'mod_coursework')]), (object) $agreementData); + ->export_data(array_merge($path, [get_string('privacy:moderator', 'mod_coursework')]), (object) $agreementData); } - -} +} \ No newline at end of file diff --git a/classes/render_helpers/grading_report/cells/_user_cell.php b/classes/render_helpers/grading_report/cells/_user_cell.php new file mode 100644 index 0000000..2ed1513 --- /dev/null +++ b/classes/render_helpers/grading_report/cells/_user_cell.php @@ -0,0 +1,113 @@ +get_allocatable(); + + /* if ($rowobject->can_view_username()) { + $content .= $OUTPUT->user_picture($user->get_raw_record()); + } else { + $renderer = $PAGE->get_renderer('core'); + // Just output the image for an anonymous user. + $defaulturl = $renderer->pix_url('u/f2'); // Default image. + $attributes = array('src' => $defaulturl); + $content .= html_writer::empty_tag('img', $attributes); + } */ + // TODO CSS for the space!! + $content .= ' ' . $rowobject->get_user_name(true); + + return $this->get_new_cell_with_class($content); + } + + /** + * @param array $options + * @return string + */ + public function get_table_header($options = array()) { + + $viewanonymous = has_capability('mod/coursework:viewanonymous', $this->coursework->get_context()); + + //adding this line so that the sortable heading function will make a sortable link unique to the table + //if tablename is set + $tablename = (!empty($options['tablename'])) ? $options['tablename'] : '' ; + + // allow to sort users only if CW is not set to blind marking or a user has capability to view anonymous + if($viewanonymous || !$this->coursework->blindmarking) { + $sort_by_first_name = $this->helper_sortable_heading(get_string('firstname'), + 'firstname', + $options['sorthow'], + $options['sortby'], + $tablename); + $sort_by_last_name = $this->helper_sortable_heading(get_string('lastname'), + 'lastname', + $options['sorthow'], + $options['sortby'], + $tablename); + } else { // otherwise display header without sorting + $sort_by_first_name = get_string('firstname'); + $sort_by_last_name =get_string('lastname'); + } + + if ($this->fullname_format() == 'lf') { + $sort_by_name = $sort_by_last_name . ' / ' . $sort_by_first_name; + } else { + $sort_by_name = $sort_by_first_name . ' / ' . $sort_by_last_name; + } + return $sort_by_name; + } + + /** + * @return string + */ + public function get_table_header_class(){ + return 'studentname'; + } + + /** + * Tries to guess the full name format set at the site. + * + * @return string fl|lf + */ + private function fullname_format() { + $fake = new stdclass(); // Fake user. + $fake->lastname = 'LLLL'; + $fake->firstname = 'FFFF'; + $fullname = get_string('fullnamedisplay', '', $fake); + if (strpos($fullname, 'LLLL') < strpos($fullname, 'FFFF')) { + return 'lf'; + } else { + return 'fl'; + } + } + + /** + * @return string + */ + public function header_group() { + return 'empty'; + } +} \ No newline at end of file diff --git a/classes/render_helpers/grading_report/cells/cell_base.php b/classes/render_helpers/grading_report/cells/cell_base.php index e91fcef..82db873 100644 --- a/classes/render_helpers/grading_report/cells/cell_base.php +++ b/classes/render_helpers/grading_report/cells/cell_base.php @@ -55,16 +55,17 @@ protected function helper_sortable_heading($display_name, $field, $sort_how, $so - $url = clone($PAGE->url); - $url->params($params); + // $url = clone($PAGE->url); + // $url->params($params); // Need a little icon to show ASC or DESC. - if ($field == $sortby) { - $display_name .= ' '; // Keep them on the same line. - $display_name .= $sort_how == 'ASC' ? '▲' : '▼'; // Small unicode triangles. - } + // if ($field == $sortby) { + // $display_name .= ' '; // Keep them on the same line. + // $display_name .= $sort_how == 'ASC' ? '▲' : '▼'; // Small unicode triangles. + // } - return html_writer::link($url, $display_name); + // return html_writer::link($url, $display_name); + return $display_name; } /** @@ -98,11 +99,19 @@ public function cell_name() { */ protected function get_new_cell_with_class($content = '') { return ' - '.$content.' + '.$content.' '; } + /** + * @param array $data + * @return \html_table_cell + */ + protected function get_new_cell_with_order_data($data) { + return '' . $data['display'] . ''; + } + /** * Override for specific constructor stuff. * @@ -121,4 +130,4 @@ public function get_table_header_help_icon(){ } -} \ No newline at end of file +} diff --git a/classes/render_helpers/grading_report/cells/email_cell.php b/classes/render_helpers/grading_report/cells/email_cell.php new file mode 100644 index 0000000..cfb4059 --- /dev/null +++ b/classes/render_helpers/grading_report/cells/email_cell.php @@ -0,0 +1,50 @@ +get_email(); + + return $this->get_new_cell_with_class($content); + } + + /** + * @param array $options + * @return string + */ + public function get_table_header($options = array()) { + return "Email"; + } + + /** + * @return string + */ + public function get_table_header_class(){ + return 'email_cell'; + } + + /** + * @return string + */ + public function header_group() { + return 'empty'; + } +} \ No newline at end of file diff --git a/classes/render_helpers/grading_report/cells/first_name_cell.php b/classes/render_helpers/grading_report/cells/first_name_cell.php new file mode 100644 index 0000000..9f390a1 --- /dev/null +++ b/classes/render_helpers/grading_report/cells/first_name_cell.php @@ -0,0 +1,50 @@ +get_user_firstname(); + + return $this->get_new_cell_with_class($content); + } + + /** + * @param array $options + * @return string + */ + public function get_table_header($options = array()) { + return "First Name"; + } + + /** + * @return string + */ + public function get_table_header_class(){ + return 'firstname_cell'; + } + + /** + * @return string + */ + public function header_group() { + return 'empty'; + } +} \ No newline at end of file diff --git a/classes/render_helpers/grading_report/cells/first_name_letter_cell.php b/classes/render_helpers/grading_report/cells/first_name_letter_cell.php new file mode 100644 index 0000000..9cf7d91 --- /dev/null +++ b/classes/render_helpers/grading_report/cells/first_name_letter_cell.php @@ -0,0 +1,58 @@ +get_allocatable(); + + mb_internal_encoding('utf-8'); + $content .= ' ' . mb_substr($user->firstname, 0, 1); + + return $this->get_new_cell_with_class($content); + } + + /** + * @param array $options + * @return string + */ + public function get_table_header($options = array()) { + return "First Letter - First Name"; + } + + /** + * @return string + */ + public function get_table_header_class(){ + return 'firstname_letter_cell'; + } + + /** + * @return string + */ + public function header_group() { + return 'empty'; + } +} \ No newline at end of file diff --git a/classes/render_helpers/grading_report/cells/group_cell.php b/classes/render_helpers/grading_report/cells/group_cell.php index 60cc1e7..fa75824 100644 --- a/classes/render_helpers/grading_report/cells/group_cell.php +++ b/classes/render_helpers/grading_report/cells/group_cell.php @@ -81,7 +81,7 @@ protected function add_group_member_name($group_member, $row_object) { if ($this->coursework->blindmarking_enabled() && !has_capability('mod/coursework:viewanonymous', $this->coursework->get_context()) && !$row_object->is_published()) { $text .= 'Hidden'; } else { - $text .= $group_member->profile_link(false); + $text .= $group_member->profile_link(false) . ' ('. $group_member->email.')'; } $text .= ''; return $text; diff --git a/classes/render_helpers/grading_report/cells/idnumber_cell.php b/classes/render_helpers/grading_report/cells/idnumber_cell.php new file mode 100644 index 0000000..e967acc --- /dev/null +++ b/classes/render_helpers/grading_report/cells/idnumber_cell.php @@ -0,0 +1,66 @@ +get_idnumber(); + + return $this->get_new_cell_with_class($content); + } + + /** + * @param array $options + * @return string + */ + public function get_table_header($options = array()) { + + $viewanonymous = has_capability('mod/coursework:viewanonymous', $this->coursework->get_context()); + + //adding this line so that the sortable heading function will make a sortable link unique to the table + //if tablename is set + $tablename = (!empty($options['tablename'])) ? $options['tablename'] : '' ; + + // allow to sort users only if CW is not set to blind marking or a user has capability to view anonymous + if($viewanonymous || !$this->coursework->blindmarking) { + $sort_header= $this->helper_sortable_heading(get_string('idnumber'), + 'idnumber', + $options['sorthow'], + $options['sortby'], + $tablename); + + } else { // otherwise display header without sorting + $sort_header = get_string('idnumber'); + } + + return $sort_header; + } + + /** + * @return string + */ + public function get_table_header_class(){ + return 'tableheadidnumber'; + } + + /** + * @return string + */ + public function header_group() { + return 'empty'; + } +} diff --git a/classes/render_helpers/grading_report/cells/last_name_cell.php b/classes/render_helpers/grading_report/cells/last_name_cell.php new file mode 100644 index 0000000..ac53c55 --- /dev/null +++ b/classes/render_helpers/grading_report/cells/last_name_cell.php @@ -0,0 +1,50 @@ +get_user_lastname(); + + return $this->get_new_cell_with_class($content); + } + + /** + * @param array $options + * @return string + */ + public function get_table_header($options = array()) { + return "Last Name"; + } + + /** + * @return string + */ + public function get_table_header_class(){ + return 'lastname_cell'; + } + + /** + * @return string + */ + public function header_group() { + return 'empty'; + } +} \ No newline at end of file diff --git a/classes/render_helpers/grading_report/cells/last_name_letter_cell.php b/classes/render_helpers/grading_report/cells/last_name_letter_cell.php new file mode 100644 index 0000000..f75a12e --- /dev/null +++ b/classes/render_helpers/grading_report/cells/last_name_letter_cell.php @@ -0,0 +1,58 @@ +get_allocatable(); + + mb_internal_encoding('utf-8'); + $content .= ' ' . mb_substr($user->lastname, 0, 1); + + return $this->get_new_cell_with_class($content); + } + + /** + * @param array $options + * @return string + */ + public function get_table_header($options = array()) { + return "Last Name Letter"; + } + + /** + * @return string + */ + public function get_table_header_class(){ + return 'lastname_letter_cell'; + } + + /** + * @return string + */ + public function header_group() { + return 'empty'; + } +} \ No newline at end of file diff --git a/classes/render_helpers/grading_report/cells/multiple_agreed_grade_cell.php b/classes/render_helpers/grading_report/cells/multiple_agreed_grade_cell.php index 316cb23..64903c0 100644 --- a/classes/render_helpers/grading_report/cells/multiple_agreed_grade_cell.php +++ b/classes/render_helpers/grading_report/cells/multiple_agreed_grade_cell.php @@ -33,16 +33,26 @@ protected function after_initialisation($items) { * @param grading_table_row_base $rowobject * @return string */ - public function get_table_cell($rowobject) { + public function get_table_cell($rowobject) { + $content = $this->get_content($rowobject); + return $this->get_new_cell_with_class($content); + } + + /** + * @param $rowobject + * @return \html_table_cell|string + */ + public function get_content($rowobject) { + global $USER, $OUTPUT; //if coursework uses sampling check if any enabled for this submission, otherwise there is no agreed grade if($rowobject->get_coursework()->sampling_enabled() && $rowobject->get_submission() && !$rowobject->get_submission()->sampled_feedback_exists()){ $content = get_string('singlemarker', 'coursework'); - return $this->get_new_cell_with_class($content); + return $content; } - $ability = new ability(user::find($USER), $rowobject->get_coursework()); + $ability = new ability(user::find($USER, false), $rowobject->get_coursework()); $content = ''; @@ -66,12 +76,14 @@ public function get_table_cell($rowobject) { $feedback_route_params = array( 'feedback' => $finalfeedback ); - $link = $this->get_router()->get_path('edit feedback', $feedback_route_params); + $link = $this->get_router()->get_path('ajax edit feedback', $feedback_route_params); $iconlink = $OUTPUT->action_icon($link, $icon, null, - array('id' => 'edit_final_feedback_' . $rowobject->get_coursework() + array( + 'class' => 'edit_final_feedback', + 'id' => 'edit_final_feedback_' . $rowobject->get_coursework() ->get_allocatable_identifier_hash($rowobject->get_allocatable()))); } else if ($rowobject->has_submission()) { // New @@ -92,7 +104,7 @@ public function get_table_cell($rowobject) { 'assessor' => $USER, 'stage' => $this->stage, ); - $link = $this->get_router()->get_path('new final feedback', $feedback_route_params); + $link = $this->get_router()->get_path('ajax new final feedback', $feedback_route_params); $iconlink = $OUTPUT->action_link($link, $title, @@ -129,9 +141,8 @@ public function get_table_cell($rowobject) { $content .= ' by: ' . $finalfeedback->get_assesor_username(); } } - - - return $this->get_new_cell_with_class($content); + + return $content; } /** @@ -162,4 +173,4 @@ public function get_table_header_class(){ public function header_group() { return 'grades'; } -} \ No newline at end of file +} diff --git a/classes/render_helpers/grading_report/cells/personal_deadline_cell.php b/classes/render_helpers/grading_report/cells/personal_deadline_cell.php index aa4fd28..dcbca2f 100644 --- a/classes/render_helpers/grading_report/cells/personal_deadline_cell.php +++ b/classes/render_helpers/grading_report/cells/personal_deadline_cell.php @@ -22,7 +22,8 @@ public function get_table_cell($row_object) { global $OUTPUT, $USER; $coursework = $row_object->get_coursework(); - $content = userdate($coursework->get_deadline(), '%a, %d %b %Y, %H:%M'); + $deadline = $coursework->get_deadline(); + $content = '
'; $new_personal_deadline_params = array( 'allocatableid' => $row_object->get_allocatable()->id(), @@ -32,21 +33,28 @@ public function get_table_cell($row_object) { $personal_deadline = personal_deadline::find_or_build($new_personal_deadline_params); if ($personal_deadline->personal_deadline){ - $content = userdate($personal_deadline->personal_deadline, '%a, %d %b %Y, %H:%M'); + $deadline = $personal_deadline->personal_deadline; + } + $date = userdate($deadline, '%a, %d %b %Y, %H:%M'); + $content .= '
'.$date.'
'; + $ability = new ability(user::find($USER, false), $row_object->get_coursework()); + $class = 'edit_personal_deadline'; + if(!$ability->can('edit', $personal_deadline)) { + $class .= ' display-none'; } - $ability = new ability(user::find($USER), $row_object->get_coursework()); - if($ability->can('edit', $personal_deadline)) { - $link = $this->get_router()->get_path('edit personal deadline', $new_personal_deadline_params); - $icon = new pix_icon('edit', 'Edit personal deadline', 'coursework'); + $link = $this->get_router()->get_path('edit personal deadline', $new_personal_deadline_params); + // $link = '/'; + $icon = new pix_icon('edit', 'Edit personal deadline', 'coursework'); + $new_personal_deadline_params['multipleuserdeadlines'] = 0; - $content .= $OUTPUT->action_icon($link, - $icon, - null, - array('class' => 'edit_personal_deadline')); - } + $content .= $OUTPUT->action_icon($link, + $icon, + null, + array('class' => $class,'data-get' =>json_encode($new_personal_deadline_params), 'data-time' => date('d-m-Y H:i', $deadline) )); + $content .= '
'; - return $this->get_new_cell_with_class($content); + return $this->get_new_cell_with_order_data(['display' => $content, '@data-order' => $deadline]); } /** @@ -58,10 +66,10 @@ public function get_table_header($options = array()) { $tablename = (!empty($options['tablename'])) ? $options['tablename'] : '' ; return $this->helper_sortable_heading(get_string('tableheadpersonaldeadline', 'coursework'), - 'personaldeadline', - $options['sorthow'], - $options['sortby'], - $tablename); + 'personaldeadline', + $options['sorthow'], + $options['sortby'], + $tablename); } /** @@ -77,4 +85,4 @@ public function get_table_header_class(){ public function header_group() { return 'empty'; } -} \ No newline at end of file +} diff --git a/classes/render_helpers/grading_report/cells/single_assessor_feedback_cell.php b/classes/render_helpers/grading_report/cells/single_assessor_feedback_cell.php index 2007aa7..6729498 100644 --- a/classes/render_helpers/grading_report/cells/single_assessor_feedback_cell.php +++ b/classes/render_helpers/grading_report/cells/single_assessor_feedback_cell.php @@ -10,6 +10,7 @@ use mod_coursework\allocation\allocatable; use mod_coursework\grade_judge; use mod_coursework\grading_table_row_base; +use mod_coursework\models\allocation; use mod_coursework\models\coursework; use mod_coursework\models\feedback; use mod_coursework\models\user; @@ -45,6 +46,12 @@ protected function after_initialisation($items) { * @return string */ public function get_table_cell($rowobject) { + $content = $this->get_content($rowobject); + return $this->get_new_cell_with_class($content); + } + + public function get_content($rowobject) { + global $USER; // Single: @@ -53,6 +60,8 @@ public function get_table_cell($rowobject) { $ability = new ability(user::find($USER), $rowobject->get_coursework()); $content = ''; + $submission = $rowobject->get_submission(); + $allocatable = $rowobject->get_allocatable(); // Add new feedback if ($rowobject->has_submission() && @@ -106,7 +115,7 @@ public function get_table_cell($rowobject) { } - return $this->get_new_cell_with_class($content); + return $content; } /** @@ -154,7 +163,7 @@ private function edit_feedback_button($rowobject) { return $OUTPUT->action_icon($link, $icon, null, - array('id' => $link_id)); + array('id' => $link_id, 'class' => 'edit_final_feedback')); } @@ -195,7 +204,7 @@ private function new_feedback_button($rowobject, $assessor) { 'assessor' => $assessor, 'stage' => $this->stage, ); - $link = $this->get_router()->get_path('new final feedback', $feedback_params); + $link = $this->get_router()->get_path('ajax new final feedback', $feedback_params); $link_id = 'new_final_feedback_' . $rowobject->get_coursework() ->get_allocatable_identifier_hash($rowobject->get_allocatable()); @@ -208,4 +217,4 @@ private function new_feedback_button($rowobject, $assessor) { array('class'=>'new_final_feedback','id' => $link_id)); } -} \ No newline at end of file +} diff --git a/classes/render_helpers/grading_report/cells/submission_cell.php b/classes/render_helpers/grading_report/cells/submission_cell.php index 140ba8d..657cffc 100644 --- a/classes/render_helpers/grading_report/cells/submission_cell.php +++ b/classes/render_helpers/grading_report/cells/submission_cell.php @@ -63,7 +63,8 @@ public function get_table_cell($rowobject) { if (($rowobject->get_submission()&& !$rowobject->get_submission()->finalised) || !$rowobject->get_submission()) { - if ($ability->can('new', $submission_on_behalf_of_allocatable)) { + if ($ability->can('new', $submission_on_behalf_of_allocatable) && (!$rowobject->get_coursework()->has_deadline() + || $rowobject->get_coursework()->allow_late_submissions() || ($rowobject->get_personal_deadlines() >= time() || ($rowobject->has_extension() && $rowobject->get_extension()->extended_deadline > time())))) { // New submission on behalf of button diff --git a/classes/render_helpers/grading_report/cells/time_submitted_cell.php b/classes/render_helpers/grading_report/cells/time_submitted_cell.php index 2a04767..da4eff2 100644 --- a/classes/render_helpers/grading_report/cells/time_submitted_cell.php +++ b/classes/render_helpers/grading_report/cells/time_submitted_cell.php @@ -21,9 +21,17 @@ class time_submitted_cell extends cell_base { * @return string */ public function get_table_cell($row_object) { + + $data = $this->prepare_content_cell($row_object); + + return $this->get_new_cell_with_order_data($data); + } + + public function prepare_content_cell($row_object) { global $OUTPUT, $USER; $content = ''; + $time_submitted = $displayeddeadline = 0; $coursework = $row_object->get_coursework(); $submission = $row_object->get_submission(); @@ -33,7 +41,7 @@ public function get_table_cell($row_object) { // If we have groups enabled and this is not the student who submitted the // group files, show who did. if ($coursework->is_configured_to_have_group_submissions() && !$row_object->has_submission()) { - $user = core_user::get_user($submission->userid); + $user = user::get_object($submission->userid); if ($row_object->can_view_username()) { $content .= "Submitted by"; @@ -88,19 +96,38 @@ public function get_table_cell($row_object) { } - + $content .= '
'; + $allocatableid = $row_object->get_allocatable()->id(); + $allocatabletype = $row_object->get_allocatable()->type(); + $coursework = $row_object->get_coursework(); $new_extension_params = array( - 'allocatableid' => $row_object->get_allocatable()->id(), - 'allocatabletype' => $row_object->get_allocatable()->type(), - 'courseworkid' => $row_object->get_coursework()->id, + 'allocatableid' => $allocatableid, + 'allocatabletype' => $allocatabletype, + 'courseworkid' => $coursework->id, ); + $extension = deadline_extension::find_or_build($new_extension_params); $ability = new ability(user::find($USER), $row_object->get_coursework()); if ($extension->persisted()) { $content .= 'Extension:
'.userdate($extension->extended_deadline, '%a, %d %b %Y, %H:%M'); + $displayeddeadline = $extension->extended_deadline; + } + + + if($extension->id) { + $new_extension_params['id'] = $extension->id; + } + if($submission) { + $new_extension_params['submissionid'] = $submission->id; } + $deadline = $deadline ?? $coursework->deadline; + $content_time = [ + 'time' => date('d-m-Y H:i',$deadline), + 'time_content' => userdate($deadline), + 'is_have_deadline' => ($coursework->deadline > 0)? 1 : 0, + ]; if ($ability->can('new', $extension) && $coursework->extensions_enabled()) { $link = $this->get_router()->get_path('new deadline extension', $new_extension_params); @@ -108,19 +135,42 @@ public function get_table_cell($row_object) { $content .= $OUTPUT->action_link($link, $title, null, - array('class' => 'new_deadline_extension')); + array('class' => 'new_deadline_extension', 'data-name' => $row_object->get_allocatable()->name(), 'data-params' => json_encode($new_extension_params), 'data-time' =>json_encode($content_time) )); } else if ($ability->can('edit', $extension) && $coursework->extensions_enabled()) { $link = $this->get_router()->get_path('edit deadline extension', array('id' => $extension->id)); $icon = new pix_icon('edit', 'Edit extension', 'coursework'); $content .= $OUTPUT->action_icon($link, - $icon, - null, - array('class' => 'edit_deadline_extension')); + $icon, + null, + array('class' => 'edit_deadline_extension', 'data-name' => $row_object->get_allocatable()->name(), 'data-params' => json_encode($new_extension_params), 'data-time' =>json_encode($content_time))); } - return $this->get_new_cell_with_class($content); + + $content .= '
'; + + return ['display' => $content, '@data-order' => $this->standardize_time_for_compare($time_submitted) . '|' . $this->standardize_time_for_compare($displayeddeadline)]; + } + + /** + * return 11-char string + * + * @param $time + * @return mixed + */ + private function standardize_time_for_compare($time) { + $zerotoadd = 10; + if ($time > 1) { + $length = ceil(log10($time)); + $zerotoadd = 11 - $length; + $zerotoadd = $zerotoadd < 0 ? 0 : $zerotoadd; + } + $result = $time; + for ($i = 0; $i < $zerotoadd; $i++) { + $result = '0' . $result; + } + return $result; } /** @@ -134,10 +184,10 @@ public function get_table_header($options = array()) { $tablename = (!empty($options['tablename'])) ? $options['tablename'] : '' ; return $this->helper_sortable_heading(get_string('tableheadsubmissiondate', 'coursework'), - 'timesubmitted', - $options['sorthow'], - $options['sortby'], - $tablename); + 'timesubmitted', + $options['sorthow'], + $options['sortby'], + $tablename); } /** @@ -153,4 +203,4 @@ public function get_table_header_class(){ public function header_group() { return 'submission'; } -} \ No newline at end of file +} diff --git a/classes/render_helpers/grading_report/cells/user_cell.php b/classes/render_helpers/grading_report/cells/user_cell.php index 2ed1513..2e43a67 100644 --- a/classes/render_helpers/grading_report/cells/user_cell.php +++ b/classes/render_helpers/grading_report/cells/user_cell.php @@ -23,11 +23,6 @@ public function get_table_cell($rowobject) { $content = ''; - /** - * @var user $user - */ - $user = $rowobject->get_allocatable(); - /* if ($rowobject->can_view_username()) { $content .= $OUTPUT->user_picture($user->get_raw_record()); } else { @@ -39,6 +34,18 @@ public function get_table_cell($rowobject) { } */ // TODO CSS for the space!! $content .= ' ' . $rowobject->get_user_name(true); + $content .= "
".$rowobject->get_email(); + $user = $rowobject->get_allocatable(); +/* + $candidatenumber = $user->candidate_number(); + + if (!empty($candidatenumber)) { + + $content .= '
('.$candidatenumber.')'; + + } + +*/ return $this->get_new_cell_with_class($content); } @@ -67,17 +74,37 @@ public function get_table_header($options = array()) { $options['sorthow'], $options['sortby'], $tablename); + $sort_by_email = $this->helper_sortable_heading(get_string('email', 'mod_coursework'), + 'email', + $options['sorthow'], + $options['sortby'], + $tablename); + } else { // otherwise display header without sorting $sort_by_first_name = get_string('firstname'); - $sort_by_last_name =get_string('lastname'); + $sort_by_last_name = get_string('lastname'); + $sort_by_email = get_string('email', 'mod_coursework'); } if ($this->fullname_format() == 'lf') { - $sort_by_name = $sort_by_last_name . ' / ' . $sort_by_first_name; + $sort_by_first_name = ' / ' . $sort_by_first_name; + } else { + $sort_by_last_name = ' / ' . $sort_by_last_name; + } + + $sort_by_first_name = ''.$sort_by_first_name.''; + + $sort_by_last_name = ''.$sort_by_last_name.''; + + $sort_by_email = ''.$sort_by_email.''; + + if ($this->fullname_format() == 'lf') { + $sort_by_name = $sort_by_last_name . $sort_by_first_name; } else { - $sort_by_name = $sort_by_first_name . ' / ' . $sort_by_last_name; + $sort_by_name = $sort_by_first_name . $sort_by_last_name; } - return $sort_by_name; + $sort = $sort_by_name ."
" .$sort_by_email; + return $sort; } /** @@ -110,4 +137,4 @@ private function fullname_format() { public function header_group() { return 'empty'; } -} \ No newline at end of file +} diff --git a/classes/render_helpers/grading_report/sub_rows/multi_marker_feedback_sub_rows.php b/classes/render_helpers/grading_report/sub_rows/multi_marker_feedback_sub_rows.php index e107afb..ce32546 100644 --- a/classes/render_helpers/grading_report/sub_rows/multi_marker_feedback_sub_rows.php +++ b/classes/render_helpers/grading_report/sub_rows/multi_marker_feedback_sub_rows.php @@ -50,6 +50,25 @@ protected function get_renderer() { return $PAGE->get_renderer('mod_coursework', 'object'); } + /** + * @param $feedback_row + * @param $coursework + * @param null $ability + * @return string + */ + public function get_grade_cell_content($feedback_row, $coursework, $ability = null) { + global $USER; + + if (empty($ability)) { + $ability = new ability(user::find($USER), $coursework); + } + $gradedby = ($coursework->allocation_enabled() && $feedback_row->has_feedback() && $feedback_row->get_graded_by() != $feedback_row->get_assessor())? + ' (Graded by: '. $feedback_row->get_graders_name().')' : ''; + $editable = (!$feedback_row->has_feedback() || $feedback_row->get_feedback()->finalised)? '' : '
'.get_string('notfinalised', 'coursework'); + $result = $this->comment_for_row($feedback_row, $ability) . $gradedby . $editable; + return $result; + } + /** * Renders the table of feedbacks from assessors, which appears under each student's submission in the * grading report of the multiple marker courseworks. @@ -62,7 +81,7 @@ protected function render_assessor_feedback_table(assessor_feedback_table $asses global $USER, $PAGE; $coursework = $assessor_feedback_table->get_coursework(); - $ability = new ability(user::find($USER), $coursework); + $ability = new ability(user::find($USER, false), $coursework); $feedbackrows = $assessor_feedback_table->get_renderable_feedback_rows(); @@ -95,21 +114,16 @@ protected function render_assessor_feedback_table(assessor_feedback_table $asses '.get_string('notincludedinsample','mod_coursework').' '; } else { - $gradedby = ($coursework->allocation_enabled() && $feedback_row->has_feedback() && $feedback_row->get_graded_by() != $feedback_row->get_assessor())? - ' (Graded by: '. $feedback_row->get_graders_name().')':''; $assessor_details = (empty($feedback_row->get_assessor()->id()) && $coursework->allocation_enabled()) ? get_string('assessornotallocated','mod_coursework') : $this->profile_link($feedback_row); - - - - $editable = (!$feedback_row->has_feedback() || $feedback_row->get_feedback()->finalised)? '' : '
'.get_string('notfinalised', 'coursework'); - $output_rows .= ' - ' . $assessor_details. ' - ' . $this->comment_for_row($feedback_row, $ability) .$gradedby. $editable.' - ' . $this->date_for_column($feedback_row) . ' - - '; + + $output_rows .= + '' . $assessor_details. ' + ' . + $this->get_grade_cell_content($feedback_row, $coursework, $ability) . + ' + ' . $this->date_for_column($feedback_row) . ''; } } @@ -118,7 +132,7 @@ protected function render_assessor_feedback_table(assessor_feedback_table $asses $allocation_string = ($coursework->allocation_enabled())? get_string('allocatedtoassessor', 'mod_coursework'): get_string('assessor', 'mod_coursework'); - +/* $table_html = ' @@ -137,6 +151,19 @@ protected function render_assessor_feedback_table(assessor_feedback_table $asses '; +*/ + $table_html = ' + + + + + '; + + $table_html .= $output_rows; + + $table_html .= ' + '; return $table_html; } else { @@ -172,8 +199,8 @@ protected function edit_existing_feedback_link($feedback_row) { $icon = new pix_icon('edit', $linktitle, 'coursework'); $link_id = "edit_feedback_" . $feedback_row->get_feedback()->id; $link = $this->get_router() - ->get_path('edit feedback', array('feedback' => $feedback_row->get_feedback())); - $iconlink = $OUTPUT->action_icon($link, $icon, null, array('id' => $link_id)); + ->get_path('ajax edit feedback', array('feedback' => $feedback_row->get_feedback())); + $iconlink = $OUTPUT->action_icon($link, $icon, null, array('id' => $link_id, 'class' => 'edit_feedback')); return $iconlink; } @@ -230,10 +257,10 @@ protected function new_feedaback_link($feedback_row) { $new_feedback_params = array( 'submission' => $feedback_row->get_submission(), - 'assessor' => user::find($USER), + 'assessor' => user::find($USER, false), 'stage' => $feedback_row->get_stage() ); - $link = $this->get_router()->get_path('new feedback', $new_feedback_params); + $link = $this->get_router()->get_path('ajax new feedback', $new_feedback_params); $iconlink = $OUTPUT->action_link($link, $linktitle, null, @@ -245,7 +272,7 @@ protected function new_feedaback_link($feedback_row) { * @param assessor_feedback_row $feedback_row * @return string */ - protected function profile_link($feedback_row) { + public function profile_link($feedback_row) { global $COURSE; $assessor = $feedback_row->get_assessor(); @@ -270,7 +297,7 @@ protected function row_class($feedback_row) { * @param assessor_feedback_row $feedback_row * @return string */ - protected function date_for_column($feedback_row) { + public function date_for_column($feedback_row) { if ($feedback_row->has_feedback()) { return userdate($feedback_row->get_feedback()->timecreated, '%a, %d %b %Y, %H:%M '); } @@ -321,4 +348,4 @@ protected function comment_for_row($feedback_row, $ability) { return $html; } -} \ No newline at end of file +} diff --git a/classes/router.php b/classes/router.php index 2d7749c..17b740d 100644 --- a/classes/router.php +++ b/classes/router.php @@ -95,6 +95,14 @@ public function get_path($path_name, $items = array(), $as_url_object = false, $ case 'assessor grading': + case 'ajax new feedback': + $url = new moodle_url('/mod/coursework/actions/feedbacks/new.php', + array('submissionid' => $items['submission']->id, + 'stage_identifier' => $items['stage']->identifier(), + 'assessorid' => $items['assessor']->id, + 'ajax' => 1)); + break; + case 'new feedback': $url = new moodle_url('/mod/coursework/actions/feedbacks/new.php', array('submissionid' => $items['submission']->id, @@ -102,6 +110,14 @@ public function get_path($path_name, $items = array(), $as_url_object = false, $ 'assessorid' => $items['assessor']->id)); break; + case 'ajax new final feedback': + $params = array('submissionid' => $items['submission']->id, + 'stage_identifier' => $items['stage']->identifier(), + 'isfinalgrade' => 1, + 'ajax' => 1); + $url = new moodle_url('/mod/coursework/actions/feedbacks/new.php', $params); + break; + case 'new final feedback': $params = array('submissionid' => $items['submission']->id, 'stage_identifier' => $items['stage']->identifier(), @@ -125,11 +141,21 @@ public function get_path($path_name, $items = array(), $as_url_object = false, $ )); break; + case 'ajax edit feedback': + $url = new moodle_url('/mod/coursework/actions/feedbacks/edit.php', + array('feedbackid' => $items['feedback']->id, 'ajax' => 1)); + break; + case 'edit feedback': $url = new moodle_url('/mod/coursework/actions/feedbacks/edit.php', array('feedbackid' => $items['feedback']->id)); break; + case 'ajax update feedback': + $url = new moodle_url('/mod/coursework/actions/feedbacks/update.php', + array('feedbackid' => $items['feedback']->id, 'ajax' => 1)); + break; + case 'update feedback': $url = new moodle_url('/mod/coursework/actions/feedbacks/update.php', array('feedbackid' => $items['feedback']->id)); diff --git a/classes/stages/base.php b/classes/stages/base.php index abce684..971ff9d 100644 --- a/classes/stages/base.php +++ b/classes/stages/base.php @@ -42,6 +42,13 @@ abstract class base { */ protected $allocatables_with_moderation; + /** + * @var array + */ + private static $self_cache = [ + 'user_is_assessor' => [] + ]; + /** * @param coursework $coursework @@ -52,6 +59,7 @@ public function __construct($coursework, $stage_identifier) { $this->stage_identifier = $stage_identifier; } + /** * @return strategy_base */ @@ -96,17 +104,14 @@ protected function get_coursework() { * @return bool */ private function already_allocated($allocatable) { - - global $DB; - - $params = array( - 'stage_identifier' => $this->identifier(), - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - 'courseworkid' => $this->get_coursework_id(), + $coursework_id = $this->get_coursework_id(); + allocation::fill_pool_coursework($coursework_id); + $record = allocation::get_object( + $coursework_id, + 'allocatableid-allocatabletype-stage_identifier', + [$allocatable->id(), $allocatable->type(), $this->identifier()] ); - return $DB->record_exists('coursework_allocation_pairs', $params); - + return !empty($record); } /** @@ -116,17 +121,16 @@ private function already_allocated($allocatable) { */ public function assessor_already_allocated_for_this_submission($allocatable, $assessor) { - global $DB; - if(!empty($assessor)) { - $params = array( - 'assessorid' => $assessor->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - 'courseworkid' => $this->get_coursework_id(), + $coursework_id = $this->get_coursework_id(); + allocation::fill_pool_coursework($coursework_id); + $record = allocation::get_object( + $coursework_id, + 'allocatableid-allocatabletype-assessorid', + [$allocatable->id(), $allocatable->type(), $assessor->id] ); - return $DB->record_exists('coursework_allocation_pairs', $params); - } else { + return !empty($record); + } else { return false; } } @@ -163,6 +167,8 @@ public function make_manual_allocation($allocatable, $teacher) { $allocation = $this->prepare_allocation_to_save($allocatable, $teacher); $allocation->manual = 1; $allocation->save(); + + allocation::fill_pool_coursework($this->get_coursework()->id()); return $allocation; } @@ -252,14 +258,18 @@ private function prepare_allocation_to_save($allocatable, $teacher) { * @return bool */ public function allocation_is_manual($allocatable) { - $params = array( - 'courseworkid' => $this->coursework->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - 'manual' => 1, - 'stage_identifier' => $this->identifier(), + + $coursework_id = $this->get_coursework_id(); + allocation::fill_pool_coursework($coursework_id); + $record = allocation::get_object( + $coursework_id, + 'allocatableid-allocatabletype-stage_identifier', + [$allocatable->id(), $allocatable->type(), $this->identifier()] ); - return allocation::exists($params); + if ($record && $record->manual == 1) { + return true; + } + return false; } /** @@ -300,21 +310,15 @@ abstract protected function assessor_capability(); * @return bool */ public function has_feedback($allocatable) { - global $DB; - - if (!isset($this->allocatables_with_feedback)) { - $sql = "SELECT allocatableid - FROM {coursework_submissions} s - INNER JOIN {coursework_feedbacks} f - ON f.submissionid = s.id - WHERE s.courseworkid = ? - AND f.stage_identifier = ? - "; - $allocatables = $DB->get_records_sql($sql, array($this->get_coursework()->id, $this->stage_identifier)); - $this->allocatables_with_feedback = array_keys($allocatables); + $feedback = null; + $coursework_id = $this->get_coursework_id(); + submission::fill_pool_coursework($coursework_id); + $submission = submission::get_object($coursework_id, 'allocatableid', [$allocatable->id]); + if ($submission) { + feedback::fill_pool_coursework($coursework_id); + $feedback = feedback::get_object($coursework_id, 'submissionid-stage_identifier', [$submission->id, $this->identifier()]); } - - return in_array($allocatable->id, $this->allocatables_with_feedback); + return !empty($feedback); } /** @@ -356,12 +360,8 @@ public function get_moderation($submission) { * @return bool|feedback */ public function get_feedback_for_allocatable($allocatable) { - $submission_params = array( - 'courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - ); - $submission = submission::find($submission_params); + $params = [$allocatable->id(), $allocatable->type()]; + $submission = submission::get_object($this->get_coursework()->id, 'allocatableid-allocatabletype', $params); if ($submission) { return $this->get_feedback_for_submission($submission); @@ -374,9 +374,10 @@ public function get_feedback_for_allocatable($allocatable) { * @return feedback|bool */ public function get_single_feedback($submission){ - $feedback_params = array('submissionid' => $submission->id, 'stage_identifier'=> 'assessor_1'); + feedback::fill_pool_coursework($submission->courseworkid); + $result = feedback::get_object($submission->courseworkid, 'submissionid-stage_identifier', [$submission->id, 'assessor_1']); - return feedback::find($feedback_params); + return $result; } /** @@ -384,16 +385,12 @@ public function get_single_feedback($submission){ * @return bool */ public function has_allocation($allocatable) { - global $DB; - if (!isset($this->allocatables_with_allocations)) { - $sql = "SELECT allocatableid - FROM {coursework_allocation_pairs} a - WHERE a.courseworkid = ? - AND a.stage_identifier = ? - "; - $allocatables = $DB->get_records_sql($sql, array($this->get_coursework()->id, $this->stage_identifier)); - $this->allocatables_with_allocations = array_keys($allocatables); + $coursework_id = $this->get_coursework()->id; + if (!isset(allocation::$pool[$coursework_id]['stage_identifier'])) { + allocation::fill_pool_coursework($coursework_id); + } + $this->allocatables_with_allocations = array_column(allocation::$pool[$coursework_id]['stage_identifier'][$this->stage_identifier] ?? [], 'allocatableid'); } return in_array($allocatable->id, $this->allocatables_with_allocations); @@ -406,15 +403,11 @@ public function has_allocation($allocatable) { * @return bool */ public function stage_has_allocation(){ - global $DB; + $coursework_id = $this->get_coursework_id(); + allocation::fill_pool_coursework($coursework_id); + $record = allocation::get_object($coursework_id, 'stage_identifier', [$this->stage_identifier]); - $sql = "SELECT allocatableid - FROM {coursework_allocation_pairs} a - WHERE a.courseworkid = :courseworkid - AND a.stage_identifier = :stageidentifier - "; - return $DB->record_exists_sql($sql, array('courseworkid' => $this->coursework->id(), - 'stageidentifier' => $this->stage_identifier)); + return !empty($record); } @@ -431,13 +424,10 @@ public function destroy_allocation($allocatable) { * @return allocation|bool */ public function get_allocation($allocatable) { - $params = array( - 'courseworkid' => $this->coursework->id, - 'stage_identifier' => $this->identifier(), - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - ); - return allocation::find($params); + $courseworkid = $this->coursework->id; + $params = [$allocatable->id(), $allocatable->type(), $this->identifier()]; + $allocation = allocation::get_object($courseworkid, 'allocatableid-allocatabletype-stage_identifier', $params); + return $allocation; } /** @@ -445,13 +435,13 @@ public function get_allocation($allocatable) { * @return user */ public function allocated_teacher_for($allocatable) { - $params = array( - 'courseworkid' => $this->coursework->id, - 'stage_identifier' => $this->identifier(), - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), + $coursework_id = $this->get_coursework_id(); + allocation::fill_pool_coursework($coursework_id); + $allocation = allocation::get_object( + $coursework_id, + 'allocatableid-allocatabletype-stage_identifier', + [$allocatable->id(), $allocatable->type(), $this->identifier()] ); - $allocation = allocation::find($params); if ($allocation) { return $allocation->assessor(); @@ -511,13 +501,12 @@ public function allocatable_is_in_sample($allocatable) { return true; } - $params = array( - 'courseworkid' => $this->coursework->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - 'stage_identifier' => $this->stage_identifier - ); - return assessment_set_membership::exists($params); + assessment_set_membership::fill_pool_coursework($this->coursework->id); + $record = assessment_set_membership::get_object( + $this->coursework->id, + 'allocatableid-allocatabletype-stage_identifier', + [$allocatable->id(), $allocatable->type(), $this->stage_identifier]); + return !empty($record); } /** @@ -560,8 +549,12 @@ public function remove_allocatable_from_sampling($allocatable) { * @return bool */ public function user_is_assessor($assessor) { - $enrolled = is_enrolled($this->coursework->get_course_context(), $assessor, $this->assessor_capability()); - return $enrolled || is_primary_admin($assessor->id); + if (!isset(self::$self_cache['user_is_assessor'][$this->coursework->id][$assessor->id])) { + $enrolled = is_enrolled($this->coursework->get_course_context(), $assessor, $this->assessor_capability()); + $res = $enrolled || is_primary_admin($assessor->id); + self::$self_cache['user_is_assessor'][$this->coursework->id][$assessor->id] = $res; + } + return self::$self_cache['user_is_assessor'][$this->coursework->id][$assessor->id]; } /** @@ -580,16 +573,16 @@ public function user_is_moderator($moderator) { * @return bool */ public function assessor_has_allocation($allocatable){ - global $DB, $USER; - $params = array( - 'courseworkid' => $this->coursework->id, - 'assessorid' => $USER->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - 'stage_identifier'=> $this->stage_identifier + global $USER; + allocation::fill_pool_coursework($this->coursework->id); + $allocation = allocation::get_object( + $this->coursework->id, + 'allocatableid-allocatabletype-stage_identifier', + [$allocatable->id(), $allocatable->type(), $this->stage_identifier] ); - return $DB->record_exists('coursework_allocation_pairs', $params); -} + $result = ($allocation && $allocation->assessorid == $USER->id); + return $result; + } /** * @return bool @@ -642,17 +635,15 @@ public function prerequisite_stages_have_feedback($allocatable) { $previous_stage_ok = true; $current_stage = false; $current_stage_ok = true; + $coursework_id = $this->get_coursework_id(); + submission::fill_pool_coursework($coursework_id); foreach ($all_stages as $stage) { // if coursework has sampling enabled, each stage must be checked if it uses sampling if ($this->get_coursework()->sampling_enabled()) { - $submission_params = array( - 'courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - ); - $submission = submission::find($submission_params); + + $submission = submission::get_object($coursework_id, 'allocatableid-allocatabletype', [$allocatable->id(), $allocatable->type()]); if (count($submission->get_assessor_feedbacks()) >= $submission->max_number_of_feedbacks() && $submission->sampled_feedback_exists()){ @@ -703,11 +694,9 @@ public function group_assessor_enabled() { * @return feedback|bool */ public function get_feedback_for_submission($submission) { - $feedback_params = array( - 'submissionid' => $submission->id, - 'stage_identifier' => $this->identifier(), - ); - return feedback::find($feedback_params); + $stage_identifier = $this->identifier(); + $feedback = feedback::get_object($submission->courseworkid, 'submissionid-stage_identifier', [$submission->id, $stage_identifier]); + return $feedback; } /** @@ -910,25 +899,26 @@ private function currently_allocated_assessor($allocatable) { */ private function in_editable_period($allocatable) { - global $CFG; + $result = $this->get_coursework()->get_grade_editing_time(); //the feedback is not in the editable period if the editable setting is disabled if (empty($this->get_coursework()->get_grade_editing_time())) return false; - $submission_params = array( - 'courseworkid' => $this->get_coursework()->id, - 'allocatableid' => $allocatable->id(), - 'allocatabletype' => $allocatable->type(), - ); - $submission = submission::find($submission_params); + $coursework_id = $this->get_coursework_id(); + submission::fill_pool_coursework($coursework_id); + $submission = submission::get_object($coursework_id, 'allocatableid-allocatabletype', [$allocatable->id(), $allocatable->type()]); $feedback = $this->get_feedback_for_submission($submission); + if ($feedback) { + $result += $feedback->timecreated; + } - return $feedback->timecreated + $this->get_coursework()->get_grade_editing_time() > time(); + return $result > time(); } + private function selected_allocation_in_session($dropdownname) { global $SESSION; @@ -948,32 +938,31 @@ private function selected_allocation_in_session($dropdownname) { } - public function get_assessor_from_moodle_course_group($allocatable){ + public function get_assessor_from_moodle_course_group($allocatable){ - $assessor = ''; - // get allocatables group - if ($this->coursework->is_configured_to_have_group_submissions()){ - $groupid = $allocatable->id; - } else { - $user = user::find($allocatable->id); - $group = $this->coursework->get_student_group($user); - $groupid = ($group)? $group->id: 0; - } - - if($groupid) { - // find 1st assessor in the group - $first_group_assessor = get_enrolled_users($this->coursework->get_context(), $this->assessor_capability(), - $groupid, 'u.*', 'id ASC', 0, 1); + $assessor = ''; + // get allocatables group + if ($this->coursework->is_configured_to_have_group_submissions()){ + $groupid = $allocatable->id; + } else { + $user = user::get_object($allocatable->id); + $group = $this->coursework->get_student_group($user); + $groupid = ($group)? $group->id: 0; + } - $assessor = array_column($first_group_assessor, 'id'); + if($groupid) { + // find 1st assessor in the group + $first_group_assessor = get_enrolled_users($this->coursework->get_context(), $this->assessor_capability(), + $groupid, 'u.*', 'id ASC', 0, 1); - if ($assessor) { - $assessorid = $assessor[0]; - $assessor = user::find($assessorid); - } - } + $assessor = array_column($first_group_assessor, 'id'); - return $assessor; - } + if ($assessor) { + $assessorid = $assessor[0]; + $assessor = user::get_object($assessorid); + } + } + return $assessor; + } } \ No newline at end of file diff --git a/classes/traits/allocatable_functions.php b/classes/traits/allocatable_functions.php index a3ce3c4..f4ef42a 100644 --- a/classes/traits/allocatable_functions.php +++ b/classes/traits/allocatable_functions.php @@ -1,6 +1,7 @@ numberofmarkers; + $expected_markers = $coursework->numberofmarkers; $sql = " SELECT COUNT(*) @@ -101,25 +102,25 @@ public function has_all_initial_feedbacks($coursework) { AND s.courseworkid = :courseworkid "; $feedbacks = $DB->count_records_sql($sql, - array('id' => $this->id(), - 'courseworkid' => $coursework->id())); + array('id' => $this->id(), + 'courseworkid' => $coursework->id())); - // when sampling is enabled, calculate how many stages are in sample - if ($coursework->sampling_enabled()) { + // when sampling is enabled, calculate how many stages are in sample + if ($coursework->sampling_enabled()) { - $sql = "SELECT COUNT(*) + $sql = "SELECT COUNT(*) FROM {coursework_sample_set_mbrs} WHERE courseworkid = :courseworkid AND allocatableid = :allocatableid AND allocatabletype = :allocatabletype"; - $markers = $DB->count_records_sql($sql, - array('courseworkid' => $coursework->id(), - 'allocatableid' => $this->id(), - 'allocatabletype' => $this->type())); + $markers = $DB->count_records_sql($sql, + array('courseworkid' => $coursework->id(), + 'allocatableid' => $this->id(), + 'allocatabletype' => $this->type())); - $expected_markers = $markers + 1; // there is always a marker for stage 1 - } + $expected_markers = $markers + 1; // there is always a marker for stage 1 + } return $feedbacks == $expected_markers; } @@ -129,21 +130,14 @@ public function has_all_initial_feedbacks($coursework) { * @return array */ public function get_initial_feedbacks($coursework) { - global $DB; - $sql = " - SELECT f.* - FROM {coursework_feedbacks} f - INNER JOIN {coursework_submissions} s - ON f.submissionid = s.id - WHERE f.stage_identifier LIKE 'assess%' - AND s.allocatableid = :id - AND s.courseworkid = :courseworkid - "; - $result = $DB->get_records_sql($sql, - array('id' => $this->id(), - 'courseworkid' => $coursework->id())); - $result_as_classes = array_map(function($raw) { return new feedback($raw); }, $result); - return $result_as_classes; + $this->fill_submission_and_feedback($coursework); + $result = []; + $submission = $this->get_submission($coursework); + if ($submission) { + $result = isset(feedback::$pool[$coursework->id]['submissionid-stage_identifier_index'][$submission->id . '-others']) ? + feedback::$pool[$coursework->id]['submissionid-stage_identifier_index'][$submission->id . '-others'] : []; + } + return $result; } /** @@ -151,7 +145,18 @@ public function get_initial_feedbacks($coursework) { * @return submission */ public function get_submission($coursework) { - return submission::find(array('allocatableid' => $this->id(), - 'courseworkid' => $coursework->id())); + $this->fill_submission_and_feedback($coursework); + $result = submission::get_object($coursework->id, 'allocatableid', [$this->id]); + return $result; + } + + /** + * + * @param $coursework + */ + private function fill_submission_and_feedback($coursework) { + $coursework_id = $coursework->id; + submission::fill_pool_coursework($coursework_id); + feedback::fill_pool_coursework($coursework_id); } } \ No newline at end of file diff --git a/classes/user_row.php b/classes/user_row.php index 666bea4..d330486 100644 --- a/classes/user_row.php +++ b/classes/user_row.php @@ -35,6 +35,10 @@ public function get_allocatable_id(); public function get_user_name(); + public function get_idnumber(); + + public function get_email(); + public function can_view_username(); /** diff --git a/classes/utils/cs_editor.php b/classes/utils/cs_editor.php new file mode 100644 index 0000000..bb04605 --- /dev/null +++ b/classes/utils/cs_editor.php @@ -0,0 +1,109 @@ +libdir.'/editor/atto/lib.php'); + +class cs_editor extends \atto_texteditor +{ + /** + * + * @param $elementid + * @param $options + * @param null $fpoptions + * @return array + * @throws coding_exception + * @throws dml_exception + */ + public function get_options($elementid) { + $options = $this->get_element_options(); + $fpoptions = []; + $js_plugins = $this->get_js_plugins($elementid, $options, $fpoptions); + $result = $this->get_init_params($elementid, $options, $fpoptions, $js_plugins); + return $result; + } + + /** + * @return array + * @throws dml_exception + */ + protected function get_element_options() { + global $PAGE; + $result = [ + 'subdirs' => 0, + 'maxbytes' => 0, + 'maxfiles' => 0, + 'changeformat' => 0, + 'areamaxbytes' => FILE_AREA_MAX_BYTES_UNLIMITED, + 'context' => !empty($PAGE->context->id) ? $PAGE->context : context_system::instance(), + 'noclean' => 0, + 'trusttext' => 0, + 'return_types' => 15, + 'enable_filemanagement' => true, + 'removeorphaneddrafts' => false, + 'autosave' => true + ]; + return $result; + } + + /** + * @param $elementid + * @param $options + * @param $fpoptions + * @return array + * @throws coding_exception + * @throws dml_exception + */ + protected function get_js_plugins($elementid, $options, $fpoptions) { + global $PAGE; + if (array_key_exists('atto:toolbar', $options)) { + $configstr = $options['atto:toolbar']; + } else { + $configstr = get_config('editor_atto', 'toolbar'); + } + + $grouplines = explode("\n", $configstr); + $groups = array(); + + foreach ($grouplines as $groupline) { + $line = explode('=', $groupline); + if (count($line) > 1) { + $group = trim(array_shift($line)); + $plugins = array_map('trim', explode(',', array_shift($line))); + $groups[$group] = $plugins; + } + } + $jsplugins = array(); + foreach ($groups as $group => $plugins) { + $groupplugins = array(); + foreach ($plugins as $plugin) { + // Do not die on missing plugin. + if (!\core_component::get_component_directory('atto_' . $plugin)) { + continue; + } + + // Remove manage files if requested. + if ($plugin == 'managefiles' && isset($options['enable_filemanagement']) && !$options['enable_filemanagement']) { + continue; + } + + $jsplugin = array(); + $jsplugin['name'] = $plugin; + $jsplugin['params'] = array(); + $modules[] = 'moodle-atto_' . $plugin . '-button'; + + component_callback('atto_' . $plugin, 'strings_for_js'); + $extra = component_callback('atto_' . $plugin, 'params_for_js', array($elementid, $options, $fpoptions)); + + if ($extra) { + $jsplugin = array_merge($jsplugin, $extra); + } + // We always need the plugin name. + $PAGE->requires->string_for_js('pluginname', 'atto_' . $plugin); + $groupplugins[] = $jsplugin; + } + $jsplugins[] = array('group'=>$group, 'plugins'=>$groupplugins); + } + return $jsplugins; + } +} diff --git a/classes/warnings.php b/classes/warnings.php index c63aebc..e684a7d 100644 --- a/classes/warnings.php +++ b/classes/warnings.php @@ -389,4 +389,22 @@ private function students_who_are_not_in_any_grouping_group($student_sql, $stude $students = $DB->get_records_sql($sql, $params); return $students; } + + /** + * Alert markers that filter A to Z filter is on + * @return string + * @throws \coding_exception + */ + public function a_to_z_filter_on() { + return $this->alert_div(get_string('namefilternon', 'mod_coursework')); + } + + /** + * Alert markers that there may be more submissions to grade + * @return string + * @throws \coding_exception + */ + public function filters_warning() { + return $this->alert_div(get_string('filteronwarning', 'mod_coursework')); + } } \ No newline at end of file diff --git a/datatables/css/buttons.datatables.min.css b/datatables/css/buttons.datatables.min.css new file mode 100644 index 0000000..9b90ce6 --- /dev/null +++ b/datatables/css/buttons.datatables.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 3px 8px rgba(0,0,0,0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:0.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dt-button-collection-title{text-align:center;padding:0.3em 0 0.5em;font-size:0.9em}div.dt-button-collection-title:empty{display:none}button.dt-button,div.dt-button,a.dt-button{position:relative;display:inline-block;box-sizing:border-box;margin-right:0.333em;margin-bottom:0.333em;padding:0.5em 1em;border:1px solid #999;border-radius:2px;cursor:pointer;font-size:0.88em;line-height:1.6em;color:black;white-space:nowrap;overflow:hidden;background-color:#e9e9e9;background-image:-webkit-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:-moz-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:-ms-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:-o-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:linear-gradient(to bottom, #fff 0%, #e9e9e9 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='white', EndColorStr='#e9e9e9');-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;outline:none;text-overflow:ellipsis}button.dt-button.disabled,div.dt-button.disabled,a.dt-button.disabled{color:#999;border:1px solid #d0d0d0;cursor:default;background-color:#f9f9f9;background-image:-webkit-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:-moz-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:-ms-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:-o-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:linear-gradient(to bottom, #fff 0%, #f9f9f9 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#ffffff', EndColorStr='#f9f9f9')}button.dt-button:active:not(.disabled),button.dt-button.active:not(.disabled),div.dt-button:active:not(.disabled),div.dt-button.active:not(.disabled),a.dt-button:active:not(.disabled),a.dt-button.active:not(.disabled){background-color:#e2e2e2;background-image:-webkit-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:-moz-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:-ms-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:-o-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:linear-gradient(to bottom, #f3f3f3 0%, #e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f3f3f3', EndColorStr='#e2e2e2');box-shadow:inset 1px 1px 3px #999999}button.dt-button:active:not(.disabled):hover:not(.disabled),button.dt-button.active:not(.disabled):hover:not(.disabled),div.dt-button:active:not(.disabled):hover:not(.disabled),div.dt-button.active:not(.disabled):hover:not(.disabled),a.dt-button:active:not(.disabled):hover:not(.disabled),a.dt-button.active:not(.disabled):hover:not(.disabled){box-shadow:inset 1px 1px 3px #999999;background-color:#cccccc;background-image:-webkit-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:-moz-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:-ms-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:-o-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:linear-gradient(to bottom, #eaeaea 0%, #ccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#eaeaea', EndColorStr='#cccccc')}button.dt-button:hover,div.dt-button:hover,a.dt-button:hover{text-decoration:none}button.dt-button:hover:not(.disabled),div.dt-button:hover:not(.disabled),a.dt-button:hover:not(.disabled){border:1px solid #666;background-color:#e0e0e0;background-image:-webkit-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:-moz-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:-ms-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:-o-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:linear-gradient(to bottom, #f9f9f9 0%, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f9f9f9', EndColorStr='#e0e0e0')}button.dt-button:focus:not(.disabled),div.dt-button:focus:not(.disabled),a.dt-button:focus:not(.disabled){border:1px solid #426c9e;text-shadow:0 1px 0 #c4def1;outline:none;background-color:#79ace9;background-image:-webkit-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:-moz-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:-ms-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:-o-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:linear-gradient(to bottom, #bddef4 0%, #79ace9 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#bddef4', EndColorStr='#79ace9')}.dt-button embed{outline:none}div.dt-buttons{position:relative;float:left}div.dt-buttons.buttons-right{float:right}div.dataTables_layout_cell div.dt-buttons{float:none}div.dataTables_layout_cell div.dt-buttons.buttons-right{float:none}div.dt-button-collection{position:absolute;top:0;left:0;width:150px;margin-top:3px;padding:8px 8px 4px 8px;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.4);background-color:white;overflow:hidden;z-index:2002;border-radius:5px;box-shadow:3px 3px 5px rgba(0,0,0,0.3);box-sizing:border-box}div.dt-button-collection button.dt-button,div.dt-button-collection div.dt-button,div.dt-button-collection a.dt-button{position:relative;left:0;right:0;width:100%;display:block;float:none;margin-bottom:4px;margin-right:0}div.dt-button-collection button.dt-button:active:not(.disabled),div.dt-button-collection button.dt-button.active:not(.disabled),div.dt-button-collection div.dt-button:active:not(.disabled),div.dt-button-collection div.dt-button.active:not(.disabled),div.dt-button-collection a.dt-button:active:not(.disabled),div.dt-button-collection a.dt-button.active:not(.disabled){background-color:#dadada;background-image:-webkit-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:-moz-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:-ms-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:-o-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f0f0f0', EndColorStr='#dadada');box-shadow:inset 1px 1px 3px #666}div.dt-button-collection.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);background:-ms-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:-moz-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:-o-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:-webkit-gradient(radial, center center, 0, center center, 497, color-stop(0, rgba(0,0,0,0.3)), color-stop(1, rgba(0,0,0,0.7)));background:-webkit-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:radial-gradient(ellipse farthest-corner at center, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);z-index:2001}@media screen and (max-width: 640px){div.dt-buttons{float:none !important;text-align:center}}button.dt-button.processing,div.dt-button.processing,a.dt-button.processing{color:rgba(0,0,0,0.2)}button.dt-button.processing:after,div.dt-button.processing:after,a.dt-button.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:' ';border:2px solid #282828;border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear} diff --git a/datatables/css/datatables.bootstrap.min.css b/datatables/css/datatables.bootstrap.min.css new file mode 100644 index 0000000..7913766 --- /dev/null +++ b/datatables/css/datatables.bootstrap.min.css @@ -0,0 +1 @@ +table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable{border-right-width:0}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:1px}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} diff --git a/datatables/css/jquery.datetimepicker.css b/datatables/css/jquery.datetimepicker.css new file mode 100644 index 0000000..4ed981a --- /dev/null +++ b/datatables/css/jquery.datetimepicker.css @@ -0,0 +1,568 @@ +.xdsoft_datetimepicker { + box-shadow: 0 5px 15px -5px rgba(0, 0, 0, 0.506); + background: #fff; + border-bottom: 1px solid #bbb; + border-left: 1px solid #ccc; + border-right: 1px solid #ccc; + border-top: 1px solid #ccc; + color: #333; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + padding: 8px; + padding-left: 0; + padding-top: 2px; + position: absolute; + z-index: 9999; + -moz-box-sizing: border-box; + box-sizing: border-box; + display: none; +} +.xdsoft_datetimepicker.xdsoft_rtl { + padding: 8px 0 8px 8px; +} + +.xdsoft_datetimepicker iframe { + position: absolute; + left: 0; + top: 0; + width: 75px; + height: 210px; + background: transparent; + border: none; +} + +/*For IE8 or lower*/ +.xdsoft_datetimepicker button { + border: none !important; +} + +.xdsoft_noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.xdsoft_noselect::selection { background: transparent } +.xdsoft_noselect::-moz-selection { background: transparent } + +.xdsoft_datetimepicker.xdsoft_inline { + display: inline-block; + position: static; + box-shadow: none; +} + +.xdsoft_datetimepicker * { + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; + margin: 0; +} + +.xdsoft_datetimepicker .xdsoft_datepicker, .xdsoft_datetimepicker .xdsoft_timepicker { + display: none; +} + +.xdsoft_datetimepicker .xdsoft_datepicker.active, .xdsoft_datetimepicker .xdsoft_timepicker.active { + display: block; +} + +.xdsoft_datetimepicker .xdsoft_datepicker { + width: 224px; + float: left; + margin-left: 8px; +} +.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_datepicker { + float: right; + margin-right: 8px; + margin-left: 0; +} + +.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_datepicker { + width: 256px; +} + +.xdsoft_datetimepicker .xdsoft_timepicker { + width: 58px; + float: left; + text-align: center; + margin-left: 8px; + margin-top: 0; +} +.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker { + float: right; + margin-right: 8px; + margin-left: 0; +} + +.xdsoft_datetimepicker .xdsoft_datepicker.active+.xdsoft_timepicker { + margin-top: 8px; + margin-bottom: 3px +} + +.xdsoft_datetimepicker .xdsoft_monthpicker { + position: relative; + text-align: center; +} + +.xdsoft_datetimepicker .xdsoft_label i, +.xdsoft_datetimepicker .xdsoft_prev, +.xdsoft_datetimepicker .xdsoft_next, +.xdsoft_datetimepicker .xdsoft_today_button { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Q0NBRjI1NjM0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Q0NBRjI1NjQ0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDQ0FGMjU2MTQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpDQ0FGMjU2MjQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoNEP54AAAIOSURBVHja7Jq9TsMwEMcxrZD4WpBYeKUCe+kTMCACHZh4BFfHO/AAIHZGFhYkBBsSEqxsLCAgXKhbXYOTxh9pfJVP+qutnZ5s/5Lz2Y5I03QhWji2GIcgAokWgfCxNvcOCCGKqiSqhUp0laHOne05vdEyGMfkdxJDVjgwDlEQgYQBgx+ULJaWSXXS6r/ER5FBVR8VfGftTKcITNs+a1XpcFoExREIDF14AVIFxgQUS+h520cdud6wNkC0UBw6BCO/HoCYwBhD8QCkQ/x1mwDyD4plh4D6DDV0TAGyo4HcawLIBBSLDkHeH0Mg2yVP3l4TQMZQDDsEOl/MgHQqhMNuE0D+oBh0CIr8MAKyazBH9WyBuKxDWgbXfjNf32TZ1KWm/Ap1oSk/R53UtQ5xTh3LUlMmT8gt6g51Q9p+SobxgJQ/qmsfZhWywGFSl0yBjCLJCMgXail3b7+rumdVJ2YRss4cN+r6qAHDkPWjPjdJCF4n9RmAD/V9A/Wp4NQassDjwlB6XBiCxcJQWmZZb8THFilfy/lfrTvLghq2TqTHrRMTKNJ0sIhdo15RT+RpyWwFdY96UZ/LdQKBGjcXpcc1AlSFEfLmouD+1knuxBDUVrvOBmoOC/rEcN7OQxKVeJTCiAdUzUJhA2Oez9QTkp72OTVcxDcXY8iKNkxGAJXmJCOQwOa6dhyXsOa6XwEGAKdeb5ET3rQdAAAAAElFTkSuQmCC); +} + +.xdsoft_datetimepicker .xdsoft_label i { + opacity: 0.5; + background-position: -92px -19px; + display: inline-block; + width: 9px; + height: 20px; + vertical-align: middle; +} + +.xdsoft_datetimepicker .xdsoft_prev { + float: left; + background-position: -20px 0; +} +.xdsoft_datetimepicker .xdsoft_today_button { + float: left; + background-position: -70px 0; + margin-left: 5px; +} + +.xdsoft_datetimepicker .xdsoft_next { + float: right; + background-position: 0 0; +} + +.xdsoft_datetimepicker .xdsoft_next, +.xdsoft_datetimepicker .xdsoft_prev , +.xdsoft_datetimepicker .xdsoft_today_button { + background-color: transparent; + background-repeat: no-repeat; + border: 0 none; + cursor: pointer; + display: block; + height: 30px; + opacity: 0.5; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + outline: medium none; + overflow: hidden; + padding: 0; + position: relative; + text-indent: 100%; + white-space: nowrap; + width: 20px; + min-width: 0; +} + +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev, +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_next { + float: none; + background-position: -40px -15px; + height: 15px; + width: 30px; + display: block; + margin-left: 14px; + margin-top: 7px; +} +.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker .xdsoft_prev, +.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker .xdsoft_next { + float: none; + margin-left: 0; + margin-right: 14px; +} + +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev { + background-position: -40px 0; + margin-bottom: 7px; + margin-top: 0; +} + +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box { + height: 151px; + overflow: hidden; + border-bottom: 1px solid #ddd; +} + +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div { + background: #f5f5f5; + border-top: 1px solid #ddd; + color: #666; + font-size: 12px; + text-align: center; + border-collapse: collapse; + cursor: pointer; + border-bottom-width: 0; + height: 25px; + line-height: 25px; +} + +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div > div:first-child { + border-top-width: 0; +} + +.xdsoft_datetimepicker .xdsoft_today_button:hover, +.xdsoft_datetimepicker .xdsoft_next:hover, +.xdsoft_datetimepicker .xdsoft_prev:hover { + opacity: 1; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; +} + +.xdsoft_datetimepicker .xdsoft_label { + display: inline; + position: relative; + z-index: 9999; + margin: 0; + padding: 5px 3px; + font-size: 14px; + line-height: 20px; + font-weight: bold; + background-color: #fff; + float: left; + width: 182px; + text-align: center; + cursor: pointer; +} + +.xdsoft_datetimepicker .xdsoft_label:hover>span { + text-decoration: underline; +} + +.xdsoft_datetimepicker .xdsoft_label:hover i { + opacity: 1.0; +} + +.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select { + border: 1px solid #ccc; + position: absolute; + right: 0; + top: 30px; + z-index: 101; + display: none; + background: #fff; + max-height: 160px; + overflow-y: hidden; +} + +.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select.xdsoft_monthselect{ right: -7px } +.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select.xdsoft_yearselect{ right: 2px } +.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option:hover { + color: #fff; + background: #ff8000; +} + +.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option { + padding: 2px 10px 2px 5px; + text-decoration: none !important; +} + +.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option.xdsoft_current { + background: #33aaff; + box-shadow: #178fe5 0 1px 3px 0 inset; + color: #fff; + font-weight: 700; +} + +.xdsoft_datetimepicker .xdsoft_month { + width: 100px; + text-align: right; +} + +.xdsoft_datetimepicker .xdsoft_calendar { + clear: both; +} + +.xdsoft_datetimepicker .xdsoft_year{ + width: 48px; + margin-left: 5px; +} + +.xdsoft_datetimepicker .xdsoft_calendar table { + border-collapse: collapse; + width: 100%; + +} + +.xdsoft_datetimepicker .xdsoft_calendar td > div { + padding-right: 5px; +} + +.xdsoft_datetimepicker .xdsoft_calendar th { + height: 25px; +} + +.xdsoft_datetimepicker .xdsoft_calendar td,.xdsoft_datetimepicker .xdsoft_calendar th { + width: 14.2857142%; + background: #f5f5f5; + border: 1px solid #ddd; + color: #666; + font-size: 12px; + text-align: right; + vertical-align: middle; + padding: 0; + border-collapse: collapse; + cursor: pointer; + height: 25px; +} +.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar td,.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar th { + width: 12.5%; +} + +.xdsoft_datetimepicker .xdsoft_calendar th { + background: #f1f1f1; +} + +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_today { + color: #33aaff; +} + +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_default { + background: #ffe9d2; + box-shadow: #ffb871 0 1px 4px 0 inset; + color: #000; +} +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_mint { + background: #c1ffc9; + box-shadow: #00dd1c 0 1px 4px 0 inset; + color: #000; +} + +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_default, +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current, +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_current { + background: #33aaff; + box-shadow: #178fe5 0 1px 3px 0 inset; + color: #fff; + font-weight: 700; +} + +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month, +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled, +.xdsoft_datetimepicker .xdsoft_time_box >div >div.xdsoft_disabled { + opacity: 0.5; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + cursor: default; +} + +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month.xdsoft_disabled { + opacity: 0.2; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; +} + +.xdsoft_datetimepicker .xdsoft_calendar td:hover, +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div:hover { + color: #fff !important; + background: #ff8000 !important; + box-shadow: none !important; +} + +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current.xdsoft_disabled:hover, +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div.xdsoft_current.xdsoft_disabled:hover { + background: #33aaff !important; + box-shadow: #178fe5 0 1px 3px 0 inset !important; + color: #fff !important; +} + +.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled:hover, +.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_disabled:hover { + color: inherit !important; + background: inherit !important; + box-shadow: inherit !important; +} + +.xdsoft_datetimepicker .xdsoft_calendar th { + font-weight: 700; + text-align: center; + color: #999; + cursor: default; +} + +.xdsoft_datetimepicker .xdsoft_copyright { + color: #ccc !important; + font-size: 10px; + clear: both; + float: none; + margin-left: 8px; +} + +.xdsoft_datetimepicker .xdsoft_copyright a { color: #eee !important } +.xdsoft_datetimepicker .xdsoft_copyright a:hover { color: #aaa !important } + +.xdsoft_time_box { + position: relative; + border: 1px solid #ccc; +} +.xdsoft_scrollbar >.xdsoft_scroller { + background: #ccc !important; + height: 20px; + border-radius: 3px; +} +.xdsoft_scrollbar { + position: absolute; + width: 7px; + right: 0; + top: 0; + bottom: 0; + cursor: pointer; +} +.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_scrollbar { + left: 0; + right: auto; +} +.xdsoft_scroller_box { + position: relative; +} + +.xdsoft_datetimepicker.xdsoft_dark { + box-shadow: 0 5px 15px -5px rgba(255, 255, 255, 0.506); + background: #000; + border-bottom: 1px solid #444; + border-left: 1px solid #333; + border-right: 1px solid #333; + border-top: 1px solid #333; + color: #ccc; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box { + border-bottom: 1px solid #222; +} +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div { + background: #0a0a0a; + border-top: 1px solid #222; + color: #999; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label { + background-color: #000; +} +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select { + border: 1px solid #333; + background: #000; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select > div > .xdsoft_option:hover { + color: #000; + background: #007fff; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select > div > .xdsoft_option.xdsoft_current { + background: #cc5500; + box-shadow: #b03e00 0 1px 3px 0 inset; + color: #000; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label i, +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_prev, +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_next, +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_today_button { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QUExQUUzOTA0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QUExQUUzOTE0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBQTFBRTM4RTQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQTFBRTM4RjQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pp0VxGEAAAIASURBVHja7JrNSgMxEMebtgh+3MSLr1T1Xn2CHoSKB08+QmR8Bx9A8e7RixdB9CKCoNdexIugxFlJa7rNZneTbLIpM/CnNLsdMvNjM8l0mRCiQ9Ye61IKCAgZAUnH+mU3MMZaHYChBnJUDzWOFZdVfc5+ZFLbrWDeXPwbxIqrLLfaeS0hEBVGIRQCEiZoHQwtlGSByCCdYBl8g8egTTAWoKQMRBRBcZxYlhzhKegqMOageErsCHVkk3hXIFooDgHB1KkHIHVgzKB4ADJQ/A1jAFmAYhkQqA5TOBtocrKrgXwQA8gcFIuAIO8sQSA7hidvPwaQGZSaAYHOUWJABhWWw2EMIH9QagQERU4SArJXo0ZZL18uvaxejXt/Em8xjVBXmvFr1KVm/AJ10tRe2XnraNqaJvKE3KHuUbfK1E+VHB0q40/y3sdQSxY4FHWeKJCunP8UyDdqJZenT3ntVV5jIYCAh20vT7ioP8tpf6E2lfEMwERe+whV1MHjwZB7PBiCxcGQWwKZKD62lfGNnP/1poFAA60T7rF1UgcKd2id3KDeUS+oLWV8DfWAepOfq00CgQabi9zjcgJVYVD7PVzQUAUGAQkbNJTBICDhgwYTjDYD6XeW08ZKh+A4pYkzenOxXUbvZcWz7E8ykRMnIHGX1XPl+1m2vPYpL+2qdb8CDAARlKFEz/ZVkAAAAABJRU5ErkJggg==); +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td, +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th { + background: #0a0a0a; + border: 1px solid #222; + color: #999; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th { + background: #0e0e0e; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_today { + color: #cc5500; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_default { + background: #ffe9d2; + box-shadow: #ffb871 0 1px 4px 0 inset; + color:#000; +} +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_mint { + background: #c1ffc9; + box-shadow: #00dd1c 0 1px 4px 0 inset; + color:#000; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_default, +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_current, +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_current { + background: #cc5500; + box-shadow: #b03e00 0 1px 3px 0 inset; + color: #000; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td:hover, +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div:hover { + color: #000 !important; + background: #007fff !important; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th { + color: #666; +} + +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright { color: #333 !important } +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a { color: #111 !important } +.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a:hover { color: #555 !important } + +.xdsoft_dark .xdsoft_time_box { + border: 1px solid #333; +} + +.xdsoft_dark .xdsoft_scrollbar >.xdsoft_scroller { + background: #333 !important; +} +.xdsoft_datetimepicker .xdsoft_save_selected { + display: block; + border: 1px solid #dddddd !important; + margin-top: 5px; + width: 100%; + color: #454551; + font-size: 13px; +} +.xdsoft_datetimepicker .blue-gradient-button { + font-family: "museo-sans", "Book Antiqua", sans-serif; + font-size: 12px; + font-weight: 300; + color: #82878c; + height: 28px; + position: relative; + padding: 4px 17px 4px 33px; + border: 1px solid #d7d8da; + background: -moz-linear-gradient(top, #fff 0%, #f4f8fa 73%); + /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(73%, #f4f8fa)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #fff 0%, #f4f8fa 73%); + /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #fff 0%, #f4f8fa 73%); + /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #fff 0%, #f4f8fa 73%); + /* IE10+ */ + background: linear-gradient(to bottom, #fff 0%, #f4f8fa 73%); + /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff', endColorstr='#f4f8fa',GradientType=0 ); +/* IE6-9 */ +} +.xdsoft_datetimepicker .blue-gradient-button:hover, .xdsoft_datetimepicker .blue-gradient-button:focus, .xdsoft_datetimepicker .blue-gradient-button:hover span, .xdsoft_datetimepicker .blue-gradient-button:focus span { + color: #454551; + background: -moz-linear-gradient(top, #f4f8fa 0%, #FFF 73%); + /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f4f8fa), color-stop(73%, #FFF)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f4f8fa 0%, #FFF 73%); + /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f4f8fa 0%, #FFF 73%); + /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #f4f8fa 0%, #FFF 73%); + /* IE10+ */ + background: linear-gradient(to bottom, #f4f8fa 0%, #FFF 73%); + /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f8fa', endColorstr='#FFF',GradientType=0 ); + /* IE6-9 */ +} diff --git a/datatables/css/responsive.bootstrap.min.css b/datatables/css/responsive.bootstrap.min.css new file mode 100644 index 0000000..0c1d371 --- /dev/null +++ b/datatables/css/responsive.bootstrap.min.css @@ -0,0 +1 @@ +table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>td.dtr-control,table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>th.dtr-control{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>th.dtr-control:before{top:50%;left:5px;height:14px;width:14px;margin-top:-9px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control:before{left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details>li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul.dtr-details>li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details>li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}div.dtr-bs-modal table.table tr:first-child td{border-top:none} diff --git a/datatables/css/searchpanes.datatables.min.css b/datatables/css/searchpanes.datatables.min.css new file mode 100644 index 0000000..cf3d2cb --- /dev/null +++ b/datatables/css/searchpanes.datatables.min.css @@ -0,0 +1 @@ +div.dtsp-topRow{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-around;align-content:flex-start;align-items:flex-start}div.dtsp-topRow input.dtsp-search{text-overflow:ellipsis}div.dtsp-topRow div.dtsp-subRow1{display:flex;flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:0}div.dtsp-topRow div.dtsp-searchCont{display:flex;flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:0}div.dtsp-topRow button.dtsp-nameButton{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAK2SURBVFgJ7ZY9j41BFICvryCExrJBQ6HyEYVEIREaUZDQIRoR2ViJKCioxV+gkVXYTVZEQiEUhG2EQnxUCh0FKolY4ut5XnM2cyfva3Pt5m7EPcmzZ2bemTNnzjkzd1utnvQi0IvAfxiBy5z5FoxO89kPY+8mbMjtzs47RXs5/WVpbAG6bWExt5PuIibvhVkwmC+ck3eK9ln6/fAddFojYzBVuYSBpcnIEvRaqOw2RcaN18FPuJH0JvRUxbT3wWf4ltiKPgfVidWlbGZgPozDFfgAC+EA/K2EI4cwcAJ+gPaeQ+VQU2SOMMGcPgPl/m/V2p50rrbRsRgt9Iv5h6xtpP22Bz7Ce1C+gFFxfKzOmShcU+Qmyh2w3w8rIJfddHTck66EukL/xPhj+JM8rHNmFys0Pg4v0up3aFNlwR9NYyodd3OL/C64zpsymcTFcf6ElM4YzjAWKYrJkaq8kE/yUYNP4BoYvS1QRo+hNtF5xfkTUjoTheukSFFMjlTFm6PjceOca/SMpKfeCR1L6Uzk/y2WIkVhNFJlJAZhP+hYns7b9D3IPuhY5mYrIv8OrQJvR5NYyNaW4jsU8pSGNySiVx4o5tXq3JkoXE/mg5R/M8dGJCJpKhaDcjBRdbI/Rm8g69c122om33BHmj2CHoV5qa9jUXBraJ+G1fAVjIBO1klc87ro1K4JZ/K35SWW3TwcyDd6TecqnAEd8cGq2+w84xvBm1n3vS0izKkkwh5XNC/GmFPqqAtPF89AOScKuemaNzoTV1SD5dtSbmLf1/RV+tC0WTgcj6R7HEtrVGWaqu/lYDZ/2pvxQ/kIyw/gFByHC9AHw910hv1aUUumyd8yy0QfhmEkfiNod0Xusct68J1qc8Tdux0Z97Q+hsDb+AYGYEbF/4Guw2Q/qDPqZG/zXgT+3Qj8AtKnfWhFwmuAAAAAAElFTkSuQmCC");background-repeat:no-repeat;background-position:center;background-size:23px;vertical-align:bottom}div.dtsp-topRow button.dtsp-countButton{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAG5SURBVEgN3VU9LwVBFF0fiYhofUSlEQkKhU7z/oBCQkIiGr9BgUbhVzy9BAnhFyjV/AYFiU5ICM7ZN+c5Zud5dm3lJmfmzrkz9+7cu3c3y/6jjOBSF8CxXS7FmTkbwqIJjDpJvTcmsJ4K3KPZUpyZsx0sxoB9J6mnAkyC7wGuuCFIipNtEcpcWExgXpOBc78vgj6N+QO4NVsjwdFM59tUIDxDrHMBOeIQ34C5ZDregXuAQm4YcI68nN9B3wr2PcwPAIPkN2EqtJH6b+QZm1ajjTx7BqwAr26Lb+C2Kvpbt0Mb2HAJ7NrGFGfmXO3DeA4UshDfQAVmH0gaUFg852TTTDvlxwBlCtxy9zXyBhQFaq0wMmIdRebrfgosA3zb2hKnqG0oqchp4QbuR8X0TjzABhbdOT8jnQ/atcgqpnfwOA7yqZyTU587ZkIGdesLTt2EkynOnbreMUUKMI/dA4B/QVOcO13CQh+5wWCgDwo/75u59odB/wjmfhbgvACcAOyZPHihMWAoIwxyCLgf1oxfgjzVbgBXSTzIN+f0pg6s5DkcesLMRpsBrgE2XO3CN64JFP7JtUeKHX4CKtRRXFZ+7dEAAAAASUVORK5CYII=");background-repeat:no-repeat;background-position:center;background-size:18px;vertical-align:bottom}div.dtsp-topRow button.dtsp-searchIcon{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAEnSURBVCgVpdG7SgNBFIDh1RhJsBBEsDIgIhaWFjZa2GtpKb6AnU0MprKOWEjK2IuFFxCxS2lhZyOWXh5AQVER/X+zuwwywoIHvp3dM3Nm55Ik/4i+P2or5FewiBIe0cEt8ogVz9LbhEVf+cgkcew1tvAZ5PPXGm9HOMEanMAYQhunaCAazuqA1UjvILl9HGPc/n4fabjPGbzjMM2FjfkDuPw5O8JilzgA9/OKWDynyWnbsPiF7yc4SRWxmEyTN7ZhsSd7gTLW8TuGSSzBcZd2hsV+n+MNC9jGCNzjPDwsz8XCO/x02Bqeptcxhg+4gjD8YxetLOkBGRbuwcIr+NdRLMPl3uMM2YHx2gsLd+D97qKEQuGe65jCAzbgVRWOCUZuovAfs5m/AdVxL0R1AIsLAAAAAElFTkSuQmCC");background-repeat:no-repeat;background-position:center;background-size:12px}div.dt-button-collection{z-index:2002}div.dataTables_scrollBody{background:white !important}div.dtsp-columns-1{min-width:98%;max-width:98%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-2{min-width:48%;max-width:48%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-3{min-width:30.333%;max-width:30.333%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-4{min-width:23%;max-width:23%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-5{min-width:18%;max-width:18%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-6{min-width:15.666%;max-width:15.666%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dtsp-columns-7{min-width:13.28%;max-width:13.28%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dtsp-columns-8{min-width:11.5%;max-width:11.5%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dtsp-columns-9{min-width:11.111%;max-width:11.111%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dt-button-collection{float:none}div.dtsp-panesContainer{width:100%}div.dtsp-panesContainer{font-family:'Roboto', sans-serif;padding:5px;border:1px solid #ccc;border-radius:6px;margin:5px 0;clear:both;text-align:center}div.dtsp-panesContainer div.dtsp-searchPanes{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:stretch;clear:both;text-align:start}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-hidden{display:none !important}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane{flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:280px;justify-content:space-around;align-content:flex-start;align-items:stretch;padding-top:0px;padding-bottom:0px;margin:5px;margin-top:0px;margin-bottom:0px;font-size:0.9em}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper{flex:1;margin:1em 0.5%;margin-top:0px;border-bottom:2px solid #f0f0f0;border:2px solid #f0f0f0;border-radius:4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper:hover{border:2px solid #cfcfcf}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper div.dataTables_filter{display:none}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-selected{border:2px solid #3276b1;border-radius:4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-selected:hover{border:2px solid #286092}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-around;align-content:flex-start;align-items:flex-start}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont{display:flex;flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:0}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search{flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:90px;min-height:20px;max-width:none;min-width:50px;border:none;padding-left:12px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search::placeholder{color:black;font-size:16px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont div.dtsp-searchButtonCont{display:inline-block;flex-direction:row;flex-wrap:nowrap;flex-grow:0;flex-shrink:0;flex-basis:0}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont div.dtsp-searchButtonCont .dtsp-searchIcon{position:relative;display:inline-block;margin:4px;display:block;top:-2px;right:0px;font-size:16px;margin-bottom:0px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow button.dtsp-dull{cursor:context-menu !important;color:#7c7c7c}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow button.dtsp-dull:hover{background-color:transparent}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-paneInputButton,div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton{height:35px;width:35px;max-width:35px;min-width:0;display:inline-block;margin:2px;border:0px solid transparent;background-color:transparent;font-size:16px;margin-bottom:0px;flex-grow:0;flex-shrink:0;flex-basis:35px;font-family:sans-serif}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-paneInputButton:hover,div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton:hover{background-color:#f0f0f0;border-radius:2px;cursor:pointer}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton{opacity:0.6}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-disabledButton{height:35px;width:35px;max-width:35px;min-width:0;display:inline-block;margin:2px;border:0px solid transparent;background-color:transparent;font-size:16px;margin-bottom:0px;flex-grow:0;flex-shrink:0;flex-basis:35px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollHead{display:none !important}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody{border-bottom:none}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody td.dtsp-countColumn{text-align:right}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody td.dtsp-countColumn div.dtsp-pill{background-color:#cfcfcf;text-align:center;border:1px solid #cfcfcf;border-radius:10px;width:auto;display:inline-block;min-width:30px;color:black;font-size:0.9em;padding:0 4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody span.dtsp-pill{float:right;background-color:#cfcfcf;text-align:center;border:1px solid #cfcfcf;border-radius:10px;width:auto;min-width:30px;color:black;font-size:0.9em;padding:0 4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane tr>th,div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane tr>td{padding:5px 10px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane td.dtsp-countColumn{text-align:right}div.dtsp-panesContainer div.dtsp-title{float:left;margin:20px;margin-bottom:0px;margin-top:13px}div.dtsp-panesContainer button.dtsp-clearAll{float:right;margin:20px;border:1px solid transparent;background-color:transparent;padding:10px;margin-bottom:0px;margin-top:5px}div.dtsp-panesContainer button.dtsp-clearAll:hover{background-color:#f0f0f0;border-radius:2px}div.dt-button-collection div.panes{padding:0px;border:none;margin:0px}div.dtsp-hidden{display:none !important}div.dtsp-narrow{flex-direction:column !important}div.dtsp-narrow div.dtsp-subRows{width:100%;text-align:right}@media screen and (max-width: 767px){div.dtsp-columns-4,div.dtsp-columns-5,div.dtsp-columns-6{max-width:31% !important;min-width:31% !important}}@media screen and (max-width: 640px){div.dtsp-searchPanes{flex-direction:column !important}div.dtsp-searchPane{max-width:98% !important;min-width:98% !important}} diff --git a/datatables/css/select.bootstrap.min.css b/datatables/css/select.bootstrap.min.css new file mode 100644 index 0000000..123e40a --- /dev/null +++ b/datatables/css/select.bootstrap.min.css @@ -0,0 +1 @@ +table.dataTable tbody>tr.selected,table.dataTable tbody>tr>.selected{background-color:#08C}table.dataTable.stripe tbody>tr.odd.selected,table.dataTable.stripe tbody>tr.odd>.selected,table.dataTable.display tbody>tr.odd.selected,table.dataTable.display tbody>tr.odd>.selected{background-color:#0085c7}table.dataTable.hover tbody>tr.selected:hover,table.dataTable.hover tbody>tr>.selected:hover,table.dataTable.display tbody>tr.selected:hover,table.dataTable.display tbody>tr>.selected:hover{background-color:#0083c5}table.dataTable.order-column tbody>tr.selected>.sorting_1,table.dataTable.order-column tbody>tr.selected>.sorting_2,table.dataTable.order-column tbody>tr.selected>.sorting_3,table.dataTable.order-column tbody>tr>.selected,table.dataTable.display tbody>tr.selected>.sorting_1,table.dataTable.display tbody>tr.selected>.sorting_2,table.dataTable.display tbody>tr.selected>.sorting_3,table.dataTable.display tbody>tr>.selected{background-color:#0085c8}table.dataTable.display tbody>tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_1{background-color:#0081c1}table.dataTable.display tbody>tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_2{background-color:#0082c2}table.dataTable.display tbody>tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_3{background-color:#0083c4}table.dataTable.display tbody>tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_1{background-color:#0085c8}table.dataTable.display tbody>tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_2{background-color:#0086ca}table.dataTable.display tbody>tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_3{background-color:#0087cb}table.dataTable.display tbody>tr.odd>.selected,table.dataTable.order-column.stripe tbody>tr.odd>.selected{background-color:#0081c1}table.dataTable.display tbody>tr.even>.selected,table.dataTable.order-column.stripe tbody>tr.even>.selected{background-color:#0085c8}table.dataTable.display tbody>tr.selected:hover>.sorting_1,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_1{background-color:#007dbb}table.dataTable.display tbody>tr.selected:hover>.sorting_2,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_2{background-color:#007ebd}table.dataTable.display tbody>tr.selected:hover>.sorting_3,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_3{background-color:#007fbf}table.dataTable.display tbody>tr:hover>.selected,table.dataTable.display tbody>tr>.selected:hover,table.dataTable.order-column.hover tbody>tr:hover>.selected,table.dataTable.order-column.hover tbody>tr>.selected:hover{background-color:#007dbb}table.dataTable tbody td.select-checkbox,table.dataTable tbody th.select-checkbox{position:relative}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody td.select-checkbox:after,table.dataTable tbody th.select-checkbox:before,table.dataTable tbody th.select-checkbox:after{display:block;position:absolute;top:1.2em;left:50%;width:12px;height:12px;box-sizing:border-box}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody th.select-checkbox:before{content:' ';margin-top:-6px;margin-left:-6px;border:1px solid black;border-radius:3px}table.dataTable tr.selected td.select-checkbox:after,table.dataTable tr.selected th.select-checkbox:after{content:'\2714';margin-top:-11px;margin-left:-4px;text-align:center;text-shadow:1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9}div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0.5em}@media screen and (max-width: 640px){div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0;display:block}}table.dataTable tbody tr.selected,table.dataTable tbody th.selected,table.dataTable tbody td.selected{color:white}table.dataTable tbody tr.selected a,table.dataTable tbody th.selected a,table.dataTable tbody td.selected a{color:#a2d4ed} diff --git a/datatables/css/select.dataTables.min.css b/datatables/css/select.dataTables.min.css new file mode 100644 index 0000000..2f5623a --- /dev/null +++ b/datatables/css/select.dataTables.min.css @@ -0,0 +1 @@ +table.dataTable tbody>tr.selected,table.dataTable tbody>tr>.selected{background-color:#B0BED9}table.dataTable.stripe tbody>tr.odd.selected,table.dataTable.stripe tbody>tr.odd>.selected,table.dataTable.display tbody>tr.odd.selected,table.dataTable.display tbody>tr.odd>.selected{background-color:#acbad4}table.dataTable.hover tbody>tr.selected:hover,table.dataTable.hover tbody>tr>.selected:hover,table.dataTable.display tbody>tr.selected:hover,table.dataTable.display tbody>tr>.selected:hover{background-color:#aab7d1}table.dataTable.order-column tbody>tr.selected>.sorting_1,table.dataTable.order-column tbody>tr.selected>.sorting_2,table.dataTable.order-column tbody>tr.selected>.sorting_3,table.dataTable.order-column tbody>tr>.selected,table.dataTable.display tbody>tr.selected>.sorting_1,table.dataTable.display tbody>tr.selected>.sorting_2,table.dataTable.display tbody>tr.selected>.sorting_3,table.dataTable.display tbody>tr>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody>tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody>tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody>tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody>tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody>tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody>tr.odd>.selected,table.dataTable.order-column.stripe tbody>tr.odd>.selected{background-color:#a6b4cd}table.dataTable.display tbody>tr.even>.selected,table.dataTable.order-column.stripe tbody>tr.even>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.selected:hover>.sorting_1,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody>tr.selected:hover>.sorting_2,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody>tr.selected:hover>.sorting_3,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_3{background-color:#a5b2cb}table.dataTable.display tbody>tr:hover>.selected,table.dataTable.display tbody>tr>.selected:hover,table.dataTable.order-column.hover tbody>tr:hover>.selected,table.dataTable.order-column.hover tbody>tr>.selected:hover{background-color:#a2aec7}table.dataTable tbody td.select-checkbox,table.dataTable tbody th.select-checkbox{position:relative}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody td.select-checkbox:after,table.dataTable tbody th.select-checkbox:before,table.dataTable tbody th.select-checkbox:after{display:block;position:absolute;top:1.2em;left:50%;width:12px;height:12px;box-sizing:border-box}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody th.select-checkbox:before{content:' ';margin-top:-6px;margin-left:-6px;border:1px solid black;border-radius:3px}table.dataTable tr.selected td.select-checkbox:after,table.dataTable tr.selected th.select-checkbox:after{content:'\2714';margin-top:-11px;margin-left:-4px;text-align:center;text-shadow:1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9}div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0.5em}@media screen and (max-width: 640px){div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0;display:block}} diff --git a/datatables/images/sort_asc.png b/datatables/images/sort_asc.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ba61a8055fcb18273f2468d335572204667b1f GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3I*bWaz@5R22v2@;zYta_*?F5u6Q zWR@in#&u+WgT?Hi<}D3B3}GOXuX|8Oj3tosHiJ3*4TN zC7>_x-r1O=t(?KoTC+`+>7&2GzdqLHBg&F)2Q?&EGZ+}|Rpsc~9`m>jw35No)z4*} HQ$iB}HK{Sd literal 0 HcmV?d00001 diff --git a/datatables/images/sort_asc_disabled.png b/datatables/images/sort_asc_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..fb11dfe24a6c564cb7ddf8bc96703ebb121df1e7 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S0wixl{&NRX(Vi}jAsXkC6BcOhI9!^3NY?Do zDX;f`c1`y6n0RgO@$!H7chZT&|Jn0dmaqO^XNm-CGtk!Ur<_=Jws3;%W$<+Mb6Mw<&;$T1GdZXL literal 0 HcmV?d00001 diff --git a/datatables/images/sort_both.png b/datatables/images/sort_both.png new file mode 100644 index 0000000000000000000000000000000000000000..af5bc7c5a10b9d6d57cb641aeec752428a07f0ca GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S0wixl{&NRX6FglULp08Bycxyy87-Q;~nRxO8@-UU*I^KVWyN+&SiMHu5xDOu|HNvwzODfTdXjhVyNu1 z#7^XbGKZ7LW3XeONb$RKLeE*WhqbYpIXPIqK@r4)v+qN8um%99%MPpS9d#7Ed7SL@Bp00i_>zopr0H-Zb Aj{pDw literal 0 HcmV?d00001 diff --git a/datatables/images/sort_desc.png b/datatables/images/sort_desc.png new file mode 100644 index 0000000000000000000000000000000000000000..0e156deb5f61d18f9e2ec5da4f6a8c94a5b4fb41 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3I*R8JSj5R22v2@yo z(czD9$NuDl3Ljm9c#_#4$vXUz=f1~&WY3aa=h!;z7fOEN>ySP9QA=6C-^Dmb&tuM= z4Z&=WZU;2WF>e%GI&mWJk^K!jrbro{W;-I>FeCfLGJl3}+Z^2)3Kw?+EoAU?^>bP0 Hl+XkKC^j|Q{b@g3TV7E(Grjn^aLC2o)_ptHrtUEoT$S@q)~)7U@V;W{6)!%@ u>N?4t-1qslpJw9!O?PJ&w0Cbyloading').insertAfter(paginationelement); + $('').insertBefore(wrapperelement.find('.dt-button > span')); + + + $.ajax({ + url: '/mod/coursework/actions/ajax/datatable/allocation.php', + type: 'POST', + data: options + }).done(function(response) { + + var tablerows = ""; + //the table rows returned often have text spaces that jquery turns into objects + //we dont like this so remove all elements in response of type text node + tablerows = $(response).filter(function() { return this.nodeType != Node.TEXT_NODE; }); + tableobject.rows.add($(tablerows)).draw(false); + wrapperelement.find('.pagination-loading').remove(); + wrapperelement.find('thead, .dt-button').css('pointer-events', 'auto'); + wrapperelement.find('.dataTables_paginate, .dataTables_info, .dataTables_length, .dataTables_filter').css('visibility', 'visible'); + + }); + + //aatach all event handlers to elements in the datatable after the datatable has been drawn + tableobject.on('draw',function (i,e) { + M.mod_coursework_datatables.init_allocation_dropdowns(courseworkid); + M.mod_coursework_datatables.init_allocation_pin_checkboxes(courseworkid); + M.mod_coursework_datatables.init_sample_set_checkboxes(courseworkid); + }) + + M.mod_coursework_datatables.init_allocation_checkboxes(); + + + + }); + }, + + init_allocation_dropdowns: function(courseworkid) { + + + $('.assessor_id_dropdown').change(function () { + + }); + + + + $('.assessor_id_dropdown').each(function (e) { + + + + var allocationoptions = { + 'courseworkid': courseworkid, + 'allocatableinfo': $(this).attr('id') + }; + + //unbind any change events as we call rebind on every load we need to make sure + //this code is only called once per change + $(this).unbind('change'); + + $(this).change(function () { + + var $dropdown = $(this); + var $checkbox = $dropdown.prevAll('.sampling_set_checkbox'); + var isAssessorSelectedAlready = []; + + var $currentselection = $dropdown.attr('id'); + + if ($checkbox.length) { + if ($dropdown.val() === '') { + $checkbox.prop('checked', false); + } else { + $checkbox.prop('checked', true); + } + } + + var $row = $dropdown.closest('tr'); + var $selected_val = $dropdown.val(); + + $row.find('td').each(function () { + + // dropdown + var $celldropdown = $(this).find('.assessor_id_dropdown'); + var $celldropdown_id = $celldropdown.attr('id'); + var $celldropdown_val = $celldropdown.val(); + // link + var $atag = $(this).find('a'); + var $id_from_label = $atag.data('assessorid'); + + if ($currentselection != $celldropdown_id && ($celldropdown_val == $selected_val || $id_from_label == $selected_val)) { + // alert('Assessor already allocated. \n Choose different assessor.'); + $('
' + M.util.get_string('sameassessorerror', 'coursework') + '
').insertAfter($('#' + $currentselection)); + $dropdown.val(''); + isAssessorSelectedAlready.push(true); + } else if ($dropdown.val() != '') { + $("#same_assessor").remove(); + } + }); + + + if ($.inArray(true,isAssessorSelectedAlready) == -1) { + processingicon = $('
processing
').insertAfter($(this)); + allocationoptions.value = $(this).val(); + var dropdown = $(this); + + $.ajax({ + url: '/mod/coursework/actions/update_allocated_assessor.php', + type: 'POST', + data: allocationoptions + }).done(function (response) { + var saveresponse = JSON.parse(response); + + if (saveresponse.result != 'false') { + + var parentcell = $(dropdown).parent(); + + var loadedcells = $(saveresponse.content).filter(function () { + return this.nodeType != Node.TEXT_NODE; + }); + + loadedcells.each(function (index, el) { + + if ($(el).attr('class') == $(dropdown).parent().attr('class')) { + parentcell.empty(); + parentcell.html($(el).html()); + } + }); + + M.mod_coursework_datatables.init_allocation_dropdowns(courseworkid); + M.mod_coursework_datatables.init_allocation_pin_checkboxes(courseworkid); + M.mod_coursework_datatables.init_sample_set_checkboxes(courseworkid); + + } + processingicon.remove(); + processingicon.empty(); + + }); + } + }); + + + }); + + + }, + + init_allocation_pin_checkboxes: function(courseworkid) { + $('.pin_1').each(function(index, element) { + + //unbind any click events as we call rebind on every load we need to make sure + //this code is only called once per click + $(element).unbind('click'); + + $(element).click(function () { + + + pinned = ($(element).prop('checked') == true) ? 1 : 0; + + pinneddata = { + + 'allocatableinfo': $(element).prop('name').replaceAll('[',':').replaceAll(']',''), + 'courseworkid': courseworkid, + 'pinned': pinned + } + + + $.ajax({ + url: '/mod/coursework/actions/update_allocated_pinned.php', + type: 'POST', + data: pinneddata + }).done(function (response) { + + }); + }); + + + }); + + + $('.pin_2').each(function(index, element) { + + //unbind any click events as we call rebind on every load we need to make sure + //this code is only called once per click + $(element).unbind('click'); + + $(element).click(function () { + + + pinned = ($(element).prop('checked') == true) ? 1 : 0; + + pinneddata = { + + 'allocatableinfo': $(element).prop('name').replaceAll('[',':').replaceAll(']',''), + 'courseworkid': courseworkid, + 'pinned': pinned + } + + + $.ajax({ + url: '/mod/coursework/actions/update_allocated_pinned.php', + type: 'POST', + data: pinneddata + }).done(function (response) { + + }); + }); + + + }); + + + $('.pin_3').each(function(index, element) { + + //unbind any click events as we call rebind on every load we need to make sure + //this code is only called once per click + $(element).unbind('click'); + + $(element).click(function () { + + + pinned = ($(element).prop('checked') == true) ? 1 : 0; + + pinneddata = { + + 'allocatableinfo': $(element).prop('name').replaceAll('[',':').replaceAll(']',''), + 'courseworkid': courseworkid, + 'pinned': pinned + } + + + $.ajax({ + url: '/mod/coursework/actions/update_allocated_pinned.php', + type: 'POST', + data: pinneddata + }).done(function (response) { + + }); + }); + + + }); + }, + + + init_allocation_checkboxes: function() { + + if (!$('.pin_1')) { + $(this).hide(); + } + + //stop propagation of click event when select all asessor for column is clicked in header + $('#selectall_1').click(function (e) { + e.stopPropagation(); + }); + + $('#selectall_2').click(function (e) { + e.stopPropagation(); + }); + + $('#selectall_3').click(function (e) { + e.stopPropagation(); + }); + + + + var datatable = $('#allocation_table').DataTable(); + + $('#selectall_1').change(function () { + + //get datatables object + //check or uncheck all pin boxes regardless of whether they are on screen or not + + var ischecked = $(this).is(":checked"); + + datatable.$('.pin_1').each(function(index, element) { + if (ischecked) { + $(element).prop('checked', true); + + } else { + $(element).prop('checked', false); + } + }); + + }); + + $('#selectall_2').change(function () { + + var ischecked = $(this).is(":checked"); + + datatable.$('.pin_2').each(function(index, element) { + if (ischecked) { + $(element).prop('checked', true); + + } else { + $(element).prop('checked', false); + } + }); + }); + + $('#selectall_3').change(function () { + + var ischecked = $(this).is(":checked"); + + datatable.$('.pin_3').each(function(index, element) { + if (ischecked) { + $(element).prop('checked', true); + + } else { + $(element).prop('checked', false); + } + }); + }); + + $('#selectall_mod').change(function () { + var ischecked = $(this).is(":checked"); + + datatable.$('.pin_r').each(function(index, element) { + if (ischecked) { + $(element).prop('checked', true); + + } else { + $(element).prop('checked', false); + } + }); + + }); + }, + + + init_sample_set_checkboxes: function (courseworkid) { + + // Unchecked 'Include in sample' checkbox disables + // dropdown automatically. + $('.sampling_set_checkbox').click(function (index,element) { + + var $checkbox = $(this); + var $dropdown = $checkbox.nextAll('.assessor_id_dropdown'); + + var $pinned = $checkbox.nextAll('.existing-assessor'); + var $child = $pinned.children('.pinned'); + + var insampleset = ($checkbox.prop('checked') == true) ? 1 : 0; + + if ($dropdown.length) { + if (insampleset) { + $dropdown.prop("disabled", false); + $child.prop("disabled", false); + + } else { + $dropdown.val(''); + $dropdown.prop("disabled", true); + $child.prop("disabled", true); + $("#same_assessor").remove(); + + $(this).siblings().each(function(index,element) { + if ($(this).hasClass('existing-assessor')) { + $(this).remove(); + } + }); + } + + allocstablesampledata = { + + 'allocatableinfo': $(this).prop('name').replaceAll('[',':').replaceAll(']',''), + 'courseworkid': courseworkid, + 'insample': insampleset + } + + + $.ajax({ + url: '/mod/coursework/actions/update_allocatable_in_sample.php', + type: 'POST', + data: allocstablesampledata + }).done(function (response) { + + }); + } + }); + + + $('.sampling_set_checkbox').each(function () { + + var $checkbox = $(this); + + var $assessddname = $checkbox.attr('id').replace('_samplecheckbox', ''); + + var $assessdd = $('#' + $assessddname); + + if ($checkbox.is(":checked")) { + $assessdd.prop("disabled", false) + } else { + $assessdd.prop("disabled", true); + } + + }); + + + } +} diff --git a/datatables/js/bulkplagiarismflag_datatables.js b/datatables/js/bulkplagiarismflag_datatables.js new file mode 100644 index 0000000..3d08bdd --- /dev/null +++ b/datatables/js/bulkplagiarismflag_datatables.js @@ -0,0 +1,239 @@ +/** + * This defines all javascript needed in the coursework module. + * + * @package mod + * @subpackage coursework + * @copyright 2011 University of London Computer Centre {@link http://ulcc.ac.uk} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +M.mod_coursework_datatables = { + + + + + /** + * This is to set up the listeners etc for the page elements on the allocations page. + */ + init_plagiarism_flag_page: function (e, courseworkid, numberofmarkers, pagesize) { + + var options = { + 'courseworkid' :courseworkid, + 'group':0, + 'sortby':'', + 'sorthow':'', + 'perpage':10 + }; +console.log('calling datatables'); + $('.plagiarism_flag').each(function() { + if(jQuery().DataTable()) { + console.log('Datatables loaded'); + } else { + console.log('Datatables not loaded'); + } + var numbersofcolumns = $("table").find("th:first td").length; + + numberofmarkers= parseInt(numberofmarkers); + var tableobject = $(this).DataTable({ + "language": { + "search": "Search _INPUT_ use \"\" for exact word search" + }, + "pageLength": pagesize + + }); + + var tableelement = $(this); + + var wrapperelement = tableelement.parent('.dataTables_wrapper'); + var paginationelement = wrapperelement.find('.dataTables_paginate'); + + // hide buttons + wrapperelement.find('.dataTables_paginate, .dataTables_info, .dataTables_length, .dataTables_filter').css('visibility', 'hidden'); + wrapperelement.find('thead, .dt-button').each(function() { + var me = $(this); + me.css('pointer-events', 'none'); + if (me.hasClass('dt-button')) { + me.find('span').html(' ' + me.find('span').html()); + } + }); + $('
loading
').insertAfter(paginationelement); + $('').insertBefore(wrapperelement.find('.dt-button > span')); + + + $.ajax({ + url: '/mod/coursework/actions/ajax/datatable/bulkplagiarismflag.php', + type: 'POST', + data: options + }).done(function(response) { + + var tablerows = ""; + //the table rows returned often have text spaces that jquery turns into objects + //we dont like this so remove all elements in response of type text node + tablerows = $(response).filter(function() { return this.nodeType != Node.TEXT_NODE; }); + tableobject.rows.add($(tablerows)).draw(false); + wrapperelement.find('.pagination-loading').remove(); + wrapperelement.find('thead, .dt-button').css('pointer-events', 'auto'); + wrapperelement.find('.dataTables_paginate, .dataTables_info, .dataTables_length, .dataTables_filter').css('visibility', 'visible'); + + }); + + //aatach all event handlers to elements in the datatable after the datatable has been drawn + tableobject.on('draw',function (i,e) { + + + }) +console.log("this is a test"); + M.mod_coursework_datatables.init_allocation_checkboxes(); + M.mod_coursework_datatables.init_submission_button(); + + + + }); + }, + + init_allocation_checkboxes: function() { + //stop propagation of click event when select all asessor for column is clicked in header + $('#selectall').click(function (e) { + e.stopPropagation(); + }); + + + var datatable = $('#plagiarismflag_table').DataTable(); + + + $('#selectall').change(function () { + var ischecked = $(this).is(":checked"); + if (ischecked) { + + datatable.$('.submission_checkbox').each(function (n, element) { + if ($(element).prop('disabled')== false) { + $(element).prop('checked', true); + } + }) + } else { + datatable.$('.submission_checkbox').prop('checked', false); + } + }); + + + + + }, + + init_submission_button: function () { + +var datatable = $('#plagiarismflag_table').DataTable(); + $('#selected_dates').click(function () { + + var submissionselected = false; + + var selectedsubmissions = []; + +console.log(datatable); + datatable.$('.submission_checkbox').each(function (n, element) { + + + + if ($(element).is(":checked")) { + submissionselected = true; + selectedsubmissions.push($(element).val()); + } + + }) + + if (submissionselected == true) { + + + + console.log('updating submissions'); + $.each(selectedsubmissions, function(i,subid){ + $('').attr('type', 'hidden') + .attr('name', 'submissionid_arr['+subid+']') + .attr('value', subid) + .appendTo('#coursework_plagiarism_flag_form'); + }); + console.log('added submissions'); + + + $('#coursework_plagiarism_flag_form').submit(); + } else { + alert('You must make at least one selection'); + } + + + }); + + }, + + + init_sample_set_checkboxes: function (courseworkid) { + + // Unchecked 'Include in sample' checkbox disables + // dropdown automatically. + $('.sampling_set_checkbox').click(function (index,element) { + + var $checkbox = $(this); + var $dropdown = $checkbox.nextAll('.assessor_id_dropdown'); + + var $pinned = $checkbox.nextAll('.existing-assessor'); + var $child = $pinned.children('.pinned'); + + var insampleset = ($checkbox.prop('checked') == true) ? 1 : 0; + + if ($dropdown.length) { + if (insampleset) { + $dropdown.prop("disabled", false); + $child.prop("disabled", false); + + } else { + $dropdown.val(''); + $dropdown.prop("disabled", true); + $child.prop("disabled", true); + $("#same_assessor").remove(); + + $(this).siblings().each(function(index,element) { + if ($(this).hasClass('existing-assessor')) { + $(this).remove(); + } + }); + } + + allocstablesampledata = { + + 'allocatableinfo': $(this).prop('name').replaceAll('[',':').replaceAll(']',''), + 'courseworkid': courseworkid, + 'insample': insampleset + } + + + $.ajax({ + url: '/mod/coursework/actions/update_allocatable_in_sample.php', + type: 'POST', + data: allocstablesampledata + }).done(function (response) { + + }); + } + }); + + + $('.sampling_set_checkbox').each(function () { + + var $checkbox = $(this); + + var $assessddname = $checkbox.attr('id').replace('_samplecheckbox', ''); + + var $assessdd = $('#' + $assessddname); + + if ($checkbox.is(":checked")) { + $assessdd.prop("disabled", false) + } else { + $assessdd.prop("disabled", true); + } + + }); + + + } +} diff --git a/datatables/js/datatables.buttons.js b/datatables/js/datatables.buttons.js new file mode 100644 index 0000000..22e7dc7 --- /dev/null +++ b/datatables/js/datatables.buttons.js @@ -0,0 +1,2104 @@ +/*! Buttons for DataTables 1.6.2 + * ©2016-2020 SpryMedia Ltd - datatables.net/license + */ + +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + root = window; + } + + if ( ! $ || ! $.fn.dataTable ) { + $ = require('datatables.net')(root, $).$; + } + + return factory( $, root, root.document ); + }; + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { +'use strict'; +var DataTable = $.fn.dataTable; + + +// Used for namespacing events added to the document by each instance, so they +// can be removed on destroy +var _instCounter = 0; + +// Button namespacing counter for namespacing events on individual buttons +var _buttonCounter = 0; + +var _dtButtons = DataTable.ext.buttons; + +// Allow for jQuery slim +function _fadeIn(el, duration, fn) { + if ($.fn.animate) { + el + .stop() + .fadeIn( duration, fn ); + } + else { + el.css('display', 'block'); + + if (fn) { + fn.call(el); + } + } +} + +function _fadeOut(el, duration, fn) { + if ($.fn.animate) { + el + .stop() + .fadeOut( duration, fn ); + } + else { + el.css('display', 'none'); + + if (fn) { + fn.call(el); + } + } +} + +/** + * [Buttons description] + * @param {[type]} + * @param {[type]} + */ +var Buttons = function( dt, config ) +{ + // If not created with a `new` keyword then we return a wrapper function that + // will take the settings object for a DT. This allows easy use of new instances + // with the `layout` option - e.g. `topLeft: $.fn.dataTable.Buttons( ... )`. + if ( !(this instanceof Buttons) ) { + return function (settings) { + return new Buttons( settings, dt ).container(); + }; + } + + // If there is no config set it to an empty object + if ( typeof( config ) === 'undefined' ) { + config = {}; + } + + // Allow a boolean true for defaults + if ( config === true ) { + config = {}; + } + + // For easy configuration of buttons an array can be given + if ( $.isArray( config ) ) { + config = { buttons: config }; + } + + this.c = $.extend( true, {}, Buttons.defaults, config ); + + // Don't want a deep copy for the buttons + if ( config.buttons ) { + this.c.buttons = config.buttons; + } + + this.s = { + dt: new DataTable.Api( dt ), + buttons: [], + listenKeys: '', + namespace: 'dtb'+(_instCounter++) + }; + + this.dom = { + container: $('<'+this.c.dom.container.tag+'/>') + .addClass( this.c.dom.container.className ) + }; + + this._constructor(); +}; + + +$.extend( Buttons.prototype, { + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Public methods + */ + + /** + * Get the action of a button + * @param {int|string} Button index + * @return {function} + *//** + * Set the action of a button + * @param {node} node Button element + * @param {function} action Function to set + * @return {Buttons} Self for chaining + */ + action: function ( node, action ) + { + var button = this._nodeToButton( node ); + + if ( action === undefined ) { + return button.conf.action; + } + + button.conf.action = action; + + return this; + }, + + /** + * Add an active class to the button to make to look active or get current + * active state. + * @param {node} node Button element + * @param {boolean} [flag] Enable / disable flag + * @return {Buttons} Self for chaining or boolean for getter + */ + active: function ( node, flag ) { + var button = this._nodeToButton( node ); + var klass = this.c.dom.button.active; + var jqNode = $(button.node); + + if ( flag === undefined ) { + return jqNode.hasClass( klass ); + } + + jqNode.toggleClass( klass, flag === undefined ? true : flag ); + + return this; + }, + + /** + * Add a new button + * @param {object} config Button configuration object, base string name or function + * @param {int|string} [idx] Button index for where to insert the button + * @return {Buttons} Self for chaining + */ + add: function ( config, idx ) + { + var buttons = this.s.buttons; + + if ( typeof idx === 'string' ) { + var split = idx.split('-'); + var base = this.s; + + for ( var i=0, ien=split.length-1 ; i=0 ; i-- ) { + this.remove( button.buttons[i].node ); + } + } + + // Allow the button to remove event handlers, etc + if ( button.conf.destroy ) { + button.conf.destroy.call( dt.button(node), dt, $(node), button.conf ); + } + + this._removeKey( button.conf ); + + $(button.node).remove(); + + var idx = $.inArray( button, host ); + host.splice( idx, 1 ); + + return this; + }, + + /** + * Get the text for a button + * @param {int|string} node Button index + * @return {string} Button text + *//** + * Set the text for a button + * @param {int|string|function} node Button index + * @param {string} label Text + * @return {Buttons} Self for chaining + */ + text: function ( node, label ) + { + var button = this._nodeToButton( node ); + var buttonLiner = this.c.dom.collection.buttonLiner; + var linerTag = button.inCollection && buttonLiner && buttonLiner.tag ? + buttonLiner.tag : + this.c.dom.buttonLiner.tag; + var dt = this.s.dt; + var jqNode = $(button.node); + var text = function ( opt ) { + return typeof opt === 'function' ? + opt( dt, jqNode, button.conf ) : + opt; + }; + + if ( label === undefined ) { + return text( button.conf.text ); + } + + button.conf.text = label; + + if ( linerTag ) { + jqNode.children( linerTag ).html( text(label) ); + } + else { + jqNode.html( text(label) ); + } + + return this; + }, + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Constructor + */ + + /** + * Buttons constructor + * @private + */ + _constructor: function () + { + var that = this; + var dt = this.s.dt; + var dtSettings = dt.settings()[0]; + var buttons = this.c.buttons; + + if ( ! dtSettings._buttons ) { + dtSettings._buttons = []; + } + + dtSettings._buttons.push( { + inst: this, + name: this.c.name + } ); + + for ( var i=0, ien=buttons.length ; i'); + + built.conf._collection = built.collection; + + this._expandButton( built.buttons, built.conf.buttons, true, attachPoint ); + } + + // init call is made here, rather than buildButton as it needs to + // be selectable, and for that it needs to be in the buttons array + if ( conf.init ) { + conf.init.call( dt.button( built.node ), dt, $(built.node), conf ); + } + + buttonCounter++; + } + }, + + /** + * Create an individual button + * @param {object} config Resolved button configuration + * @param {boolean} inCollection `true` if a collection button + * @return {jQuery} Created button node (jQuery) + * @private + */ + _buildButton: function ( config, inCollection ) + { + var buttonDom = this.c.dom.button; + var linerDom = this.c.dom.buttonLiner; + var collectionDom = this.c.dom.collection; + var dt = this.s.dt; + var text = function ( opt ) { + return typeof opt === 'function' ? + opt( dt, button, config ) : + opt; + }; + + if ( inCollection && collectionDom.button ) { + buttonDom = collectionDom.button; + } + + if ( inCollection && collectionDom.buttonLiner ) { + linerDom = collectionDom.buttonLiner; + } + + // Make sure that the button is available based on whatever requirements + // it has. For example, Flash buttons require Flash + if ( config.available && ! config.available( dt, config ) ) { + return false; + } + + var action = function ( e, dt, button, config ) { + config.action.call( dt.button( button ), e, dt, button, config ); + + $(dt.table().node()).triggerHandler( 'buttons-action.dt', [ + dt.button( button ), dt, button, config + ] ); + }; + + var tag = config.tag || buttonDom.tag; + var clickBlurs = config.clickBlurs === undefined ? true : config.clickBlurs + var button = $('<'+tag+'/>') + .addClass( buttonDom.className ) + .attr( 'tabindex', this.s.dt.settings()[0].iTabIndex ) + .attr( 'aria-controls', this.s.dt.table().node().id ) + .on( 'click.dtb', function (e) { + e.preventDefault(); + + if ( ! button.hasClass( buttonDom.disabled ) && config.action ) { + action( e, dt, button, config ); + } + if( clickBlurs ) { + button.trigger('blur'); + } + } ) + .on( 'keyup.dtb', function (e) { + if ( e.keyCode === 13 ) { + if ( ! button.hasClass( buttonDom.disabled ) && config.action ) { + action( e, dt, button, config ); + } + } + } ); + + // Make `a` tags act like a link + if ( tag.toLowerCase() === 'a' ) { + button.attr( 'href', '#' ); + } + + // Button tags should have `type=button` so they don't have any default behaviour + if ( tag.toLowerCase() === 'button' ) { + button.attr( 'type', 'button' ); + } + + if ( linerDom.tag ) { + var liner = $('<'+linerDom.tag+'/>') + .html( text( config.text ) ) + .addClass( linerDom.className ); + + if ( linerDom.tag.toLowerCase() === 'a' ) { + liner.attr( 'href', '#' ); + } + + button.append( liner ); + } + else { + button.html( text( config.text ) ); + } + + if ( config.enabled === false ) { + button.addClass( buttonDom.disabled ); + } + + if ( config.className ) { + button.addClass( config.className ); + } + + if ( config.titleAttr ) { + button.attr( 'title', text( config.titleAttr ) ); + } + + if ( config.attr ) { + button.attr( config.attr ); + } + + if ( ! config.namespace ) { + config.namespace = '.dt-button-'+(_buttonCounter++); + } + + var buttonContainer = this.c.dom.buttonContainer; + var inserter; + if ( buttonContainer && buttonContainer.tag ) { + inserter = $('<'+buttonContainer.tag+'/>') + .addClass( buttonContainer.className ) + .append( button ); + } + else { + inserter = button; + } + + this._addKey( config ); + + // Style integration callback for DOM manipulation + // Note that this is _not_ documented. It is currently + // for style integration only + if( this.c.buttonCreated ) { + inserter = this.c.buttonCreated( config, inserter ); + } + + return { + conf: config, + node: button.get(0), + inserter: inserter, + buttons: [], + inCollection: inCollection, + collection: null + }; + }, + + /** + * Get the button object from a node (recursive) + * @param {node} node Button node + * @param {array} [buttons] Button array, uses base if not defined + * @return {object} Button object + * @private + */ + _nodeToButton: function ( node, buttons ) + { + if ( ! buttons ) { + buttons = this.s.buttons; + } + + for ( var i=0, ien=buttons.length ; i 30 ) { + // Protect against misconfiguration killing the browser + throw 'Buttons: Too many iterations'; + } + } + + return $.isArray( base ) ? + base : + $.extend( {}, base ); + }; + + conf = toConfObject( conf ); + + while ( conf && conf.extend ) { + // Use `toConfObject` in case the button definition being extended + // is itself a string or a function + if ( ! _dtButtons[ conf.extend ] ) { + throw 'Cannot extend unknown button type: '+conf.extend; + } + + var objArray = toConfObject( _dtButtons[ conf.extend ] ); + if ( $.isArray( objArray ) ) { + return objArray; + } + else if ( ! objArray ) { + // This is a little brutal as it might be possible to have a + // valid button without the extend, but if there is no extend + // then the host button would be acting in an undefined state + return false; + } + + // Stash the current class name + var originalClassName = objArray.className; + + conf = $.extend( {}, objArray, conf ); + + // The extend will have overwritten the original class name if the + // `conf` object also assigned a class, but we want to concatenate + // them so they are list that is combined from all extended buttons + if ( originalClassName && conf.className !== originalClassName ) { + conf.className = originalClassName+' '+conf.className; + } + + // Buttons to be added to a collection -gives the ability to define + // if buttons should be added to the start or end of a collection + var postfixButtons = conf.postfixButtons; + if ( postfixButtons ) { + if ( ! conf.buttons ) { + conf.buttons = []; + } + + for ( i=0, ien=postfixButtons.length ; i') + .addClass('dt-button-collection') + .addClass(options.collectionLayout) + .css('display', 'none'); + + content = $(content) + .addClass(options.contentClassName) + .attr('role', 'menu') + .appendTo(display); + + hostNode.attr( 'aria-expanded', 'true' ); + + if ( hostNode.parents('body')[0] !== document.body ) { + hostNode = document.body.lastChild; + } + + if ( options.collectionTitle ) { + display.prepend('
'+options.collectionTitle+'
'); + } + + _fadeIn( display.insertAfter( hostNode ) ); + + var tableContainer = $( hostButton.table().container() ); + var position = display.css( 'position' ); + + if ( options.align === 'dt-container' ) { + hostNode = hostNode.parent(); + display.css('width', tableContainer.width()); + } + + if ( position === 'absolute' ) { + + var hostPosition = hostNode.position(); + + display.css( { + top: hostPosition.top + hostNode.outerHeight(), + left: hostPosition.left + } ); + + // calculate overflow when positioned beneath + var collectionHeight = display.outerHeight(); + var collectionWidth = display.outerWidth(); + var tableBottom = tableContainer.offset().top + tableContainer.height(); + var listBottom = hostPosition.top + hostNode.outerHeight() + collectionHeight; + var bottomOverflow = listBottom - tableBottom; + + // calculate overflow when positioned above + var listTop = hostPosition.top - collectionHeight; + var tableTop = tableContainer.offset().top; + var topOverflow = tableTop - listTop; + + // if bottom overflow is larger, move to the top because it fits better, or if dropup is requested + var moveTop = hostPosition.top - collectionHeight - 5; + if ( (bottomOverflow > topOverflow || options.dropup) && -moveTop < tableTop ) { + display.css( 'top', moveTop); + } + + // Get the size of the container (left and width - and thus also right) + var tableLeft = tableContainer.offset().left; + var tableWidth = tableContainer.width(); + var tableRight = tableLeft + tableWidth; + + // Get the size of the popover (left and width - and ...) + var popoverLeft = display.offset().left; + var popoverWidth = display.width(); + var popoverRight = popoverLeft + popoverWidth; + + // Get the size of the host buttons (left and width - and ...) + var buttonsLeft = hostNode.offset().left; + var buttonsWidth = hostNode.outerWidth() + var buttonsRight = buttonsLeft + buttonsWidth; + + // You've then got all the numbers you need to do some calculations and if statements, + // so we can do some quick JS maths and apply it only once + // If it has the right align class OR the buttons are right aligned, + // then calculate left position for the popover to align the popover to the right hand + // side of the button - check to see if the left of the popover is inside the table container. + // If not, move the popover so it is, but not more than it means that the popover is to the right of the table container + var popoverShuffle = 0; + if ( display.hasClass( options.rightAlignClassName ) || options.align === 'button-right' ) { + popoverShuffle = buttonsRight - popoverRight; + if(tableLeft > (popoverLeft + popoverShuffle)){ + var leftGap = tableLeft - (popoverLeft + popoverShuffle); + var rightGap = tableRight - (popoverRight + popoverShuffle); + + if(leftGap > rightGap){ + popoverShuffle += rightGap; + } + else { + popoverShuffle += leftGap; + } + } + } + // else attempt to left align the popover to the button. Similar to above, if the popover's right goes past the table container's right, + // then move it back, but not so much that it goes past the left of the table container + else { + popoverShuffle = tableLeft - popoverLeft; + + if(tableRight < (popoverRight + popoverShuffle)){ + var leftGap = tableLeft - (popoverLeft + popoverShuffle); + var rightGap = tableRight - (popoverRight + popoverShuffle); + + if(leftGap > rightGap ){ + popoverShuffle += rightGap; + } + else { + popoverShuffle += leftGap; + } + + } + } + + display.css('left', display.position().left + popoverShuffle); + + } + else { + // Fix position - centre on screen + var top = display.height() / 2; + if ( top > $(window).height() / 2 ) { + top = $(window).height() / 2; + } + + display.css( 'marginTop', top*-1 ); + } + + if ( options.background ) { + Buttons.background( true, options.backgroundClassName, options.fade, hostNode ); + } + + // This is bonkers, but if we don't have a click listener on the + // background element, iOS Safari will ignore the body click + // listener below. An empty function here is all that is + // required to make it work... + $('div.dt-button-background').on( 'click.dtb-collection', function () {} ); + + $('body') + .on( 'click.dtb-collection', function (e) { + // andSelf is deprecated in jQ1.8, but we want 1.7 compat + var back = $.fn.addBack ? 'addBack' : 'andSelf'; + + if ( ! $(e.target).parents()[back]().filter( content ).length ) { + close(); + } + } ) + .on( 'keyup.dtb-collection', function (e) { + if ( e.keyCode === 27 ) { + close(); + } + } ); + + if ( options.autoClose ) { + setTimeout( function () { + dt.on( 'buttons-action.b-internal', function (e, btn, dt, node) { + if ( node[0] === hostNode[0] ) { + return; + } + close(); + } ); + }, 0); + } + + $(display).trigger('buttons-popover.dt'); + } +} ); + + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Statics + */ + +/** + * Show / hide a background layer behind a collection + * @param {boolean} Flag to indicate if the background should be shown or + * hidden + * @param {string} Class to assign to the background + * @static + */ +Buttons.background = function ( show, className, fade, insertPoint ) { + if ( fade === undefined ) { + fade = 400; + } + if ( ! insertPoint ) { + insertPoint = document.body; + } + + if ( show ) { + _fadeIn( + $('
') + .addClass( className ) + .css( 'display', 'none' ) + .insertAfter( insertPoint ), + fade + ); + } + else { + _fadeOut( + $('div.'+className), + fade, + function () { + $(this) + .removeClass( className ) + .remove(); + } + ); + } +}; + +/** + * Instance selector - select Buttons instances based on an instance selector + * value from the buttons assigned to a DataTable. This is only useful if + * multiple instances are attached to a DataTable. + * @param {string|int|array} Instance selector - see `instance-selector` + * documentation on the DataTables site + * @param {array} Button instance array that was attached to the DataTables + * settings object + * @return {array} Buttons instances + * @static + */ +Buttons.instanceSelector = function ( group, buttons ) +{ + if ( group === undefined || group === null ) { + return $.map( buttons, function ( v ) { + return v.inst; + } ); + } + + var ret = []; + var names = $.map( buttons, function ( v ) { + return v.name; + } ); + + // Flatten the group selector into an array of single options + var process = function ( input ) { + if ( $.isArray( input ) ) { + for ( var i=0, ien=input.length ; i` in IE - it has to be `` + tag: 'ActiveXObject' in window ? + 'a' : + 'button', + className: 'dt-button', + active: 'active', + disabled: 'disabled' + }, + buttonLiner: { + tag: 'span', + className: '' + } + } +}; + +/** + * Version information + * @type {string} + * @static + */ +Buttons.version = '1.6.2'; + + +$.extend( _dtButtons, { + collection: { + text: function ( dt ) { + return dt.i18n( 'buttons.collection', 'Collection' ); + }, + className: 'buttons-collection', + init: function ( dt, button, config ) { + button.attr( 'aria-expanded', false ); + }, + action: function ( e, dt, button, config ) { + e.stopPropagation(); + + if ( config._collection.parents('body').length ) { + this.popover(false, config); + } + else { + this.popover(config._collection, config); + } + }, + attr: { + 'aria-haspopup': true + } + // Also the popover options, defined in Buttons.popover + }, + copy: function ( dt, conf ) { + if ( _dtButtons.copyHtml5 ) { + return 'copyHtml5'; + } + if ( _dtButtons.copyFlash && _dtButtons.copyFlash.available( dt, conf ) ) { + return 'copyFlash'; + } + }, + csv: function ( dt, conf ) { + // Common option that will use the HTML5 or Flash export buttons + if ( _dtButtons.csvHtml5 && _dtButtons.csvHtml5.available( dt, conf ) ) { + return 'csvHtml5'; + } + if ( _dtButtons.csvFlash && _dtButtons.csvFlash.available( dt, conf ) ) { + return 'csvFlash'; + } + }, + excel: function ( dt, conf ) { + // Common option that will use the HTML5 or Flash export buttons + if ( _dtButtons.excelHtml5 && _dtButtons.excelHtml5.available( dt, conf ) ) { + return 'excelHtml5'; + } + if ( _dtButtons.excelFlash && _dtButtons.excelFlash.available( dt, conf ) ) { + return 'excelFlash'; + } + }, + pdf: function ( dt, conf ) { + // Common option that will use the HTML5 or Flash export buttons + if ( _dtButtons.pdfHtml5 && _dtButtons.pdfHtml5.available( dt, conf ) ) { + return 'pdfHtml5'; + } + if ( _dtButtons.pdfFlash && _dtButtons.pdfFlash.available( dt, conf ) ) { + return 'pdfFlash'; + } + }, + pageLength: function ( dt ) { + var lengthMenu = dt.settings()[0].aLengthMenu; + var vals = $.isArray( lengthMenu[0] ) ? lengthMenu[0] : lengthMenu; + var lang = $.isArray( lengthMenu[0] ) ? lengthMenu[1] : lengthMenu; + var text = function ( dt ) { + return dt.i18n( 'buttons.pageLength', { + "-1": 'Show all rows', + _: 'Show %d rows' + }, dt.page.len() ); + }; + + return { + extend: 'collection', + text: text, + className: 'buttons-page-length', + autoClose: true, + buttons: $.map( vals, function ( val, i ) { + return { + text: lang[i], + className: 'button-page-length', + action: function ( e, dt ) { + dt.page.len( val ).draw(); + }, + init: function ( dt, node, conf ) { + var that = this; + var fn = function () { + that.active( dt.page.len() === val ); + }; + + dt.on( 'length.dt'+conf.namespace, fn ); + fn(); + }, + destroy: function ( dt, node, conf ) { + dt.off( 'length.dt'+conf.namespace ); + } + }; + } ), + init: function ( dt, node, conf ) { + var that = this; + dt.on( 'length.dt'+conf.namespace, function () { + that.text( conf.text ); + } ); + }, + destroy: function ( dt, node, conf ) { + dt.off( 'length.dt'+conf.namespace ); + } + }; + } +} ); + + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * DataTables API + * + * For complete documentation, please refer to the docs/api directory or the + * DataTables site + */ + +// Buttons group and individual button selector +DataTable.Api.register( 'buttons()', function ( group, selector ) { + // Argument shifting + if ( selector === undefined ) { + selector = group; + group = undefined; + } + + this.selector.buttonGroup = group; + + var res = this.iterator( true, 'table', function ( ctx ) { + if ( ctx._buttons ) { + return Buttons.buttonSelector( + Buttons.instanceSelector( group, ctx._buttons ), + selector + ); + } + }, true ); + + res._groupSelector = group; + return res; +} ); + +// Individual button selector +DataTable.Api.register( 'button()', function ( group, selector ) { + // just run buttons() and truncate + var buttons = this.buttons( group, selector ); + + if ( buttons.length > 1 ) { + buttons.splice( 1, buttons.length ); + } + + return buttons; +} ); + +// Active buttons +DataTable.Api.registerPlural( 'buttons().active()', 'button().active()', function ( flag ) { + if ( flag === undefined ) { + return this.map( function ( set ) { + return set.inst.active( set.node ); + } ); + } + + return this.each( function ( set ) { + set.inst.active( set.node, flag ); + } ); +} ); + +// Get / set button action +DataTable.Api.registerPlural( 'buttons().action()', 'button().action()', function ( action ) { + if ( action === undefined ) { + return this.map( function ( set ) { + return set.inst.action( set.node ); + } ); + } + + return this.each( function ( set ) { + set.inst.action( set.node, action ); + } ); +} ); + +// Enable / disable buttons +DataTable.Api.register( ['buttons().enable()', 'button().enable()'], function ( flag ) { + return this.each( function ( set ) { + set.inst.enable( set.node, flag ); + } ); +} ); + +// Disable buttons +DataTable.Api.register( ['buttons().disable()', 'button().disable()'], function () { + return this.each( function ( set ) { + set.inst.disable( set.node ); + } ); +} ); + +// Get button nodes +DataTable.Api.registerPlural( 'buttons().nodes()', 'button().node()', function () { + var jq = $(); + + // jQuery will automatically reduce duplicates to a single entry + $( this.each( function ( set ) { + jq = jq.add( set.inst.node( set.node ) ); + } ) ); + + return jq; +} ); + +// Get / set button processing state +DataTable.Api.registerPlural( 'buttons().processing()', 'button().processing()', function ( flag ) { + if ( flag === undefined ) { + return this.map( function ( set ) { + return set.inst.processing( set.node ); + } ); + } + + return this.each( function ( set ) { + set.inst.processing( set.node, flag ); + } ); +} ); + +// Get / set button text (i.e. the button labels) +DataTable.Api.registerPlural( 'buttons().text()', 'button().text()', function ( label ) { + if ( label === undefined ) { + return this.map( function ( set ) { + return set.inst.text( set.node ); + } ); + } + + return this.each( function ( set ) { + set.inst.text( set.node, label ); + } ); +} ); + +// Trigger a button's action +DataTable.Api.registerPlural( 'buttons().trigger()', 'button().trigger()', function () { + return this.each( function ( set ) { + set.inst.node( set.node ).trigger( 'click' ); + } ); +} ); + +// Button resolver to the popover +DataTable.Api.register( 'button().popover()', function (content, options) { + return this.map( function ( set ) { + return set.inst._popover( content, this.button(this[0].node), options ); + } ); +} ); + +// Get the container elements +DataTable.Api.register( 'buttons().containers()', function () { + var jq = $(); + var groupSelector = this._groupSelector; + + // We need to use the group selector directly, since if there are no buttons + // the result set will be empty + this.iterator( true, 'table', function ( ctx ) { + if ( ctx._buttons ) { + var insts = Buttons.instanceSelector( groupSelector, ctx._buttons ); + + for ( var i=0, ien=insts.length ; i'+title+'' : ''; + + _fadeIn( + $('
') + .html( title ) + .append( $('
')[ typeof message === 'string' ? 'html' : 'append' ]( message ) ) + .css( 'display', 'none' ) + .appendTo( 'body' ) + ); + + if ( time !== undefined && time !== 0 ) { + _infoTimer = setTimeout( function () { + that.buttons.info( false ); + }, time ); + } + + this.on('destroy.btn-info', function () { + that.buttons.info(false); + }); + + return this; +} ); + +// Get data from the table for export - this is common to a number of plug-in +// buttons so it is included in the Buttons core library +DataTable.Api.register( 'buttons.exportData()', function ( options ) { + if ( this.context.length ) { + return _exportData( new DataTable.Api( this.context[0] ), options ); + } +} ); + +// Get information about the export that is common to many of the export data +// types (DRY) +DataTable.Api.register( 'buttons.exportInfo()', function ( conf ) { + if ( ! conf ) { + conf = {}; + } + + return { + filename: _filename( conf ), + title: _title( conf ), + messageTop: _message(this, conf.message || conf.messageTop, 'top'), + messageBottom: _message(this, conf.messageBottom, 'bottom') + }; +} ); + + + +/** + * Get the file name for an exported file. + * + * @param {object} config Button configuration + * @param {boolean} incExtension Include the file name extension + */ +var _filename = function ( config ) +{ + // Backwards compatibility + var filename = config.filename === '*' && config.title !== '*' && config.title !== undefined && config.title !== null && config.title !== '' ? + config.title : + config.filename; + + if ( typeof filename === 'function' ) { + filename = filename(); + } + + if ( filename === undefined || filename === null ) { + return null; + } + + if ( filename.indexOf( '*' ) !== -1 ) { + filename = $.trim( filename.replace( '*', $('head > title').text() ) ); + } + + // Strip characters which the OS will object to + filename = filename.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g, ""); + + var extension = _stringOrFunction( config.extension ); + if ( ! extension ) { + extension = ''; + } + + return filename + extension; +}; + +/** + * Simply utility method to allow parameters to be given as a function + * + * @param {undefined|string|function} option Option + * @return {null|string} Resolved value + */ +var _stringOrFunction = function ( option ) +{ + if ( option === null || option === undefined ) { + return null; + } + else if ( typeof option === 'function' ) { + return option(); + } + return option; +}; + +/** + * Get the title for an exported file. + * + * @param {object} config Button configuration + */ +var _title = function ( config ) +{ + var title = _stringOrFunction( config.title ); + + return title === null ? + null : title.indexOf( '*' ) !== -1 ? + title.replace( '*', $('head > title').text() || 'Exported data' ) : + title; +}; + +var _message = function ( dt, option, position ) +{ + var message = _stringOrFunction( option ); + if ( message === null ) { + return null; + } + + var caption = $('caption', dt.table().container()).eq(0); + if ( message === '*' ) { + var side = caption.css( 'caption-side' ); + if ( side !== position ) { + return null; + } + + return caption.length ? + caption.text() : + ''; + } + + return message; +}; + + + + + + + +var _exportTextarea = $('",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + + + +'; + +//$PAGE->requires->js('/mod/coursework/datatables/js/jquery-3.3.1.min.js'); +//$PAGE->requires->js('/mod/coursework/datatables/js/jquery.datatables.js'); // Finish the page. echo $OUTPUT->footer(); From 3c3cf401bda188a0a089b57830d30bbaf6e171b6 Mon Sep 17 00:00:00 2001 From: Alex Morris Date: Thu, 17 Aug 2023 14:55:46 +1200 Subject: [PATCH 2/9] Remove mysqlisms and hardcoded table prefixes Resolves #29 --- backup/moodle2/backup_coursework_stepslib.php | 34 +++++++++---------- classes/models/outstanding_marking.php | 6 ++-- classes/warnings.php | 4 +-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/backup/moodle2/backup_coursework_stepslib.php b/backup/moodle2/backup_coursework_stepslib.php index 9c37f42..e092e90 100644 --- a/backup/moodle2/backup_coursework_stepslib.php +++ b/backup/moodle2/backup_coursework_stepslib.php @@ -5,7 +5,7 @@ class backup_coursework_activity_structure_step extends backup_activity_structur protected function define_structure() { global $DB; - + foreach(array('coursework_submissions', 'coursework_allocation_pairs', 'coursework_mod_set_members', @@ -13,13 +13,13 @@ protected function define_structure() 'coursework_extensions', 'coursework_person_deadlines') as $tablename) { - $DB->execute("update {{$tablename}} set `allocatableuser`=0, `allocatablegroup`=0"); - $DB->execute("update {{$tablename}} set `allocatableuser`=`allocatableid` where allocatabletype='user'"); - $DB->execute("update {{$tablename}} set `allocatablegroup`=`allocatableid` where allocatabletype='group'"); + $DB->execute("update {{$tablename}} set allocatableuser=0, allocatablegroup=0"); + $DB->execute("update {{$tablename}} set allocatableuser=allocatableid where allocatabletype='user'"); + $DB->execute("update {{$tablename}} set allocatablegroup=allocatableid where allocatabletype='group'"); } - + $userinfo = $this->get_setting_value('userinfo'); - + $coursework=new backup_nested_element('coursework',array('id'), array('formid', 'course', @@ -317,7 +317,7 @@ protected function define_structure() $coursework->add_child($allocation_configs); //And sample members $coursework->add_child($sample_members); - + //And submissions are made up from individual submission instances $submissions->add_child($submission); //Submissions have multiple feedback items @@ -378,30 +378,30 @@ protected function define_structure() $allocation_config->set_source_table('coursework_allocation_config', array('courseworkid'=>backup::VAR_PARENTID)); - + //Mark important foreign keys $feedback->annotate_ids('user','assessorid'); $feedback->annotate_ids('user','lasteditedbyuser'); $feedback->annotate_ids('user','markernumber'); - + $submission->annotate_ids('user','userid'); $submission->annotate_ids('user','createdby'); $submission->annotate_ids('user','lastupdatedby'); - $submission->annotate_ids('user','allocatableuser'); + $submission->annotate_ids('user','allocatableuser'); $submission->annotate_ids('group','allocatablegroup'); $reminder->annotate_ids('user','userid'); $pair->annotate_ids('user','assessorid'); - $pair->annotate_ids('user','allocatableuser'); + $pair->annotate_ids('user','allocatableuser'); $pair->annotate_ids('group','allocatablegroup'); - + $allocation_config->annotate_ids('user','assessorid'); - $modsetmember->annotate_ids('user','allocatableuser'); + $modsetmember->annotate_ids('user','allocatableuser'); $modsetmember->annotate_ids('group','allocatablegroup'); - $extension->annotate_ids('user','allocatableuser'); + $extension->annotate_ids('user','allocatableuser'); $extension->annotate_ids('group','allocatablegroup'); $personal_deadline->annotate_ids('user','allocatableuser'); @@ -424,8 +424,8 @@ protected function define_structure() $coursework->annotate_ids('grouping','grouping_id'); $coursework->set_source_table('coursework',array('id'=>backup::VAR_ACTIVITYID)); - - return $this->prepare_activity_structure($coursework); - + + return $this->prepare_activity_structure($coursework); + } } \ No newline at end of file diff --git a/classes/models/outstanding_marking.php b/classes/models/outstanding_marking.php index f9e44d6..8061592 100644 --- a/classes/models/outstanding_marking.php +++ b/classes/models/outstanding_marking.php @@ -247,9 +247,9 @@ private function get_to_grade_agreed_grade_sampled_submissions($courseworkid) { COUNT(a.id) AS ssmID FROM( SELECT f.id AS fid, cs.id AS csid, cs.allocatableid ,ssm.id, COUNT(f.id) AS count_feedback, cs.courseworkid - FROM mdl_coursework_submissions cs LEFT JOIN - mdl_coursework_feedbacks f ON f.submissionid= cs.id - LEFT JOIN `mdl_coursework_sample_set_mbrs` ssm + FROM {coursework_submissions} cs LEFT JOIN + {coursework_feedbacks} f ON f.submissionid= cs.id + LEFT JOIN {coursework_sample_set_mbrs} ssm ON cs.courseworkid = ssm.courseworkid AND cs.allocatableid =ssm.allocatableid WHERE cs.courseworkid = :courseworkid GROUP BY cs.allocatableid, ssm.stage_identifier diff --git a/classes/warnings.php b/classes/warnings.php index e684a7d..590c9ac 100644 --- a/classes/warnings.php +++ b/classes/warnings.php @@ -129,8 +129,8 @@ public function students_in_mutiple_grouos() { } else { $sql = "SELECT groups.id,groups.name - FROM mdl_groups groups - INNER JOIN mdl_groups_members gm + FROM {groups} groups + INNER JOIN {groups_members} gm ON gm.groupid = groups.id WHERE groups.courseid = :courseid AND gm.userid = :userid"; From dbcbfa137b20c916b845d4eb9e560276bd7cde5e Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Thu, 16 Nov 2023 15:55:38 +1300 Subject: [PATCH 3/9] Prevent activity name and description from showing twice --- renderers/object_renderer.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/renderers/object_renderer.php b/renderers/object_renderer.php index ffb8248..2c6cde5 100644 --- a/renderers/object_renderer.php +++ b/renderers/object_renderer.php @@ -474,20 +474,26 @@ public function render_plagiarism_links($files) { */ protected function render_mod_coursework_coursework(mod_coursework_coursework $coursework) { - global $PAGE, $USER; + global $CFG, $PAGE, $USER; + + $out = ''; - // Show the details of the assessment (Name and introduction. - $out = html_writer::tag('h2', $coursework->name); + if ($CFG->branch < 400) { + // Show the details of the assessment (Name and introduction. + $out .= html_writer::tag('h2', $coursework->name); + } if (has_capability('mod/coursework:allocate', $coursework->get_context())) { $warnings = new warnings($coursework); $out .= $warnings->not_enough_assessors(); } - // Intro has it's own

tags etc. - $out .= '

'; - $out .= format_module_intro('coursework', $coursework, $coursework->get_coursemodule_id()); - $out .= '
'; + if ($CFG->branch < 400) { + // Intro has it's own

tags etc. + $out .= '

'; + $out .= format_module_intro('coursework', $coursework, $coursework->get_coursemodule_id()); + $out .= '
'; + } // Deadlines section. $out .= html_writer::tag('h3', get_string('deadlines', 'coursework')); @@ -1502,7 +1508,7 @@ private function render_personal_deadline_table_row($personal_deadline_row) { 'courseworkid' => $personal_deadline_row->get_coursework()->id, 'setpersonaldeadlinespage' => '1' ); - + $allocatable_cell_helper = $personal_deadline_row->get_allocatable_cell(); $personaldeadlines_cell_helper = $personal_deadline_row->get_personal_deadline_cell(); From fc1bd88e4210a5e45a24d932fa62817be18bcf74 Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Thu, 16 Nov 2023 15:56:46 +1300 Subject: [PATCH 4/9] Fix db query for postgresSQL. The use of aggregate function reference in HAVING() --- classes/models/outstanding_marking.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/classes/models/outstanding_marking.php b/classes/models/outstanding_marking.php index 8061592..9668b63 100644 --- a/classes/models/outstanding_marking.php +++ b/classes/models/outstanding_marking.php @@ -192,8 +192,8 @@ private function get_multiple_to_mark_initial_grade_submissions($courseworkid,$u {coursework_feedbacks} feed JOIN {coursework_submissions} sub ON sub.id = feed.submissionid WHERE assessorid = :subassessorid AND sub.courseworkid= :subcourseworkid) - GROUP BY cs.id - HAVING (count_feedback < :numofmarkers)"; + GROUP BY cs.id, f.id + HAVING (COUNT(f.id) < :numofmarkers)"; $sqlparams['subassessorid'] = $userid; @@ -223,7 +223,7 @@ private function get_to_grade_agreed_grade_submissions($courseworkid,$numberofma AND cs.finalised = 1 AND cs.courseworkid = :courseworkid GROUP BY cs.id - HAVING (count_feedback = :numofmarkers)"; + HAVING (COUNT(cs.id) = :numofmarkers)"; $sqlparams['numofmarkers'] = $numberofmarkers; @@ -320,4 +320,4 @@ private function should_get_to_mark_agreed_grade_info($courseworkid,$userid) return $user_has_agreed_grade_capability; } -} \ No newline at end of file +} From 6aa8c1a71f923a63bcbb120a4b32b7a712f6de95 Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Thu, 16 Nov 2023 15:57:20 +1300 Subject: [PATCH 5/9] Auto-finalised submitted work when deadline passes. Fix cache purge in view page. --- classes/cron.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/cron.php b/classes/cron.php index 6f35aef..7cb4159 100644 --- a/classes/cron.php +++ b/classes/cron.php @@ -338,7 +338,7 @@ private static function finalise_any_submissions_where_the_deadline_has_passed() // done. Would not want them to check straight away and then find they could still // edit it. $submission->update_attribute('finalised', 1); - + submission::remove_cache($submission->courseworkid); // Slightly wasteful to keep re-fetching the coursework :-/ $mailer = new mailer($submission->get_coursework()); foreach ($submission->get_students() as $student) { From 79e673080a01b9ee4258b313859726372d04f559 Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Thu, 16 Nov 2023 15:57:48 +1300 Subject: [PATCH 6/9] Automatic agreement grades not working for multiple graders issue fixed. --- classes/models/submission.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/models/submission.php b/classes/models/submission.php index 65785f4..37029ef 100644 --- a/classes/models/submission.php +++ b/classes/models/submission.php @@ -1380,7 +1380,7 @@ public function editable_final_feedback_exist() { if ($final_feedback->timecreated + $grade_editing_time > time()) { $this->editable_final_feedback = true; } - } elseif ($final_feedback->finalised == 0) { + } elseif ($final_feedback->finalised == 0 && $final_feedback->assessorid <> 0) { $this->editable_final_feedback = true; } } From 21ff41f9132a8565bf87022db367256689855412 Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Thu, 16 Nov 2023 15:58:45 +1300 Subject: [PATCH 7/9] Fix db query for PostgresSQL. Use CASE instead of IF. Fixes error on dashboard when 'sampling' or 'automatic agreed grades' feature is used in a coursework activity --- classes/models/outstanding_marking.php | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/classes/models/outstanding_marking.php b/classes/models/outstanding_marking.php index 9668b63..582b4a8 100644 --- a/classes/models/outstanding_marking.php +++ b/classes/models/outstanding_marking.php @@ -120,26 +120,26 @@ private function get_multiple_to_mark_sampled_initial_grade_submissions($coursew global $DB; - $sql = " SELECT *, - IF (a.id IS NULL , 0, COUNT(a.id))+1 AS count_samples, + $countsamples = 'CASE WHEN a.id = NULL THEN 0 ELSE COUNT(a.id)+1 END'; + $sql = " SELECT *, + $countsamples AS count_samples, COUNT(a.id) AS ssmID FROM( - SELECT cs.id AS csid, f.id AS fid, cs.allocatableid ,ssm.id, COUNT(f.id) AS count_feedback, + SELECT cs.id AS csid, f.id AS fid, cs.allocatableid ,ssm.id, COUNT(f.id) AS count_feedback, cs.courseworkid FROM {coursework_submissions} cs LEFT JOIN - {coursework_feedbacks} f ON f.submissionid= cs.id - LEFT JOIN {coursework_sample_set_mbrs} ssm - ON cs.courseworkid = ssm.courseworkid AND cs.allocatableid =ssm.allocatableid + {coursework_feedbacks} f ON f.submissionid= cs.id + LEFT JOIN {coursework_sample_set_mbrs} ssm + ON cs.courseworkid = ssm.courseworkid AND cs.allocatableid =ssm.allocatableid WHERE cs.courseworkid = :courseworkid - - AND cs.id NOT IN (SELECT sub.id - FROM {coursework_feedbacks} feed - JOIN {coursework_submissions} sub ON sub.id = feed.submissionid - WHERE assessorid = :subassessorid AND sub.courseworkid= :subcourseworkid) - GROUP BY cs.allocatableid, ssm.stage_identifier - ) a - GROUP BY a.allocatableid - HAVING (count_feedback < count_samples )"; + AND cs.id NOT IN (SELECT sub.id + FROM {coursework_feedbacks} feed + JOIN {coursework_submissions} sub ON sub.id = feed.submissionid + WHERE assessorid = :subassessorid AND sub.courseworkid= :subcourseworkid) + GROUP BY cs.allocatableid, ssm.stage_identifier, f.id, cs.id, ssm.id + ) a + GROUP BY a.allocatableid, a.csid, a.fid, a.id, a.count_feedback, a.courseworkid + HAVING (count_feedback < $countsamples )"; $sqlparams = array(); $sqlparams['subassessorid'] = $userid; @@ -242,21 +242,21 @@ private function get_to_grade_agreed_grade_sampled_submissions($courseworkid) { global $DB; - $sql = "SELECT *, - IF (a.id IS NULL , 0, COUNT(a.id))+1 AS count_samples, + $countsamples = 'CASE WHEN a.id = NULL THEN 0 ELSE COUNT(a.id)+1 END'; + $sql = "SELECT *, + $countsamples AS count_samples, COUNT(a.id) AS ssmID FROM( - SELECT f.id AS fid, cs.id AS csid, cs.allocatableid ,ssm.id, COUNT(f.id) AS count_feedback, + SELECT f.id AS fid, cs.id AS csid, cs.allocatableid ,ssm.id, COUNT(f.id) AS count_feedback, cs.courseworkid FROM {coursework_submissions} cs LEFT JOIN {coursework_feedbacks} f ON f.submissionid= cs.id LEFT JOIN {coursework_sample_set_mbrs} ssm - ON cs.courseworkid = ssm.courseworkid AND cs.allocatableid =ssm.allocatableid + ON cs.courseworkid = ssm.courseworkid AND cs.allocatableid =ssm.allocatableid WHERE cs.courseworkid = :courseworkid - GROUP BY cs.allocatableid, ssm.stage_identifier + GROUP BY cs.allocatableid, ssm.stage_identifier, f.id, cs.id, ssm.id ) a - GROUP BY a.allocatableid - HAVING (count_feedback = count_samples AND count_samples > 1 );"; - + GROUP BY a.allocatableid, a.csid, a.fid, a.id, a.count_feedback, a.courseworkid + HAVING (count_feedback = $countsamples AND $countsamples > 1 );"; $sqlparams['courseworkid'] = $courseworkid; From 9f79c100db4772778b618915ccbab1a8d719f291 Mon Sep 17 00:00:00 2001 From: Aaron Wells Date: Wed, 23 Oct 2024 14:17:52 +1300 Subject: [PATCH 8/9] Fix bug when no submissions notifiers are selected --- classes/controllers/submissions_controller.php | 7 ++++--- classes/forms/student_submission_form.php | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/classes/controllers/submissions_controller.php b/classes/controllers/submissions_controller.php index 14e44b2..ec6eea5 100644 --- a/classes/controllers/submissions_controller.php +++ b/classes/controllers/submissions_controller.php @@ -151,10 +151,11 @@ protected function create_submission() { if ($submission->finalised) { if (!$submission->get_coursework()->has_deadline()) { - $userids = explode(',',$submission->get_coursework()->get_submission_notification_users()); + $useridcommaseparatedlist = $submission->get_coursework()->get_submission_notification_users(); - if (!empty($userids)) { - foreach($userids as $u) { + if (!empty($useridcommaseparatedlist)) { + $userids = explode(',', $useridcommaseparatedlist); + foreach($userids as $u) { $notifyuser = $DB->get_record('user',array('id'=>trim($u))); if (!empty($notifyuser)) $mailer->send_submission_notification($notifyuser); diff --git a/classes/forms/student_submission_form.php b/classes/forms/student_submission_form.php index 8c34db3..1a2c03d 100644 --- a/classes/forms/student_submission_form.php +++ b/classes/forms/student_submission_form.php @@ -176,9 +176,10 @@ public function handle() { if (!$submission->get_coursework()->has_deadline()) { - $userids = explode(',',$submission->get_coursework()->get_submission_notification_users()); + $useridcommaseparatedlist = $submission->get_coursework()->get_submission_notification_users(); - if (!empty($userids)) { + if (!empty($useridcommaseparatedlist)) { + $userids = explode(',', $useridcommaseparatedlist); foreach($userids as $u) { $notifyuser = $DB->get_record('user',array('id'=>trim($u))); $mailer = new mailer($coursework); From f42380e1abc7cd43fc26f578fce0bb341384f443 Mon Sep 17 00:00:00 2001 From: Sumaiya Javed Date: Mon, 25 Aug 2025 10:03:51 +1200 Subject: [PATCH 9/9] update readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 3f1a6d5..ed36dde 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +Archived +====================================================================== +Visit UCL Repo for latest commits +https://github.com/ucl-isd/moodle-mod_coursework + Coursework Activity ====================================================================== Copyright University of London.