From b859dc533fe32afeaa8e00125fdd092227200abc Mon Sep 17 00:00:00 2001 From: David Monllao Date: Mon, 20 Aug 2012 16:35:28 +0800 Subject: [PATCH 01/96] MDL-31496 mod_data Replacing inline styles for CSS classes in textarea elements --- mod/data/field/checkbox/mod.html | 2 +- mod/data/field/menu/mod.html | 2 +- mod/data/field/multimenu/mod.html | 2 +- mod/data/field/picture/field.class.php | 4 ++-- mod/data/field/radiobutton/mod.html | 2 +- mod/data/lib.php | 2 +- mod/data/styles.css | 11 +++++++++++ 7 files changed, 18 insertions(+), 7 deletions(-) diff --git a/mod/data/field/checkbox/mod.html b/mod/data/field/checkbox/mod.html index 6d8bef38234..a8812926b95 100644 --- a/mod/data/field/checkbox/mod.html +++ b/mod/data/field/checkbox/mod.html @@ -9,6 +9,6 @@ - + diff --git a/mod/data/field/menu/mod.html b/mod/data/field/menu/mod.html index 58fa709c2bc..f92fb136b23 100644 --- a/mod/data/field/menu/mod.html +++ b/mod/data/field/menu/mod.html @@ -9,6 +9,6 @@ - + diff --git a/mod/data/field/multimenu/mod.html b/mod/data/field/multimenu/mod.html index 6d8bef38234..a8812926b95 100644 --- a/mod/data/field/multimenu/mod.html +++ b/mod/data/field/multimenu/mod.html @@ -9,6 +9,6 @@ - + diff --git a/mod/data/field/picture/field.class.php b/mod/data/field/picture/field.class.php index 6b3935ef4e8..37b320d8a9d 100644 --- a/mod/data/field/picture/field.class.php +++ b/mod/data/field/picture/field.class.php @@ -152,13 +152,13 @@ class data_field_picture extends data_field_base { if ($template == 'listtemplate') { $src = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$this->context->id.'/mod_data/content/'.$content->id.'/'.'thumb_'.$content->content); // no need to add width/height, because the thumb is resized properly - $str = ''.s($alt).''; + $str = ''.s($alt).''; } else { $src = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$this->context->id.'/mod_data/content/'.$content->id.'/'.$content->content); $width = $this->field->param1 ? ' width="'.s($this->field->param1).'" ':' '; $height = $this->field->param2 ? ' height="'.s($this->field->param2).'" ':' '; - $str = ''.s($alt).''; + $str = ''.s($alt).''; } return $str; diff --git a/mod/data/field/radiobutton/mod.html b/mod/data/field/radiobutton/mod.html index 6d8bef38234..a8812926b95 100644 --- a/mod/data/field/radiobutton/mod.html +++ b/mod/data/field/radiobutton/mod.html @@ -9,6 +9,6 @@ - + diff --git a/mod/data/lib.php b/mod/data/lib.php index fa660c0dc50..d12e916ad8c 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -244,7 +244,7 @@ class data_field_base { // Base class for Database Field Types (see field/*/ $str = '
'; $str .= ''; - $str .= ''; + $str .= ''; $str .= '
'; return $str; diff --git a/mod/data/styles.css b/mod/data/styles.css index 4f6846540c8..c2ce0dc1497 100644 --- a/mod/data/styles.css +++ b/mod/data/styles.css @@ -7,6 +7,13 @@ .path-mod-data-field .c0, #page-mod-data-view #sortsearch .c0 {text-align: right;} #page-mod-data-view .approve img.icon {width:34px;height:34px;} +#page-mod-data-view img.list_picture { + border:0px; +} + +#page-mod-data-edit .basefieldinput { + width:300px; +} /** Styles for preset.php **/ #page-mod-data-preset .presetmapping table {text-align: left;margin-left: auto;margin-right: auto;} @@ -20,6 +27,10 @@ .path-mod-data-field .sortdefault select {margin-left: 1em;} .path-mod-data-field .fieldname, .path-mod-data-field .fielddescription {width:300px;} +.path-mod-data-field textarea.optionstextarea { + width:300px; + height:150px; +} /** UI Usability Hacks **/ #page-mod-data-export #notice span {padding:0 10px;} From 0d284a8bd68cc55d9e367244af809729ef7c3a78 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Mon, 20 Aug 2012 17:16:15 +0800 Subject: [PATCH 02/96] MDL-31496 mod_data Replacing inline styles for CSS classes in search containers --- mod/data/lib.php | 26 ++++++++++---------------- mod/data/styles.css | 10 ++++++++++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/mod/data/lib.php b/mod/data/lib.php index d12e916ad8c..83303be9fe2 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -1480,14 +1480,16 @@ function data_print_preference_form($data, $perpage, $search, $sort='', $order=' $pagesizes = array(2=>2,3=>3,4=>4,5=>5,6=>6,7=>7,8=>8,9=>9,10=>10,15=>15, 20=>20,30=>30,40=>40,50=>50,100=>100,200=>200,300=>300,400=>400,500=>500,1000=>1000); echo html_writer::select($pagesizes, 'perpage', $perpage, false, array('id'=>'pref_perpage')); - echo ''; + echo ''; echo '    '; // foreach field, print the option echo ''; echo '
'; - echo '
'; + echo '
'; echo ''; // print ASC or DESC @@ -1620,7 +1614,7 @@ function data_print_preference_form($data, $perpage, $search, $sort='', $order=' echo format_text($newtext, FORMAT_HTML, $options); echo ''; - echo ''; + echo ''; echo '


'; echo '
'; echo '
'; diff --git a/mod/data/styles.css b/mod/data/styles.css index c2ce0dc1497..b9636ac11d9 100644 --- a/mod/data/styles.css +++ b/mod/data/styles.css @@ -10,6 +10,16 @@ #page-mod-data-view img.list_picture { border:0px; } +#page-mod-data-view div.search_none { + display: none; +} +#page-mod-data-view div.search_inline { + display: inline; +} +#page-mod-data-view div#data_adv_form { + margin-left:auto; + margin-right:auto; +} #page-mod-data-edit .basefieldinput { width:300px; From f70a4f124b3958b02ea89ad9108dcf8601b72854 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Mon, 20 Aug 2012 17:17:24 +0800 Subject: [PATCH 03/96] MDL-31496 mod_data Replacing inline styles for CSS classes in text inputs --- mod/data/field/picture/mod.html | 8 ++++---- mod/data/field/textarea/mod.html | 4 ++-- mod/data/styles.css | 6 ++++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/mod/data/field/picture/mod.html b/mod/data/field/picture/mod.html index 4b32474bbe7..2ea07789ab3 100644 --- a/mod/data/field/picture/mod.html +++ b/mod/data/field/picture/mod.html @@ -16,27 +16,27 @@ - + - + - + - + diff --git a/mod/data/field/textarea/mod.html b/mod/data/field/textarea/mod.html index fb684245bfa..e75365abf9f 100644 --- a/mod/data/field/textarea/mod.html +++ b/mod/data/field/textarea/mod.html @@ -14,7 +14,7 @@ - field->param2)) { echo('"60"'); @@ -28,7 +28,7 @@ - field->param3)) { echo('"35"'); diff --git a/mod/data/styles.css b/mod/data/styles.css index b9636ac11d9..ba002fd8703 100644 --- a/mod/data/styles.css +++ b/mod/data/styles.css @@ -41,6 +41,12 @@ width:300px; height:150px; } +.path-mod-data-field input.textareafieldsize { + width:50px; +} +.path-mod-data-field input.picturefieldsize { + width:70px; +} /** UI Usability Hacks **/ #page-mod-data-export #notice span {padding:0 10px;} From 13f87cb21e3284917ba541c1e4491b18aefb5de3 Mon Sep 17 00:00:00 2001 From: Eric Merrill Date: Mon, 20 Aug 2012 17:09:40 -0400 Subject: [PATCH 04/96] MDL-34985 gradebook Fixing issue where default parent in Full View Add Category could be inconsistent. Turns out the correct default was already computed, but was not being applied to the setting. --- grade/edit/tree/category_form.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grade/edit/tree/category_form.php b/grade/edit/tree/category_form.php index 5a1d8f7c30e..c155565ffe4 100644 --- a/grade/edit/tree/category_form.php +++ b/grade/edit/tree/category_form.php @@ -225,7 +225,7 @@ class edit_category_form extends moodleform { $mform->addElement('header', 'headerparent', get_string('parentcategory', 'grades')); $options = array(); - $default = ''; + $default = -1; $categories = grade_category::fetch_all(array('courseid'=>$COURSE->id)); foreach ($categories as $cat) { @@ -238,6 +238,7 @@ class edit_category_form extends moodleform { if (count($categories) > 1) { $mform->addElement('select', 'parentcategory', get_string('parentcategory', 'grades'), $options); + $mform->setDefault('parentcategory', $default); $mform->addElement('static', 'currentparentaggregation', get_string('currentparentaggregation', 'grades')); } From 649ac85dec64bfa59fc422519b15fef2dc756deb Mon Sep 17 00:00:00 2001 From: David Monllao Date: Mon, 20 Aug 2012 17:24:34 +0800 Subject: [PATCH 05/96] MDL-31496 mod_data Replacing inline styles for CSS classes in templates and browse view --- mod/data/field/latlong/field.class.php | 9 +++++---- mod/data/styles.css | 7 ++++++- mod/data/templates.php | 14 +++++++------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/mod/data/field/latlong/field.class.php b/mod/data/field/latlong/field.class.php index a6f688961fb..e94169996bc 100644 --- a/mod/data/field/latlong/field.class.php +++ b/mod/data/field/latlong/field.class.php @@ -130,7 +130,6 @@ class data_field_latlong extends data_field_base { } else { $compasslong = sprintf('%01.4f', $long) . '°E'; } - $str = '
'; // Now let's create the jump-to-services link $servicesshown = explode(',', $this->field->param1); @@ -148,10 +147,11 @@ class data_field_latlong extends data_field_base { ); if(sizeof($servicesshown)==1 && $servicesshown[0]) { - $str .= " linkoutservices[$servicesshown[0]]) ."' title='$servicesshown[0]'>$compasslat, $compasslong"; } elseif (sizeof($servicesshown)>1) { + $str = ''; $str .= "$compasslat, $compasslong\n"; $str .= ""; $str .= ""; + $str .= '
'; } else { - $str.= "$compasslat, $compasslong"; + $str = "$compasslat, $compasslong"; } - $str.= ''; + return $str; } return false; diff --git a/mod/data/styles.css b/mod/data/styles.css index ba002fd8703..600c79d4d8b 100644 --- a/mod/data/styles.css +++ b/mod/data/styles.css @@ -13,7 +13,8 @@ #page-mod-data-view div.search_none { display: none; } -#page-mod-data-view div.search_inline { +#page-mod-data-view div.search_inline, +#page-mod-data-view form#latlongfieldbrowse { display: inline; } #page-mod-data-view div#data_adv_form { @@ -57,6 +58,10 @@ .mod-data-default-template .template-token {text-align:left;} .mod-data-default-template .controls {text-align:center;} .mod-data-default-template searchcontrols {text-align:right;} +#page-mod-data-templates td.save_template, +#page-mod-data-templates .template_heading { + text-align:center; +} .dir-rtl .mod-data-default-template .template-field {text-align:left;} .dir-rtl .mod-data-default-template .template-token {text-align:right;} diff --git a/mod/data/templates.php b/mod/data/templates.php index 4751732f8ec..c551e03a10c 100644 --- a/mod/data/templates.php +++ b/mod/data/templates.php @@ -141,7 +141,7 @@ if (($mytemplate = data_submitted()) && confirm_sesskey()) { } } } else { - echo '
'.get_string('header'.$mode,'data').'
'; + echo '
'.get_string('header'.$mode,'data').'
'; } /// If everything is empty then generate some defaults @@ -198,7 +198,7 @@ if ($mode == 'listtemplate'){ echo ''; echo ' '; echo ''; - echo '
'; + echo '
'; $field = 'listtemplateheader'; $editor->use_editor($field, $options); @@ -290,9 +290,9 @@ echo ''; echo ''; if ($mode == 'listtemplate'){ - echo '
'; + echo '
'; } else { - echo '
'; + echo '
'; } $field = 'template'; @@ -305,7 +305,7 @@ if ($mode == 'listtemplate'){ echo ''; echo ' '; echo ''; - echo '
'; + echo '
'; $field = 'listtemplatefooter'; $editor->use_editor($field, $options); @@ -316,7 +316,7 @@ if ($mode == 'listtemplate'){ echo ''; echo ' '; echo ''; - echo '
'; + echo '
'; $field = 'rsstitletemplate'; $editor->use_editor($field, $options); @@ -325,7 +325,7 @@ if ($mode == 'listtemplate'){ echo ''; } -echo ''; +echo ''; echo ' '; echo ''; From 2b7ece005eb097bf23d5a8e35d665adec7b7986e Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Mon, 20 Aug 2012 13:56:56 +0800 Subject: [PATCH 06/96] MDL-6424 - blocks - Adding a warning screen when the user clicks the delete link. --- lang/en/block.php | 2 ++ lib/blocklib.php | 52 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/lang/en/block.php b/lang/en/block.php index c0b8f01c1f6..a685f0ef0e6 100644 --- a/lang/en/block.php +++ b/lang/en/block.php @@ -37,6 +37,8 @@ $string['defaultregion'] = 'Default region'; $string['defaultregion_help'] = 'Themes may define one or more named block regions where blocks are displayed. This setting defines which of these you want this block to appear in by default. The region may be overridden on specific pages if required.'; $string['defaultweight'] = 'Default weight'; $string['defaultweight_help'] = 'The default weight allows you to choose roughly where you want the block to appear in the chosen region, either at the top or the bottom. The final location is calculated from all the blocks in that region (for example, only one block can actually be at the top). This value can be overridden on specific pages if required.'; +$string['deletecheck'] = 'Delete {$a} block?'; +$string['deleteblockcheck'] = 'Are you sure that you want to delete this block titled {$a}?'; $string['moveblockhere'] = 'Move block here'; $string['movingthisblockcancel'] = 'Moving this block ({$a})'; $string['onthispage'] = 'On this page'; diff --git a/lib/blocklib.php b/lib/blocklib.php index b74272f1276..074285e6571 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -1108,25 +1108,65 @@ class block_manager { * @return boolean true if anything was done. False if not. */ public function process_url_delete() { + global $CFG; + $blockid = optional_param('bui_deleteid', null, PARAM_INT); + $confirmdelete = optional_param('bui_confirm', null, PARAM_INT); + if (!$blockid) { return false; } require_sesskey(); - $block = $this->page->blocks->find_instance($blockid); - if (!$block->user_can_edit() || !$this->page->user_can_edit_blocks() || !$block->user_can_addto($this->page)) { throw new moodle_exception('nopermissions', '', $this->page->url->out(), get_string('deleteablock')); } - blocks_delete_instance($block->instance); + if (!$confirmdelete) { + $deletepage = new moodle_page(); + $deletepage->set_pagelayout('admin'); + $deletepage->set_course($this->page->course); + $deletepage->set_context($this->page->context); + if ($this->page->cm) { + $deletepage->set_cm($this->page->cm); + } - // If the page URL was a guess, it will contain the bui_... param, so we must make sure it is not there. - $this->page->ensure_param_not_in_url('bui_deleteid'); + $deleteurlbase = str_replace($CFG->wwwroot . '/', '/', $this->page->url->out_omit_querystring()); + $deleteurlparams = $this->page->url->params(); + $deletepage->set_url($deleteurlbase, $deleteurlparams); + $deletepage->set_block_actions_done(); + // At this point we are either going to redirect, or display the form, so + // overwrite global $PAGE ready for this. (Formslib refers to it.) + $PAGE = $deletepage; + //some functions like MoodleQuickForm::addHelpButton use $OUTPUT so we need to replace that too + $output = $deletepage->get_renderer('core'); + $OUTPUT = $output; - return true; + $site = get_site(); + $blocktitle = $block->get_title(); + $strdeletecheck = get_string('deletecheck', 'block', $blocktitle); + $message = get_string('deleteblockcheck', 'block', $blocktitle); + + $PAGE->navbar->add($strdeletecheck); + $PAGE->set_title($blocktitle . ': ' . $strdeletecheck); + $PAGE->set_heading($site->fullname); + echo $OUTPUT->header(); + $confirmurl = new moodle_url("$deletepage->url?", array('sesskey' => sesskey(), 'bui_deleteid' => $block->instance->id, 'bui_confirm' => 1)); + $cancelurl = new moodle_url($deletepage->url); + $yesbutton = new single_button($confirmurl, get_string('yes')); + $nobutton = new single_button($cancelurl, get_string('no')); + echo $OUTPUT->confirm($message, $yesbutton, $nobutton); + echo $OUTPUT->footer(); + // Make sure that nothing else happens after we have displayed this form. + exit; + } else { + blocks_delete_instance($block->instance); + // bui_deleteid and bui_confirm should not be in the PAGE url. + $this->page->ensure_param_not_in_url('bui_deleteid'); + $this->page->ensure_param_not_in_url('bui_confirm'); + return true; + } } /** From 9949e91a5721814621f7a64e5af0f2adc1e6c354 Mon Sep 17 00:00:00 2001 From: Aparup Banerjee Date: Tue, 21 Aug 2012 15:13:41 +0800 Subject: [PATCH 07/96] MDL-34991 Messaging : Fixed rendering of notifications with CRLF to go through text_to_html() in message notifications view page. --- message/lib.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/message/lib.php b/message/lib.php index db95cea31a9..1e7135d848f 100644 --- a/message/lib.php +++ b/message/lib.php @@ -844,7 +844,7 @@ function message_print_recent_notifications($user=null) { $showicontext = false; $showotheruser = false; - message_print_recent_messages_table($notifications, $user, $showotheruser, $showicontext); + message_print_recent_messages_table($notifications, $user, $showotheruser, $showicontext, true); } /** @@ -854,9 +854,10 @@ function message_print_recent_notifications($user=null) { * @param object $user the current user * @param bool $showotheruser display information on the other user? * @param bool $showicontext show text next to the action icons? + * @param bool $forcetexttohtml Force text to go through @see text_to_html() via @see format_text() * @return void */ -function message_print_recent_messages_table($messages, $user=null, $showotheruser=true, $showicontext=false) { +function message_print_recent_messages_table($messages, $user=null, $showotheruser=true, $showicontext=false, $forcetexttohtml=false) { global $OUTPUT; static $dateformat; @@ -914,7 +915,7 @@ function message_print_recent_messages_table($messages, $user=null, $showotherus } echo html_writer::tag('span', userdate($message->timecreated, $dateformat), array('class' => 'messagedate')); - echo html_writer::tag('span', format_text($messagetoprint, FORMAT_HTML), array('class' => 'themessage')); + echo html_writer::tag('span', format_text($messagetoprint, $forcetexttohtml?FORMAT_MOODLE:FORMAT_HTML), array('class' => 'themessage')); echo message_format_contexturl($message); echo html_writer::end_tag('div');//end singlemessage } From 4db11a24e8bb5ad1ecc72aebb4887f5913b0f24f Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Thu, 16 Aug 2012 10:53:22 +0800 Subject: [PATCH 08/96] MDL-26504 blog: made it delete external blog posts when the external blog is deleted --- blog/external_blogs.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/blog/external_blogs.php b/blog/external_blogs.php index bdf30ef0c48..bc91d4061dd 100644 --- a/blog/external_blogs.php +++ b/blog/external_blogs.php @@ -44,7 +44,16 @@ $message = null; if ($delete && confirm_sesskey()) { $externalbloguserid = $DB->get_field('blog_external', 'userid', array('id' => $delete)); if ($externalbloguserid == $USER->id) { + // Delete the external blog $DB->delete_records('blog_external', array('id' => $delete)); + + // Delete the external blog's posts + $deletewhere = 'module = :module + AND userid = :userid + AND ' . $DB->sql_isnotempty('post', 'uniquehash', false, false) . ' + AND ' . $DB->sql_compare_text('content') . ' = ' . $DB->sql_compare_text(':delete'); + $DB->delete_records_select('post', $deletewhere, array('module' => 'blog_external', 'userid' => $USER->id, 'delete' => $delete)); + $message = get_string('externalblogdeleted', 'blog'); } } From 6109f7afce35d8cde4c3fbed9d1cb1b4597da927 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Tue, 21 Aug 2012 11:58:13 +0800 Subject: [PATCH 09/96] MDL-30022 message: made the messaging history display code able to deal with multiple messages with the same timecreated value --- message/lib.php | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/message/lib.php b/message/lib.php index db95cea31a9..423e6bcee80 100644 --- a/message/lib.php +++ b/message/lib.php @@ -749,29 +749,15 @@ function message_get_recent_conversations($user, $limitfrom=0, $limitto=100) { } } - //Sort the conversations. This is a bit complicated as we need to sort by $conversation->timecreated - //and there may be multiple conversations with the same timecreated value. - //The conversations array contains both read and unread messages (different tables) so sorting by ID won't work - usort($conversations, "conversationsort"); + // Sort the conversations by $conversation->timecreated, newest to oldest + // There may be multiple conversations with the same timecreated + // The conversations array contains both read and unread messages (different tables) so sorting by ID won't work + $result = collatorlib::asort_objects_by_property($conversations, 'timecreated', collatorlib::SORT_NUMERIC); + $conversations = array_reverse($conversations); return $conversations; } -/** - * Sort function used to order conversations - * - * @param object $a A conversation object - * @param object $b A conversation object - * @return integer - */ -function conversationsort($a, $b) -{ - if ($a->timecreated == $b->timecreated) { - return 0; - } - return ($a->timecreated > $b->timecreated) ? -1 : 1; -} - /** * Get the users recent event notifications * @@ -1804,7 +1790,7 @@ function message_get_history($user1, $user2, $limitnum=0, $viewingnewmessages=fa array($user1->id, $user2->id, $user2->id, $user1->id, $user1->id), "timecreated $sort", '*', 0, $limitnum)) { foreach ($messages_read as $message) { - $messages[$message->timecreated] = $message; + $messages[] = $message; } } if ($messages_new = $DB->get_records_select('message', "((useridto = ? AND useridfrom = ?) OR @@ -1812,15 +1798,16 @@ function message_get_history($user1, $user2, $limitnum=0, $viewingnewmessages=fa array($user1->id, $user2->id, $user2->id, $user1->id, $user1->id), "timecreated $sort", '*', 0, $limitnum)) { foreach ($messages_new as $message) { - $messages[$message->timecreated] = $message; + $messages[] = $message; } } + $result = collatorlib::asort_objects_by_property($messages, 'timecreated', collatorlib::SORT_NUMERIC); + //if we only want the last $limitnum messages - ksort($messages); $messagecount = count($messages); - if ($limitnum>0 && $messagecount>$limitnum) { - $messages = array_slice($messages, $messagecount-$limitnum, $limitnum, true); + if ($limitnum > 0 && $messagecount > $limitnum) { + $messages = array_slice($messages, $messagecount - $limitnum, $limitnum, true); } return $messages; From 2c8695502788fd623c4077d9d4fb5d9a70dafe29 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 21 Aug 2012 17:40:03 +0800 Subject: [PATCH 10/96] MDL-34945 Repository: Creating an instance requires the user to have the permission to view it --- repository/lib.php | 6 ++++++ repository/manage_instances.php | 16 ++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/repository/lib.php b/repository/lib.php index e8a60f4b72e..c5a93f58e95 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1490,6 +1490,12 @@ abstract class repository { $types = repository::get_editable_types($context); foreach ($types as $type) { if (!empty($type) && $type->get_visible()) { + // If the user does not have the permission to view the repository, it won't be displayed in + // the list of instances. Hiding the link to create new instances will prevent the + // user from creating them without being able to find them afterwards, which looks like a bug. + if (!has_capability('repository/'.$type->get_typename().':view', $context)) { + continue; + } $instanceoptionnames = repository::static_function($type->get_typename(), 'get_instance_option_names'); if (!empty($instanceoptionnames)) { $baseurl->param('new', $type->get_typename()); diff --git a/repository/manage_instances.php b/repository/manage_instances.php index a6b1363798d..5d7f76c61d7 100644 --- a/repository/manage_instances.php +++ b/repository/manage_instances.php @@ -106,12 +106,16 @@ if (!empty($new)){ $type = repository::get_type_by_id($instance->options['typeid']); } -if (isset($type) && !$type->get_visible()) { - print_error('typenotvisible', 'repository', $baseurl); -} - -if (isset($type) && !$type->get_contextvisibility($context)) { - print_error('usercontextrepositorydisabled', 'repository', $baseurl); +if (isset($type)) { + if (!$type->get_visible()) { + print_error('typenotvisible', 'repository', $baseurl); + } + // Prevents the user from creating/editing an instance if the repository is not visible in + // this context OR if the user does not have the capability to view this repository in this context. + $canviewrepository = has_capability('repository/'.$type->get_typename().':view', $context); + if (!$type->get_contextvisibility($context) || !$canviewrepository) { + print_error('usercontextrepositorydisabled', 'repository', $baseurl); + } } /// Create navigation links From 9e7cd6e7a542bfb8e4df57c67d3bc0a28b780d90 Mon Sep 17 00:00:00 2001 From: Rajesh Taneja Date: Mon, 27 Aug 2012 13:59:09 +0800 Subject: [PATCH 11/96] MDL-34537 Enrolments: Assign alternative odd and even class to manual enrol popup --- enrol/manual/yui/quickenrolment/quickenrolment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enrol/manual/yui/quickenrolment/quickenrolment.js b/enrol/manual/yui/quickenrolment/quickenrolment.js index 08f8d702823..4efe9674723 100644 --- a/enrol/manual/yui/quickenrolment/quickenrolment.js +++ b/enrol/manual/yui/quickenrolment/quickenrolment.js @@ -339,7 +339,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) { count++; var user = result.response.users[i]; users.append(create('
') - .addClass((i%2)?CSS.ODD:CSS.EVEN) + .addClass((count%2)?CSS.ODD:CSS.EVEN) .append(create('
'+count+'
')) .append(create('
') .append(create(user.picture))) From 222ada3765b2df1b04e4e115048eb7c3cad23c0c Mon Sep 17 00:00:00 2001 From: Ankit Agarwal Date: Mon, 27 Aug 2012 14:23:17 +0800 Subject: [PATCH 12/96] MDL-26882 profile: Apllying the cap check site:viewuseridentity to user profile --- user/profile.php | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/user/profile.php b/user/profile.php index 979319dcd88..87ef59eca16 100644 --- a/user/profile.php +++ b/user/profile.php @@ -118,6 +118,12 @@ if (has_capability('moodle/user:viewhiddendetails', $context)) { $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); } +if (has_capability('moodle/site:viewuseridentity', $context)) { + $identityfields = array_flip(explode(',', $CFG->showuseridentity)); +} else { + $identityfields = array(); +} + // Start setting up the page $strpublicprofile = get_string('publicprofile'); @@ -247,16 +253,28 @@ if (! isset($hiddenfields['city']) && $user->city) { print_row(get_string('city') . ':', $user->city); } -if (has_capability('moodle/user:viewhiddendetails', $context)) { - if ($user->address) { - print_row(get_string("address").":", "$user->address"); - } - if ($user->phone1) { +if (isset($identityfields['address']) && $user->address) { + print_row(get_string("address").":", "$user->address"); +} + +if (isset($identityfields['phone1']) && $user->phone1) { print_row(get_string("phone").":", "$user->phone1"); - } - if ($user->phone2) { +} + +if (isset($identityfields['phone2']) && $user->phone2) { print_row(get_string("phone2").":", "$user->phone2"); - } +} + +if (isset($identityfields['institution']) && $user->institution) { + print_row(get_string("institution").":", "$user->institution"); +} + +if (isset($identityfields['department']) && $user->department) { + print_row(get_string("department").":", "$user->department"); +} + +if (isset($identityfields['idnumber']) && $user->idnumber) { + print_row(get_string("idnumber").":", "$user->idnumber"); } if ($currentuser From b48ceab75b765f08f50997c77ba734b8020d132d Mon Sep 17 00:00:00 2001 From: Ankit Agarwal Date: Mon, 27 Aug 2012 14:38:57 +0800 Subject: [PATCH 13/96] MDL-26882 profile: Applying cap check (site:viewuseridentity) to email field in user profile --- user/profile.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user/profile.php b/user/profile.php index 87ef59eca16..7e02c123ca2 100644 --- a/user/profile.php +++ b/user/profile.php @@ -277,10 +277,11 @@ if (isset($identityfields['idnumber']) && $user->idnumber) { print_row(get_string("idnumber").":", "$user->idnumber"); } -if ($currentuser +if (($currentuser or $user->maildisplay == 1 or has_capability('moodle/course:useremail', $context) - or ($user->maildisplay == 2 and enrol_sharing_course($user, $USER))) { + or ($user->maildisplay == 2 and enrol_sharing_course($user, $USER))) + and isset($identityfields['email'])) { print_row(get_string("email").":", obfuscate_mailto($user->email, '')); } From 25ac6d30f2b694764a251e8df70556e26cb499ee Mon Sep 17 00:00:00 2001 From: Rajesh Taneja Date: Mon, 27 Aug 2012 14:55:02 +0800 Subject: [PATCH 14/96] MDl-34606 Administration: id for clear button is set properly, and disabled on page load --- user/selector/module.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user/selector/module.js b/user/selector/module.js index e6818c88f7c..68b743d3b0a 100644 --- a/user/selector/module.js +++ b/user/selector/module.js @@ -77,8 +77,9 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc var clearbtn = Y.one('#'+this.name + '_clearbutton'); this.clearbutton = Y.Node.create(''); clearbtn.replace(Y.Node.getDOMNode(this.clearbutton)); - this.clearbutton.set('id',+this.name+"_clearbutton"); + this.clearbutton.set('id', this.name+"_clearbutton"); this.clearbutton.on('click', this.handle_clear, this); + this.clearbutton.set('disabled', (this.get_search_text() == '')); this.send_query(false); }, From 65cd3f9c1698f649e134032a51b6a1689eb2a2d7 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Thu, 23 Aug 2012 17:23:00 +0800 Subject: [PATCH 15/96] MDL-35029 core_session Refresh guest session if autologin as guest is enabled --- lib/sessionlib.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/sessionlib.php b/lib/sessionlib.php index 4729b6bea81..9e5c29f7384 100644 --- a/lib/sessionlib.php +++ b/lib/sessionlib.php @@ -605,7 +605,11 @@ class database_session extends session_stub { $ignoretimeout = false; if (!empty($record->userid)) { // skips not logged in if ($user = $this->database->get_record('user', array('id'=>$record->userid))) { - if (!isguestuser($user)) { + + // Refresh session if logged as a guest + if (isguestuser($user)) { + $ignoretimeout = true; + } else { $authsequence = get_enabled_auth_plugins(); // auths, in sequence foreach($authsequence as $authname) { $authplugin = get_auth_plugin($authname); @@ -925,9 +929,12 @@ function session_gc() { } $rs->close(); + // Extending the timeout period for guest sessions as they are renewed. $purgebefore = time() - $maxlifetime; + $purgebeforeguests = time() - ($maxlifetime * 5); + // delete expired sessions for guest user account - $DB->delete_records_select('sessions', 'userid = ? AND timemodified < ?', array($CFG->siteguest, $purgebefore)); + $DB->delete_records_select('sessions', 'userid = ? AND timemodified < ?', array($CFG->siteguest, $purgebeforeguests)); // delete expired sessions for userid = 0 (not logged in) $DB->delete_records_select('sessions', 'userid = 0 AND timemodified < ?', array($purgebefore)); } catch (dml_exception $ex) { From 2ea5b0bb876753d34def2356b9cd0f96490cf17a Mon Sep 17 00:00:00 2001 From: Aaron Barnes Date: Tue, 21 Aug 2012 13:07:57 +1200 Subject: [PATCH 16/96] MDL-30020 blocks/completion: Some criteria do not display complete Also includes a spruce up of the code --- .../block_completionstatus.php | 29 ++- blocks/completionstatus/details.php | 245 ++++++++++-------- .../lang/en/block_completionstatus.php | 1 + 3 files changed, 147 insertions(+), 128 deletions(-) diff --git a/blocks/completionstatus/block_completionstatus.php b/blocks/completionstatus/block_completionstatus.php index 2b598ce767f..b339e9a74cd 100644 --- a/blocks/completionstatus/block_completionstatus.php +++ b/blocks/completionstatus/block_completionstatus.php @@ -19,15 +19,14 @@ * * @package block * @subpackage completion - * @copyright 2009 Catalyst IT Ltd + * @copyright 2009-2012 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir.'/completionlib.php'); +require_once("{$CFG->libdir}/completionlib.php"); /** * Course completion status @@ -36,25 +35,28 @@ require_once($CFG->libdir.'/completionlib.php'); class block_completionstatus extends block_base { public function init() { - $this->title = get_string('pluginname', 'block_completionstatus'); + $this->title = get_string('pluginname', 'block_completionstatus'); } public function get_content() { - global $USER, $CFG, $DB, $COURSE; + global $USER; // If content is cached if ($this->content !== NULL) { return $this->content; } + $course = $this->page->course; + $context = context_course::instance($course->id); + // Create empty content - $this->content = new stdClass; + $this->content = new stdClass(); // Can edit settings? - $can_edit = has_capability('moodle/course:update', context_course::instance($this->page->course->id)); + $can_edit = has_capability('moodle/course:update', $context); // Get course completion data - $info = new completion_info($this->page->course); + $info = new completion_info($course); // Don't display if completion isn't enabled! if (!completion_info::is_enabled_for_site()) { @@ -84,9 +86,9 @@ class block_completionstatus extends block_base { // Check this user is enroled if (!$info->is_tracked_user($USER->id)) { // If not enrolled, but are can view the report: - if (has_capability('report/completion:view', context_course::instance($COURSE->id))) { - $this->content->text = ''.get_string('viewcoursereport', 'completion').''; + if (has_capability('report/completion:view', $context)) { + $report = new moodle_url('/report/completion/index.php', array('course' => $course->id)); + $this->content->text = ''.get_string('viewcoursereport', 'completion').''; return $this->content; } @@ -187,7 +189,7 @@ class block_completionstatus extends block_base { // Load course completion $params = array( 'userid' => $USER->id, - 'course' => $COURSE->id + 'course' => $course->id ); $ccompletion = new completion_completion($params); @@ -221,7 +223,8 @@ class block_completionstatus extends block_base { $this->content->text .= $shtml.''; // Display link to detailed view - $this->content->footer = '
'.get_string('moredetails', 'completion').''; + $details = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id)); + $this->content->footer = '
'.get_string('moredetails', 'completion').''; return $this->content; } diff --git a/blocks/completionstatus/details.php b/blocks/completionstatus/details.php index a71da02635a..bb1b051562f 100644 --- a/blocks/completionstatus/details.php +++ b/blocks/completionstatus/details.php @@ -19,27 +19,23 @@ * * @package block * @subpackage completion - * @copyright 2009 Catalyst IT Ltd + * @copyright 2009-2012 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require_once('../../config.php'); -require_once($CFG->libdir.'/completionlib.php'); - - -// TODO: Make this page Moodle 2.0 compliant +require_once(dirname(__FILE__).'/../../config.php'); +require_once("{$CFG->libdir}/completionlib.php"); /// /// Load data /// $id = required_param('course', PARAM_INT); -// User id $userid = optional_param('user', 0, PARAM_INT); // Load course -$course = $DB->get_record('course', array('id' => $id)); +$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); // Load user if ($userid) { @@ -76,21 +72,13 @@ if (!$can_view) { // Load completion data $info = new completion_info($course); -$returnurl = "{$CFG->wwwroot}/course/view.php?id={$id}"; +$returnurl = new moodle_url('/course/view.php', array('id' => $id)); // Don't display if completion isn't enabled! if (!$info->is_enabled()) { print_error('completionnotenabled', 'completion', $returnurl); } -// Load criteria to display -$completions = $info->get_completions($user->id); - -// Check if this course has any criteria -if (empty($completions)) { - print_error('nocriteriaset', 'completion', $returnurl); -} - // Check this user is enroled if (!$info->is_tracked_user($user->id)) { if ($USER->id == $user->id) { @@ -104,6 +92,7 @@ if (!$info->is_tracked_user($user->id)) { /// /// Display page /// +$PAGE->set_context(context_course::instance($course->id)); // Print header $page = get_string('completionprogressdetails', 'block_completionstatus'); @@ -111,7 +100,7 @@ $title = format_string($course->fullname) . ': ' . $page; $PAGE->navbar->add($page); $PAGE->set_pagelayout('standard'); -$PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id)); +$PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id)); $PAGE->set_title(get_string('course') . ': ' . $course->fullname); $PAGE->set_heading($title); echo $OUTPUT->header(); @@ -135,122 +124,148 @@ $coursecomplete = $info->is_course_complete($user->id); // Has this user completed any criteria? $criteriacomplete = $info->count_course_user_data($user->id); +// Load course completion +$params = array( + 'userid' => $user->id, + 'course' => $course->id, +); +$ccompletion = new completion_completion($params); + if ($coursecomplete) { echo get_string('complete'); -} else if (!$criteriacomplete) { +} else if (!$criteriacomplete && !$ccompletion->timestarted) { echo ''.get_string('notyetstarted', 'completion').''; } else { echo ''.get_string('inprogress','completion').''; } echo ''; -echo ''.get_string('required').': '; -// Get overall aggregation method -$overall = $info->get_aggregation_method(); +// Load criteria to display +$completions = $info->get_completions($user->id); -if ($overall == COMPLETION_AGGREGATION_ALL) { - echo get_string('criteriarequiredall', 'completion'); +// Check if this course has any criteria +if (empty($completions)) { + echo '
'; + echo $OUTPUT->box(get_string('err_nocriteria', 'completion'), 'noticebox'); + echo ''; } else { - echo get_string('criteriarequiredany', 'completion'); -} + echo ''.get_string('required').': '; -echo ''; + // Get overall aggregation method + $overall = $info->get_aggregation_method(); -// Generate markup for criteria statuses -echo ''; -echo ''; -echo ''; -echo ''; -echo ''; -echo ''; -echo ''; -echo ''; -echo ''; - -// Save row data -$rows = array(); - -global $COMPLETION_CRITERIA_TYPES; - -// Loop through course criteria -foreach ($completions as $completion) { - $criteria = $completion->get_criteria(); - $complete = $completion->is_complete(); - - $row = array(); - $row['type'] = $criteria->criteriatype; - $row['title'] = $criteria->get_title(); - $row['status'] = $completion->get_status(); - $row['timecompleted'] = $completion->timecompleted; - $row['details'] = $criteria->get_details($completion); - $rows[] = $row; -} - -// Print table -$last_type = ''; -$agg_type = false; - -foreach ($rows as $row) { - - // Criteria group - echo ''; - // Criteria title - echo ''; + echo '
'.get_string('criteriagroup', 'block_completionstatus').''.get_string('criteria', 'completion').''.get_string('requirement', 'block_completionstatus').''.get_string('status').''.get_string('complete').''.get_string('completiondate', 'report_completion').'
'; - if ($last_type !== $row['details']['type']) { - $last_type = $row['details']['type']; - echo $last_type; - - // Reset agg type - $agg_type = true; + if ($overall == COMPLETION_AGGREGATION_ALL) { + echo get_string('criteriarequiredall', 'completion'); } else { - // Display aggregation type - if ($agg_type) { - $agg = $info->get_aggregation_method($row['type']); - - echo '('; - - if ($agg == COMPLETION_AGGREGATION_ALL) { - echo strtolower(get_string('all', 'completion')); - } else { - echo strtolower(get_string('any', 'completion')); - } - - echo ' '.strtolower(get_string('required')).')'; - $agg_type = false; - } + echo get_string('criteriarequiredany', 'completion'); } - echo ''; - echo $row['details']['criteria']; - echo '
'; - // Requirement - echo ''; - echo $row['details']['requirement']; - echo ''; - - // Status - echo ''; - echo $row['details']['status']; - echo ''; - - // Is complete - echo ''; - echo ($row['status'] === get_string('yes')) ? get_string('yes') : get_string('no'); - echo ''; - - // Completion data - echo ''; - if ($row['timecompleted']) { - echo userdate($row['timecompleted'], '%e %B %G'); - } else { - echo '-'; - } - echo ''; + // Generate markup for criteria statuses + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; echo ''; + + // Save row data + $rows = array(); + + // Loop through course criteria + foreach ($completions as $completion) { + $criteria = $completion->get_criteria(); + + $row = array(); + $row['type'] = $criteria->criteriatype; + $row['title'] = $criteria->get_title(); + $row['status'] = $completion->get_status(); + $row['complete'] = $completion->is_complete(); + $row['timecompleted'] = $completion->timecompleted; + $row['details'] = $criteria->get_details($completion); + $rows[] = $row; + } + + // Print table + $last_type = ''; + $agg_type = false; + $oddeven = 0; + + foreach ($rows as $row) { + + echo ''; + + // Criteria group + echo ''; + + // Criteria title + echo ''; + + // Requirement + echo ''; + + // Status + echo ''; + + // Is complete + echo ''; + + // Completion data + echo ''; + echo ''; + // for row striping + $oddeven = $oddeven ? 0 : 1; + } + + echo '
'.get_string('criteriagroup', 'block_completionstatus').''.get_string('criteria', 'completion').''.get_string('requirement', 'block_completionstatus').''.get_string('status').''.get_string('complete').''.get_string('completiondate', 'report_completion').'
'; + if ($last_type !== $row['details']['type']) { + $last_type = $row['details']['type']; + echo $last_type; + + // Reset agg type + $agg_type = true; + } else { + // Display aggregation type + if ($agg_type) { + $agg = $info->get_aggregation_method($row['type']); + + echo '('; + + if ($agg == COMPLETION_AGGREGATION_ALL) { + echo strtolower(get_string('aggregateall', 'completion')); + } else { + echo strtolower(get_string('aggregateany', 'completion')); + } + + echo ' '.strtolower(get_string('required')).')'; + $agg_type = false; + } + } + echo ''; + echo $row['details']['criteria']; + echo ''; + echo $row['details']['requirement']; + echo ''; + echo $row['details']['status']; + echo ''; + echo $row['complete'] ? get_string('yes') : get_string('no'); + echo ''; + if ($row['timecompleted']) { + echo userdate($row['timecompleted'], get_string('strftimedate', 'langconfig')); + } else { + echo '-'; + } + echo '
'; } -echo ''; +echo '
'; +$courseurl = new moodle_url("/course/view.php", array('id' => $course->id)); +echo $OUTPUT->single_button($courseurl, get_string('returntocourse', 'block_completionstatus'), 'get'); +echo '
'; echo $OUTPUT->footer(); diff --git a/blocks/completionstatus/lang/en/block_completionstatus.php b/blocks/completionstatus/lang/en/block_completionstatus.php index fcc965ac7fd..6658c17dff4 100644 --- a/blocks/completionstatus/lang/en/block_completionstatus.php +++ b/blocks/completionstatus/lang/en/block_completionstatus.php @@ -5,3 +5,4 @@ $string['criteriagroup'] = 'Criteria group'; $string['firstofsecond'] = '{$a->first} of {$a->second}'; $string['pluginname'] = 'Course completion status'; $string['requirement'] = 'Requirement'; +$string['returntocourse'] = 'Return to course'; From 2b64e24ebe637b49188103671b5f043918eb1057 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Tue, 28 Aug 2012 01:38:09 +0200 Subject: [PATCH 17/96] MDL-35089 conditionals: detected one place missing sectioncache Thanks to MDL-34936 and unit tests this was discovered like 2 new places calling to get_fast_modinfo() without the sectioncache column contents. Potential performance problem, leading to reseting and recalculation of caches all the time. --- lib/conditionlib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/conditionlib.php b/lib/conditionlib.php index 7557190f189..96b958069d2 100644 --- a/lib/conditionlib.php +++ b/lib/conditionlib.php @@ -767,7 +767,7 @@ abstract class condition_info_base { $course = $COURSE; } else { $course = $DB->get_record('course', array('id' => $this->item->course), - 'id, enablecompletion, modinfo', MUST_EXIST); + 'id, enablecompletion, modinfo, sectioncache', MUST_EXIST); } foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) { if (!$modinfo) { @@ -929,7 +929,7 @@ abstract class condition_info_base { $course = $COURSE; } else { $course = $DB->get_record('course', array('id' => $this->item->course), - 'id, enablecompletion, modinfo', MUST_EXIST); + 'id, enablecompletion, modinfo, sectioncache', MUST_EXIST); } $completion = new completion_info($course); From f361678362dddc74552be117558281d29b506656 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Tue, 28 Aug 2012 10:41:02 +0800 Subject: [PATCH 18/96] MDL-26504 blog: added upgrade code to remove orphan external blog posts --- lib/db/upgrade.php | 9 +++++++++ version.php | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index d5b98563d2c..e9bef7f5fc5 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1119,6 +1119,15 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2012081600.01); } + if ($oldversion < 2012082300.01) { + $subquery = 'SELECT b.id FROM {blog_external} b where ' . $DB->sql_compare_text('b.id') . ' = ' . $DB->sql_compare_text('{post}.content'); + $sql = 'DELETE FROM {post} + WHERE {post}.module = \'blog_external\' + AND NOT EXISTS (' . $subquery . ') + AND ' . $DB->sql_isnotempty('post', 'uniquehash', false, false); + $DB->execute($sql); + upgrade_main_savepoint(true, 2012082300.01); + } return true; } diff --git a/version.php b/version.php index 528c92696df..9c9909f1d31 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012082300.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012082300.01; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes From ac31c38eedbb2fbb03b3aeb97617e71c14b5eb49 Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Tue, 28 Aug 2012 15:54:01 +0800 Subject: [PATCH 19/96] MDL-34318 - blog - Removal of blog associations from the database on delete. Thanks to Erik Lundberg for this patch. --- blog/locallib.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blog/locallib.php b/blog/locallib.php index 43b36af0418..9a07fbace76 100644 --- a/blog/locallib.php +++ b/blog/locallib.php @@ -292,11 +292,10 @@ class blog_entry implements renderable { * @return void */ public function delete() { - global $DB, $USER; - - $returnurl = ''; + global $DB; $this->delete_attachments(); + $this->remove_associations(); $DB->delete_records('post', array('id' => $this->id)); tag_set('post', $this->id, array()); From 720f7c12e85e5eca5362678ff8aa0291b62422c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Tue, 28 Aug 2012 22:52:31 +0200 Subject: [PATCH 20/96] MDL-35109 Improve unittests for cron based fetching of available updates The expected behaviour is as follows: * If the recently fetched data is older than 48 hours, it is considered as outdated and the new fetch is executed * else, if the recently fetched data is younger than 24 hours, it is considered as fresh enough and no fetch is executed * else, if the current time is after 01:00 AM plus a certain offset (which is randomly generated for each site), the fetch is executed. --- lib/tests/pluginlib_test.php | 47 +++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/tests/pluginlib_test.php b/lib/tests/pluginlib_test.php index eb29268a55b..2ec4d0dd2f8 100644 --- a/lib/tests/pluginlib_test.php +++ b/lib/tests/pluginlib_test.php @@ -103,7 +103,7 @@ class available_update_checker_test extends advanced_testcase { */ public function test_cron_has_fresh_fetch() { $provider = testable_available_update_checker::instance(); - $provider->fakerecentfetch = time() - 59 * MINSECS; // fetched an hour ago + $provider->fakerecentfetch = time() - 23 * HOURSECS; // fetched 23 hours ago $provider->fakecurrenttimestamp = -1; $provider->cron(); $this->assertTrue(true); // we should get here with no exception thrown @@ -127,23 +127,52 @@ class available_update_checker_test extends advanced_testcase { */ public function test_cron_offset_execution_not_yet() { $provider = testable_available_update_checker::instance(); - $provider->fakerecentfetch = time() - 24 * HOURSECS; - $provider->fakecurrenttimestamp = mktime(1, 40, 02); // 01:40:02 AM + $provider->fakecurrenttimestamp = mktime(1, 40, 02); // 01:40:02 AM today + $provider->fakerecentfetch = $provider->fakecurrenttimestamp - 24 * HOURSECS; $provider->cron(); $this->assertTrue(true); // we should get here with no exception thrown } /** - * The first cron after 01:42 AM today should fetch the data + * The first cron after 01:42 AM today should fetch the data and then + * it is supposed to wait next 24 hours. * * @see testable_available_update_checker::cron_execution_offset() */ public function test_cron_offset_execution() { $provider = testable_available_update_checker::instance(); - $provider->fakerecentfetch = time() - 24 * HOURSECS; - $provider->fakecurrenttimestamp = mktime(1, 45, 02); // 01:45:02 AM - $this->setExpectedException('testable_available_update_checker_cron_executed'); - $provider->cron(); + + // the cron at 01:45 should fetch the data + $provider->fakecurrenttimestamp = mktime(1, 45, 02); // 01:45:02 AM today + $provider->fakerecentfetch = $provider->fakecurrenttimestamp - 24 * HOURSECS - 1; + $executed = false; + try { + $provider->cron(); + } catch (testable_available_update_checker_cron_executed $e) { + $executed = true; + } + $this->assertTrue($executed, 'Cron should be executed at 01:45:02 but it was not.'); + + // another cron at 06:45 should still consider data as fresh enough + $provider->fakerecentfetch = $provider->fakecurrenttimestamp; + $provider->fakecurrenttimestamp = mktime(6, 45, 03); // 06:45:03 AM + $executed = false; + try { + $provider->cron(); + } catch (testable_available_update_checker_cron_executed $e) { + $executed = true; + } + $this->assertFalse($executed, 'Cron should not be executed at 06:45:03 but it was.'); + + // the next scheduled execution should happen the next day + $provider->fakecurrenttimestamp = $provider->fakerecentfetch + 24 * HOURSECS + 1; + $executed = false; + try { + $provider->cron(); + } catch (testable_available_update_checker_cron_executed $e) { + $executed = true; + } + $this->assertTrue($executed, 'Cron should be executed the next night but it was not.'); } public function test_compare_responses_both_empty() { @@ -503,7 +532,7 @@ class testable_available_update_checker extends available_update_checker { } protected function cron_execute() { - throw new testable_available_update_checker_cron_executed('Cron executed but it should not!'); + throw new testable_available_update_checker_cron_executed('Cron executed!'); } } From 7092ea5da5dd6660ffb30dfcfa9e147f27e38f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Tue, 28 Aug 2012 22:56:18 +0200 Subject: [PATCH 21/96] MDL-35109 Fix available_update_checker::cron_has_fresh_fetch() For the purpose of cron based fetching, recently fetched data are valid for 24 hours. --- lib/pluginlib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 61d2a6ea609..b1139ebe8e0 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -1085,7 +1085,7 @@ class available_update_checker { return true; } - if ($now - $recent > HOURSECS) { + if ($now - $recent > 24 * HOURSECS) { return false; } From b677b83d571c0feb9ab237cc52774f9ebedd16a7 Mon Sep 17 00:00:00 2001 From: Andrew Robert Nicols Date: Wed, 29 Aug 2012 09:19:18 +0100 Subject: [PATCH 22/96] MDL-31973 Remove inadvertantly committed function call from testing --- mod/page/lib.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mod/page/lib.php b/mod/page/lib.php index ea2133f2e08..0f03e2abcfb 100644 --- a/mod/page/lib.php +++ b/mod/page/lib.php @@ -515,7 +515,3 @@ function page_dndupload_handle($uploadinfo) { return page_add_instance($data, null); } - -function mod_page_allow_group_member_remove($itemid, $groupid, $userid) { - return true; -} From b615c46821ed03bc5d2f9eb05231c089415fc9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Wed, 29 Aug 2012 15:33:22 +0200 Subject: [PATCH 23/96] MDL-34725 Make the signatures of process_legacy_element() method consistent The patch fixes the missing parameter type declaration in all subclasses of the moodle1_workshopform_handler class. --- mod/workshop/form/comments/backup/moodle1/lib.php | 7 ++++++- mod/workshop/form/numerrors/backup/moodle1/lib.php | 5 ++++- mod/workshop/form/rubric/backup/moodle1/lib.php | 5 ++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mod/workshop/form/comments/backup/moodle1/lib.php b/mod/workshop/form/comments/backup/moodle1/lib.php index 5d557a443c6..2e9a0c01e43 100644 --- a/mod/workshop/form/comments/backup/moodle1/lib.php +++ b/mod/workshop/form/comments/backup/moodle1/lib.php @@ -32,8 +32,13 @@ class moodle1_workshopform_comments_handler extends moodle1_workshopform_handler /** * Converts into + * + * @param array $data legacy element data + * @param array $raw raw element data + * + * @return array converted */ - public function process_legacy_element($data, $raw) { + public function process_legacy_element(array $data, array $raw) { // prepare a fake record and re-use the upgrade logic $fakerecord = (object)$data; $converted = (array)workshopform_comments_upgrade_element($fakerecord, 12345678); diff --git a/mod/workshop/form/numerrors/backup/moodle1/lib.php b/mod/workshop/form/numerrors/backup/moodle1/lib.php index d2c63103811..e18a2f26701 100644 --- a/mod/workshop/form/numerrors/backup/moodle1/lib.php +++ b/mod/workshop/form/numerrors/backup/moodle1/lib.php @@ -49,9 +49,12 @@ class moodle1_workshopform_numerrors_handler extends moodle1_workshopform_handle /** * Converts into and stores it for later writing * + * @param array $data legacy element data + * @param array $raw raw element data + * * @return array to be written to workshop.xml */ - public function process_legacy_element($data, $raw) { + public function process_legacy_element(array $data, array $raw) { $workshop = $this->parenthandler->get_current_workshop(); diff --git a/mod/workshop/form/rubric/backup/moodle1/lib.php b/mod/workshop/form/rubric/backup/moodle1/lib.php index be0a6c6b11a..fe604f807d2 100644 --- a/mod/workshop/form/rubric/backup/moodle1/lib.php +++ b/mod/workshop/form/rubric/backup/moodle1/lib.php @@ -46,8 +46,11 @@ class moodle1_workshopform_rubric_handler extends moodle1_workshopform_handler { /** * Processes one + * + * @param array $data legacy element data + * @param array $raw raw element data */ - public function process_legacy_element($data, $raw) { + public function process_legacy_element(array $data, array $raw) { $this->elements[] = $data; $this->rubrics[$data['id']] = array(); } From 89269fc2c82cc50c9deca7bdeb832305a4e96e7b Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 29 Aug 2012 17:42:53 +0100 Subject: [PATCH 24/96] MDL-33306 qtype multichoice: fix display of right answer. Also, fix higlighing of last edited question in the qeustion bank, when it co-incides with a grey row. --- mod/quiz/styles.css | 1 + question/type/multichoice/styles.css | 3 +++ 2 files changed, 4 insertions(+) diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index 63610241324..d07442b9980 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -178,6 +178,7 @@ table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;bac #page-mod-quiz-edit h2.main{display:inline;padding-right:1em;clear:left;} #categoryquestions .r1 {background: #e4e4e4;} +#categoryquestions .r1.highlight {background-color:#AAFFAA;} #categoryquestions .header {text-align: center;padding: 0 2px;border: 0 none;} #categoryquestions th.modifiername .sorters, #categoryquestions th.creatorname .sorters {font-weight: normal;font-size: 0.8em;} diff --git a/question/type/multichoice/styles.css b/question/type/multichoice/styles.css index 3cadf621f90..4aa77fd2a9f 100644 --- a/question/type/multichoice/styles.css +++ b/question/type/multichoice/styles.css @@ -10,3 +10,6 @@ .que.multichoice .answer div.r1 { padding: 0.3em; } +.que.multichoice .feedback .rightanswer * { + display: inline; +} From 2079c732f9e0f112f703962fb4fe0e416c9903f3 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 29 Aug 2012 18:41:37 +0100 Subject: [PATCH 25/96] MDL-29662 quiz overrides: update dates during course reset. Also, fix duplicate SQL clause in similar code in lib, and fix error when saving overrides. --- lib/moodlelib.php | 2 +- mod/quiz/lib.php | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 55cfd1ffa16..eda42c8faf1 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -4788,7 +4788,7 @@ function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) { foreach ($fields as $field) { $updatesql = "UPDATE {".$modname."} SET $field = $field + ? - WHERE course=? AND $field<>0 AND $field<>0"; + WHERE course=? AND $field<>0"; $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return; } diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 06a2e513b92..a14fc8fc659 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -1150,8 +1150,14 @@ function quiz_update_events($quiz, $override = null) { $addopen = empty($current->id) || !empty($current->timeopen); $addclose = empty($current->id) || !empty($current->timeclose); + if (!empty($quiz->coursemodule)) { + $cmid = $quiz->coursemodule; + } else { + $cmid = get_coursemodule_from_instance('quiz', $quiz->id, $courseid)->id; + } + $event = new stdClass(); - $event->description = format_module_intro('quiz', $quiz, $quiz->coursemodule); + $event->description = format_module_intro('quiz', $quiz, $cmid); // Events module won't show user events when the courseid is nonzero. $event->courseid = ($userid) ? 0 : $quiz->course; $event->groupid = $groupid; @@ -1341,8 +1347,18 @@ function quiz_reset_userdata($data) { // Updating dates - shift may be negative too. if ($data->timeshift) { + $DB->execute("UPDATE {quiz_overrides} + SET timeopen = timeopen + ? + WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?) + AND timeopen <> 0", array($data->timeshift, $data->courseid)); + $DB->execute("UPDATE {quiz_overrides} + SET timeclose = timeclose + ? + WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?) + AND timeclose <> 0", array($data->timeshift, $data->courseid)); + shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid); + $status[] = array( 'component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), From e231a3ffa2b667d07ea18e541d0dd91963506a29 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 30 Aug 2012 13:15:52 +0100 Subject: [PATCH 26/96] MDL-27799 question bank: remove last traces of qtype constants. Using consants to refer to plugin names is crazy. The whole point of plugins is that you can install more, hence the list of constants will never be complete. --- mod/lesson/format.php | 142 ++++++++++++++++++------- mod/lesson/pagetypes/multichoice.php | 2 +- question/format.php | 23 ---- question/format/aiken/format.php | 2 +- question/format/examview/format.php | 30 +++--- question/format/gift/examples.txt | 8 +- question/format/gift/format.php | 50 ++++----- question/format/learnwise/format.php | 2 +- question/format/missingword/format.php | 4 +- question/format/webct/format.php | 34 +++--- question/format/xhtml/format.php | 14 +-- question/format/xml/format.php | 16 +-- 12 files changed, 185 insertions(+), 142 deletions(-) diff --git a/mod/lesson/format.php b/mod/lesson/format.php index 574bee16bbc..d859e4afb18 100644 --- a/mod/lesson/format.php +++ b/mod/lesson/format.php @@ -29,27 +29,6 @@ defined('MOODLE_INTERNAL') || die(); -/**#@+ - * The core question types. - * - * These used to be in lib/questionlib.php, but are being deprecated. Copying them - * here to keep this code working for now. - */ -if (!defined('SHORTANSWER')) { - define("SHORTANSWER", "shortanswer"); - define("TRUEFALSE", "truefalse"); - define("MULTICHOICE", "multichoice"); - define("RANDOM", "random"); - define("MATCH", "match"); - define("RANDOMSAMATCH", "randomsamatch"); - define("DESCRIPTION", "description"); - define("NUMERICAL", "numerical"); - define("MULTIANSWER", "multianswer"); - define("CALCULATED", "calculated"); - define("ESSAY", "essay"); -} -/**#@-*/ - /** * Given some question info and some data about the the answers * this function parses, organises and saves the question @@ -59,10 +38,10 @@ if (!defined('SHORTANSWER')) { * Lifted from mod/quiz/lib.php - * 1. all reference to oldanswers removed * 2. all reference to quiz_multichoice table removed - * 3. In SHORTANSWER questions usecase is store in the qoption field - * 4. In NUMERIC questions store the range as two answers - * 5. TRUEFALSE options are ignored - * 6. For MULTICHOICE questions with more than one answer the qoption field is true + * 3. In shortanswer questions usecase is store in the qoption field + * 4. In numeric questions store the range as two answers + * 5. truefalse options are ignored + * 6. For multichoice questions with more than one answer the qoption field is true * * @param opject $question Contains question data like question, type and answers. * @return object Returns $result->error or $result->notice. @@ -116,7 +95,7 @@ function lesson_save_question_options($question, $lesson) { } break; - case LESSON_PAGE_NUMERICAL: // Note similarities to SHORTANSWER + case LESSON_PAGE_NUMERICAL: // Note similarities to shortanswer. $answers = array(); $maxfraction = -1; @@ -305,11 +284,11 @@ class qformat_default { var $displayerrors = true; var $category = NULL; var $questionids = array(); - var $qtypeconvert = array(NUMERICAL => LESSON_PAGE_NUMERICAL, - MULTICHOICE => LESSON_PAGE_MULTICHOICE, - TRUEFALSE => LESSON_PAGE_TRUEFALSE, - SHORTANSWER => LESSON_PAGE_SHORTANSWER, - MATCH => LESSON_PAGE_MATCHING + var $qtypeconvert = array('numerical' => LESSON_PAGE_NUMERICAL, + 'multichoice' => LESSON_PAGE_MULTICHOICE, + 'truefalse' => LESSON_PAGE_TRUEFALSE, + 'shortanswer' => LESSON_PAGE_SHORTANSWER, + 'match' => LESSON_PAGE_MATCHING ); // Importing functions @@ -352,11 +331,11 @@ class qformat_default { case 'category': break; // the good ones - case SHORTANSWER : - case NUMERICAL : - case TRUEFALSE : - case MULTICHOICE : - case MATCH : + case 'shortanswer' : + case 'numerical' : + case 'truefalse' : + case 'multichoice' : + case 'match' : $count++; //Show nice formated question in one line. @@ -366,12 +345,12 @@ class qformat_default { $newpage->lessonid = $lesson->id; $newpage->qtype = $this->qtypeconvert[$question->qtype]; switch ($question->qtype) { - case SHORTANSWER : + case 'shortanswer' : if (isset($question->usecase)) { $newpage->qoption = $question->usecase; } break; - case MULTICHOICE : + case 'multichoice' : if (isset($question->single)) { $newpage->qoption = !$question->single; } @@ -580,6 +559,93 @@ class qformat_default { return html_to_text(format_text($question->questiontext, $question->questiontextformat, $formatoptions), 0, false); } + + /** + * Since the lesson module tries to re-use the question bank import classes in + * a crazy way, this is necessary to stop things breaking. + */ + protected function add_blank_combined_feedback($question) { + return $question; + } } +/** + * Since the lesson module tries to re-use the question bank import classes in + * a crazy way, this is necessary to stop things breaking. This should be exactly + * the same as the class defined in question/format.php. + */ +class qformat_based_on_xml extends qformat_default { + /** + * A lot of imported files contain unwanted entities. + * This method tries to clean up all known problems. + * @param string str string to correct + * @return string the corrected string + */ + public function cleaninput($str) { + + $html_code_list = array( + "'" => "'", + "’" => "'", + "“" => "\"", + "”" => "\"", + "–" => "-", + "—" => "-", + ); + $str = strtr($str, $html_code_list); + // Use textlib entities_to_utf8 function to convert only numerical entities. + $str = textlib::entities_to_utf8($str, false); + return $str; + } + + /** + * Return the array moodle is expecting + * for an HTML text. No processing is done on $text. + * qformat classes that want to process $text + * for instance to import external images files + * and recode urls in $text must overwrite this method. + * @param array $text some HTML text string + * @return array with keys text, format and files. + */ + public function text_field($text) { + return array( + 'text' => trim($text), + 'format' => FORMAT_HTML, + 'files' => array(), + ); + } + + /** + * Return the value of a node, given a path to the node + * if it doesn't exist return the default value. + * @param array xml data to read + * @param array path path to node expressed as array + * @param mixed default + * @param bool istext process as text + * @param string error if set value must exist, return false and issue message if not + * @return mixed value + */ + public function getpath($xml, $path, $default, $istext=false, $error='') { + foreach ($path as $index) { + if (!isset($xml[$index])) { + if (!empty($error)) { + $this->error($error); + return false; + } else { + return $default; + } + } + + $xml = $xml[$index]; + } + + if ($istext) { + if (!is_string($xml)) { + $this->error(get_string('invalidxml', 'qformat_xml')); + } + $xml = trim($xml); + } + + return $xml; + } +} diff --git a/mod/lesson/pagetypes/multichoice.php b/mod/lesson/pagetypes/multichoice.php index f9307560885..089f621a970 100644 --- a/mod/lesson/pagetypes/multichoice.php +++ b/mod/lesson/pagetypes/multichoice.php @@ -126,7 +126,7 @@ class lesson_page_type_multichoice extends lesson_page { } if ($this->properties->qoption) { - // MULTIANSWER allowed, user's answer is an array + // Multianswer allowed, user's answer is an array if (empty($data->answer) || !is_array($data->answer)) { $result->noanswer = true; diff --git a/question/format.php b/question/format.php index 7748b6f04de..980f20973b7 100644 --- a/question/format.php +++ b/question/format.php @@ -27,29 +27,6 @@ defined('MOODLE_INTERNAL') || die(); -/**#@+ - * The core question types. - * - * These used to be in lib/questionlib.php, but are being deprecated. Copying - * them here to keep the import/export code working for now (there are 135 - * references to these constants which I don't want to try to fix at the moment.) - */ -if (!defined('SHORTANSWER')) { - define("SHORTANSWER", "shortanswer"); - define("TRUEFALSE", "truefalse"); - define("MULTICHOICE", "multichoice"); - define("RANDOM", "random"); - define("MATCH", "match"); - define("RANDOMSAMATCH", "randomsamatch"); - define("DESCRIPTION", "description"); - define("NUMERICAL", "numerical"); - define("MULTIANSWER", "multianswer"); - define("CALCULATED", "calculated"); - define("ESSAY", "essay"); -} -/**#@-*/ - - /** * Base class for question import and export formats. * diff --git a/question/format/aiken/format.php b/question/format/aiken/format.php index 4e64c2f6a93..8c3cbc99883 100644 --- a/question/format/aiken/format.php +++ b/question/format/aiken/format.php @@ -94,7 +94,7 @@ class qformat_aiken extends qformat_default { continue; } else { // Must be the first line of a new question, since no recognised prefix. - $question->qtype = MULTICHOICE; + $question->qtype = 'multichoice'; $question->name = shorten_text(s($nowline), 50); $question->questiontext = htmlspecialchars(trim($nowline), ENT_NOQUOTES); $question->questiontextformat = FORMAT_HTML; diff --git a/question/format/examview/format.php b/question/format/examview/format.php index 9cb9bb5c553..d13126619f6 100644 --- a/question/format/examview/format.php +++ b/question/format/examview/format.php @@ -37,18 +37,18 @@ require_once($CFG->libdir . '/xmlize.php'); class qformat_examview extends qformat_based_on_xml { public $qtypes = array( - 'tf' => TRUEFALSE, - 'mc' => MULTICHOICE, - 'yn' => TRUEFALSE, - 'co' => SHORTANSWER, - 'ma' => MATCH, + 'tf' => 'truefalse', + 'mc' => 'multichoice', + 'yn' => 'truefalse', + 'co' => 'shortanswer', + 'ma' => 'match', 'mtf' => 99, - 'nr' => NUMERICAL, + 'nr' => 'numerical', 'pr' => 99, - 'es' => ESSAY, + 'es' => 'essay', 'ca' => 99, 'ot' => 99, - 'sa' => SHORTANSWER, + 'sa' => 'shortanswer', ); public $matching_questions = array(); @@ -132,7 +132,7 @@ class qformat_examview extends qformat_based_on_xml { $question->questiontextformat = FORMAT_HTML; $question->questiontextfiles = array(); $question->name = shorten_text( $question->questiontext, 250 ); - $question->qtype = MATCH; + $question->qtype = 'match'; $question = $this->add_blank_combined_feedback($question); $question->subquestions = array(); $question->subanswers = array(); @@ -203,23 +203,23 @@ class qformat_examview extends qformat_based_on_xml { $question->name = shorten_text( $question->questiontext, 250 ); switch ($question->qtype) { - case MULTICHOICE: + case 'multichoice': $question = $this->parse_mc($qrec['#'], $question); break; - case MATCH: + case 'match': $groupname = trim($qrec['@']['group']); $question = $this->parse_ma($qrec['#'], $groupname); break; - case TRUEFALSE: + case 'truefalse': $question = $this->parse_tf_yn($qrec['#'], $question); break; - case SHORTANSWER: + case 'shortanswer': $question = $this->parse_co($qrec['#'], $question); break; - case ESSAY: + case 'essay': $question = $this->parse_es($qrec['#'], $question); break; - case NUMERICAL: + case 'numerical': $question = $this->parse_nr($qrec['#'], $question); break; break; diff --git a/question/format/gift/examples.txt b/question/format/gift/examples.txt index c0c1e0e2b7c..e65d4f0db64 100644 --- a/question/format/gift/examples.txt +++ b/question/format/gift/examples.txt @@ -2,7 +2,7 @@ // by Paul Tsuchido Shew, January 2004. //-----------------------------------------// -// EXAMPLES FROM DESCRIPTION +// Examples from the class description. //-----------------------------------------// Who's buried in Grant's tomb?{~Grant ~Jefferson =no one} @@ -17,7 +17,7 @@ When was Ulysses S. Grant born?{#1822:1} //-----------------------------------------// -// EXAMPLES FROM DOCUMENTATION +// Examples from the documentation. //-----------------------------------------// // ===Multiple Choice=== @@ -161,7 +161,7 @@ Which of the following is NOT a control character for the GIFT import format? { //-----------------------------------------// -// EXAMPLES FROM gift/format.php +// Examples from gift/format.php. //-----------------------------------------// Who's buried in Grant's tomb?{~Grant ~Jefferson =no one} @@ -178,7 +178,7 @@ Match the following countries with their corresponding capitals.{=Canada->Ottawa =Italy->Rome =Japan->Tokyo} //-----------------------------------------// -// MORE COMPLICATED EXAMPLES +// More complicated examples. //-----------------------------------------// ::Grant's Tomb::Grant is { diff --git a/question/format/gift/format.php b/question/format/gift/format.php index 48171a4d3c2..eb608393b1a 100644 --- a/question/format/gift/format.php +++ b/question/format/gift/format.php @@ -283,26 +283,26 @@ class qformat_gift extends qformat_default { } if ($description) { - $question->qtype = DESCRIPTION; + $question->qtype = 'description'; } else if ($answertext == '') { - $question->qtype = ESSAY; + $question->qtype = 'essay'; } else if ($answertext{0} == '#') { - $question->qtype = NUMERICAL; + $question->qtype = 'numerical'; } else if (strpos($answertext, '~') !== false) { // only Multiplechoice questions contain tilde ~ - $question->qtype = MULTICHOICE; + $question->qtype = 'multichoice'; } else if (strpos($answertext, '=') !== false && strpos($answertext, '->') !== false) { // only Matching contains both = and -> - $question->qtype = MATCH; + $question->qtype = 'match'; - } else { // either TRUEFALSE or SHORTANSWER + } else { // either truefalse or shortanswer - // TRUEFALSE question check + // truefalse question check $truefalse_check = $answertext; if (strpos($answertext, '#') > 0) { // strip comments to check for TrueFalse question @@ -311,10 +311,10 @@ class qformat_gift extends qformat_default { $valid_tf_answers = array('T', 'TRUE', 'F', 'FALSE'); if (in_array($truefalse_check, $valid_tf_answers)) { - $question->qtype = TRUEFALSE; + $question->qtype = 'truefalse'; - } else { // Must be SHORTANSWER - $question->qtype = SHORTANSWER; + } else { // Must be shortanswer + $question->qtype = 'shortanswer'; } } @@ -325,12 +325,12 @@ class qformat_gift extends qformat_default { } switch ($question->qtype) { - case DESCRIPTION: + case 'description': $question->defaultmark = 0; $question->length = 0; return $question; - case ESSAY: + case 'essay': $question->responseformat = 'editor'; $question->responsefieldlines = 15; $question->attachments = 0; @@ -338,7 +338,7 @@ class qformat_gift extends qformat_default { 'text' => '', 'format' => FORMAT_HTML, 'files' => array()); return $question; - case MULTICHOICE: + case 'multichoice': if (strpos($answertext,"=") === false) { $question->single = 0; // multiple answers are enabled if no single answer is 100% correct } else { @@ -382,7 +382,7 @@ class qformat_gift extends qformat_default { return $question; - case MATCH: + case 'match': $question = $this->add_blank_combined_feedback($question); $answers = explode('=', $answertext); @@ -413,7 +413,7 @@ class qformat_gift extends qformat_default { return $question; - case TRUEFALSE: + case 'truefalse': list($answer, $wrongfeedback, $rightfeedback) = $this->split_truefalse_comment($answertext, $question->questiontextformat); @@ -431,8 +431,8 @@ class qformat_gift extends qformat_default { return $question; - case SHORTANSWER: - // SHORTANSWER Question + case 'shortanswer': + // Shortanswer question. $answers = explode("=", $answertext); if (isset($answers[0])) { $answers[0] = trim($answers[0]); @@ -464,7 +464,7 @@ class qformat_gift extends qformat_default { return $question; - case NUMERICAL: + case 'numerical': // Note similarities to ShortAnswer $answertext = substr($answertext, 1); // remove leading "#" @@ -645,12 +645,12 @@ class qformat_gift extends qformat_default { $expout .= "\$CATEGORY: $question->category\n"; break; - case DESCRIPTION: + case 'description': $expout .= $this->write_name($question->name); $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); break; - case ESSAY: + case 'essay': $expout .= $this->write_name($question->name); $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); $expout .= "{"; @@ -658,7 +658,7 @@ class qformat_gift extends qformat_default { $expout .= "}\n"; break; - case TRUEFALSE: + case 'truefalse': $trueanswer = $question->options->answers[$question->options->trueanswer]; $falseanswer = $question->options->answers[$question->options->falseanswer]; if ($trueanswer->fraction == 1) { @@ -690,7 +690,7 @@ class qformat_gift extends qformat_default { $expout .= "}\n"; break; - case MULTICHOICE: + case 'multichoice': $expout .= $this->write_name($question->name); $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); $expout .= "{\n"; @@ -715,7 +715,7 @@ class qformat_gift extends qformat_default { $expout .= "}\n"; break; - case SHORTANSWER: + case 'shortanswer': $expout .= $this->write_name($question->name); $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); $expout .= "{\n"; @@ -729,7 +729,7 @@ class qformat_gift extends qformat_default { $expout .= "}\n"; break; - case NUMERICAL: + case 'numerical': $expout .= $this->write_name($question->name); $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); $expout .= "{#\n"; @@ -748,7 +748,7 @@ class qformat_gift extends qformat_default { $expout .= "}\n"; break; - case MATCH: + case 'match': $expout .= $this->write_name($question->name); $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); $expout .= "{\n"; diff --git a/question/format/learnwise/format.php b/question/format/learnwise/format.php index fa804756af6..3bd31b418e2 100644 --- a/question/format/learnwise/format.php +++ b/question/format/learnwise/format.php @@ -125,7 +125,7 @@ class qformat_learnwise extends qformat_default { } $question = $this->defaultquestion(); - $question->qtype = MULTICHOICE; + $question->qtype = 'multichoice'; $question->name = substr($questiontext, 0, 30); if (strlen($questiontext) > 30) { $question->name .= '...'; diff --git a/question/format/missingword/format.php b/question/format/missingword/format.php index 15e1d711045..a1b3cea580d 100644 --- a/question/format/missingword/format.php +++ b/question/format/missingword/format.php @@ -112,7 +112,7 @@ class qformat_missingword extends qformat_default { return false; case 1: - $question->qtype = SHORTANSWER; + $question->qtype = 'shortanswer'; $answer = trim($answers[0]); if ($answer[0] == "=") { @@ -125,7 +125,7 @@ class qformat_missingword extends qformat_default { return $question; default: - $question->qtype = MULTICHOICE; + $question->qtype = 'multichoice'; foreach ($answers as $key => $answer) { $answer = trim($answer); diff --git a/question/format/webct/format.php b/question/format/webct/format.php index c599ca8bbf9..6facb80fce8 100644 --- a/question/format/webct/format.php +++ b/question/format/webct/format.php @@ -314,7 +314,7 @@ class qformat_webct extends qformat_default { } } switch ($question->qtype) { - case SHORTANSWER: + case 'shortanswer': if ($maxfraction != 1) { $maxfraction = $maxfraction * 100; $errors[] = "'$question->name': ".get_string("wronggrade", "qformat_webct", $nLineCounter).' '.get_string("fractionsnomax", "question", $maxfraction); @@ -322,7 +322,7 @@ class qformat_webct extends qformat_default { } break; - case MULTICHOICE: + case 'multichoice': if ($question->single) { if ($maxfraction != 1) { $maxfraction = $maxfraction * 100; @@ -339,7 +339,7 @@ class qformat_webct extends qformat_default { } break; - case CALCULATED: + case 'calculated': foreach ($question->answers as $answer) { if ($formulaerror = qtype_calculated_find_formula_errors($answer)) { $warnings[] = "'$question->name': ". $formulaerror; @@ -352,7 +352,7 @@ class qformat_webct extends qformat_default { $question->import_process=TRUE ; unset($question->answer); //not used in calculated question break; - case MATCH: + case 'match': // MDL-10680: // switch subquestions and subanswers foreach ($question->subquestions as $id=>$subquestion) { @@ -391,7 +391,7 @@ class qformat_webct extends qformat_default { // Multiple Choice Question with only one good answer $question = $this->defaultquestion(); $question->feedback = array(); - $question->qtype = MULTICHOICE; + $question->qtype = 'multichoice'; $question->single = 1; // Only one answer is allowed $ignore_rest_of_question = FALSE; continue; @@ -401,7 +401,7 @@ class qformat_webct extends qformat_default { // Multiple Choice Question with several good answers $question = $this->defaultquestion(); $question->feedback = array(); - $question->qtype = MULTICHOICE; + $question->qtype = 'multichoice'; $question->single = 0; // Many answers allowed $ignore_rest_of_question = FALSE; continue; @@ -411,7 +411,7 @@ class qformat_webct extends qformat_default { // Short Answer Question $question = $this->defaultquestion(); $question->feedback = array(); - $question->qtype = SHORTANSWER; + $question->qtype = 'shortanswer'; $question->usecase = 0; // Ignore case $ignore_rest_of_question = FALSE; continue; @@ -420,7 +420,7 @@ class qformat_webct extends qformat_default { if (preg_match("~^:TYPE:C~i",$line)) { // Calculated Question $question = $this->defaultquestion(); - $question->qtype = CALCULATED; + $question->qtype = 'calculated'; $question->answers = array(); // No problem as they go as :FORMULA: from webct $question->units = array(); $question->dataset = array(); @@ -438,7 +438,7 @@ class qformat_webct extends qformat_default { if (preg_match("~^:TYPE:M~i",$line)) { // Match Question $question = $this->defaultquestion(); - $question->qtype = MATCH; + $question->qtype = 'match'; $question->feedback = array(); $ignore_rest_of_question = FALSE; // match question processing is not debugged continue; @@ -487,7 +487,7 @@ class qformat_webct extends qformat_default { if (!isset($question)) { continue; } - if (isset($question->qtype ) && CALCULATED == $question->qtype && preg_match( + if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match( "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?($webctnumberregex)~", $line, $webct_options)) { $datasetname = preg_replace('/^::/', '', $webct_options[1]); $datasetvalue = qformat_webct_convert_formula($webct_options[4]); @@ -519,7 +519,7 @@ class qformat_webct extends qformat_default { continue; } - if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i",$line,$webct_options)) { /// SHORTANSWER + if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i",$line,$webct_options)) { // Shortanswer. $currentchoice=$webct_options[1]; $answertext=$webct_options[2]; // Start gathering next lines $question->fraction[$currentchoice]=($webct_options[3]/100); @@ -534,7 +534,7 @@ class qformat_webct extends qformat_default { } if (preg_match('~^:FORMULA:(.*)~i', $line, $webct_options)) { - // Answer for a CALCULATED question + // Answer for a calculated question ++$currentchoice; $question->answers[$currentchoice] = qformat_webct_convert_formula($webct_options[1]); @@ -591,20 +591,20 @@ class qformat_webct extends qformat_default { continue; } - if (isset($question->qtype ) && CALCULATED == $question->qtype && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webct_options)) { + if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webct_options)) { // We can but hope that this always appear before the ANSTYPE property $question->correctanswerlength[$currentchoice] = $webct_options[1]; continue; } - if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match("~^:TOL:($webctnumberregex)~i", $line, $webct_options)) { + if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match("~^:TOL:($webctnumberregex)~i", $line, $webct_options)) { // We can but hope that this always appear before the TOL property $question->tolerance[$currentchoice] = qformat_webct_convert_formula($webct_options[1]); continue; } - if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) { + if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) { // Percentage case is handled as relative in Moodle: $question->tolerance[$currentchoice] /= 100; $question->tolerancetype[$currentchoice] = 1; // Relative @@ -639,11 +639,11 @@ class qformat_webct extends qformat_default { continue; } - if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) { + if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) { $question->correctanswerformat[$currentchoice]='1'; continue; } - if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) { + if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) { $question->correctanswerformat[$currentchoice]='2'; continue; } diff --git a/question/format/xhtml/format.php b/question/format/xhtml/format.php index 0b70c45010d..6110d276182 100644 --- a/question/format/xhtml/format.php +++ b/question/format/xhtml/format.php @@ -72,7 +72,7 @@ class qformat_xhtml extends qformat_default { // selection depends on question type switch($question->qtype) { - case TRUEFALSE: + case 'truefalse': $st_true = get_string('true', 'qtype_truefalse'); $st_false = get_string('false', 'qtype_truefalse'); $expout .= "
    \n"; @@ -80,7 +80,7 @@ class qformat_xhtml extends qformat_default { $expout .= "
  • $st_false
  • \n"; $expout .= "
\n"; break; - case MULTICHOICE: + case 'multichoice': $expout .= "
    \n"; foreach($question->options->answers as $answer) { $ans_text = $this->repchar( $answer->answer ); @@ -93,17 +93,17 @@ class qformat_xhtml extends qformat_default { } $expout .= "
\n"; break; - case SHORTANSWER: + case 'shortanswer': $expout .= "
    \n"; $expout .= "
  • \n"; $expout .= "
\n"; break; - case NUMERICAL: + case 'numerical': $expout .= "
    \n"; $expout .= "
  • \n"; $expout .= "
\n"; break; - case MATCH: + case 'match': $expout .= "
    \n"; // build answer list @@ -128,9 +128,9 @@ class qformat_xhtml extends qformat_default { } $expout .= "
\n"; break; - case DESCRIPTION: + case 'description': break; - case MULTIANSWER: + case 'multichoice': $expout .= "\n"; break; default: diff --git a/question/format/xml/format.php b/question/format/xml/format.php index 740279f057f..7cdb23c5a8e 100644 --- a/question/format/xml/format.php +++ b/question/format/xml/format.php @@ -387,7 +387,7 @@ class qformat_xml extends qformat_default { $qo = $this->import_headers($question); // 'header' parts particular to multichoice - $qo->qtype = MULTICHOICE; + $qo->qtype = 'multichoice'; $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true'); $qo->single = $this->trans_single($single); $shuffleanswers = $this->getpath($question, @@ -473,7 +473,7 @@ class qformat_xml extends qformat_default { $qo = $this->import_headers($question); // 'header' parts particular to true/false - $qo->qtype = TRUEFALSE; + $qo->qtype = 'truefalse'; // In the past, it used to be assumed that the two answers were in the file // true first, then false. Howevever that was not always true. Now, we @@ -548,7 +548,7 @@ class qformat_xml extends qformat_default { $qo = $this->import_headers($question); // header parts particular to shortanswer - $qo->qtype = SHORTANSWER; + $qo->qtype = 'shortanswer'; // get usecase $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase); @@ -578,7 +578,7 @@ class qformat_xml extends qformat_default { // get common parts $qo = $this->import_headers($question); // header parts particular to shortanswer - $qo->qtype = DESCRIPTION; + $qo->qtype = 'description'; $qo->defaultmark = 0; $qo->length = 0; return $qo; @@ -594,7 +594,7 @@ class qformat_xml extends qformat_default { $qo = $this->import_headers($question); // header parts particular to numerical - $qo->qtype = NUMERICAL; + $qo->qtype = 'numerical'; // get answers array $answers = $question['#']['answer']; @@ -706,7 +706,7 @@ class qformat_xml extends qformat_default { $qo = $this->import_headers($question); // header parts particular to essay - $qo->qtype = ESSAY; + $qo->qtype = 'essay'; $qo->responseformat = $this->getpath($question, array('#', 'responseformat', 0, '#'), 'editor'); @@ -734,7 +734,7 @@ class qformat_xml extends qformat_default { $qo = $this->import_headers($question); // header parts particular to calculated - $qo->qtype = CALCULATED; + $qo->qtype = 'calculated'; $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0); $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true'); $qo->single = $this->trans_single($single); @@ -1363,7 +1363,7 @@ class qformat_xml extends qformat_default { $expout .= "\n"; $expout .= " ".$this->writetext($def->status)."\n"; $expout .= " ".$this->writetext($def->name)."\n"; - if ($question->qtype == CALCULATED) { + if ($question->qtype == 'calculated') { $expout .= " calculated\n"; } else { $expout .= " calculatedsimple\n"; From 34bc5e2f3a39076d511384fe0153ddf8277d3e3e Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 29 Aug 2012 22:25:28 +0100 Subject: [PATCH 27/96] MDL-35133 accesslib: context_module::get_context_name should format_string Otherwise activites with multilang names are displayed badly. --- lib/accesslib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/accesslib.php b/lib/accesslib.php index 523086375fe..2163435043d 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -6676,7 +6676,7 @@ class context_module extends context { if ($withprefix){ $name = get_string('modulename', $cm->modname).': '; } - $name .= $mod->name; + $name .= format_string($mod->name, true, array('context' => $this)); } } return $name; From fc51c2bce23b0146904f7a6f46c6c9c31fc81cdb Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 29 Aug 2012 22:43:28 +0100 Subject: [PATCH 28/96] MDL-27818 question bank: multilang category names don't work. --- lib/questionlib.php | 12 +++++++----- question/category_class.php | 7 +++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/questionlib.php b/lib/questionlib.php index 7dc749afab8..7298d75c34e 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -1117,16 +1117,18 @@ function question_category_options($contexts, $top = false, $currentcat = 0, // sort cats out into different contexts $categoriesarray = array(); - foreach ($pcontexts as $pcontext) { - $contextstring = print_context_name( - context::instance_by_id($pcontext), true, true); + foreach ($pcontexts as $contextid) { + $context = context::instance_by_id($contextid); + $contextstring = $context->get_context_name(true, true); foreach ($categories as $category) { - if ($category->contextid == $pcontext) { + if ($category->contextid == $contextid) { $cid = $category->id; if ($currentcat != $cid || $currentcat == 0) { $countstring = !empty($category->questioncount) ? " ($category->questioncount)" : ''; - $categoriesarray[$contextstring][$cid] = $category->indentedname.$countstring; + $categoriesarray[$contextstring][$cid] = + format_string($category->indentedname, true, + array('context' => $context)) . $countstring; } } } diff --git a/question/category_class.php b/question/category_class.php index 79339f29e1c..34ff811f3fd 100644 --- a/question/category_class.php +++ b/question/category_class.php @@ -102,9 +102,12 @@ class question_category_list_item extends list_item { /// Each section adds html to be displayed as part of this list item $questionbankurl = new moodle_url("/question/edit.php", ($this->parentlist->pageurl->params() + array('category'=>"$category->id,$category->contextid"))); $catediturl = $this->parentlist->pageurl->out(true, array('edit' => $this->id)); - $item = "edit}\" href=\"$catediturl\">".$category->name ." ".'('.$category->questioncount.')'; + $item = "edit}\" href=\"$catediturl\">" . + format_string($category->name, true, array('context' => $this->parentlist->context)) . + " ".'('.$category->questioncount.')'; - $item .= ' '. $category->info; + $item .= ' ' . format_text($category->info, $category->infoformat, + array('context' => $this->parentlist->context, 'noclean' => true)); // don't allow delete if this is the last category in this context. if (count($this->parentlist->records) != 1) { From 6910d97d8b430fbd0ec1c863df122a5722b7777a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Tue, 28 Aug 2012 08:08:30 +0200 Subject: [PATCH 29/96] MDL-35077 fix stats report navigation links --- report/stats/lib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/report/stats/lib.php b/report/stats/lib.php index c185fb3a2c5..1c191a453c1 100644 --- a/report/stats/lib.php +++ b/report/stats/lib.php @@ -36,7 +36,7 @@ defined('MOODLE_INTERNAL') || die; */ function report_stats_extend_navigation_course($navigation, $course, $context) { global $CFG; - if (!empty($CFG->enablestats)) { + if (empty($CFG->enablestats)) { return; } if (has_capability('report/stats:view', $context)) { @@ -54,7 +54,7 @@ function report_stats_extend_navigation_course($navigation, $course, $context) { */ function report_stats_extend_navigation_user($navigation, $user, $course) { global $CFG; - if (!empty($CFG->enablestats)) { + if (empty($CFG->enablestats)) { return; } if (report_stats_can_access_user_report($user, $course)) { From 5d911452cd9ef60f8f79ed9bd39fc8302211a96c Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Fri, 11 May 2012 15:02:56 -0400 Subject: [PATCH 30/96] MDL-30643 - Fixed speed issues with stats generation. --- lib/statslib.php | 240 ++++++++++++++++++++++++++++------------------- 1 file changed, 143 insertions(+), 97 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index 6b2220fbe25..936bb465743 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -80,12 +80,12 @@ function stats_daily_progress($ident) { static $init = 0; if ($ident == 'init') { - $init = $start = time(); + $init = $start = microtime(true); return; } - $elapsed = time() - $start; - $start = time(); + $elapsed = round(microtime(true) - $start); + $start = microtime(true); if (debugging('', DEBUG_ALL)) { mtrace("$ident:$elapsed ", ''); @@ -176,29 +176,27 @@ function stats_cron_daily($maxdays=1) { $daystart = time(); - $timesql = "l.time >= $timestart AND l.time < $nextmidnight"; - $timesql1 = "l1.time >= $timestart AND l1.time < $nextmidnight"; - $timesql2 = "l2.time >= $timestart AND l2.time < $nextmidnight"; - stats_daily_progress('init'); + if ( !stats_create_logs_view($timestart, $nextmidnight) ) { + $failed = true; + break; + } + + stats_daily_progress('gen'); + /// find out if any logs available for this day - $sql = "SELECT 'x' - FROM {log} l - WHERE $timesql"; + $sql = "SELECT 'x' FROM {tmp_stats_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); /// process login info first $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads) - SELECT 'logins', timeend, courseid, userid, count(statsreads) - FROM ( - SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, l.userid, l.id AS statsreads - FROM {log} l - WHERE action = 'login' AND $timesql - ) inline_view - GROUP BY timeend, courseid, userid + SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads + FROM {tmp_stats_log1} l + WHERE action = 'login' + GROUP BY timeend, courseid, userid HAVING count(statsreads) > 0"; if ($logspresent and !$DB->execute($sql)) { @@ -210,13 +208,9 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0, - COALESCE((SELECT SUM(statsreads) - FROM {stats_user_daily} s1 - WHERE s1.stattype = 'logins' AND timeend = $nextmidnight), 0) AS stat1, - (SELECT COUNT('x') - FROM {stats_user_daily} s2 - WHERE s2.stattype = 'logins' AND timeend = $nextmidnight) AS stat2" . - $DB->sql_null_from_clause(); + COALESCE(SUM(statsreads), 0) as stat1, COUNT('x') as stat2 + FROM {stats_user_daily} + WHERE stattype = 'logins' AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { $failed = true; @@ -237,15 +231,13 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments', timeend, courseid, roleid, COUNT(DISTINCT userid), 0 - FROM ( - SELECT $nextmidnight AS timeend, e.courseid, ra.roleid, ue.userid - FROM {role_assignments} ra - JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) - JOIN {enrol} e ON e.courseid = c.instanceid - JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - ) inline_view - GROUP BY timeend, courseid, roleid"; + SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, + COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 + FROM {role_assignments} ra + JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) + JOIN {enrol} e ON e.courseid = c.instanceid + JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) + GROUP BY courseid, roleid"; if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; @@ -263,15 +255,14 @@ function stats_cron_daily($maxdays=1) { WHERE ra.roleid = {stats_daily}.roleid AND e.courseid = {stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {log} l + FROM {tmp_stats_log1} l WHERE l.course = {stats_daily}.courseid AND - l.userid = ra.userid AND $timesql)) + l.userid = ra.userid)) WHERE {stats_daily}.stattype = 'enrolments' AND {stats_daily}.timeend = $nextmidnight AND {stats_daily}.courseid IN - (SELECT DISTINCT l.course - FROM {log} l - WHERE $timesql)"; + (SELECT DISTINCT course + FROM {tmp_stats_log2})"; if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; @@ -282,13 +273,11 @@ function stats_cron_daily($maxdays=1) { /// now get course total enrolments (roleid==0) - except frontpage $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments', timeend, id, nroleid, COUNT(DISTINCT userid), 0 - FROM ( - SELECT $nextmidnight AS timeend, e.courseid AS id, 0 AS nroleid, ue.userid - FROM {enrol} e - JOIN {user_enrolments} ue ON ue.enrolid = e.id - ) inline_view - GROUP BY timeend, id, nroleid + SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, + COUNT(DISTINCT userid) AS stat1, 0 AS stat2 + FROM {enrol} e + JOIN {user_enrolments} ue ON ue.enrolid = e.id + GROUP BY courseid HAVING COUNT(DISTINCT userid) > 0"; if ($logspresent and !$DB->execute($sql)) { @@ -303,16 +292,16 @@ function stats_cron_daily($maxdays=1) { JOIN {user_enrolments} ue ON ue.enrolid = e.id WHERE e.courseid = {stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {log} l + FROM {tmp_stats_log1} l WHERE l.course = {stats_daily}.courseid AND - l.userid = ue.userid AND $timesql)) + l.userid = ue.userid)) WHERE {stats_daily}.stattype = 'enrolments' AND {stats_daily}.timeend = $nextmidnight AND {stats_daily}.roleid = 0 AND {stats_daily}.courseid IN (SELECT l.course - FROM {log} l - WHERE $timesql AND l.course <> ".SITEID.")"; + FROM {tmp_stats_log2} l + WHERE l.course <> ".SITEID.")"; if ($logspresent and !$DB->execute($sql, array())) { $failed = true; @@ -320,7 +309,7 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('6'); - /// frontapge(==site) enrolments total + /// frontpage(==site) enrolments total $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", 0, @@ -329,8 +318,8 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {log} l ON l.userid = u.id - WHERE u.deleted = 0 AND $timesql) AS stat2" . + JOIN {tmp_stats_log1} l ON l.userid = u.id + WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause(); if ($logspresent and !$DB->execute($sql)) { @@ -360,8 +349,8 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {log} l ON l.userid = u.id - WHERE u.deleted = 0 AND $timesql) AS stat2" . + JOIN {tmp_stats_log1} l ON l.userid = u.id + WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause();; if ($logspresent and !$DB->execute($sql)) { @@ -376,29 +365,17 @@ function stats_cron_daily($maxdays=1) { } - /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible list($viewactionssql, $params1) = $DB->get_in_or_equal($viewactions, SQL_PARAMS_NAMED, 'view'); list($postactionssql, $params2) = $DB->get_in_or_equal($postactions, SQL_PARAMS_NAMED, 'post'); $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) - SELECT 'activity' AS stattype, $nextmidnight AS timeend, d.courseid, d.userid, - (SELECT COUNT('x') - FROM {log} l - WHERE l.userid = d.userid AND - l.course = d.courseid AND $timesql AND - l.action $viewactionssql) AS statsreads, - (SELECT COUNT('x') - FROM {log} l - WHERE l.userid = d.userid AND - l.course = d.courseid AND $timesql AND - l.action $postactionssql) AS statswrites - FROM (SELECT DISTINCT u.id AS userid, l.course AS courseid - FROM {user} u, {log} l - WHERE u.id = l.userid AND $timesql - UNION - SELECT 0 AS userid, ".SITEID." AS courseid" . $DB->sql_null_from_clause() . ") d"; - // can not use group by here because pg can not handle it :-( + SELECT 'activity' AS stattype, $nextmidnight AS timeend, course as courseid, userid, + SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, + SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites + FROM {tmp_stats_log1} l + WHERE !(course = 0 AND userid = 0) + GROUP BY userid, courseid"; if ($logspresent and !$DB->execute($sql, array_merge($params1, $params2))) { $failed = true; @@ -411,18 +388,11 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0, - (SELECT COUNT('x') - FROM {log} l1 - WHERE l1.course = c.id AND l1.action $viewactionssql AND - $timesql1) AS stat1, - (SELECT COUNT('x') - FROM {log} l2 - WHERE l2.course = c.id AND l2.action $postactionssql AND - $timesql2) AS stat2 - FROM {course} c - WHERE EXISTS (SELECT 'x' - FROM {log} l - WHERE l.course = c.id and $timesql)"; + SUM(CASE WHEN l.action $viewactionssql THEN 1 ELSE 0 END) AS stat1, + SUM(CASE WHEN l.action $postactionssql THEN 1 ELSE 0 END) AS stat2 + FROM {course} c, {tmp_stats_log1} l + WHERE l.course = c.id + GROUP BY courseid"; if ($logspresent and !$DB->execute($sql, array_merge($params1, $params2))) { $failed = true; @@ -435,9 +405,9 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) + SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( - SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites + SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, e.courseid FROM {role_assignments} ra @@ -466,9 +436,10 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites) + SELECT 'activity', $nextmidnight as timeend, courseid, $guestrole AS roleid, + SUM(statsreads), SUM(statswrites) FROM ( - SELECT $nextmidnight AS timeend, sud.courseid, $guestrole AS nroleid, sud.statsreads, sud.statswrites + SELECT sud.courseid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND @@ -478,7 +449,7 @@ function stats_cron_daily($maxdays=1) { JOIN {enrol} e ON ue.enrolid = e.id WHERE e.courseid = sud.courseid)) ) inline_view - GROUP BY timeend, courseid, nroleid + GROUP BY timeend, courseid, roleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; if ($logspresent and !$DB->execute($sql, array())) { @@ -491,9 +462,10 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) + SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, + SUM(statsreads), SUM(statswrites) FROM ( - SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites + SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, c.instanceid AS courseid FROM {role_assignments} ra @@ -521,9 +493,10 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for default frontpage role on frontpage only $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites) + SELECT 'activity', timeend, courseid, $defaultfproleid AS roleid, + SUM(statsreads), SUM(statswrites) FROM ( - SELECT sud.timeend AS timeend, sud.courseid, $defaultfproleid AS nroleid, sud.statsreads, sud.statswrites + SELECT sud.timeend AS timeend, sud.courseid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud WHERE sud.timeend = :nextm AND sud.courseid = :siteid AND sud.stattype='activity' AND @@ -533,7 +506,7 @@ function stats_cron_daily($maxdays=1) { WHERE ra.roleid <> $guestrole AND ra.roleid <> $defaultfproleid AND ra.contextid = :fpcontext) ) inline_view - GROUP BY timeend, courseid, nroleid + GROUP BY timeend, courseid, roleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; if ($logspresent and !$DB->execute($sql, array('fpcontext'=>$fpcontext->id, 'siteid'=>SITEID, 'nextm'=>$nextmidnight))) { @@ -545,9 +518,10 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for guests or not-logged-in on frontpage $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites) + SELECT 'activity', $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS roleid, + SUM(statsreads), SUM(statswrites) FROM ( - SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS nroleid, pl.statsreads, pl.statswrites + SELECT pl.statsreads, pl.statswrites FROM ( SELECT sud.statsreads, sud.statswrites FROM {stats_user_daily} sud @@ -556,7 +530,7 @@ function stats_cron_daily($maxdays=1) { sud.stattype='activity' ) pl ) inline_view - GROUP BY timeend, courseid, nroleid + GROUP BY timeend, courseid, roleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; if ($logspresent and !$DB->execute($sql)) { @@ -573,6 +547,8 @@ function stats_cron_daily($maxdays=1) { $nextmidnight = stats_get_next_day_start($nextmidnight); } + stats_drop_logs_view(); + set_cron_lock('statsrunning', null); if ($failed) { @@ -1462,3 +1438,73 @@ function stats_check_uptodate($courseid=0) { //return error as string return get_string('statscatchupmode','error',$a); } + +/** + * Creates tmp log table + * + * @param timestart timestamp of the start time of logs view + * @param timeend timestamp of the end time of logs view + * @returns boolen success (true) or failure(false) + */ +function stats_create_logs_view ( $timestart, $timeend ) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); // We are going to use database_manager services + + stats_drop_logs_view(); + + $table = new xmldb_table('tmp_stats_log1'); + + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); + + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('tmp_stats_log_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); + $table->add_index('tmp_stats_log_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); + $table->add_index('tmp_stats_log_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $table->add_index('tmp_stats_log_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); + + $dbman->create_temp_table($table); + + $table->name = 'tmp_stats_log2'; + + $dbman->create_temp_table($table); + + $sql = "INSERT INTO {tmp_stats_log1} + SELECT id, userid, course, action FROM {log} l + WHERE l.time >= $timestart AND l.time < $timeend"; + + if (!$DB->execute($sql)) { + return false; + } + + $sql = "INSERT INTO {tmp_stats_log2} + SELECT * FROM {tmp_stats_log1}"; + + if (!$DB->execute($sql)) { + return false; + } + + return true; +} + +/** + * Deletes summary logs table for stats calculation + */ +function stats_drop_logs_view() { + global $DB; + + $dbman = $DB->get_manager(); + + if ( $dbman->table_exists('tmp_stats_log1') ) { + $table = new xmldb_table('tmp_stats_log1'); + $dbman->drop_temp_table($table); + } + + if ( $dbman->table_exists('tmp_stats_log2') ) { + $table = new xmldb_table('tmp_stats_log2'); + $dbman->drop_temp_table($table); + } +} From e710925a24bbbe0a4f0ddfa0b760380091890083 Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Fri, 18 May 2012 10:07:48 -0400 Subject: [PATCH 31/96] MDL-30643 - Added additional temporary tables to deal with log growth in daily tables. --- lib/statslib.php | 165 +++++++++++++++++++++++++++++------------------ 1 file changed, 104 insertions(+), 61 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index 936bb465743..93173992ab0 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -187,14 +187,14 @@ function stats_cron_daily($maxdays=1) { /// find out if any logs available for this day - $sql = "SELECT 'x' FROM {tmp_stats_log1} l"; + $sql = "SELECT 'x' FROM {tmp_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); /// process login info first - $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads) + $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads - FROM {tmp_stats_log1} l + FROM {tmp_log1} l WHERE action = 'login' GROUP BY timeend, courseid, userid HAVING count(statsreads) > 0"; @@ -205,11 +205,11 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('1'); - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0, COALESCE(SUM(statsreads), 0) as stat1, COUNT('x') as stat2 - FROM {stats_user_daily} + FROM {tmp_stats_user_daily} WHERE stattype = 'logins' AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -229,7 +229,7 @@ function stats_cron_daily($maxdays=1) { // in that case, we'll count non-deleted users. // - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 @@ -246,23 +246,23 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('3'); // using table alias in UPDATE does not work in pg < 8.2 - $sql = "UPDATE {stats_daily} + $sql = "UPDATE {tmp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ra.userid) FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) JOIN {enrol} e ON e.courseid = c.instanceid JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - WHERE ra.roleid = {stats_daily}.roleid AND - e.courseid = {stats_daily}.courseid AND + WHERE ra.roleid = {tmp_stats_daily}.roleid AND + e.courseid = {tmp_stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_stats_log1} l - WHERE l.course = {stats_daily}.courseid AND + FROM {tmp_log1} l + WHERE l.course = {tmp_stats_daily}.courseid AND l.userid = ra.userid)) - WHERE {stats_daily}.stattype = 'enrolments' AND - {stats_daily}.timeend = $nextmidnight AND - {stats_daily}.courseid IN + WHERE {tmp_stats_daily}.stattype = 'enrolments' AND + {tmp_stats_daily}.timeend = $nextmidnight AND + {tmp_stats_daily}.courseid IN (SELECT DISTINCT course - FROM {tmp_stats_log2})"; + FROM {tmp_log2})"; if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; @@ -271,7 +271,7 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('4'); /// now get course total enrolments (roleid==0) - except frontpage - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, COUNT(DISTINCT userid) AS stat1, 0 AS stat2 @@ -286,21 +286,21 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('5'); - $sql = "UPDATE {stats_daily} + $sql = "UPDATE {tmp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ue.userid) FROM {enrol} e JOIN {user_enrolments} ue ON ue.enrolid = e.id - WHERE e.courseid = {stats_daily}.courseid AND + WHERE e.courseid = {tmp_stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_stats_log1} l - WHERE l.course = {stats_daily}.courseid AND + FROM {tmp_log1} l + WHERE l.course = {tmp_stats_daily}.courseid AND l.userid = ue.userid)) - WHERE {stats_daily}.stattype = 'enrolments' AND - {stats_daily}.timeend = $nextmidnight AND - {stats_daily}.roleid = 0 AND - {stats_daily}.courseid IN + WHERE {tmp_stats_daily}.stattype = 'enrolments' AND + {tmp_stats_daily}.timeend = $nextmidnight AND + {tmp_stats_daily}.roleid = 0 AND + {tmp_stats_daily}.courseid IN (SELECT l.course - FROM {tmp_stats_log2} l + FROM {tmp_log2} l WHERE l.course <> ".SITEID.")"; if ($logspresent and !$DB->execute($sql, array())) { @@ -310,7 +310,7 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('6'); /// frontpage(==site) enrolments total - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", 0, (SELECT COUNT('x') @@ -318,7 +318,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_stats_log1} l ON l.userid = u.id + JOIN {tmp_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause(); @@ -332,7 +332,7 @@ function stats_cron_daily($maxdays=1) { if ($defaultfproleid) { // first remove default frontpage role counts if created by previous query $sql = "DELETE - FROM {stats_daily} + FROM {tmp_stats_daily} WHERE stattype = 'enrolments' AND courseid = ".SITEID." AND roleid = $defaultfproleid AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -341,7 +341,7 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('8'); - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", $defaultfproleid, (SELECT COUNT('x') @@ -349,7 +349,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_stats_log1} l ON l.userid = u.id + JOIN {tmp_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause();; @@ -368,12 +368,12 @@ function stats_cron_daily($maxdays=1) { /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible list($viewactionssql, $params1) = $DB->get_in_or_equal($viewactions, SQL_PARAMS_NAMED, 'view'); list($postactionssql, $params2) = $DB->get_in_or_equal($postactions, SQL_PARAMS_NAMED, 'post'); - $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) + $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) SELECT 'activity' AS stattype, $nextmidnight AS timeend, course as courseid, userid, SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites - FROM {tmp_stats_log1} l + FROM {tmp_log1} l WHERE !(course = 0 AND userid = 0) GROUP BY userid, courseid"; @@ -385,12 +385,12 @@ function stats_cron_daily($maxdays=1) { /// how many view/post actions in each course total - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0, SUM(CASE WHEN l.action $viewactionssql THEN 1 ELSE 0 END) AS stat1, SUM(CASE WHEN l.action $postactionssql THEN 1 ELSE 0 END) AS stat2 - FROM {course} c, {tmp_stats_log1} l + FROM {course} c, {tmp_log1} l WHERE l.course = c.id GROUP BY courseid"; @@ -403,12 +403,12 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for each course+role - excluding guests and frontpage - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {stats_user_daily} sud, + FROM {tmp_stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, e.courseid FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) @@ -434,13 +434,13 @@ function stats_cron_daily($maxdays=1) { /// how many view actions from guests only in each course - excluding frontpage /// normal users may enter course with temporary guest access too - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight as timeend, courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.courseid, sud.statsreads, sud.statswrites - FROM {stats_user_daily} sud + FROM {tmp_stats_user_daily} sud WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND (sud.userid = $guest OR sud.userid @@ -460,13 +460,13 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {stats_user_daily} sud, + FROM {tmp_stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, c.instanceid AS courseid FROM {role_assignments} ra JOIN {context} c ON c.id = ra.contextid @@ -491,13 +491,13 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for default frontpage role on frontpage only - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', timeend, courseid, $defaultfproleid AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.timeend AS timeend, sud.courseid, sud.statsreads, sud.statswrites - FROM {stats_user_daily} sud + FROM {tmp_stats_user_daily} sud WHERE sud.timeend = :nextm AND sud.courseid = :siteid AND sud.stattype='activity' AND sud.userid <> $guest AND sud.userid <> 0 AND sud.userid @@ -516,7 +516,7 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('15'); /// how many view actions for guests or not-logged-in on frontpage - $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) @@ -524,7 +524,7 @@ function stats_cron_daily($maxdays=1) { SELECT pl.statsreads, pl.statswrites FROM ( SELECT sud.statsreads, sud.statswrites - FROM {stats_user_daily} sud + FROM {tmp_stats_user_daily} sud WHERE (sud.userid = $guest OR sud.userid = 0) AND sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND sud.stattype='activity' @@ -539,6 +539,20 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('16'); + $sql = "INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2) + SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}"; + if (!$DB->execute($sql)) { + $failed = true; + break; + } + + $sql = "INSERT INTO {stats_user_daily} (courseid, userid, roleid, timeend, statsreads, statswrites, stattype) + SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype FROM {tmp_stats_user_daily}"; + if (!$DB->execute($sql)) { + $failed = true; + break; + } + // remember processed days set_config('statslastdaily', $nextmidnight); mtrace(" finished until $nextmidnight: ".userdate($nextmidnight)." (in ".(time()-$daystart)." s)"); @@ -1440,7 +1454,7 @@ function stats_check_uptodate($courseid=0) { } /** - * Creates tmp log table + * Create temporary tables to speed up log generation * * @param timestart timestamp of the start time of logs view * @param timeend timestamp of the end time of logs view @@ -1453,7 +1467,7 @@ function stats_create_logs_view ( $timestart, $timeend ) { stats_drop_logs_view(); - $table = new xmldb_table('tmp_stats_log1'); + $table = new xmldb_table('tmp_log1'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); @@ -1461,18 +1475,18 @@ function stats_create_logs_view ( $timestart, $timeend ) { $table->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->add_index('tmp_stats_log_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); - $table->add_index('tmp_stats_log_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); - $table->add_index('tmp_stats_log_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $table->add_index('tmp_stats_log_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); + $table->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); + $table->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); + $table->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $table->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); $dbman->create_temp_table($table); - $table->name = 'tmp_stats_log2'; + $table->name = 'tmp_log2'; $dbman->create_temp_table($table); - $sql = "INSERT INTO {tmp_stats_log1} + $sql = "INSERT INTO {tmp_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; @@ -1480,13 +1494,43 @@ function stats_create_logs_view ( $timestart, $timeend ) { return false; } - $sql = "INSERT INTO {tmp_stats_log2} - SELECT * FROM {tmp_stats_log1}"; + $sql = "INSERT INTO {tmp_log2} + SELECT * FROM {tmp_log1}"; if (!$DB->execute($sql)) { return false; } + $table = new xmldb_table('tmp_stats_daily'); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); + $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + + $table->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $table->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + $table->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + + $dbman->create_temp_table($table); + + $table = new xmldb_table('tmp_stats_user_daily'); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); + + $table->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $table->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $table->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + $table->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + + $dbman->create_temp_table($table); + return true; } @@ -1498,13 +1542,12 @@ function stats_drop_logs_view() { $dbman = $DB->get_manager(); - if ( $dbman->table_exists('tmp_stats_log1') ) { - $table = new xmldb_table('tmp_stats_log1'); - $dbman->drop_temp_table($table); - } + $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); - if ( $dbman->table_exists('tmp_stats_log2') ) { - $table = new xmldb_table('tmp_stats_log2'); - $dbman->drop_temp_table($table); + foreach ($tables as $name) { + if ($dbman->table_exists($name)) { + $table = new xmldb_table($name); + $dbman->drop_temp_table($table); + } } } From 678f53c5f781d45424020530435382645325ab73 Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Thu, 14 Jun 2012 08:18:01 -0400 Subject: [PATCH 32/96] MDL-30643 - Moved table create/drop out of loop to fix performance issue. - Added additional output to weekly and monthly stat generation --- lib/statslib.php | 298 ++++++++++++++++++++++++++++++----------------- 1 file changed, 192 insertions(+), 106 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index 93173992ab0..f44ae1ff571 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -75,7 +75,7 @@ define('STATS_MODE_RANKED',3); // admins only - ranks courses * Print daily cron progress * @param string $ident */ -function stats_daily_progress($ident) { +function stats_progress($ident) { static $start = 0; static $init = 0; @@ -156,14 +156,21 @@ function stats_cron_daily($maxdays=1) { mtrace("Running daily statistics gathering, starting at $timestart:"); - $days = 0; - $failed = false; // failed stats flag + $days = 0; + $total = 0; + $failed = false; // failed stats flag + $timeout = false; - while ($now > $nextmidnight) { + if (!stats_temp_table_create()) { + $days = 1; + $failed = true; + } + mtrace("Temporary tables created"); + + while (!$failed && ($now > $nextmidnight)) { if ($days >= $maxdays) { - mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time."); - set_cron_lock('statsrunning', null); - return false; + $timeout = true; + break; } $days++; @@ -176,15 +183,14 @@ function stats_cron_daily($maxdays=1) { $daystart = time(); - stats_daily_progress('init'); + stats_progress('init'); - if ( !stats_create_logs_view($timestart, $nextmidnight) ) { + if (!stats_temp_table_fill($timestart, $nextmidnight)) { $failed = true; break; } - stats_daily_progress('gen'); - + stats_progress('in'); /// find out if any logs available for this day $sql = "SELECT 'x' FROM {tmp_log1} l"; @@ -203,7 +209,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('1'); + stats_progress('1'); $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -216,7 +222,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('2'); + stats_progress('2'); // Enrolments and active enrolled users @@ -243,7 +249,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('3'); + stats_progress('3'); // using table alias in UPDATE does not work in pg < 8.2 $sql = "UPDATE {tmp_stats_daily} @@ -268,7 +274,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('4'); + stats_progress('4'); /// now get course total enrolments (roleid==0) - except frontpage $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -284,7 +290,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('5'); + stats_progress('5'); $sql = "UPDATE {tmp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ue.userid) @@ -307,7 +313,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('6'); + stats_progress('6'); /// frontpage(==site) enrolments total $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -326,7 +332,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('7'); + stats_progress('7'); /// Default frontpage role enrolments are all site users (not deleted) if ($defaultfproleid) { @@ -339,7 +345,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('8'); + stats_progress('8'); $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -357,11 +363,11 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('9'); + stats_progress('9'); } else { - stats_daily_progress('x'); - stats_daily_progress('x'); + stats_progress('x'); + stats_progress('x'); } @@ -381,7 +387,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('10'); + stats_progress('10'); /// how many view/post actions in each course total @@ -398,7 +404,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('11'); + stats_progress('11'); /// how many view actions for each course+role - excluding guests and frontpage @@ -429,7 +435,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('12'); + stats_progress('12'); /// how many view actions from guests only in each course - excluding frontpage /// normal users may enter course with temporary guest access too @@ -456,7 +462,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('13'); + stats_progress('13'); /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role @@ -487,7 +493,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('14'); + stats_progress('14'); /// how many view actions for default frontpage role on frontpage only @@ -513,7 +519,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('15'); + stats_progress('15'); /// how many view actions for guests or not-logged-in on frontpage $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -537,31 +543,23 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_daily_progress('16'); + stats_progress('16'); - $sql = "INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2) - SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}"; - if (!$DB->execute($sql)) { - $failed = true; - break; - } + stats_temp_table_clean(); - $sql = "INSERT INTO {stats_user_daily} (courseid, userid, roleid, timeend, statsreads, statswrites, stattype) - SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype FROM {tmp_stats_user_daily}"; - if (!$DB->execute($sql)) { - $failed = true; - break; - } + stats_progress('out'); // remember processed days set_config('statslastdaily', $nextmidnight); - mtrace(" finished until $nextmidnight: ".userdate($nextmidnight)." (in ".(time()-$daystart)." s)"); + $elapsed = time()-$daystart; + mtrace(" finished until $nextmidnight: ".userdate($nextmidnight)." (in $elapsed s)"); + $total += $elapsed; $timestart = $nextmidnight; $nextmidnight = stats_get_next_day_start($nextmidnight); } - stats_drop_logs_view(); + stats_temp_table_drop(); set_cron_lock('statsrunning', null); @@ -570,8 +568,12 @@ function stats_cron_daily($maxdays=1) { mtrace("...error occurred, completed $days days of statistics."); return false; + } else if ($timeout) { + mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time."); + return false; + } else { - mtrace("...completed $days days of statistics."); + mtrace("...completed $days days of statistics in {$total} s."); return true; } } @@ -624,6 +626,9 @@ function stats_cron_weekly() { $logtimesql = "l.time >= $timestart AND l.time < $nextstartweek"; $stattimesql = "timeend > $timestart AND timeend <= $nextstartweek"; + $weekstart = time(); + stats_progress('init'); + /// process login info first $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads) @@ -638,6 +643,8 @@ function stats_cron_weekly() { $DB->execute($sql); + stats_progress('1'); + $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextstartweek AS timeend, ".SITEID." as courseid, 0, @@ -651,6 +658,7 @@ function stats_cron_weekly() { $DB->execute($sql); + stats_progress('2'); /// now enrolments averages $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -665,6 +673,7 @@ function stats_cron_weekly() { $DB->execute($sql); + stats_progress('3'); /// activity read/write averages $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -679,6 +688,7 @@ function stats_cron_weekly() { $DB->execute($sql); + stats_progress('4'); /// user read/write averages $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads, statswrites) @@ -693,8 +703,11 @@ function stats_cron_weekly() { $DB->execute($sql); + stats_progress('5'); + set_config('statslastweekly', $nextstartweek); - mtrace(" finished until $nextstartweek: ".userdate($nextstartweek)); + $elapsed = time()-$weekstart; + mtrace(" finished until $nextstartweek: ".userdate($nextstartweek) ." ( in $elapsed s)"); $timestart = $nextstartweek; $nextstartweek = stats_get_next_week_start($nextstartweek); @@ -755,6 +768,9 @@ function stats_cron_monthly() { $logtimesql = "l.time >= $timestart AND l.time < $nextstartmonth"; $stattimesql = "timeend > $timestart AND timeend <= $nextstartmonth"; + $monthstart = time(); + stats_progress('init'); + /// process login info first $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads) @@ -768,6 +784,8 @@ function stats_cron_monthly() { $DB->execute($sql); + stats_progress('1'); + $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextstartmonth AS timeend, ".SITEID." as courseid, 0, @@ -781,6 +799,7 @@ function stats_cron_monthly() { $DB->execute($sql); + stats_progress('2'); /// now enrolments averages $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -795,6 +814,7 @@ function stats_cron_monthly() { $DB->execute($sql); + stats_progress('3'); /// activity read/write averages $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -809,6 +829,7 @@ function stats_cron_monthly() { $DB->execute($sql); + stats_progress('4'); /// user read/write averages $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads, statswrites) @@ -823,8 +844,11 @@ function stats_cron_monthly() { $DB->execute($sql); + stats_progress('5'); + set_config('statslastmonthly', $nextstartmonth); - mtrace(" finished until $nextstartmonth: ".userdate($nextstartmonth)); + $elapsed = time() - $monthstart; + mtrace(" finished until $nextstartmonth: ".userdate($nextstartmonth) ." (in $elapsed s)"); $timestart = $nextstartmonth; $nextstartmonth = stats_get_next_month_start($nextstartmonth); @@ -1455,36 +1479,107 @@ function stats_check_uptodate($courseid=0) { /** * Create temporary tables to speed up log generation + */ +function stats_temp_table_create() { + global $CFG, $DB; + + $dbman = $DB->get_manager(); // We are going to use database_manager services + + stats_temp_table_drop(); + + $log = new xmldb_table('tmp_log1'); + + $log->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $log->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $log->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $log->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); + + $log->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $log->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); + $log->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); + $log->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $log->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); + + $user = new xmldb_table('tmp_stats_daily'); + $user->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); + $user->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + + $user->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $user->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + $user->add_index('tmp_tsd_statype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); + $user->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + + $daily = new xmldb_table('tmp_stats_user_daily'); + $daily->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); + + $daily->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $daily->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $daily->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + $daily->add_index('tmp_tsud_stattype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); + $daily->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + + try { + $dbman->create_temp_table($log); + + $log->name = 'tmp_log2'; + + $dbman->create_temp_table($log); + + $dbman->create_temp_table($user); + + $dbman->create_temp_table($daily); + + } catch (Exception $e) { + mtrace("Temporary table creation failed!"); + return false; + } + + return true; +} + +/** + * Deletes summary logs table for stats calculation + */ +function stats_temp_table_drop() { + global $DB; + + $dbman = $DB->get_manager(); + + $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); + + foreach ($tables as $name) { + + if ($dbman->table_exists($name)) { + $table = new xmldb_table($name); + + try { + $dbman->drop_temp_table($table); + } catch (Exception $e) { + mtrace("Error occured while dropping temporary tables!"); + } + } + } +} + +/** + * Fills the temporary stats tables with new data * * @param timestart timestamp of the start time of logs view * @param timeend timestamp of the end time of logs view * @returns boolen success (true) or failure(false) */ -function stats_create_logs_view ( $timestart, $timeend ) { - global $CFG, $DB; - - $dbman = $DB->get_manager(); // We are going to use database_manager services - - stats_drop_logs_view(); - - $table = new xmldb_table('tmp_log1'); - - $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $table->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); - - $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); - $table->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); - $table->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $table->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); - - $dbman->create_temp_table($table); - - $table->name = 'tmp_log2'; - - $dbman->create_temp_table($table); +function stats_temp_table_fill($timestart, $timeend) { + global $DB; $sql = "INSERT INTO {tmp_log1} SELECT id, userid, course, action FROM {log} l @@ -1501,53 +1596,44 @@ function stats_create_logs_view ( $timestart, $timeend ) { return false; } - $table = new xmldb_table('tmp_stats_daily'); - $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); - $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - - $table->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $table->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); - $table->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - - $dbman->create_temp_table($table); - - $table = new xmldb_table('tmp_stats_user_daily'); - $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); - - $table->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $table->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $table->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - $table->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); - - $dbman->create_temp_table($table); - return true; } + /** * Deletes summary logs table for stats calculation + * + * @returns boolen success (true) or failure(false) */ -function stats_drop_logs_view() { +function stats_temp_table_clean() { global $DB; - $dbman = $DB->get_manager(); + $sql = array(); + $sql['up1'] = 'INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2)' + .' SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}'; + + $sql['up2'] = 'INSERT INTO {stats_user_daily}' + .' (courseid, userid, roleid, timeend, statsreads, statswrites, stattype)' + .' SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype' + .' FROM {tmp_stats_user_daily}'; + + $x = 1; $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); foreach ($tables as $name) { - if ($dbman->table_exists($name)) { - $table = new xmldb_table($name); - $dbman->drop_temp_table($table); + $sql['tr'. $x] = "TRUNCATE TABLE {{$name}}"; + $x += 1; + } + + foreach ($sql as $id => $query) { + try { + $DB->execute($query); + } catch (Exception $e) { + mtrace("Error during table cleanup!"); + return false; } } + + return true; } From d352e733b3729ee820de8859d67c859cda81ee34 Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Fri, 3 Aug 2012 15:51:49 -0400 Subject: [PATCH 33/96] MDL-30643 - Now loading table definitions from XML files. --- lib/db/temp_stats_log_template.xml | 41 ++++++ lib/statslib.php | 224 ++++++++++++++--------------- 2 files changed, 149 insertions(+), 116 deletions(-) create mode 100644 lib/db/temp_stats_log_template.xml diff --git a/lib/db/temp_stats_log_template.xml b/lib/db/temp_stats_log_template.xml new file mode 100644 index 00000000000..78681cb5629 --- /dev/null +++ b/lib/db/temp_stats_log_template.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/lib/statslib.php b/lib/statslib.php index f44ae1ff571..ed720665fca 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -104,7 +104,7 @@ function stats_cron_daily($maxdays=1) { $now = time(); - $fpcontext = context_course::instance(SITEID, MUST_EXIST); + $fpcontext = get_context_instance(CONTEXT_COURSE, SITEID, MUST_EXIST); // read last execution date from db if (!$timestart = get_config(NULL, 'statslastdaily')) { @@ -190,17 +190,17 @@ function stats_cron_daily($maxdays=1) { break; } - stats_progress('in'); + stats_progress('0'); /// find out if any logs available for this day - $sql = "SELECT 'x' FROM {tmp_log1} l"; + $sql = "SELECT 'x' FROM {temp_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); /// process login info first - $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) + $sql = "INSERT INTO {temp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads - FROM {tmp_log1} l + FROM {temp_log1} l WHERE action = 'login' GROUP BY timeend, courseid, userid HAVING count(statsreads) > 0"; @@ -211,11 +211,11 @@ function stats_cron_daily($maxdays=1) { } stats_progress('1'); - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0, COALESCE(SUM(statsreads), 0) as stat1, COUNT('x') as stat2 - FROM {tmp_stats_user_daily} + FROM {temp_stats_user_daily} WHERE stattype = 'logins' AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -235,7 +235,7 @@ function stats_cron_daily($maxdays=1) { // in that case, we'll count non-deleted users. // - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 @@ -251,33 +251,34 @@ function stats_cron_daily($maxdays=1) { } stats_progress('3'); + // Set stat2 to the number distinct users with role assignments in the course that were active // using table alias in UPDATE does not work in pg < 8.2 - $sql = "UPDATE {tmp_stats_daily} + $sql = "UPDATE {temp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ra.userid) FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) JOIN {enrol} e ON e.courseid = c.instanceid JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - WHERE ra.roleid = {tmp_stats_daily}.roleid AND - e.courseid = {tmp_stats_daily}.courseid AND + WHERE ra.roleid = {temp_stats_daily}.roleid AND + e.courseid = {temp_stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_log1} l - WHERE l.course = {tmp_stats_daily}.courseid AND + FROM {temp_log1} l + WHERE l.course = {temp_stats_daily}.courseid AND l.userid = ra.userid)) - WHERE {tmp_stats_daily}.stattype = 'enrolments' AND - {tmp_stats_daily}.timeend = $nextmidnight AND - {tmp_stats_daily}.courseid IN + WHERE {temp_stats_daily}.stattype = 'enrolments' AND + {temp_stats_daily}.timeend = $nextmidnight AND + {temp_stats_daily}.courseid IN (SELECT DISTINCT course - FROM {tmp_log2})"; + FROM {temp_log2})"; - if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { + if ($logspresent and !$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; break; } stats_progress('4'); - /// now get course total enrolments (roleid==0) - except frontpage - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// now get course total enrolments (roleid==0) - except frontpage + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, COUNT(DISTINCT userid) AS stat1, 0 AS stat2 @@ -292,21 +293,22 @@ function stats_cron_daily($maxdays=1) { } stats_progress('5'); - $sql = "UPDATE {tmp_stats_daily} + // Set stat 2 to the number of enrolled users who were active in the course + $sql = "UPDATE {temp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ue.userid) FROM {enrol} e JOIN {user_enrolments} ue ON ue.enrolid = e.id - WHERE e.courseid = {tmp_stats_daily}.courseid AND + WHERE e.courseid = {temp_stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_log1} l - WHERE l.course = {tmp_stats_daily}.courseid AND + FROM {temp_log1} l + WHERE l.course = {temp_stats_daily}.courseid AND l.userid = ue.userid)) - WHERE {tmp_stats_daily}.stattype = 'enrolments' AND - {tmp_stats_daily}.timeend = $nextmidnight AND - {tmp_stats_daily}.roleid = 0 AND - {tmp_stats_daily}.courseid IN + WHERE {temp_stats_daily}.stattype = 'enrolments' AND + {temp_stats_daily}.timeend = $nextmidnight AND + {temp_stats_daily}.roleid = 0 AND + {temp_stats_daily}.courseid IN (SELECT l.course - FROM {tmp_log2} l + FROM {temp_log2} l WHERE l.course <> ".SITEID.")"; if ($logspresent and !$DB->execute($sql, array())) { @@ -315,8 +317,8 @@ function stats_cron_daily($maxdays=1) { } stats_progress('6'); - /// frontpage(==site) enrolments total - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// frontpage(==site) enrolments total + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", 0, (SELECT COUNT('x') @@ -324,7 +326,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_log1} l ON l.userid = u.id + JOIN {temp_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause(); @@ -334,11 +336,11 @@ function stats_cron_daily($maxdays=1) { } stats_progress('7'); - /// Default frontpage role enrolments are all site users (not deleted) + /// Default frontpage role enrolments are all site users (not deleted) if ($defaultfproleid) { // first remove default frontpage role counts if created by previous query $sql = "DELETE - FROM {tmp_stats_daily} + FROM {temp_stats_daily} WHERE stattype = 'enrolments' AND courseid = ".SITEID." AND roleid = $defaultfproleid AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -347,7 +349,7 @@ function stats_cron_daily($maxdays=1) { } stats_progress('8'); - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", $defaultfproleid, (SELECT COUNT('x') @@ -355,7 +357,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_log1} l ON l.userid = u.id + JOIN {temp_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause();; @@ -371,15 +373,15 @@ function stats_cron_daily($maxdays=1) { } - /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible + /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible list($viewactionssql, $params1) = $DB->get_in_or_equal($viewactions, SQL_PARAMS_NAMED, 'view'); list($postactionssql, $params2) = $DB->get_in_or_equal($postactions, SQL_PARAMS_NAMED, 'post'); - $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) + $sql = "INSERT INTO {temp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) SELECT 'activity' AS stattype, $nextmidnight AS timeend, course as courseid, userid, SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites - FROM {tmp_log1} l + FROM {temp_log1} l WHERE !(course = 0 AND userid = 0) GROUP BY userid, courseid"; @@ -390,13 +392,13 @@ function stats_cron_daily($maxdays=1) { stats_progress('10'); - /// how many view/post actions in each course total - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view/post actions in each course total + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0, SUM(CASE WHEN l.action $viewactionssql THEN 1 ELSE 0 END) AS stat1, SUM(CASE WHEN l.action $postactionssql THEN 1 ELSE 0 END) AS stat2 - FROM {course} c, {tmp_log1} l + FROM {course} c, {temp_log1} l WHERE l.course = c.id GROUP BY courseid"; @@ -407,14 +409,14 @@ function stats_cron_daily($maxdays=1) { stats_progress('11'); - /// how many view actions for each course+role - excluding guests and frontpage + /// how many view actions for each course+role - excluding guests and frontpage - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud, + FROM {temp_stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, e.courseid FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) @@ -437,16 +439,16 @@ function stats_cron_daily($maxdays=1) { } stats_progress('12'); - /// how many view actions from guests only in each course - excluding frontpage - /// normal users may enter course with temporary guest access too + /// how many view actions from guests only in each course - excluding frontpage + /// normal users may enter course with temporary guest access too - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight as timeend, courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.courseid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud + FROM {temp_stats_user_daily} sud WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND (sud.userid = $guest OR sud.userid @@ -465,14 +467,14 @@ function stats_cron_daily($maxdays=1) { stats_progress('13'); - /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud, + FROM {temp_stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, c.instanceid AS courseid FROM {role_assignments} ra JOIN {context} c ON c.id = ra.contextid @@ -496,14 +498,14 @@ function stats_cron_daily($maxdays=1) { stats_progress('14'); - /// how many view actions for default frontpage role on frontpage only - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view actions for default frontpage role on frontpage only + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', timeend, courseid, $defaultfproleid AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.timeend AS timeend, sud.courseid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud + FROM {temp_stats_user_daily} sud WHERE sud.timeend = :nextm AND sud.courseid = :siteid AND sud.stattype='activity' AND sud.userid <> $guest AND sud.userid <> 0 AND sud.userid @@ -521,8 +523,8 @@ function stats_cron_daily($maxdays=1) { } stats_progress('15'); - /// how many view actions for guests or not-logged-in on frontpage - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view actions for guests or not-logged-in on frontpage + $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) @@ -530,7 +532,7 @@ function stats_cron_daily($maxdays=1) { SELECT pl.statsreads, pl.statswrites FROM ( SELECT sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud + FROM {temp_stats_user_daily} sud WHERE (sud.userid = $guest OR sud.userid = 0) AND sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND sud.stattype='activity' @@ -565,11 +567,11 @@ function stats_cron_daily($maxdays=1) { if ($failed) { $days--; - mtrace("...error occurred, completed $days days of statistics."); + mtrace("...error occurred, completed $days days of statistics in {$total} s."); return false; } else if ($timeout) { - mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time."); + mtrace("...stopping early, reached maximum number of $maxdays days ({$total} s) - will continue next time."); return false; } else { @@ -707,7 +709,7 @@ function stats_cron_weekly() { set_config('statslastweekly', $nextstartweek); $elapsed = time()-$weekstart; - mtrace(" finished until $nextstartweek: ".userdate($nextstartweek) ." ( in $elapsed s)"); + mtrace(" finished until $nextstartweek: ".userdate($nextstartweek) ." (in $elapsed s)"); $timestart = $nextstartweek; $nextstartweek = stats_get_next_week_start($nextstartweek); @@ -1346,7 +1348,7 @@ function stats_get_report_options($courseid,$mode) { switch ($mode) { case STATS_MODE_GENERAL: $reportoptions[STATS_REPORT_ACTIVITY] = get_string('statsreport'.STATS_REPORT_ACTIVITY); - if ($courseid != SITEID && $context = context_course::instance($courseid)) { + if ($courseid != SITEID && $context = get_context_instance(CONTEXT_COURSE, $courseid)) { $sql = 'SELECT r.id, r.name FROM {role} r JOIN {stats_daily} s ON s.roleid = r.id WHERE s.courseid = :courseid GROUP BY r.id, r.name'; if ($roles = $DB->get_records_sql($sql, array('courseid' => $courseid))) { foreach ($roles as $role) { @@ -1364,13 +1366,13 @@ function stats_get_report_options($courseid,$mode) { case STATS_MODE_DETAILED: $reportoptions[STATS_REPORT_USER_ACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ACTIVITY); $reportoptions[STATS_REPORT_USER_ALLACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ALLACTIVITY); - if (has_capability('report/stats:view', context_system::instance())) { + if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { $site = get_site(); $reportoptions[STATS_REPORT_USER_LOGINS] = get_string('statsreport'.STATS_REPORT_USER_LOGINS); } break; case STATS_MODE_RANKED: - if (has_capability('report/stats:view', context_system::instance())) { + if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { $reportoptions[STATS_REPORT_ACTIVE_COURSES] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES); $reportoptions[STATS_REPORT_ACTIVE_COURSES_WEIGHTED] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES_WEIGHTED); $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES); @@ -1487,60 +1489,50 @@ function stats_temp_table_create() { stats_temp_table_drop(); - $log = new xmldb_table('tmp_log1'); + $xmlfile = $CFG->dirroot . '/lib/db/install.xml'; + $tempfile = $CFG->dirroot . '/lib/db/temp_stats_log_template.xml'; + $tables = array(); + $files = array( + $xmlfile => array('stats_daily', 'stats_user_daily'), + $tempfile => array('temp_log1', 'temp_log2'), + ); - $log->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $log->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $log->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $log->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); + foreach ($files as $file => $contents) { - $log->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $log->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); - $log->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); - $log->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $log->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); + $xmldb_file = new xmldb_file($file); + if (!$xmldb_file->fileExists()) { + throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist'); + } + $loaded = $xmldb_file->loadXMLStructure(); + if (!$loaded || !$xmldb_file->isLoaded()) { + throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??'); + } + $xmldb_structure = $xmldb_file->getStructure(); - $user = new xmldb_table('tmp_stats_daily'); - $user->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); - $user->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + foreach ($contents as $name) { + $table = $xmldb_structure->getTable($name); - $user->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $user->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - $user->add_index('tmp_tsd_statype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); - $user->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + if (is_null($table)) { + throw new ddl_exception('ddlunknowntable', null, 'The table '. $name .' is not defined in the file '. $xmlfile); + } + $table->setNext(null); + $table->setPrevious(null); + $tables[$name] = $table; + } + } - $daily = new xmldb_table('tmp_stats_user_daily'); - $daily->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); - - $daily->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $daily->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $daily->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - $daily->add_index('tmp_tsud_stattype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); - $daily->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + // Rename the standard table templates so that they don't conflict with the standard tables + $tables['stats_daily']->setName('temp_stats_daily'); + $tables['stats_user_daily']->setName('temp_stats_user_daily'); try { - $dbman->create_temp_table($log); - $log->name = 'tmp_log2'; - - $dbman->create_temp_table($log); - - $dbman->create_temp_table($user); - - $dbman->create_temp_table($daily); + foreach ($tables as $table) { + $dbman->create_temp_table($table); + } } catch (Exception $e) { - mtrace("Temporary table creation failed!"); + mtrace('Temporary table creation failed: '. $e->getMessage()); return false; } @@ -1555,7 +1547,7 @@ function stats_temp_table_drop() { $dbman = $DB->get_manager(); - $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); + $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); foreach ($tables as $name) { @@ -1563,7 +1555,7 @@ function stats_temp_table_drop() { $table = new xmldb_table($name); try { - $dbman->drop_temp_table($table); + $dbman->drop_table($table); } catch (Exception $e) { mtrace("Error occured while dropping temporary tables!"); } @@ -1581,7 +1573,7 @@ function stats_temp_table_drop() { function stats_temp_table_fill($timestart, $timeend) { global $DB; - $sql = "INSERT INTO {tmp_log1} + $sql = "INSERT INTO {temp_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; @@ -1589,8 +1581,8 @@ function stats_temp_table_fill($timestart, $timeend) { return false; } - $sql = "INSERT INTO {tmp_log2} - SELECT * FROM {tmp_log1}"; + $sql = "INSERT INTO {temp_log2} + SELECT * FROM {temp_log1}"; if (!$DB->execute($sql)) { return false; @@ -1611,15 +1603,15 @@ function stats_temp_table_clean() { $sql = array(); $sql['up1'] = 'INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2)' - .' SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}'; + .' SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {temp_stats_daily}'; $sql['up2'] = 'INSERT INTO {stats_user_daily}' .' (courseid, userid, roleid, timeend, statsreads, statswrites, stattype)' .' SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype' - .' FROM {tmp_stats_user_daily}'; + .' FROM {temp_stats_user_daily}'; $x = 1; - $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); + $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); foreach ($tables as $name) { $sql['tr'. $x] = "TRUNCATE TABLE {{$name}}"; From 8646f5a4363a4331c8673e2bc98a7372ed789182 Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Fri, 3 Aug 2012 15:53:27 -0400 Subject: [PATCH 34/96] MDL-30643 - Added statslib test file. - Tests daily stats and daily stats related functions. --- lib/tests/statslib_test.php | 1448 +++++++++++++++++++++++++++++++++++ 1 file changed, 1448 insertions(+) create mode 100644 lib/tests/statslib_test.php diff --git a/lib/tests/statslib_test.php b/lib/tests/statslib_test.php new file mode 100644 index 00000000000..3e32143a88b --- /dev/null +++ b/lib/tests/statslib_test.php @@ -0,0 +1,1448 @@ +. + +/** + * Tests for ../statslib.php + * + * @package core + * @subpackage stats + * @copyright 2012 Tyler Bannister + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/adminlib.php'); +require_once($CFG->libdir . '/statslib.php'); + +/** + * Test functions that affect daily stats + */ +class statslib_daily_testcase extends advanced_testcase { + protected $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); + + /** + * Setup function + * - Allow changes to CFG->debug for testing purposes. + */ + protected function setUp() { + global $CFG; + parent::setUp(); + + // Settings to force statistic to run during testing + $CFG->statsfirstrun = 'all'; + $CFG->statslastdaily = 0; + $CFG->statslastexecution = 0; + $CFG->statsruntimestarthour = date('H'); + $CFG->statsruntimestartminute = 0; + + $this->resetAfterTest(true); + } + + /** + * Function to setup database. + * + * @param array $logs An array of log entries to added to the database. + */ + protected function prepare_db($logs) { + global $CFG, $DB; + + $course1 = SITEID + 1; + $guest = $CFG->siteguest; + $user1 = $guest + 1; + $user2 = $user1 + 1; + + // Create users (user) + $tables['user'] = array( + array('id' => $guest, 'deleted' => 0, 'username' => 'guest'), + array('id' => $user1, 'deleted' => 0, 'username' => 'user1'), + array('id' => $user2, 'deleted' => 0, 'username' => 'user2'), + ); + + // Create courses (course) + $tables['course'] = array( + array('id' => SITEID, 'shortname' => 'site'), + array('id' => $course1, 'shortname' => 'course1'), + ); + + // Create contexts (context) + $tables['context'] = array( + array('id' => 1, 'contextlevel' => CONTEXT_SYSTEM, 'instanceid' => 0), + array('id' => 2, 'contextlevel' => CONTEXT_COURSE, 'instanceid' => SITEID), + array('id' => 3, 'contextlevel' => CONTEXT_COURSE, 'instanceid' => $course1), + array('id' => 4, 'contextlevel' => CONTEXT_USER, 'instanceid' => $guest), + array('id' => 5, 'contextlevel' => CONTEXT_USER, 'instanceid' => $user1), + array('id' => 6, 'contextlevel' => CONTEXT_USER, 'instanceid' => $user2), + ); + + // Create role assignments (role_assignment) + $tables['role_assignments'] = array( + array('id' => 1, 'roleid' => 5, 'contextid' => 3, 'userid' => $user1), + array('id' => 2, 'roleid' => 5, 'contextid' => 2, 'userid' => $user2), + ); + + // Create enrolments (enrol) + $tables['enrol'] = array( + array('id' => 1, 'enrol' => 'manual', 'status' => 0, 'courseid' => $course1, 'roleid' => 5), + array('id' => 2, 'enrol' => 'guest', 'status' => 1, 'courseid' => $course1, 'roleid' => 0), + array('id' => 3, 'enrol' => 'self', 'status' => 1, 'courseid' => $course1, 'roleid' => 5), + ); + + // Create user enrolments (user_enrolments) + $tables['user_enrolments'] = array( + array('id' => 1, 'status' => 0, 'enrolid' => 1, 'userid' => $user1), + ); + + // Create logs (log) + $tables['log'] = $logs; + + // Insert records + foreach ($tables as $table => $rows) { + $DB->delete_records($table, array()); + + foreach ($rows as $row) { + $DB->import_record($table, (object) $row); + } + } + } + + public function daily_log_provider() { + global $CFG; + + $course1 = SITEID + 1; + $guest = $CFG->siteguest; + $user1 = $guest + 1; + $user2 = $user1 + 1; + $end = 1272772800; + $fpid = (int)$CFG->defaultfrontpageroleid; + $gr = get_guest_role(); + $stid = 5; + $nologend = stats_get_next_day_start(stats_get_base_daily(stats_get_start_from('daily'))); + + + // Each test case contains a complete list of expected output because producing + // more or fewer results is an error, as is changing the results in unrelated + // stats entries. + // All queries are identified by the number printed after the query completes in the + // 4 temporary table stats code. + return array( + array( // Test #0 - No logs - Only query 3 should be processed + array( // Logs + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $nologend, + 'roleid' => 5, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + ), + ), + ), + array( // Test #1 - No login - Tests queries 2, 3, 5, 7, 9 (and 8), 10 (read) + array( // Logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $guest, + 'course' => SITEID, + 'action' => 'view' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 0, + 'stat2' => 0 + ), + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 5 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 16 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $gr->id, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 10 - read + 'courseid' => SITEID, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + array( // Test #2 - Single login - Tests queries 1, 2 (with logins), 4 + array( // Logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $user1, + 'course' => SITEID, + 'action' => 'login' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 5 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 0, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 1 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'logins' + ), + array( // Query 10 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + array( // Test #3 - Guest login and view course - Tests queries 11 (read), 13 (guest) + array( // logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $guest, + 'course' => SITEID, + 'action' => 'login' + ), + array( + 'id' => 2, + 'time' => 1272686420, + 'userid' => $guest, + 'course' => $course1, + 'action' => 'view' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 5 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 0, + 'stat2' => 0 + ), + array( // Query 11 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 13 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $gr->id, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 1 + 'courseid' => SITEID, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'logins' + ), + array( // Query 10 + 'courseid' => $course1, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + array( // Query 10 + 'courseid' => SITEID, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + array( // Test #4 - Login, view course, upload assignment - Tests queries 10 + 11 (write), 13 (guest) + array( // logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $guest, + 'course' => SITEID, + 'action' => 'login' + ), + array( + 'id' => 2, + 'time' => 1272686420, + 'userid' => $guest, + 'course' => $course1, + 'action' => 'view' + ), + array( + 'id' => 3, + 'time' => 1272686430, + 'userid' => $guest, + 'course' => $course1, + 'action' => 'add post' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 5 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 0, + 'stat2' => 0 + ), + array( // Query 11 - read + write + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 13 - read + write + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $gr->id, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 1 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 1 + 'courseid' => SITEID, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'logins' + ), + array( // Query 10 - read + write + 'courseid' => $course1, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 1, + 'stattype' => 'activity' + ), + array( // Query 10 + 'courseid' => SITEID, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + array( // Test #5 - Login and view course - Tests queries 4, 6, 10, 12, 14 (read) + array( // logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $user1, + 'course' => SITEID, + 'action' => 'login' + ), + array( + 'id' => 2, + 'time' => 1272686420, + 'userid' => $user1, + 'course' => $course1, + 'action' => 'view' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Queries 3 (stat1) and 4 (stat2) + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Queries 5 (stat1) and 6 (stat2) + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 0, + 'stat2' => 0 + ), + array( // Query 11 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 12 (read) + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 1 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'logins' + ), + array( // Query 10 + 'courseid' => $course1, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + array( // Query 10 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + array( // Test #6 - Login, view course, post - Tests queries 10, 12, 14 (write) + array( // logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $user1, + 'course' => SITEID, + 'action' => 'login' + ), + array( + 'id' => 2, + 'time' => 1272686420, + 'userid' => $user1, + 'course' => $course1, + 'action' => 'view' + ), + array( + 'id' => 3, + 'time' => 1272686430, + 'userid' => $user1, + 'course' => $course1, + 'action' => 'add post' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Queries 3 (stat1) and 4 (stat2) + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Queries 5 (stat1) and 6 (stat2) + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 0, + 'stat2' => 0 + ), + array( // Query 11 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 12 (read + write) + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 1 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 1 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'logins' + ), + array( // Query 10 + 'courseid' => $course1, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 1, + 'stattype' => 'activity' + ), + array( // Query 10 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + array( // Test #7 - Login and view course - Tests queries 13 (not enroled), 14 (not default) + array( // logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $user2, + 'course' => SITEID, + 'action' => 'login' + ), + array( + 'id' => 2, + 'time' => 1272686420, + 'userid' => $user2, + 'course' => SITEID, + 'action' => 'view' + ), + array( + 'id' => 3, + 'time' => 1272686430, + 'userid' => $user2, + 'course' => $course1, + 'action' => 'view' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 5 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 11 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 13 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $gr->id, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 14 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 1 + 'courseid' => SITEID, + 'userid' => $user2, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'logins' + ), + array( // Query 10 + 'courseid' => $course1, + 'userid' => $user2, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + array( // Query 10 + 'courseid' => SITEID, + 'userid' => $user2, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + array( // Test #8 - Login and view site - Tests queries 15 (front page views) + array( // logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $user1, + 'course' => SITEID, + 'action' => 'login' + ), + array( + 'id' => 2, + 'time' => 1272686420, + 'userid' => $user1, + 'course' => SITEID, + 'action' => 'view' + ), + ), + array( + 'stats_daily' => array( // stats_daily + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ), + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 5 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 9 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 15 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $fpid, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 1 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'logins' + ), + array( // Query 10 + 'courseid' => SITEID, + 'userid' => $user1, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ), + ), + ); + } + + /** + * Compare the expected stats to those in the database. + * + * @param array $stats An array of arrays of arrays of both types of stats + */ + protected function verify_stats($stats) { + global $DB; + + foreach ($stats as $type => $results) { + $records = $DB->get_records($type); + + $this->assertEquals(sizeof($results), sizeof($records), + 'Incorrect number of results returned for '. $type); + + foreach ($results as $result) { + $found = 0; + + foreach ($records as $key => $record) { + $record = (array) $record; + unset($record['id']); + $diff = array_merge(array_diff_assoc($result, $record), + array_diff_assoc($record, $result)); + + if (empty($diff)) { + $found = $key; + break; + } + } + $this->assertGreaterThan(0, $found, 'Expected log '. var_export($result, true) + ." was not found in $type ". var_export($records, true)); + unset($records[$found]); + } + } + } + + /** + * Test progress output when debug is on + */ + public function test_statslib_progress_debug() { + global $CFG; + + $CFG->debug = DEBUG_ALL; + $this->expectOutputString('1:0 '); + stats_progress('init'); + stats_progress('1'); + } + + /** + * Test progress output when debug is off + */ + public function test_statslib_progress_no_debug() { + global $CFG; + + $CFG->debug = DEBUG_NONE; + $this->expectOutputString('.'); + stats_progress('init'); + stats_progress('1'); + } + + /** + * Test the function that gets the start date from the config + */ + public function test_statslib_get_start_from() { + global $CFG, $DB; + + $logs = array( // logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => 1, + 'course' => 1, + 'action' => 'login' + ), + ); + + $stats = array( // stats_daily + 'courseid' => SITEID, + 'timeend' => 1272772800, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 1, + 'stat2' => 1 + ); + + $time = time(); + + // Allow 1 second difference in case we cross a second boundary. + $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (3 * 24 * 3600))); + + $this->prepare_db($logs); + $this->assertEquals(1272686410, stats_get_start_from('daily')); + + $CFG->statsfirstrun = 'none'; + $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (3 * 24 * 3600))); + + $CFG->statsfirstrun = 14515200; + $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (14515200))); + + $DB->insert_record_raw('stats_daily', $stats); + $this->assertEquals(1272772800, stats_get_start_from('daily')); + } + + /** + * Test the function that calculates the start of the day + */ + public function test_statslib_get_base_daily() { + global $CFG; + + $CFG->timezone = 99; + $this->assertEquals(1272686400, stats_get_base_daily(1272686410)); + $CFG->timezone = 5; + $this->assertEquals(1272654000, stats_get_base_daily(1272686410)); + } + + /** + * Test the function that gets the start of the next day + */ + public function test_statslib_get_next_day_start() { + $this->assertEquals(1272772800, stats_get_next_day_start(1272686410)); + } + + /** + * Test the function that gets the action names + * + * Note: The function results depend on installed modules. The hard coded lists are the + * defaults for a new Moodle 2.3 install. + */ + public function test_statslib_get_action_names() { + $basepostactions = array ( + 0 => 'add', + 1 => 'delete', + 2 => 'edit', + 3 => 'add mod', + 4 => 'delete mod', + 5 => 'edit sectionenrol', + 6 => 'loginas', + 7 => 'new', + 8 => 'unenrol', + 9 => 'update', + 10 => 'update mod', + 11 => 'upload', + 12 => 'submit', + 13 => 'submit for grading', + 14 => 'talk', + 15 => 'choose', + 16 => 'choose again', + 17 => 'record delete', + 18 => 'add discussion', + 19 => 'add post', + 20 => 'delete discussion', + 21 => 'delete post', + 22 => 'move discussion', + 23 => 'prune post', + 24 => 'update post', + 25 => 'add category', + 26 => 'add entry', + 27 => 'approve entry', + 28 => 'delete category', + 29 => 'delete entry', + 30 => 'edit category', + 31 => 'update entry', + 32 => 'end', + 33 => 'start', + 34 => 'attempt', + 35 => 'close attempt', + 36 => 'preview', + 37 => 'editquestions', + 38 => 'delete attempt', + 39 => 'manualgrade', + ); + + $baseviewactions = array ( + 0 => 'view', + 1 => 'view all', + 2 => 'history', + 3 => 'view submission', + 4 => 'view feedback', + 5 => 'print', + 6 => 'report', + 7 => 'view discussion', + 8 => 'search', + 9 => 'forum', + 10 => 'forums', + 11 => 'subscribers', + 12 => 'view forum', + 13 => 'view entry', + 14 => 'review', + 15 => 'pre-view', + 16 => 'download', + 17 => 'view form', + 18 => 'view graph', + 19 => 'view report', + ); + + $postactions = stats_get_action_names('post'); + + foreach ($basepostactions as $action) { + $this->assertContains($action, $postactions); + } + + $viewactions = stats_get_action_names('view'); + + foreach ($baseviewactions as $action) { + $this->assertContains($action, $viewactions); + } + } + + /** + * Test the temporary table creation and deletion. + */ + public function test_statslib_temp_table_create_and_drop() { + global $DB; + + foreach ($this->tables as $table) { + $this->assertFalse($DB->get_manager()->table_exists($table)); + } + + stats_temp_table_create(); + + foreach ($this->tables as $table) { + $this->assertTrue($DB->get_manager()->table_exists($table)); + } + + stats_temp_table_drop(); + + foreach ($this->tables as $table) { + $this->assertFalse($DB->get_manager()->table_exists($table)); + } + } + + /** + * Test the temporary table creation and deletion. + */ + public function test_statslib_temp_table_fill() { + global $DB; + + $logs = array( // logs + array( + 'id' => 1, + 'time' => 1272686400, + 'userid' => 1, + 'course' => 1, + 'action' => 'login' + ), + array( + 'id' => 2, + 'time' => 1272686410, + 'userid' => 1, + 'course' => 1, + 'action' => 'login' + ), + array( + 'id' => 3, + 'time' => 1272772801, + 'userid' => 1, + 'course' => 1, + 'action' => 'login' + ), + ); + $this->prepare_db($logs); + + stats_temp_table_create(); + stats_temp_table_fill(1272686410, 1272772800); + + $this->assertEquals(1, $DB->count_records('temp_log1')); + $this->assertEquals(1, $DB->count_records('temp_log2')); + } + + /** + * Test the function that clean out the temporary tables. + */ + public function test_statslib_temp_table_clean() { + global $DB; + + $rows = array( + 'temp_log1' => array('id' => 1, 'course' => 1), + 'temp_log2' => array('id' => 1, 'course' => 1), + 'temp_stats_daily' => array('id' => 1, 'courseid' => 1), + 'temp_stats_user_daily' => array('id' => 1, 'courseid' => 1), + ); + + stats_temp_table_create(); + + foreach ($rows as $table => $row) { + $DB->insert_record_raw($table, $row); + $this->assertEquals(1, $DB->count_records($table)); + } + + stats_temp_table_clean(); + + foreach ($rows as $table => $row) { + $this->assertEquals(0, $DB->count_records($table)); + } + + $this->assertEquals(1, $DB->count_records('stats_daily')); + $this->assertEquals(1, $DB->count_records('stats_user_daily')); + } + + /** + * Test the daily stats function + * + * @dataProvider daily_log_provider + */ + public function test_statslib_cron_daily($logs, $stats) { + global $CFG; + + $CFG->debug = DEBUG_NONE; + + $this->prepare_db($logs); + + // Stats cron daily uses mtrace, turn on buffering to silence output. + ob_start(); + stats_cron_daily($maxdays=1); + ob_end_clean(); + + $this->verify_stats($stats); + } + + /** + * Test the daily stats function + */ + public function test_statslib_cron_daily_no_default_profile_id() { + global $CFG; + $CFG->defaultfrontpageroleid = 0; + + $course1 = SITEID + 1; + $guest = $CFG->siteguest; + $end = 1272772800; + $stid = 5; + $fpid = 0; + $gr = get_guest_role(); + + $logs = array( // Logs + array( + 'id' => 1, + 'time' => 1272686410, + 'userid' => $guest, + 'course' => SITEID, + 'action' => 'view' + ), + ); + + $stats = array( + 'stats_daily' => array( + array( // Query 2 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'logins', + 'stat1' => 0, + 'stat2' => 0 + ), + array( // Query 3 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => $stid, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 5 + 'courseid' => $course1, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 7 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'enrolments', + 'stat1' => 3, + 'stat2' => 1 + ), + array( // Query 11 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => 0, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + array( // Query 16 + 'courseid' => SITEID, + 'timeend' => $end, + 'roleid' => $gr->id, + 'stattype' => 'activity', + 'stat1' => 1, + 'stat2' => 0 + ), + ), + 'stats_user_daily' => array( // stats_user_daily + array( // Query 10 - read + 'courseid' => SITEID, + 'userid' => $guest, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 1, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), + ); + + $CFG->debug = DEBUG_NONE; + + $this->prepare_db($logs); + + // Stats cron daily uses mtrace, turn on buffering to silence output. + ob_start(); + stats_cron_daily($maxdays=1); + ob_end_clean(); + + $this->verify_stats($stats); + } +} From 2da32bd6a5a0071756c29a4684c73694f0971664 Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Tue, 7 Aug 2012 14:41:09 -0400 Subject: [PATCH 35/96] MDL-30643 - Fixing minor log output error and test cases. --- lib/statslib.php | 14 ++++-- lib/tests/statslib_test.php | 85 ++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index ed720665fca..e9ed8c08c3a 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -190,13 +190,19 @@ function stats_cron_daily($maxdays=1) { break; } - stats_progress('0'); - /// find out if any logs available for this day $sql = "SELECT 'x' FROM {temp_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); - /// process login info first + if ($logspresent) { + // Insert blank record to force Query 10 to generate additional row when no logs for + // the site with userid 0 exist. Added for backwards compatibility. + $DB->insert_record_raw('temp_log1', array('userid' => 0, 'course' => SITEID, 'action' => '')); + } + + stats_progress('0'); + + /// process login info first $sql = "INSERT INTO {temp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads @@ -382,7 +388,6 @@ function stats_cron_daily($maxdays=1) { SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites FROM {temp_log1} l - WHERE !(course = 0 AND userid = 0) GROUP BY userid, courseid"; if ($logspresent and !$DB->execute($sql, array_merge($params1, $params2))) { @@ -1573,6 +1578,7 @@ function stats_temp_table_drop() { function stats_temp_table_fill($timestart, $timeend) { global $DB; + // Note: We need to create a placeholder here to simulate the effects of old, slow unions $sql = "INSERT INTO {temp_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; diff --git a/lib/tests/statslib_test.php b/lib/tests/statslib_test.php index 3e32143a88b..a93995ea203 100644 --- a/lib/tests/statslib_test.php +++ b/lib/tests/statslib_test.php @@ -237,6 +237,15 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), ), ), ), @@ -320,6 +329,15 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), ), ), ), @@ -435,6 +453,15 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), ), ), ), @@ -557,7 +584,16 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), ), ), array( // Test #5 - Login and view course - Tests queries 4, 6, 10, 12, 14 (read) @@ -672,6 +708,15 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), ), ), ), @@ -794,7 +839,16 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), + ), ), ), array( // Test #7 - Login and view course - Tests queries 13 (not enroled), 14 (not default) @@ -924,6 +978,15 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), ), ), ), @@ -1022,6 +1085,15 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), ), ), ), @@ -1431,6 +1503,15 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), + array( // Query 10 - default record + 'courseid' => SITEID, + 'userid' => 0, + 'roleid' => 0, + 'timeend' => $end, + 'statsreads' => 0, + 'statswrites' => 0, + 'stattype' => 'activity' + ), ), ); From c64f13178a02e06e9d5f94979b3c191acff54770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Fri, 31 Aug 2012 22:52:45 +0200 Subject: [PATCH 36/96] MDL-35172 rework editor toolbar setting --- lib/editor/tinymce/classes/plugin.php | 61 ++++++++++++++++--- lib/editor/tinymce/db/upgrade.php | 41 +++++++++++++ lib/editor/tinymce/lang/en/editor_tinymce.php | 4 +- lib/editor/tinymce/lib.php | 44 +++++++------ lib/editor/tinymce/settings.php | 7 ++- lib/editor/tinymce/version.php | 4 +- 6 files changed, 123 insertions(+), 38 deletions(-) create mode 100644 lib/editor/tinymce/db/upgrade.php diff --git a/lib/editor/tinymce/classes/plugin.php b/lib/editor/tinymce/classes/plugin.php index 305f8275c7b..35fe4f2a3ed 100644 --- a/lib/editor/tinymce/classes/plugin.php +++ b/lib/editor/tinymce/classes/plugin.php @@ -152,7 +152,12 @@ abstract class editor_tinymce_plugin { */ protected function add_button_after(array &$params, $row, $button, $after = '', $alwaysadd = true) { - $this->check_row($row); + + if ($this->is_button_present($params, $button)) { + return true; + } + + $row = $this->fix_row($params, $row); $field = 'theme_advanced_buttons' . $row; $old = $params[$field]; @@ -190,7 +195,7 @@ abstract class editor_tinymce_plugin { * to see if it succeeded. * * @param array $params TinyMCE init parameters array - * @param int $row Row to add button to (1 to 3) + * @param int $row Row to add button to (1 to 10) * @param string $button Identifier of button/plugin * @param string $before Adds button directly before the named plugin * @param bool $alwaysadd If specified $after string not found, add at start @@ -198,7 +203,11 @@ abstract class editor_tinymce_plugin { */ protected function add_button_before(array &$params, $row, $button, $before = '', $alwaysadd = true) { - $this->check_row($row); + + if ($this->is_button_present($params, $button)) { + return true; + } + $row = $this->fix_row($params, $row); $field = 'theme_advanced_buttons' . $row; $old = $params[$field]; @@ -226,15 +235,47 @@ abstract class editor_tinymce_plugin { } /** - * Checks the row value is valid. - * - * @param int $row Row to add button to (1 to 3) - * @throws coding_exception If row value is outside the range 1-3 + * Tests if button already present. + * @param array $params + * @param string $button + * @return bool */ - private function check_row($row) { - if ($row < 1 || $row > 3) { - throw new coding_exception("Invalid row option: $row"); + private function is_button_present(array $params, $button) { + for($i=1; $i<=10; $i++) { + $field = 'theme_advanced_buttons' . $i; + if (!isset($params[$field])) { + continue; + } + $buttons = explode(',', $params[$field]); + if (in_array($button, $buttons)) { + return true; + } } + return false; + } + + /** + * Checks the row value is valid, fix if necessary. + * + * @param array $params TinyMCE init parameters array + * @param int $row Row to add button if exists + * @return int requested row if exists, lower number if does not exist. + */ + private function fix_row(array &$params, $row) { + $row = ($row < 1) ? 1 : (int)$row; + $row = ($row > 10) ? 10 : $row; + + $field = 'theme_advanced_buttons' . $row; + if (isset($params[$field])) { + return $row; + } + for($i=$row; $i>=1; $i--) { + if (isset($params[$field])) { + return $row; + } + } + // This should not happen. + return 1; } /** diff --git a/lib/editor/tinymce/db/upgrade.php b/lib/editor/tinymce/db/upgrade.php new file mode 100644 index 00000000000..42d931e7807 --- /dev/null +++ b/lib/editor/tinymce/db/upgrade.php @@ -0,0 +1,41 @@ +. + +/** + * TinyMCE editor integration upgrade. + * + * @package editor_tinymce + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +function xmldb_editor_tinymce_upgrade($oldversion) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); + + + if ($oldversion < 2012083100) { + // Reset redesigned editor toolbar setting. + unset_config('customtoolbar', 'editor_tinymce'); + upgrade_plugin_savepoint(true, 2012083100, 'editor', 'tinymce'); + } + + + return true; +} diff --git a/lib/editor/tinymce/lang/en/editor_tinymce.php b/lib/editor/tinymce/lang/en/editor_tinymce.php index d08d0f11745..fa01e4e1a12 100644 --- a/lib/editor/tinymce/lang/en/editor_tinymce.php +++ b/lib/editor/tinymce/lang/en/editor_tinymce.php @@ -27,8 +27,8 @@ $string['availablebuttons'] = 'Available buttons'; $string['common:browseimage'] = 'Find or upload an image...'; $string['common:browsemedia'] = 'Find or upload a sound, video or applet...'; -$string['customtoolbar'] = 'Custom editor toolbar'; -$string['customtoolbar_desc'] = 'Each line contains a list of comma separated button names, use "|" as a group separator. Leave empty if you want standard toolbar. See {$a} for the list of default TinyMCE buttons.'; +$string['customtoolbar'] = 'Editor toolbar'; +$string['customtoolbar_desc'] = 'Each line contains a list of comma separated button names, use "|" as a group separator, empty lines are ignored. See {$a} for the list of default TinyMCE buttons.'; $string['fontselectlist'] = 'Available fonts list'; $string['media_dlg:filename'] = 'Filename'; $string['pluginname'] = 'TinyMCE HTML editor'; diff --git a/lib/editor/tinymce/lib.php b/lib/editor/tinymce/lib.php index d079dae7498..d1faa1e43b0 100644 --- a/lib/editor/tinymce/lib.php +++ b/lib/editor/tinymce/lib.php @@ -127,7 +127,6 @@ class tinymce_texteditor extends texteditor { } $fontselectlist = empty($config->fontselectlist) ? '' : $config->fontselectlist; - $fontbutton = ($fontselectlist === '') ? '' : 'fontselect,'; $params = array( 'moodle_config' => $config, @@ -154,13 +153,6 @@ class tinymce_texteditor extends texteditor { 'theme_advanced_font_sizes' => "1,2,3,4,5,6,7", 'theme_advanced_layout_manager' => "SimpleLayout", 'theme_advanced_toolbar_align' => "left", - 'theme_advanced_buttons1' => $fontbutton . 'fontsizeselect,formatselect,|,' . - 'undo,redo,|,search,replace,|,fullscreen', - 'theme_advanced_buttons2' => 'bold,italic,underline,strikethrough,sub,sup,|,' . - 'justifyleft,justifycenter,justifyright,|,' . - 'cleanup,removeformat,pastetext,pasteword,|,forecolor,backcolor,|,ltr,rtl', - 'theme_advanced_buttons3' => 'bullist,numlist,outdent,indent,|,' . - 'link,unlink,|,image,nonbreaking,charmap,table,|,code', 'theme_advanced_fonts' => $fontselectlist, 'theme_advanced_resize_horizontal' => true, 'theme_advanced_resizing' => true, @@ -170,6 +162,19 @@ class tinymce_texteditor extends texteditor { 'theme_advanced_statusbar_location' => "bottom", ); + // Should we override the default toolbar layout unconditionally? + $customtoolbar = self::parse_toolbar_setting($config->customtoolbar); + if ($customtoolbar) { + $i = 1; + foreach ($customtoolbar as $line) { + $params['theme_advanced_buttons'.$i] = $line; + $i++; + } + } else { + // At least one line is required. + $params['theme_advanced_buttons1'] = ''; + } + if (!empty($options['legacy']) or !empty($options['noclean']) or !empty($options['trusted'])) { // now deal somehow with non-standard tags, people scream when we do not make moodle code xtml strict, // but they scream even more when we strip all tags that are not strict :-( @@ -188,20 +193,6 @@ class tinymce_texteditor extends texteditor { // Allow plugins to adjust parameters. editor_tinymce_plugin::all_update_init_params($params, $context, $options); - // Should we override the default toolbar layout unconditionally? - $customtoolbar = self::parse_toolbar_setting($config->customtoolbar); - if ($customtoolbar) { - unset($params['theme_advanced_buttons1']); - unset($params['theme_advanced_buttons2']); - unset($params['theme_advanced_buttons3']); - unset($params['theme_advanced_buttons4']); - $i = 1; - foreach ($customtoolbar as $line) { - $params['theme_advanced_buttons'.$i] = $line; - $i++; - } - } - // Remove temporary parameters. unset($params['moodle_config']); @@ -221,6 +212,7 @@ class tinymce_texteditor extends texteditor { } $customtoolbar = str_replace("\r", "\n", $customtoolbar); $customtoolbar = strtolower($customtoolbar); + $i = 0; foreach (explode("\n", $customtoolbar) as $line) { $line = preg_replace('/[^a-z0-9_,\|\-]/', ',', $line); $line = str_replace('|', ',|,', $line); @@ -229,7 +221,13 @@ class tinymce_texteditor extends texteditor { if ($line === '') { continue; } - $result[] = $line; + if ($i == 9) { + // Maximum is ten lines, merge the rest to the last line. + $result[9] = $result[9].','.$line; + } else { + $result[] = $line; + $i++; + } } return $result; } diff --git a/lib/editor/tinymce/settings.php b/lib/editor/tinymce/settings.php index d7ffb88332d..56ee3d03fab 100644 --- a/lib/editor/tinymce/settings.php +++ b/lib/editor/tinymce/settings.php @@ -31,8 +31,13 @@ if ($ADMIN->fulltree) { require_once(__DIR__.'/adminlib.php'); $settings->add(new tiynce_subplugins_settings()); $settings->add(new admin_setting_heading('tinymcegeneralheader', new lang_string('settings'), '')); + $default = "fontselect,fontsizeselect,formatselect,|,undo,redo,|,search,replace,|,fullscreen + +bold,italic,underline,strikethrough,sub,sup,|,justifyleft,justifycenter,justifyright,|,cleanup,removeformat,pastetext,pasteword,|,forecolor,backcolor,|,ltr,rtl + +bullist,numlist,outdent,indent,|,link,unlink,|,image,nonbreaking,charmap,table,|,code"; $settings->add(new admin_setting_configtextarea('editor_tinymce/customtoolbar', - get_string('customtoolbar', 'editor_tinymce'), get_string('customtoolbar_desc', 'editor_tinymce', 'http://www.tinymce.com/wiki.php/Buttons/controls'), '', PARAM_RAW, 100, 6)); + get_string('customtoolbar', 'editor_tinymce'), get_string('customtoolbar_desc', 'editor_tinymce', 'http://www.tinymce.com/wiki.php/Buttons/controls'), $default, PARAM_RAW, 100, 8)); $settings->add(new admin_setting_configtextarea('editor_tinymce/fontselectlist', get_string('fontselectlist', 'editor_tinymce'), '', 'Trebuchet=Trebuchet MS,Verdana,Arial,Helvetica,sans-serif;Arial=arial,helvetica,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,times new roman,times,serif;Tahoma=tahoma,arial,helvetica,sans-serif;Times New Roman=times new roman,times,serif;Verdana=verdana,arial,helvetica,sans-serif;Impact=impact;Wingdings=wingdings', PARAM_RAW)); diff --git a/lib/editor/tinymce/version.php b/lib/editor/tinymce/version.php index 345b637d280..4d537de6065 100644 --- a/lib/editor/tinymce/version.php +++ b/lib/editor/tinymce/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2012081000; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2012061700; // Requires this Moodle version +$plugin->version = 2012083100; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2012083100; // Requires this Moodle version $plugin->component = 'editor_tinymce'; // Full name of the plugin (used for diagnostics) $plugin->release = '3.6.0'; // This is NOT a directory name, see lib.php if you need to know where is the editor code! From 03f48b47974186d2c232da86aaa395eef8046760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sat, 1 Sep 2012 11:36:13 +0200 Subject: [PATCH 37/96] MDL-35177 add tinymce plugin icons --- lib/editor/tinymce/adminlib.php | 9 ++++++++- lib/editor/tinymce/plugins/dragmath/pix/icon.png | Bin 0 -> 302 bytes .../tinymce/plugins/moodleemoticon/pix/icon.png | Bin 0 -> 1025 bytes .../tinymce/plugins/moodleimage/pix/icon.png | Bin 0 -> 1534 bytes .../tinymce/plugins/moodlemedia/pix/icon.png | Bin 0 -> 1615 bytes .../tinymce/plugins/moodlenolink/pix/icon.png | Bin 0 -> 314 bytes .../tinymce/plugins/spellchecker/pix/icon.png | Bin 0 -> 1785 bytes 7 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 lib/editor/tinymce/plugins/dragmath/pix/icon.png create mode 100644 lib/editor/tinymce/plugins/moodleemoticon/pix/icon.png create mode 100644 lib/editor/tinymce/plugins/moodleimage/pix/icon.png create mode 100644 lib/editor/tinymce/plugins/moodlemedia/pix/icon.png create mode 100644 lib/editor/tinymce/plugins/moodlenolink/pix/icon.png create mode 100644 lib/editor/tinymce/plugins/spellchecker/pix/icon.png diff --git a/lib/editor/tinymce/adminlib.php b/lib/editor/tinymce/adminlib.php index b145f996743..2aa9caa49df 100644 --- a/lib/editor/tinymce/adminlib.php +++ b/lib/editor/tinymce/adminlib.php @@ -145,7 +145,7 @@ class tiynce_subplugins_settings extends admin_setting { * @return string */ public function output_html($data, $query='') { - global $CFG, $OUTPUT; + global $CFG, $OUTPUT, $PAGE; require_once("$CFG->libdir/editorlib.php"); require_once("$CFG->libdir/pluginlib.php"); require_once(__DIR__.'/lib.php'); @@ -198,6 +198,13 @@ class tiynce_subplugins_settings extends admin_setting { $displayname = html_writer::tag('span', $namestr, array('class'=>'dimmed_text')); } + if ($PAGE->theme->resolve_image_location('icon', 'tinymce_' . $name)) { + $icon = $OUTPUT->pix_icon('icon', '', 'tinymce_' . $name, array('class' => 'smallicon pluginicon')); + } else { + $icon = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'smallicon pluginicon noicon')); + } + $displayname = $icon . ' ' . $displayname; + // Add available buttons. $buttons = implode(', ', $plugin->get_buttons()); $buttons = html_writer::tag('span', $buttons, array('class'=>'tinymcebuttons')); diff --git a/lib/editor/tinymce/plugins/dragmath/pix/icon.png b/lib/editor/tinymce/plugins/dragmath/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3fd55ee73613189e0544c102f45868eef54b2ff9 GIT binary patch literal 302 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`j)FbFd;%$g$s6nx<6 z;uvBf_;k`m!9xl>ZOzY_WLrgddC49$lv%j$(Bk#7oS(&o1cJ9b@1D2wwd3b@{g##& z@6Y6h)+l#HN1d;K)5&u;H&!7debHI1H9733e|El?ZkRZG>b}AwQPDm6_c!pdpZL9c zm(vW63j#9xi?8YJIoz=&xv4&*s>;?N{C~iy+`aQHBd_VMVAc6hHGKh>@XFSAjMog_ z8a?Y(T+OJ~c`vd(@sa`SpezwyTugy|{_b$pv-`Uct yczx5HIp3dJ257mt)m(VG?pc=O*#q;7?lDf(uR9z6sK^!QR|Zd4KbLh*2~7Ymv3rdG literal 0 HcmV?d00001 diff --git a/lib/editor/tinymce/plugins/moodleemoticon/pix/icon.png b/lib/editor/tinymce/plugins/moodleemoticon/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6d6ffc693930663f4f14ea4164bf08d9af4c0815 GIT binary patch literal 1025 zcmV+c1pfPpP)luKw_RT#&A=bn4#-pM?Y$-}lJO;SUPQZSY-qUgqj z#TLm%5L~&jZdyUQcIDcQAZ}a<1#88MpamB`0@96A8%QiQO`1#+lbCeUJZ3U8_i_C0 z88d2pZ1jUW_i?`SKi~KN|G$C%In=-5ndpu|?O@^>iH>k$*QP5yh| zrR#k3>1_%*&7}*^a{kqRv|mG+Cn0zaM;&S7IQzxL;hb@6@5$?3a2-i%f~hleZ<7x{ znPRuH%f0z3SFinmTa<&$9tk&S)~@VB*n1l=s(*=5>%vWz#ib4=zWs%jN;f+8c>e5Y zE7^~E@r5T?oJ?4l@ko*ct)_{a*UUbuEj6t!*?Je_yG(vxW&EYfRO=zfhkCgF*=yuG zVP$C-r4)ll>ugsw>&t75JYsNT!O|t+`JxrdisFQ2Vgd71iMQVQg01e7fb|TQ1L+nZMIy_U;ak9UsCar0FWg&z$Db zll^TVV_Kc1{HDMvbkR{Fd^be(b(?1^EcBM>QGIx>%j*|Ta&CN-Oi*NGq=4sdVN6y4 z#FC=;e#9n2l*m^RE~P@pe*g?Rx(E_URW5F450Cfe*pT(rpQ{X==q5HE4lUJ9yhkHc zIALrHAoMU^XhGA%tjbB)K{L`AM=B7N5f4t^nxR%VEX=Mz`~VYeqLM9k?mu8|YK2^9 zf+sv1Pq##oJ!#mMm78U4p~k?Vi;;ymtTOggpN!l#n;Q||P0V7Na$vGYsV~p5V_AY+ zDDBEw(z@qliB-`8wjF-+n>WqF!x5c%Pu!n@>LPJel}%y|;WnKZ{VoeReW z!Z?THpOw5()i!pvc#7$ptK46h$5fW>21oqmIFce$L9%I$oGzq1iXv9!s3yWBJ=C{k z<|S;)>BEn=CgitBWqpIefenWHJ%WO^^BjrSQfmm&(xRwMUH*BBB7hP9=Fq_)e$Ugk z+jh$-=m&4VVvQCFhn|knnFdGtkSl|$wXIgs!1eMJ@;QWh=orI4{Bj$o{g48t@$vg_ zSkRU76uxlP;fMH{3|)EX>W(PpWuk=Bpfo_C{~Y<>Dr@Z)e)k^^-fw($*#a-lO%XOK v6g@?k__O;f_?Qgd=aTm>UNLx@gf zd8k$!eUKlJ`~(g#kaN()3Hh}oGYt`{D4qq$VEPs$YUN*)nFM zotderF)$p_h0%3@t%}5D6_NzrfPn-TFyGp70n;z646x(si!}Ymz`$7N>Eak-A(;H< z|9^XCQ$}G^UgnvurOc5RVmvzkOFE`4ls;kb{Y<)vHAnGe--?G_9iKj}EUro5Rrqzk zRkYyj4T%cg7Ri)6y+a31tYW!V|14gL+2c;+6Irv*1@2rP!kZiw?kfEI7c5ZxZJ(gh zS%;ie)@u6`cO)M?k&yOzFft{r@f3<>qe8)^?@-zob?A2g4c=?S%#&3fGi{zZ5U%C6- z{+p*-d9TEr>G{l`9oNmB$>zUHz*m8JqQL}C#!O?!L;wGu-+1BId5hT}?K}9Bnlom| zxTwxyncbWtafD@uO%tQhiEB>6iVbsF&A1Pr{;bz^Xm8h*=a%tY%Gx4k6yqe~P2Xpp`HK#o?Wn)w5^|kYqU&wm6)u(udM2O!X_T}bDFY7h` zKhQg3Ado12QfEzEn~UwqG)0pH-y0m6mm(Ry>HX8W_D%0KsD$=(^>bP0l+XkKGKmGh literal 0 HcmV?d00001 diff --git a/lib/editor/tinymce/plugins/moodlemedia/pix/icon.png b/lib/editor/tinymce/plugins/moodlemedia/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7ebc40d3eacb5752c92cd1d34931ba8aa2ab43b GIT binary patch literal 1615 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`j)FbFd;%$g&?z`&xG z84^(v;p=0SoS&;f_?Qgd=aTm>UNLx@gf zd8k$!eUKlJ`~(g#kaN()3Hh}oGYt`{D4qq$VEPs$YUN*)nFM zotderF)$p_h0%3@t%}5D6_NzrfPn-TFyGp70n;z646x&x#c|7!fr0V7r;B5Vh2Ywd z^WIE>0{?jUouZ~>hh5v~T;jFpO~OTATb5Vx0x$0F@^ln%JpMwcWQDWi^gOvOvnL9w zr3TDk?+s`Qb}k5A}Ba?onX>cJ|D8hu}p;s0Q!Xh6& zdp0d%-4q20--u}LtSN<`m``gb*-dZu(|_=zV7;n4t4G4NnVCn{3w@~Algs|5#8SX^ ze^{+QtA`|uTN0;?fv)bpd$m2U_C4S}HqmQo;E5wkCaeiHUJ|y_Y0{DJ+va#2Tbt2% zEhb@Y(76*c_Nw~zaZQLimo`7*k;{2zJ-a=hmdy@l4|{JmL!Qh1Y=p( zN~%H{TAyde*ZC<6v3Y2>a%;%W{-(nm_F80;r>ac*l%%D{JQVNtr8X^;{HztSQAc(1 zzuwe)e=_*RCqzx_V@ZyD@4oh*h@Dwo6u i@aoPL(bN3@^8o+q=}*44?ewY#75Sd7elF{r5}E*pKr>SS literal 0 HcmV?d00001 diff --git a/lib/editor/tinymce/plugins/moodlenolink/pix/icon.png b/lib/editor/tinymce/plugins/moodlenolink/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3f21ab4b4ea0c88d879f8dcc8269f2616dacddbf GIT binary patch literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|&H|6fVg?3oVGw3ym^DWNDEP+H z#WBP}@abejzGed+SL3n=LUUSX3V*eVVd0b8s);)6&wn4~ z^z-^}nljI|%`xS+%Dk9wu_>{v@{i@8_wN(WlxlVrEQpStwqAkd(n{N_YNlEY&kSv5 z<>>6?70V5am=q`#S$CxAV-uPL?t-Z`fCNNq)Yc-kzStp-dfF8+ep9 zUa&gZthb>>FGRRsQej#N9qHbs_2|AGRfw=7>^9cJGe^&$UNT*}KULDDg4sz84;c)I$z JtaD0e0surlbg=*c literal 0 HcmV?d00001 diff --git a/lib/editor/tinymce/plugins/spellchecker/pix/icon.png b/lib/editor/tinymce/plugins/spellchecker/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7124c6acb0d05c2f0326926ddfe5210e04afd6a0 GIT binary patch literal 1785 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`j)FbFd;%$g&?z`&xG z84^(v;p=0SoS&;f_?Qgd=aTm>UNLx@gf zd8k$!eUKlJ`~(g#kaN()3Hh}oGYt`{D4qq$VEPs$YUN*)nFM zotderF)$p_h0%3@t%}5D6_NzrfPn-TFyGp70n;z646x&>>oLh;U|_2Bba4!^5L`Pc z+GBE}#PR?4quNUo*33D5%Ex0_9E%e>%i_tbvJx}Q4c9oZbSu1Cbl^qWg@;Npf-I%t zX=Q>My;;}9{HA;6O^w>wk!z(bdi}~mKT-|i@%CvKc} zNsrOJzmjbf_*^I%zalr| z*)z$TGr6xmn)vX~(`X&%^^3zLPr4fHuKsU${qo!rlk)IeKh{)UIrsNO_yX0Ni7S7f zvHP*?gLhBP`!HrdS}YSFB?6^A9FHNV|4==mi$qsyFA<-N?UhKo1&ckIm%+*ZrzH~np} zi<^N|anL*NY{9=jqeC(;-r39%IJ=$o_*R>#edccsL|?ev*gvbVD<_a)v2fuPtEi1m zGn}ffNj*t_y5Q?=E1lrg7ryNhmRxPRqavwJqEOUf)ybf()35H8I(mGY!Z9Vo43o?k zQXL6383NA^-`o@-Brj?@Wp>Q2zmu-oe|MO^Pw?SCPnlawk4@dI%K2LD`(t0`}~d8TcX*?t~WIeEJJxvX Date: Sat, 1 Sep 2012 12:35:31 +0200 Subject: [PATCH 38/96] MDL-35168 fix yui combo path disclosure --- theme/yui_combo.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theme/yui_combo.php b/theme/yui_combo.php index 2fc010815d8..bd19b5039cc 100644 --- a/theme/yui_combo.php +++ b/theme/yui_combo.php @@ -115,7 +115,8 @@ foreach ($parts as $part) { $contentfile = "$CFG->libdir/yuilib/$part"; } if (!file_exists($contentfile) or !is_file($contentfile)) { - $content .= "\n// Combo resource $part ($contentfile) not found!\n"; + $location = '$CFG->dirroot'.preg_replace('/^'.preg_quote($CFG->dirroot, '/').'/', '', $contentfile); + $content .= "\n// Combo resource $part ($location) not found!\n"; continue; } $filecontent = file_get_contents($contentfile); From fba5791323c2745d6436136b363a4bd2a62795be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sat, 1 Sep 2012 13:27:30 +0200 Subject: [PATCH 39/96] MDL-35145 add extra delete_user() parameter validation We do not want to delete local admins and guest account, we need to validate the supplied parameter is valid $user record and refetch it from database. --- lib/moodlelib.php | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/moodlelib.php b/lib/moodlelib.php index aaeddaaf6e5..02e6c8c5a14 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -3927,15 +3927,45 @@ function truncate_userinfo($info) { * Any plugin that needs to purge user data should register the 'user_deleted' event. * * @param stdClass $user full user object before delete - * @return boolean always true + * @return boolean success + * @throws coding_exception if invalid $user parameter detected */ -function delete_user($user) { +function delete_user(stdClass $user) { global $CFG, $DB; require_once($CFG->libdir.'/grouplib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/message/lib.php'); require_once($CFG->dirroot.'/tag/lib.php'); + // Make sure nobody sends bogus record type as parameter. + if (!property_exists($user, 'id') or !property_exists($user, 'username')) { + throw new coding_exception('Invalid $user parameter in delete_user() detected'); + } + + // Better not trust the parameter and fetch the latest info, + // this will be very expensive anyway. + if (!$user = $DB->get_record('user', array('id'=>$user->id))) { + debugging('Attempt to delete unknown user account.'); + return false; + } + + // There must be always exactly one guest record, + // originally the guest account was identified by username only, + // now we use $CFG->siteguest for performance reasons. + if ($user->username === 'guest' or isguestuser($user)) { + debugging('Guest user account can not be deleted.'); + return false; + } + + // Admin can be theoretically from different auth plugin, + // but we want to prevent deletion of internal accoutns only, + // if anything goes wrong ppl may force somebody to be admin via + // config.php setting $CFG->siteadmins. + if ($user->auth === 'manual' and is_siteadmin($user)) { + debugging('Local administrator accounts can not be deleted.'); + return false; + } + // delete all grades - backup is kept in grade_grades_history table grade_user_delete($user->id); From 4366ce809790538c8784419049e0854aea76243a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sat, 1 Sep 2012 14:13:40 +0200 Subject: [PATCH 40/96] MDL-35145 add basic unit tests for delete_user() --- lib/tests/moodlelib_test.php | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lib/tests/moodlelib_test.php b/lib/tests/moodlelib_test.php index dd9665a5d59..1abdf8098e4 100644 --- a/lib/tests/moodlelib_test.php +++ b/lib/tests/moodlelib_test.php @@ -1910,4 +1910,64 @@ class moodlelib_testcase extends advanced_testcase { $this->assertEquals('5.43000', format_float(5.43, 5, false)); $this->assertEquals('5.43', format_float(5.43, 5, false, true)); } + + /** + * Test deleting of users. + */ + public function test_delete_user() { + global $DB, $CFG; + + $this->resetAfterTest(); + + $guest = $DB->get_record('user', array('id'=>$CFG->siteguest), '*', MUST_EXIST); + $admin = $DB->get_record('user', array('id'=>$CFG->siteadmins), '*', MUST_EXIST); + $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); + + $user = $this->getDataGenerator()->create_user(array('idnumber'=>'abc')); + $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'xyz')); + + $result = delete_user($user); + $this->assertTrue($result); + $deluser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST); + $this->assertEquals(1, $deluser->deleted); + $this->assertEquals(0, $deluser->picture); + $this->assertSame('', $deluser->idnumber); + $this->assertSame(md5($user->username), $deluser->email); + $this->assertRegExp('/^'.preg_quote($user->email, '/').'\.\d*$/', $deluser->username); + + $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); + + // Try invalid params. + + $record = new stdClass(); + $record->grrr = 1; + try { + delete_user($record); + $this->fail('Expecting exception for invalid delete_user() $user parameter'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + $record->id = 1; + try { + delete_user($record); + $this->fail('Expecting exception for invalid delete_user() $user parameter'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + + $CFG->debug = DEBUG_MINIMAL; // Prevent standard debug warnings. + + $record = new stdClass(); + $record->id = 666; + $record->username = 'xx'; + $this->assertFalse($DB->record_exists('user', array('id'=>666))); // Any non-existent id is ok. + $result = delete_user($record); + $this->assertFalse($result); + + $result = delete_user($guest); + $this->assertFalse($result); + + $result = delete_user($admin); + $this->assertFalse($result); + } } From 3182d885e7b894c214116872d576a5388e49d6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sat, 1 Sep 2012 15:37:41 +0200 Subject: [PATCH 41/96] MDL-35080 skip views and weird tables in mysql structure upgrades --- lib/db/upgradelib.php | 49 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/db/upgradelib.php b/lib/db/upgradelib.php index 5103e0ba784..5247e49429e 100644 --- a/lib/db/upgradelib.php +++ b/lib/db/upgradelib.php @@ -30,6 +30,51 @@ defined('MOODLE_INTERNAL') || die(); +/** + * Returns all non-view and non-temp tables with sane names. + * Prints list of non-supported tables using $OUTPUT->notification() + * + * @return array + */ +function upgrade_mysql_get_supported_tables() { + global $OUTPUT, $DB; + + $tables = array(); + $patprefix = str_replace('_', '\\_', $DB->get_prefix()); + $pregprefix = preg_quote($DB->get_prefix(), '/'); + + $sql = "SHOW FULL TABLES LIKE '$patprefix%'"; + $rs = $DB->get_recordset_sql($sql); + foreach ($rs as $record) { + $record = array_change_key_case((array)$record, CASE_LOWER); + $type = $record['table_type']; + unset($record['table_type']); + $fullname = array_shift($record); + + if ($pregprefix === '') { + $name = $fullname; + } else { + $count = null; + $name = preg_replace("/^$pregprefix/", '', $fullname, -1, $count); + if ($count !== 1) { + continue; + } + } + + if (!preg_match("/^[a-z][a-z0-9_]*$/", $name)) { + echo $OUTPUT->notification("Database table with invalid name '$fullname' detected, skipping.", 'notifyproblem'); + continue; + } + if ($type === 'VIEW') { + echo $OUTPUT->notification("Unsupported database table view '$fullname' detected, skipping.", 'notifyproblem'); + continue; + } + $tables[$name] = $name; + } + $rs->close(); + + return $tables; +} /** * Remove all signed numbers from current database - mysql only. @@ -50,7 +95,7 @@ function upgrade_mysql_fix_unsigned_columns() { $pbar = new progress_bar('mysqlconvertunsigned', 500, true); $prefix = $DB->get_prefix(); - $tables = $DB->get_tables(); + $tables = upgrade_mysql_get_supported_tables(); $tablecount = count($tables); $i = 0; @@ -115,7 +160,7 @@ function upgrade_mysql_fix_lob_columns() { $pbar = new progress_bar('mysqlconvertlobs', 500, true); $prefix = $DB->get_prefix(); - $tables = $DB->get_tables(); + $tables = upgrade_mysql_get_supported_tables(); asort($tables); $tablecount = count($tables); From d7e6967e4ddde46e585f5d15bdd744399588d4c6 Mon Sep 17 00:00:00 2001 From: Mary Evans Date: Sat, 1 Sep 2012 23:04:08 +0100 Subject: [PATCH 42/96] MDL-35151 theme_anomaly: removed CSS selectors (.forumpost .content p) from style/general.php thus allowing YUI CSS to style paragraphs as normal. --- theme/anomaly/style/general.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/theme/anomaly/style/general.css b/theme/anomaly/style/general.css index d420e1e5220..d6951869be8 100644 --- a/theme/anomaly/style/general.css +++ b/theme/anomaly/style/general.css @@ -491,8 +491,7 @@ h1.headermain { padding-top: 10px; } -.forumpost .content .shortenedpost a, -.forumpost .content p { +.forumpost .content .shortenedpost a { margin: 0 10px; padding: 0; } From 52a976ff559c54d03be9197b5d243554ac45d0f6 Mon Sep 17 00:00:00 2001 From: AMOS bot Date: Sun, 2 Sep 2012 00:32:44 +0000 Subject: [PATCH 43/96] Automatically generated installer lang files --- install/lang/ko/install.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/install/lang/ko/install.php b/install/lang/ko/install.php index c8574b51f01..0dd2488181e 100644 --- a/install/lang/ko/install.php +++ b/install/lang/ko/install.php @@ -34,7 +34,8 @@ $string['admindirname'] = '관리 디렉토리'; $string['availablelangs'] = '가능한 언어 목록'; $string['chooselanguagehead'] = '언어를 선택하시오'; $string['chooselanguagesub'] = '설치 과정에서 사용할 언어를 선택하십시오. 선택한 언어는 사이트의 기본 언어로 사용할 수 있으며, 추후 다른 언어로 바꿀 수도 있습니다.'; -$string['clialreadyinstalled'] = '이미 config.php 파일이 존재함. 사이트를 업데이트하려면 admin/cli/upgrade.php를 사용하십시오'; +$string['clialreadyconfigured'] = '만일 이 사이트를 설치하고 싶은데 이미 config.php파일이 있다면, admin/cli/install_database.php 를 이용하시기 바랍니다.'; +$string['clialreadyinstalled'] = '이미 config.php 파일이 존재함. 사이트를 업그레이드하려면 admin/cli/upgrade.php를 사용하시기 바랍니다.'; $string['cliinstallheader'] = '무들 {$a} 명령 입력 설치 프로그램'; $string['databasehost'] = '데이터베이스 호스트'; $string['databasename'] = '데이터베이스 명칭'; @@ -82,10 +83,10 @@ $string['phpversionhelp'] = '

무들은 적어도 PHP4.3.0 혹은 5.1.0. 이

PHP를 업그레이드 하시거나 새버전을 제공하는 웹호스팅 업체로 이전하기를 권합니다!
(만일 5.0.x버전을 사용 중이라면 4.4.x 버전으로 다운그레이드 할 수 있습니다)

'; $string['welcomep10'] = '{$a->installername} ({$a->installerversion})'; $string['welcomep20'] = '당신의 컴퓨터에 {$a->packname} {$a->packversion} 패키지를 성공적으로 설치한 것을 축하합니다!'; -$string['welcomep30'] = '{$a->installername} 의 이 릴리스는 무들이 그 속에서 동작하는 환경을 생성하기 위한 어플리케이션을 포함하고 있습니다.'; +$string['welcomep30'] = '{$a->installername} 판본은 무들이 동작하는 환경을 생성하기 위한 어플리케이션을 포함하고 있습니다.'; $string['welcomep40'] = '이 패키지는 무들 {$a->moodlerelease} ({$a->moodleversion}) 을 포함하고 있습니다.'; $string['welcomep50'] = '이 패키지에 있는 모든 어플리케이션을 사용하는 것은 각각의 라이센스에의해 지배받습니다. 완전한{$a->installername} 패키지는
공개 소스이며 GPL 라이선스에 의해 배포됩니다.'; -$string['welcomep60'] = '다음 페이지들은 당신의 컴퓨터에 무들을 설정하고 설치하는 길라잡이 역할을 할 것입니다. 기본 설정을 선택하거나 목적에 맞게 선택적으로 수정할 수 있습니다.'; +$string['welcomep60'] = '다음 페이지들은 컴퓨터에 무들을 설치하고 설정하는 길라잡이 역할을 할 것입니다. 기본 설정을 선택하거나 목적에 맞게 선택적으로 수정할 수 있습니다.'; $string['welcomep70'] = '무들 설정을 계속하기 위해서는 "다음" 버튼을 클릭하세요.'; $string['wwwroot'] = '웹 주소'; From 949f3db40b7cf7801b7841b02656ff6e5b2cf38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sun, 2 Sep 2012 11:52:58 +0200 Subject: [PATCH 44/96] MDL-34097 keep current requirements for Moodle 2.4 --- admin/environment.xml | 118 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/admin/environment.xml b/admin/environment.xml index 004cab09808..584a9b0e617 100644 --- a/admin/environment.xml +++ b/admin/environment.xml @@ -659,5 +659,121 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 1dd274dc21baa209cdaacff2875e803084ac2fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sun, 2 Sep 2012 15:11:57 +0200 Subject: [PATCH 45/96] MDL-35185 import Flowplayer 3.2.14 --- lib/flowplayer/README.txt | 28 ++++++++++++++++ lib/flowplayer/README_audio.txt | 13 ++++++++ ...owplayer-3.2.8.js => flowplayer-3.2.11.js} | 31 +++++++++--------- lib/flowplayer/flowplayer-3.2.11.min.js | 24 ++++++++++++++ lib/flowplayer/flowplayer-3.2.14.swf | Bin 0 -> 126400 bytes lib/flowplayer/flowplayer-3.2.8.min.js | 26 --------------- lib/flowplayer/flowplayer-3.2.9.swf | Bin 126490 -> 0 bytes lib/flowplayer/flowplayer.audio-3.2.10.swf | Bin 0 -> 4282 bytes lib/flowplayer/flowplayer.audio-3.2.8.swf | Bin 4117 -> 0 bytes lib/flowplayer/flowplayer.controls-3.2.13.swf | Bin 0 -> 38336 bytes lib/flowplayer/flowplayer.controls-3.2.9.swf | Bin 37903 -> 0 bytes lib/javascript-static.js | 14 ++++---- lib/thirdpartylibs.xml | 2 +- 13 files changed, 89 insertions(+), 49 deletions(-) rename lib/flowplayer/{flowplayer-3.2.8.js => flowplayer-3.2.11.js} (96%) create mode 100644 lib/flowplayer/flowplayer-3.2.11.min.js create mode 100644 lib/flowplayer/flowplayer-3.2.14.swf delete mode 100644 lib/flowplayer/flowplayer-3.2.8.min.js delete mode 100644 lib/flowplayer/flowplayer-3.2.9.swf create mode 100644 lib/flowplayer/flowplayer.audio-3.2.10.swf delete mode 100644 lib/flowplayer/flowplayer.audio-3.2.8.swf create mode 100644 lib/flowplayer/flowplayer.controls-3.2.13.swf delete mode 100644 lib/flowplayer/flowplayer.controls-3.2.9.swf diff --git a/lib/flowplayer/README.txt b/lib/flowplayer/README.txt index 7bdb7bd1c5c..742633c27a5 100644 --- a/lib/flowplayer/README.txt +++ b/lib/flowplayer/README.txt @@ -1,8 +1,35 @@ Version history: +3.2.13 +------ +- Updated to automatically load the latest controls and audio plugins + +3.2.12 +------ +- new flowplayer.js version 3.2.11, + fixes removing the player in fullscreen mode leaves Android locked in landscape orientation (#511) +- #586 add a bitrate label with a new namespace attribute fp:bitratelabel. +- #583 fixes for handling the fullscreenOnly property better +- #494 with relative filenames with a root path strip the baseurl of paths first. + +3.2.11 +------ +- new flowplayer.js, now requires Flash 10.1 as the minimum flash version +- #526 allow click through event for flash installation message when using div containers. +- #508 disabling the stagevideo screen mask, canvas is visible without it, this was causing issues with the display list. +- #443 adding accessibility option to the playbuttonoverlay. + + +3.2.10 +------ +- Fixed #514, scrubbing was broken +- new flowplayer.js version 3.2.9, fixes #510 + 3.2.9 ----- - Fixed #490, controlbar background, buffer bar and progress bar colors were all reset to white +- #503 Update viewport when stage is added to obtain the coordnates correctly. Update viewport when in and out of fullscreen. +- #508 stage video mask was being added to the top layer and hiding all children. 3.2.8 ----- @@ -69,6 +96,7 @@ this.loadPlugin("content","../flowplayer.content.swf", { html: "test" }, functi - #461 when we have a clip base url set, we need the complete clip url sent to play2 for http streams. - #470 check for a playlist when replacing the playlist with an rss feed. - #494 regression issued caused by #412, enable base url correctly. +- #30 regression caused by character replacements, removing for now and let end user deal with them. 3.2.7 ----- diff --git a/lib/flowplayer/README_audio.txt b/lib/flowplayer/README_audio.txt index 04a2f27b6d8..746c5ee8bbd 100644 --- a/lib/flowplayer/README_audio.txt +++ b/lib/flowplayer/README_audio.txt @@ -1,5 +1,18 @@ Version history: +3.2.10 +------ +- #575 send the start event after begin +- #569 if the playlist has been reset but the audio has been already buffered, set the duration and start event. +- #582 fixes for metadata events dispatching in playlists and when replaying same audio item, cleanup duration updating once download has completed, +fixes for clearing the previous cover image display. +- #611 close the channel and sound on stream not found errors. + +3.2.9 +----- +- #501 fixes to dispatch start state correctly. +- #501 use the sound channel to listen for a complete event to finish correctly. + 3.2.8 ----- Fixes: diff --git a/lib/flowplayer/flowplayer-3.2.8.js b/lib/flowplayer/flowplayer-3.2.11.js similarity index 96% rename from lib/flowplayer/flowplayer-3.2.8.js rename to lib/flowplayer/flowplayer-3.2.11.js index ee42b3c991f..c6e51cf5061 100644 --- a/lib/flowplayer/flowplayer-3.2.8.js +++ b/lib/flowplayer/flowplayer-3.2.11.js @@ -1,5 +1,5 @@ /* - * flowplayer.js 3.2.8. The Flowplayer API + * flowplayer.js 3.2.11. The Flowplayer API * * Copyright 2009-2011 Flowplayer Oy * @@ -18,8 +18,8 @@ * You should have received a copy of the GNU General Public License * along with Flowplayer. If not, see . * - * Date: 2011-12-30 12:34:08 -0500 (Fri, 30 Dec 2011) - * Revision: 761 + * Date: 2012-06-16 10:34:45 -0400 (Sat, 16 Jun 2012) + * Revision: 808 */ (function () { function g(o) { @@ -373,6 +373,9 @@ D = true; try { if (v) { + if (v.fp_isFullscreen()) { + v.fp_toggleFullscreen() + } v.fp_close(); w._fireEvent("onUnload") } @@ -468,7 +471,7 @@ } return w }, getVersion:function () { - var I = "flowplayer.js 3.2.8"; + var I = "flowplayer.js 3.2.11"; if (w.isLoaded()) { var H = v.fp_getVersion(); H.push(I); @@ -615,7 +618,7 @@ return P }; function B() { - q.innerHTML = ''; // Moodle hack - we do not want splashscreens, unfortunately there is not switch to disable them + q.innerHTML=''; // Moodle hack - we do not want splashscreens, unfortunately there is not switch to disable them if ($f(q)) { $f(q).getParent().innerHTML = ""; p = $f(q).getIndex(); @@ -776,7 +779,7 @@ if (typeof t == "string") { t = {src:t} } - t = i({bgcolor:"#000000", version:[9, 0], expressInstall:"http://static.flowplayer.org/swf/expressinstall.swf", cachebusting:false}, t); + t = i({bgcolor:"#000000", version:[10, 1], expressInstall:"http://releases.flowplayer.org/swf/expressinstall.swf", cachebusting:false}, t); if (typeof o == "string") { if (o.indexOf(".") != -1) { var s = []; @@ -823,7 +826,7 @@ } })(); (function () { - var h = document.all, j = "http://www.adobe.com/go/getflashplayer", c = typeof jQuery == "function", e = /(\d+)[^\d]+(\d+)[^\d]*(\d*)/, b = {width:"100%", height:"100%", id:"_" + ("" + Math.random()).slice(9), allowfullscreen:true, allowscriptaccess:"always", quality:"high", version:[3, 0], onFail:null, expressInstall:null, w3c:false, cachebusting:false}; + var h = document.all, j = "http://get.adobe.com/flashplayer", c = typeof jQuery == "function", e = /(\d+)[^\d]+(\d+)[^\d]*(\d*)/, b = {width:"100%", height:"100%", id:"_" + ("" + Math.random()).slice(9), allowfullscreen:true, allowscriptaccess:"always", quality:"high", version:[3, 0], onFail:null, expressInstall:null, w3c:false, cachebusting:false}; if (window.attachEvent) { window.attachEvent("onbeforeunload", function () { __flash_unloadHandler = function () { @@ -882,7 +885,7 @@ } } f = e.exec(f); - return f ? [f[1], f[3]] : [0, 0] + return f ? [1 * f[1], 1 * f[(f[1] * 1 > 9 ? 2 : 3)] * 1] : [0, 0] }, asString:function (l) { if (l === null || l === undefined) { return null @@ -895,13 +898,11 @@ case"string": l = l.replace(new RegExp('(["\\\\])', "g"), "\\$1"); l = l.replace(/^\s?(\d+\.?\d*)%/, "$1pct"); - l = l.replace(/(%)/g, "%25").replace(/'/g, "\\u0027").replace(/"/g, "\\u0022").replace(/&/g, "%26"); return'"' + l + '"'; case"array": - return"[" + a(l, - function (o) { - return g.asString(o) - }).join(",") + "]"; + return"[" + a(l,function (o) { + return g.asString(o) + }).join(",") + "]"; case"function": return'"function()"'; case"object": @@ -963,7 +964,7 @@ } else { if (!f.innerHTML.replace(/\s/g, "")) { f.innerHTML = "

Flash version " + n.version + " or greater is required

" + (k[0] > 0 ? "Your version is " + k : "You have no flash plugin installed") + "

" + (f.tagName == "A" ? "

Click here to download latest version

" : "

Download latest version from here

"); - if (f.tagName == "A") { + if (f.tagName == "A" || f.tagName == "DIV") { f.onclick = function () { location.href = j } @@ -992,7 +993,7 @@ } if (c) { - jQuery.tools = jQuery.tools || {version:"3.2.8"}; + jQuery.tools = jQuery.tools || {version:"3.2.11"}; jQuery.tools.flashembed = {conf:b}; jQuery.fn.flashembed = function (l, f) { return this.each(function () { diff --git a/lib/flowplayer/flowplayer-3.2.11.min.js b/lib/flowplayer/flowplayer-3.2.11.min.js new file mode 100644 index 00000000000..c03505f4b02 --- /dev/null +++ b/lib/flowplayer/flowplayer-3.2.11.min.js @@ -0,0 +1,24 @@ +/* + * flowplayer.js 3.2.11. The Flowplayer API + * + * Copyright 2009-2011 Flowplayer Oy + * + * This file is part of Flowplayer. + * + * Flowplayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Flowplayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Flowplayer. If not, see . + * + * Date: 2012-06-16 10:34:45 -0400 (Sat, 16 Jun 2012) + * Revision: 808 + */ +(function(){function g(o){console.log("$f.fireEvent",[].slice.call(o))}function k(q){if(!q||typeof q!="object"){return q}var o=new q.constructor();for(var p in q){if(q.hasOwnProperty(p)){o[p]=k(q[p])}}return o}function m(t,q){if(!t){return}var o,p=0,r=t.length;if(r===undefined){for(o in t){if(q.call(t[o],o,t[o])===false){break}}}else{for(var s=t[0];p1){var t=arguments[1],q=(arguments.length==3)?arguments[2]:{};if(typeof t=="string"){t={src:t}}t=i({bgcolor:"#000000",version:[10,1],expressInstall:"http://releases.flowplayer.org/swf/expressinstall.swf",cachebusting:false},t);if(typeof o=="string"){if(o.indexOf(".")!=-1){var s=[];m(n(o),function(){s.push(new b(this,k(t),k(q)))});return new d(s)}else{var r=c(o);return new b(r!==null?r:k(o),k(t),k(q))}}else{if(o){return new b(o,k(t),k(q))}}}return null};i(window.$f,{fireEvent:function(){var o=[].slice.call(arguments);var q=$f(o[0]);return q?q._fireEvent(o.slice(1)):null},addPlugin:function(o,p){b.prototype[o]=p;return $f},each:m,extend:i});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(q,p){if(!arguments.length||typeof arguments[0]=="number"){var o=[];this.each(function(){var r=$f(this);if(r){o.push(r)}});return arguments.length?o[arguments[0]]:new d(o)}return this.each(function(){$f(this,k(q),p?k(p):{})})}}})();(function(){var h=document.all,j="http://get.adobe.com/flashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(m,l){if(l){for(var f in l){if(l.hasOwnProperty(f)){m[f]=l[f]}}}return m}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var m,f;try{f=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(o){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=m&&m.GetVariable("$version")}catch(n){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");f=m&&m.GetVariable("$version")}catch(l){}}}f=e.exec(f);return f?[1*f[1],1*f[(f[1]*1>9?2:3)]*1]:[0,0]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d*)%/,"$1pct");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n=''}o.width=o.height=o.id=o.w3c=o.src=null;o.onFail=o.version=o.expressInstall=null;for(var m in o){if(o[m]){n+=''}}var p="";if(l){for(var f in l){if(l[f]){var q=l[f];p+=f+"="+(/function|object/.test(typeof q)?g.asString(q):q)+"&"}}p=p.slice(0,-1);n+='"}n+="";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:encodeURIComponent(location.href),MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="

Flash version "+n.version+" or greater is required

"+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+"

"+(f.tagName=="A"?"

Click here to download latest version

":"

Download latest version from here

");if(f.tagName=="A"||f.tagName=="DIV"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"3.2.11"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}})(); \ No newline at end of file diff --git a/lib/flowplayer/flowplayer-3.2.14.swf b/lib/flowplayer/flowplayer-3.2.14.swf new file mode 100644 index 0000000000000000000000000000000000000000..bee3b6030bb2bbb7a56a390776519d6766cdd956 GIT binary patch literal 126400 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 literal 0 HcmV?d00001 diff --git a/lib/flowplayer/flowplayer-3.2.8.min.js b/lib/flowplayer/flowplayer-3.2.8.min.js deleted file mode 100644 index 93feb7385dd..00000000000 --- a/lib/flowplayer/flowplayer-3.2.8.min.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * flowplayer.js 3.2.8. The Flowplayer API - * - * Copyright 2009-2011 Flowplayer Oy - * - * This file is part of Flowplayer. - * - * Flowplayer is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Flowplayer is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Flowplayer. If not, see . - * - * Date: 2011-12-30 12:34:08 -0500 (Fri, 30 Dec 2011) - * Revision: 761 - * - * this file was modified for Moodle - see flowplayer-3.2.8.js - */ -(function(){function g(o){console.log("$f.fireEvent",[].slice.call(o))}function k(q){if(!q||typeof q!="object"){return q}var o=new q.constructor();for(var p in q){if(q.hasOwnProperty(p)){o[p]=k(q[p])}}return o}function m(t,q){if(!t){return}var o,p=0,r=t.length;if(r===undefined){for(o in t){if(q.call(t[o],o,t[o])===false){break}}}else{for(var s=t[0];p1){var t=arguments[1],q=(arguments.length==3)?arguments[2]:{};if(typeof t=="string"){t={src:t}}t=i({bgcolor:"#000000",version:[9,0],expressInstall:"http://static.flowplayer.org/swf/expressinstall.swf",cachebusting:false},t);if(typeof o=="string"){if(o.indexOf(".")!=-1){var s=[];m(n(o),function(){s.push(new b(this,k(t),k(q)))});return new d(s)}else{var r=c(o);return new b(r!==null?r:k(o),k(t),k(q))}}else{if(o){return new b(o,k(t),k(q))}}}return null};i(window.$f,{fireEvent:function(){var o=[].slice.call(arguments);var q=$f(o[0]);return q?q._fireEvent(o.slice(1)):null},addPlugin:function(o,p){b.prototype[o]=p;return $f},each:m,extend:i});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(q,p){if(!arguments.length||typeof arguments[0]=="number"){var o=[];this.each(function(){var r=$f(this);if(r){o.push(r)}});return arguments.length?o[arguments[0]]:new d(o)}return this.each(function(){$f(this,k(q),p?k(p):{})})}}})();(function(){var h=document.all,j="http://www.adobe.com/go/getflashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(m,l){if(l){for(var f in l){if(l.hasOwnProperty(f)){m[f]=l[f]}}}return m}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var m,f;try{f=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(o){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=m&&m.GetVariable("$version")}catch(n){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");f=m&&m.GetVariable("$version")}catch(l){}}}f=e.exec(f);return f?[f[1],f[3]]:[0,0]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d*)%/,"$1pct");l=l.replace(/(%)/g,"%25").replace(/'/g,"\\u0027").replace(/"/g,"\\u0022").replace(/&/g,"%26");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n=''}o.width=o.height=o.id=o.w3c=o.src=null;o.onFail=o.version=o.expressInstall=null;for(var m in o){if(o[m]){n+=''}}var p="";if(l){for(var f in l){if(l[f]){var q=l[f];p+=f+"="+(/function|object/.test(typeof q)?g.asString(q):q)+"&"}}p=p.slice(0,-1);n+='"}n+="";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:encodeURIComponent(location.href),MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="

Flash version "+n.version+" or greater is required

"+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+"

"+(f.tagName=="A"?"

Click here to download latest version

":"

Download latest version from here

");if(f.tagName=="A"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"3.2.8"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}})(); \ No newline at end of file diff --git a/lib/flowplayer/flowplayer-3.2.9.swf b/lib/flowplayer/flowplayer-3.2.9.swf deleted file mode 100644 index 9bdb3f9f54692f7d357ae9faa96278c46df81e80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126490 zcmV(vK9-=mOV;hvY|N60u-0cv8C8xTaIie z0qQ)<(vu}3OF~adY@lG404-%}*|!>KODQd-rR|h%(0!$i!`A6^I^E06eC>2!r^A2F zz4tAW<%HkK)4R*L`@QF$bMCq4%?P%i3WDzgg0RdY^af`ILAa*rTSO4fDW@}CiQZUv zW;$O`yTEturfj8B>e{~j=+UDcM|X7;%TwEToPGA$+dFq|-??)eKx|WI3zg)|wt~80 z(|I(5UPVonbEQhISO}v(Ia#b$&fT<$=bBFGnU<>MJk2tl+OFi4X{At6x9{lK0U4!J zU72EeI$1d{St{josU&8%eP)}QEv611O&(FUW%5Zid(L(P5Ytq0mArCZce*&KgkyPS zCcHb`ZKS8r3Qe`DsDb_-0VHlrJ7N)AnDdoKA5RKFQw2sLNw5L=K?}~(XcJA28YJq|0 zY_IEV9s=Wcp3ob3-FlADV|n9^H!g8mVSpX**z6Y$3iR*E%f3B(CJgwGT+sV+CSN>S z$|q-)vV0_`9F>R5O1_v(D`nyN=CAD*1OOB=<>a)oLkI{Rf+!G+)%FI2e$yh@judlg z4;_mR

U-J&G@T4+N=>_~d0usv_{yIqPLbq;o2!_4cwi%ib|mot{+6)*KW&Qh}apt}AD{m{#&Z zo@TgQEGgwmPEn|)pIqT}p!IV^?4ytpwsOD(x29&~7C0pMT>^Dj8 zH)*g40X_L#X>|j*E*;J$=ZjNT=m47xv)q>}U~7~#>keTTpwh`aq<2p#6<7&$R6M1!Qi8l_Dl1gM zUoKW?-$x7B8|k(NL#eEiIvmU8p?H>qYELVZ)hWADE*HzT zTp?4m9Zi-C_M_!oMG@1*(Q-1Sh&&fRg@wwb4zq9^@WPa`vQaH~T>BC=ter`69;Reo zlrywyWUE82U;%a->|mjG8bn)MRM{k}WUB=joylUp;($D-l=7M-qPtkMr#}sSpUdR1 zJ?k1xzAiI_tpG!`rXiRw!>(W-0O@`zSnWh(ImVsTSIY7kzWWK66p-I$%jg40Tx$^eU zx@a;Q=VnQ%Tf*ejQYxV&XJ)Mt6Z9~$)!|9l2d>z%P%W(%c;WR7y*psV|(qSvZn~^O$}*NX;>*EB%m%Q%mkJ{I9MiLa8a~I z!+n~MMFusmK7>t~+>AB{N0dqhCWq>!-5oE{@)U2WoSTODBg*J(NpZ+&rBunj zW21fX#F`5stMEnTWK~tdmr^oGnN(qHisg>9GH$2Tk2_aL<*R9B6bkmo`lAEA@zh`li{;soLb3!zPsN4+b~-S@3S=`{ggKQ< zvl(r}v6BY755m|EkM>D`)7b>U9_Xo-vFovHaep*1(jOnP?&#>0aPt{JOc+QjDL^bk z#R8iuDi1Jv+QS2KGfG}{DmX2H1ZlLnbr_o)wk42Erzt%!kW(uP>>a-D-ri`hJQ|lr zM!WY$?JCQjArly!iW1~QWQ4e5V*@Y`dlJ#;kW1A^*R2{u>tM5O9Vg=q60Un>*Y=&A zoo8;JtOB~v72FJmawOlPR1% z3~=R1GFfFNqPl3OVSKUzyKA`x#&KU220W7k`T#faAy_7@(=*V>LZw{Ht6g0q2Vw)X z=fkyh!R9SpVcJz;BqYK)HEa#fgxA&sMZ%M_6-5ox(MyM2Y!v%p?{?D&ns?d(sG$Fn zs5?3|8coQtME78{c@f>Qc}v(u(~JULk>6}e9zOTnun`jp8>1OMHyjM}05+x&(o}{u za73w@K^}vs*}O$6C^HpW^bT`yB#Z=AIGG1BJ3Sj_hznwYxC)n*O0`_TZ2;zGPd1lN zw+R`Cokx-_hGbPL0R5B$3t(OOE?VGG-G72J#u0l1R7jw9a$20!Ai56TmXImRjbq8A( zjy&DGoNBcMRHdrgN>w#WI_!`nSFv<;In?T;S}9vPw%X~oV-0tOB}xV&eQ%vsr<{}; z13YGf^c9yu2Xb7_ePo8rT4w-LM#8g@MI=mj+pe%H5@wriSJ(&JO#Em;qYJj4e){R* z14Z&;_hUH730-F zr=Zx9bSWFP@o=CfJX%OQFQt6dc-Wat!IrK9onFBmj``RS6$|;j!>}Li?H}6P z6&@^>p)<3|LU>KM0OJMgq)3ZUfm{lgbdt2gRt0uU!XAfZprma$>1@5Qudg>moh zD^|-Y>2iT{FjuGoRzT+nY&Edo`)Oz%_-N)Qoz5^24>*u(N6WJ~F5tuv&R1)N&xQk+ z4kpVma_V>(X+T)0kucJTVMheglyLGeIx$Q(+qVEN+)TDuhmu3iFw$6nyzDtZuR|f` zgS3iTg*gL=lbq(cR3{$B!;aESc-ywH#O63u>8w`LYHfA4dRs%SEv?I!t+ETtw!-t4 zWoO%k*6w9J&Zu**vwzt*gcaeb!gDQI@fN$l{%>kSICO5k`-Sg zE51%ve1oiTK)iDWWY>C!rS(BeTlb0{nx}2&AvTSOO`~GdnAo&mY&sw|9Tb}`7Mm^+n=Tcb-X^we7F!O9Ewb1$A+{t% zGAWXSVoOSFNsB}g$;Bech~yFx&?mW6Bw3O4i6keI%S3WmBzcici=-g7mBhBoMKU0g zvPj}0QAJV_mv0uA9}$-y6_?M5WL6|sh$JPFD@AgZNZu}zt3`5+Xq_XX`#N~8hvx=( zj=^&yJYAyut)lxTB9fcoc?Uf2gy$A`-UZLC@Z1K^?eN?I&z={2zel33xsT&yx`LA$UIp z?+?TC5qO@4=cDj^48lJS&$IA+f{1HA3GaV_=hN^!2hV5V`79A_pM&@3;rRkQ&%^Tq zkp?VM+#;_3CXrr%Sw_AKq2Gh&I6MpRyadmy@O&ShAHee?c>V*P9}_YB6C$qvDdhTd z@c#myUxNEr@O~Yh|Agni$V$lZ{}Jhbh(-Dx&f(t!#2?`KBRqd1E3G2=GZEMQ6`sGt z^E-I{mxz%$ix{~Up6lSb9&$Klk=DTMzY&}_SytMm%k1L1cUh#fY?5RXx8DiD?}q0t zc<#1{>+ZLRoezQg5z9(gEbj%^Jhbmoi!UZhP@43FMf80To+mA$NS=WNABX1?@O%=U zPr;Lun#fA0)Fw%-u+F{+8GIR@r!CSlNnH00i!?`Uj}U?X5n>UjERH%fpGB~0p*C>X z1;K!K;E$;N2sRFA=RqFg@htVW5vM3hg2hVkMe`-Z&eFLcUMs{(TyD4Ev=A$?T8PU9 z5F-3oJuVl(5VuSJ@$w&w+Y9Aeh!3h`gWU05^QTqVaA<|s{1F4{lpw9@T zabmDtD?xNGRlfPpU+2dr5tr8MZuZo8ga8KUG4+nR*4Y;Q2*JAc(!daH48n+_DXftL zhfU)7h9T5Xa`2zb{c{069%+ucrt;Aun%Z6rd|CL%O%CPK>=BD8KLLfh#?SQfDe%Oh4{ z#Wo_W?6655VT<6fNnT;6v>h_(B%Sbk4%q>Dxm-IfLS&~^*tW|ebnLPU+jm=p&fQjF z#~C&$Aao07LNCNDXIX^av#dhY555b@TZqu_6YQ3=yV7lMCBoo2L>M~PD#XvT2*c-F zgnbuSg+#YS7};YHMtdy6Sg%#s-)j*LM6JTXs71IqW)&`pS%gdXT7V3KJJuh2(%$m>jeSsh~}27Oq$}gk2WLAsxn_efX2WpAoAtHEI#EV?@a9C&Fb1 ztis`g79oGJRhYiSA`~vQ3dOfsgwnWGxP06qln)U>m5ET9AVM`sgd>wgIGQ5DOqvL@ z0h`oHx{eW-f;ngK2f*JxWf88XgC!RscN zfDm{y%t3g)0}`%zCkfit-a>-*@ViLRvF=t96xZKIg3b-Mlc3ao2MM}PyORXn8{bWW zo=xu|LGR|%h26{V0^fOe69I7P?x5?Odx*m#?u6~#;S}FX$i|?v>psZ*jQfe;f!X{3 zaoEMj3E2{Kyk%2RJo`Z+c%e2AvHCwu95!(?c?2qQ=6gw#^wxReu)-XElmvYjJVqQu z+(8~E4u{8KeIL~1+|5DzS?`C6oc{r!1jT0vPzM66#~z_i7=UrTN`O}0OKcy63fu=2 zL(q0V`4F`40iYU!jt7ayO`2>(SVdZG0@+GBY=UJM)IhNAA!pc#Z7(^~CfF|`ew*M> za4!(Age(N-jf4bjf^-jo2_Zn8VdMlii~yiBm-Tq(UO);~i_5+Dc<1r)*5enpl5ETI zeqr2kp+lH+ys{AKOzvEWe2OG@O|&k=uOsAmrX{i>zLAW7n8ZI!gz=A%2}|Z_2woZg z2(j!%{0G^IkX?L*0A8!j>T=&_Io>((QIdIvOni)FK2Dz68f+4TEfXD?8_AKaA(6R{ zk$XabfF?Q+^jVU;fiUMMNb-8JcM0yZ z>YlA3z}#CFGXKJTKJblynk>XVO-STh1n+s{4JiBu%$pSizU$E!YF(;5n9Gf1FPnoiK z3C~%lq2jL*$ZY%t5`TdR$1`mqCzklH7`XR%e6666=7nhH)Y$#zX)j2 zd+3UGlD*>4mF<@7m4~isw`Q+8^!9dJ_U(tRp13M`^`UDb=OnK=G)Kmz3nd{rM-E*} zHYKmc`g?6wtF0Td*}0I}ncR73*Ft7jau>ueWOf5YyHI^K&+ZW3=7 zPTX02^#lZDUL!3(dE*Vp$Y-%wZ9_1Ek?qYe$)Mob{eTd4YswBI7Z!v`+oZkSG9UXlDDYJVI%#VcChZVn zNxn)VB;|naONoJA=)Xy^UHE7SNX1D96fpnNqzL)W9}58TWA0-X(nS$R(V;++HDS1hcdWlBmw5Xq925YTWihUMnLr<{Z3GcI^ zRhYzkBIczVC3BEX7?*eZ@y^K2mW9ku0n@+3GQW`d893gF zj;Z;`E%?sOL$9y1I;=LFJ?%mY)>+E_5^l~x;GDKV4W9~~j@$Ro2&?Hll<;_a`Y|RPy6PNuQ zmey&2zA}R+>H!xRAno-91cZ#YXltk+G4fVzO8<&aY0K(t2ZWyv!8W$&BKo4+4wc_# zEV}Iw*`Y^vLgaQM5>s~SkvkxAhY`60B6rr*W0zKs*LgjxtFsOW-;GP_ZLqW+Bat21 z$H<|3NMvXB9&%`&L^`wchg-1`hN#+<#t)+-gY!f%5^F`L|i-(RnX8 zZ@0|90&es^Mi%0`kup0IH|!o<1(jEDj#aeUZl>EVTgUJ?CLScgGVJ?zBZ|749C`wvpCFO@EE7+V*sqB& zA9=tspR!HdPeNXtRO9a_vG+qTWC2qVUlYF}k%tj{e$p2IA7a71QpFuOWe>oPB0;rY znzUzrOXBY%w1{z#y_@w*6+4tD`p7sEC68bQtpI$+XH+y2sg&Gk@s0%n#U+;O<>`=R1iroX%gzV2AVnk zKC%F`$8X7`2pweE84Lu0uEUCB>i1h9Aql*Oh%n-59)vt$q=?V2XZ``6`9kJT(CrfT z8trzPuT$%G=cIE|!frQZdL54E#eXKyzgttT*q?|H0zMBbIqAY$_VZe9puOd$z2%v7 zXZ`|jFTDSXq<_+*HR~DHtN;+4v~qTg2yBIx zJVb&USc6z+-UDmpDGRi8KJsA;Z5^!Sjj)no(wt|39rRh4#hYG5{zBULYTz+=f$u>m z`n8Do(hA%!gurDmTo&dbfpPEzn8y+r2G2)$lr^vie17J!1O!#M&g z!1qz^6LCwW`;s$Y!C$9!<<-!ib zW&BZ?oPQ@m>`}xO&syebz(48({#OeCI+O@-uzRk)D3KC}=YLoA!scaS*L zFb898zW~UkiO{pSERQdoNX}V~XNDFcpSA!M4y+(9%kj|hh0L5K79uB-CGZ?yi0vmV zAbvTv?J_LqEaZ_6H3Ke)OaQkrL0yNym832iTrzbf`;c&a&ayC1Nk>6AtXEU!)y(VF z9ENI6>+usjzM#kNgZQEzpXBi+Jw5^Pm)jtI7wd+00V)naSXMjH+!G7!7R{YpXty#q zWK_06MyqLm#f}J29s0YE{wCmezYX$Nv3#Im#)B6Ig}LA>3kZEkm`adxr`K)CgQqD&{hwu1&s1pDdk|oG}j_64;kmOp@FinP~$*?rn z5nM925LyoQi+?%Ik@_Y&7Gk#&g0xt&8VJ(}+NUiXW(s`i$2r(z*o7G*j|}x;lsqi~wA3I<=vkf+d%Lktronp$G1#kV=#@K-=mJE)+Z2tR_8yvdJI~v1!x=Z- zM>F@MI5|^@d=;kY#4%)g@6u=T*I*`3&quzFGr2HNK?LD$n)hk6C`gWcFt{Fr_0U(7 zJIm}IeZJnv=c{XVHUw3=*EC=6Bgkim00_DbJ1qos)rmfwVfPaxn>It#QzY{Ch1gTX zQhgP`P9Rv2f?>>8Ei?)eJU|nS;6l5K$>}`Ep+~Noe+2=rYMjNBKuW>nGle4qf4!v- zwq%qw!W@LWA0)`jL(@+zu&litYTSLIU0{K+n=BSsNdWy2sUfi*)_dY+-V^TC*%^pf z--PFXCskP%E)c%%Vll$=b{W+ygpwcu`Mjz`gg4%JLkPD-zWU!EdZP5k$De%e+Mi_4 z+J4S1$5(&!V&+R7ANl4BO~3etC3S}_xcctrQm=o&a(G^F-1{X#OPGDZ{<){lFaP1& z14je@bAuk7U+Db!BNH#%eszIhdAQSlwDnz|&VCD+>}3aD{K_HWiB8MSznhEn?pt^N z@@3?w6XIvzck$DAK03GY7U8qcJ@A{KTQ*KOD~F%Fv*QtAbrB~veNbM@3j7A=i9#jP4$JQiv{^7h3EdV z{cWF2UGDw(=XbpCvop4DPdKt8E7l2Te<7e(<3o=LmwxNnzidC}6JNjFXB73}2Sbl< z7IPQmF8cL+?#k(&%twURpZ~^_A05uWu4eoX;d=Sp{yYBoqq#Femz}uJ z@~wv~hr_Pl|N8@H-+R_Q7i^lG6QmbkeSO>8h9Ca<;3nbj8OQEVW&YF|zgiGn3E`(X zQzf4Mk+|!7nXR{e`%{0od)IBZItuMGx4-jfXz1hL&zO4bqes10e)v-gwT%j{qM&TDPqz_UVZ-P^u%tnz)bt3)1% z{A_(EIsJ>uc}A(<^j~++ixcF5$G`O6!l9Xo7r%1exBmFGcmH}~#l>#rr~PL>1nWhz z0Q5}`hl{H6TmTdFc{_o5Y8A5kAN0QQ=N2t9?>pD~x3%^C`mUADisSj_ckTOfd)RX2 z1;Qh@_j|1$@64}kUg`Ku$6JT*^ygkDd#(`L&)MeudUIacz2a{d{JJuH?!W!me$xdc z`P)zZbJf_wUBB3^ERQaHbzkwfuP!6e8-+{1d^~bg_|v0q!QcJJ(Ti`*f9m1OKDqO) zo0osDlep)FJ%4-8iK~A0^gW+?^RcVWR9EerzUblU3#-4%o_qFP5y8~Q?^>pQ=ZUj6Z+~%N-E(i7w>&ny zb<^r!wH<%?lLJ3_ZQHNsHdKCe*ZlfNzAyYlvay~)1t*Jeo5O*xuYTgHAGGflydBt6 z=Q~d$s2xxeJgOjkz0(r7;q#aL^P-I(`{!*xwqS8{ZvfOB554up%`ccjp+~>{#C<<} zS!nYTowN7-S#XR$Ft+^a zX~F&lZH%!&-+1i)*Z*`+)1M!B`H7WR=ch~3(0%RqS%$xUb!yumKmNd-AN)Yy%Lg*~ zF5#m4PU|Rrq^=khTAsh<(@*X1`01|?&)LVG7M6XcQk{JAl{w2DFT>QWP94~{ z@bHSe=Ij?H1aa>zpIp6W>zw`Yb-!>nFMDeDyT0&~3oK6^6FetgT=DGQGv_S-ahc#g zw)1~~@WF3C8(s5)<$YHQ){dQjf7j=}IY-v~mted8#iorg&m3BB+W9;?-}!Z_fCIe; zThj}3*n63jrS;ywB`2)3acklE7CN8{zwq#f|M{2p-ir^~Xct}pKL_rFpR;K%Kxcl~ z<9ji&LLhIy_ZQK>$`is(cMF!~udlm$Z2tX!nVl0heCgND(f7^^rThN2?~6bCpm1>f z0x~i4kK67}{qnr;FB6=<{p`AyLlZ)F3nV!9XTdo;EUdf#>b74*|14*-g7E1VzGmC{ zOj!8(53hP^_2n-Mk&b!cnO(Ph{leS)Ye)Wy>&VI4hx%Pw`*KtPDl7Qs1>1Ik1aA1` zPv8Gq@`sNutDOe7&%3|ZyY292nl_Yw{KwB_Mpu2HYvKCWuPEGinKo_TpiqLpcUk_U z|9R2>-SmZjeawFQ{Qo^1IQD`6`ST@zU)z&82sLnNJuK-p=pd-Vk`)5?VGXQUgO9dM zu31-811x~WOv}!gIi|Da*6;pX?3TM%JS4a_36|eI_rd3$9=4R`g@=Mq-E+fvzf23R zf?eFy;2$nJ?O;XN zzT?F2fB267yXC=aUj6L@yZ`YO;h7H#k@a8u{roe}@B92e-u;;`7&F@|xWBjl+Q)to z{-n)!>$yIm2`1BDKi2d5mUtz5?ax1U<+gF>wLQba#{I8IcRwUtv~uUmLg>YZ^9xg# zCSD*5Gb>Mj;bWiqTIhz{N@2N6aP0YI?DgmNJ^6z0SV`Ev@_}0(_=T{^B}iXf@m#j& zH<=5BNAD1VpU6TtdbDnI>)m(-{Cc4q#p8mlL$L53`}FU2+kfzjL}0FVI17QU&tCl2 zKTmDGU-;=d7ln>33CkEcLHTXW|A?uOUuRijRr&k5H{Q7@aotd~L^Lt^_ zi}zn9NWa_k{M3cxuU`4$e;#@!_G;XB@AZ#7_v?T8-31GhPn%m_txCSX)3sx1NKW)5 z3rCXbKyj)l*ct-FbUy`hG$u;$%Eb|-3@^di5MmF#(9vDUP1D=1f=g(8!(>v)_Z9O< z-gr(0L9amyzIred1m2evo^S4Yht;?izLQ=HZxhHJ)<0TtaQq(l5v{L_ep$?#I7>J)il1d_MC(cm6K=_Y!~q?EIfx(fst@ zpL~n^zpTMM&)(lW|7R=MZFGKtyx}EqiC)#|0cYLSbbg1t@kMa=0DaM`GL3G2duXow z^3(D<0DeIWVtffjEmq4Zgm~d$zK1npP^n~#X|2^*mfzF^Sfa$TDkv3cEHS`F!NFdh{y0rSVM8NIstThqYXq@7 zUgq0_uHGDNT(Ugt)q5UGu@5H;Fw^vf8N_azDkjxri4UZ>w`g=IkI8A5eN?F*yNg~F z$9$bwLG*c9YxY!gxDbN~w5M9B6bl?p*btk%ITH&EMe(k}J7jq;4Ed29Nq^daZ%4t=#)0?N#C|)njDfu+n$}C#&xyG92zet&#EGElozVv;pWUVXx zbh={u*xL4E*(0=UHvFcZG;ZMewBBZk`1GGriqld8)LL1PD%LN#D=vmrMH^yB3>c`~0Y9F~T)8@kS(WIm4# z#k*-O>_S*Ll`IsBmGGnz#=Bolrnx@NdR}!Vl#&9;%YgwHnq+xzQ4c8bCD2oZiK0}L z!2V*sI;|K@_QF7eub|LH=s}dG^}CPG0USlPnhh`w!UaLt$uOq^?5>y*;4&)e(H0_3 z3}-)|Fs}ZwQaV||aP1l%#|zUqD!viC?>Aab7F0A$^#Msz5QIO<=o&v&hBVC$=$hO1J=X>Qa;Rm${= zEL{*Cm#M{qbm2&RXta1(DY$w2D5_z-Jp%Q@>)9jOWLZh`>!5X0FP6mG&(fvYKMl}q zY1pa718V7l*`DC0CK}s`5T_~bRz-SCyT3rMB8qs!74PY4_aVcLjH#HnUP;Xsk1|A^ zr6JV4zEQH;MP}u8Sn4b~h$?h0AQcaruxWZx*~>lblCrZ{U2mU@q_6mfhF|np zcd@WXf!U$-sH!!ks(#JSu$3Yv*?YrDbKy5|WfcP)at?dn#a@F+^{|II8QtcU6;nr{ zjg2y{0~y8@>J7at%1j1E8-!sF7UfT-)37=ZYny=|`o!$_A<&*=>hKi3=ZewvQZ=;_ zqNsLSITtmrKr8Nr{HP;Q%`0lah+xx8Di@2)?*qWmVnXu<=zVolfR85Cz0Fwtc)7Qx zHcc34cF+R{I}PV9!?`=a26{wK3}cJ-Kwp@00Uf^xN zAAkvm@f5$}e(N#My*4A~n$@~`(fXy^STDPmFG8M}6~|XN^_)1l_aj?NQ9qkvDxh%6 ze#U_v)|`-?o0p11;!!KfGN(q=W;%qdm3k&3?sG7zkb_R$d#IbQOIQ}Y#VXQ$7=()? zhx9J>X@GROI&<8jP?J&d7;yG<0r^?jkaCnEi)L(5PgTWX)(B;_l>yZUhtR|V&J$yf zh8E+eHokl*Ax24Hr0}MeuSjg~09n(2vk&GZpQ8P0?ZbrQ@D?rRmGvscXnTNR(kWW=Ll|*+{ z5$MXpW97UH-v@Gqbn&PSmP0yF2Y0YO;9^q%<5hgf81*WtB4e$cP(xMe85!|22Ud%c z!X`hh-{vL7L|wIFA|xM>4b?)S+PJp%ttFh`M=0BDKk?Wh(eVHR*JIv%#s zGP*QWJ18i=h`iYmL|`W;uT{GYZB;$Yo2G|r#uO?1q>>K!x4g5hF>vHTucmo z>2fTOh6mB(W9`yLRsxcjiMRYnG@E2wA}?os!x5^H``Bf)F#sfB8$q#PMo zRs99}8ZCZbChCZMkL$|YE(fKi>|PCZKX@B-P^ zMY%RE3UJd!KYA314iznyQJ0+t0!gN=a88ENNcVm~Gmt4V3+oz+?7K`5s~STvEbc6E z3eL+}M{ZS+b8%c09ng6*9^fCY^`kvPW**invtmY$W&rC1bVia_$SWtS>0FWfTon1y z0Tel78RA5nsWK9Bia1beK&wV?F-4b+lNHvlapO3@{2JNEM9Qk$h*Hr=u2yvqxHZW; zjnF(noN}ya-n=HA`7q?$%{;3b!ds--OciklWjQplkQ(Vc5G5MJxi&?}W2!Um5-joOsb<)nI;r)1_T&12^=Pl$Re za_vha0|O`^PtyI^U@_#p=5$jj6Y~U9#`eHybP1}*Ty0EF)2;cnG)N&W3T)9S1~ZOr zNf&z;(+++kl=_=Ze*I9$C)dy*ZWF)cV-?dm^mH*%6W|8Spt-j+G!otDY`|G8j1_QE zgfwEtBr-8GqtTfh5;ud@ibB&&GY1pvI@p|9c@os`EOI(7z$_W!4#NV%jVo(NdVHL; z(S#SWdNh~G03VOhlAPjGt3_@k775YBw1@T6CmK}i)6t#`x$%N{xn+J`)!apo zAL#Lz$c11`n3vw1D?kcFSOW%82rOtU$0uWdW(by_l5RC@T++j+f!t&nn1G8nWvVSU1~|z>;(I?Gz>+#aGRhs4J>3YVF+6{d97QS z)+}e6Olq@yp)8&qu-d;W}!&hs>>T@hr5>FByx=x^0O`(>7bDk?hIOAqYwvJ9mZX|WrMx;)uxs7q;6A6 z51)U?DK9#81`ZTekF?ia4!_8oQINHz~v@; zla-`b1E-QK-8YOxpoml?ynR||f)a)7kO&+eEgX3i2V+$rG6u_4&FYDm7pXf=q!`H* z7paioVD=}eR zcRDR;hh0p-%npC6DzpIb1azuTbHtK4pt1v+S3}n>#PeER*r_9ewJu=#G%`0WJV(gOD>0+^xr2r`=_3_C?z%sc4H^T1Mz1)Vn%uEF{ z<(i<3yY(JZh{$Mo*vqwj!6hTtN>N+D7{Wd!4+G$l8Sg|FyI1C{6ZP2vKO9k-^o|<@ zA|qGI=Wv%d^^@8prxu;y2V@l%0TvVIB4s?wDREpc7qnR(VEw8udq7nw!ydXMLtO{Z zz&pB9MHH+Am=Sigf6NxMZYLTi91}WgQzNK03aL?^QJa}isW#9~%`ZFgq+xoxSU^d; z&XM>T>1c2$rbu6HoQEy}Z3cozn|OL)kc%)zi%iAl2%tMY+JK>bP+Q*3Y4lFjc->{g z?MLybDGm=@P!y3XR#j7QkP0D90l;7J#Sb2d_A!-fEwWUMK@_ zYOA4HbHvKUX~e{+fZ=0S>3eyUQ(G`jeZABy5SOaUJzex%h^`hFUo}vww`N)RYF9;r zj}3Ut>m*RWOdM>LIJHI6s(&XTtxaOwaHW(n&c?F!z`W}_z^mv5k8#@I%Be$u4t%v{S><&{NP-S{W&)awrL9g$j=z z=8*jJ0TO$SQDJnvpp0aT(2F`+@z4dvo99tYp3q{n?yti;ZWG#ZYFNh&>7pF(`0LQl zl1DR!u3D2^T6B&`H+CY7Ed9nJs52)FxDIkXNln&aG#$Jbv@KY5Chw2WhVngHo&JxEW0hYwzVeQDY zrX5Ep&+5WsWRt9eIXx^5=W!-Y3ek#}@mN%-7cY6z#StS_njYsek1t&uHD%(jQ-AQ) zsz=yxetPgHq2%fEaHvdQP^Dk0;Dd~7PCUY-Sj`4Ic_3HGGWwNEp>*{^7gctll?@s2 zn#jptP{JN5=H!d81^MhnLJXHtefJb*olD_h zQ2d2*G%XOx_@)%DzEs7;_Ns3FeXbZZ-@%U9Tvf`d;5hnfyU(quX+Tq!66Wj5*QYgX zPW{4n8#+L08&``N!f)*54RXAdo;ShQ^L^<-{hcv)Ee`CTTFh3p0(KvKzaXF*s#ON3BTaDo5ydVPu#? zJQtmkYm?bbst;fE<(n1t0i#8pM$_}qyxtmbD}MNj>VlSXd}X3eqMPf#=rapXI9y!! z2+32#3TEeki*0zGQEeiTW1nssnzs|+RVIs zKjooZL6b7qZNPM+*dNg&MXL%sY+t^p-2Mz8?lcTA?gqf9Yej^Rhc2t^OE8@0W*Y`{ z4%;r0vki}q>NL1HZ&O|_A6^2FNHVK=S!D!X*@?0d?EJ>6cs$Y})5Yk;$<#+HU&pj>5yrGV0N@`nz~qQ+Rz?D+@J}^ zwJp3(j@W)Gg$@ML6hIk4-1x+|=GKYF%=PQ=5mr;>1t-!{2)%ykLlBKpbZVNehO~ul?tWP(3D_BcOo=(B zlgn0>hpkXX>lr5xduBPh!;gW*O?k2{)>Eb>E9DsE*P?l94?0lKCQ}(WkzH=)mS=OSfxl~UD~-wJfnSk3=2jn&&DOY zt!A(AF#OJr4IDB>;1FhDZI`TnlZ3J|VPm z2gVGY2x44q(sjriGA~B7%TVlY2x-#$Uc}-s+3vQ+06jp$zehAnK(AinWQp;(BFC4S zhDZ;;s({=uJ5Fe-A&WVxGM{9+O`q>`u-sfm^5qS2Hl=!)@E@@=B6zC$tR794w4GaD zMi{dUhm23glBLqdU*iw--D!(4CyGd`xVu_4; z_ZjNCjkJ1c{`8Bl zpy)Y4KcD6?=jJhI=G5|XYFYVebMn??bW)8qWOCb2bp(w#$FJWq#+;S%hDKdh2ZL&k zrD{dwDqbF5E%tqK?$HmMsSno95?ZxffNGcVYj|#5E21B|9q>g3kN!t#jPY?t8-ltF zNJ`Zg3yQ~_0O#tkmc;aBNPq8aAvv8(X$3d#q&gr+x2i%bRy6z&H;wZ4o3BU)`s;Y~ z4Gk|ichw|vX;(e_d)J-<+@T)Ml}3wg23VIlClNnYI2`Q_n6jZ)wY?hDUi|psn)>Lj znrLTAlQ)<7WmK=GmPGX)Bv}KHWDSBYOAOsJ+us{V7G|lZFR3zI4*U!djrh?;?J&g2 zvK%eCs)YgW3@_1o{kEyMRL!f5fjE-Pn_HPf3f8f=h{d$khj+1XcU4sqh%G2F4Q@?< zDbs9WHpAAHV&ANGYWH!MFUm}dvBP{x&N`H&3i$ZY?#m7oO5jjJAk+zdJ+2DG~T}U z=CvBg@ej2FN~F>w`tf2dl?TsMHIL7vVv4G)F(bsIq z`V59l(zR2Ax=R~@Q-A+_y5Y6k`ha4J*vBrBDqcB19X0G&u>tCL)P z1~oF68<4jjrcm){B3Vd7_X8atI8tYlz==m&CFny*S3mF;YOEAgOjZicS-P^PzSF;1 z9!B|ZofQGoDQjRPb6()_OQ-sw*VN>peC0 zAXpfxa@43edu8(oIBDJJ_QB+RICL`12xYp2dO65DI2eB^^5Pt6s>bT#95l2=Oc$A! z2y*8rcy<}}mh@~LtN|h~5VFB|AG}N_ssY#n3{_;nYRgRl3}mQSi6NGBb44yZdJN`N zZSPDdNjz(ArD`E%O)n*@Bq>_8VfXv%#xZ0%vgA@fk&nBN>woZ;x{Q%rZqf>_9Xzi- z^|9n2+c}IOPq$2&he?-18A|x3>9I^#_ODkVkWI~RGOMtSRO#odo7hd%n&}u|dUHlV z&@4q^*TG;sV1fl??f$5Cyl+yQ>!DF;e;tU2kq|{nY^+~e{aq7qNxH5(Q(R)#BOHZd zC6}3XYH{tSlD)GnO}FpC3=^@7pvdK|sEg_{qC6-j)H<{=H)w`%p_DQP%t3xOWabjt zV%>B~Zj!;-(Avnlli4HbLWq;xSwaSH%hH&$ZZ=X`dM*!JgUIJ{6Q15EwFY8VFIgOR zZ{xwN(GX-Lle&j0u}T~_bXT|>06mRcq+2a=$VYG|v(sORGL$$*s~a+2TC8@@2i8=jzCuXSlJQK<;Oe&vpQPk051$Z~Pj+Su9RM|!3o2ng#MM=Ai1fB!sWT}L*{c?Y_;S~%sfS4)Koze+! zikBpCu={BkM3rwgHOi#HIeADs{V-xqvi|Cf1BqOzSenun+}egRN#oqz$E92kh5=SY)hsF#>3?9 z%q&g7>_N+rL302#CW)se?0D5=&~oikXTv-`@@BArt_fUE2|6|P$i{eGi`{VY!vHF| z2`$#IbFpIqwB{hnutZRT!d`ABVPFaA9r>EI5Uf#ypvOLpI_gEsKg@~LhgT~Th`IEv zZVisff7=`L__aVTs_0Ec#2z`N8#Ys!vOkMpE6K}LxHy29(PrG|Hl>vGi+LVqE{zqm zzHx2LAUEA zYYE6z4Gh$EY4+$Gsaar)RZV==gFagt2F@;{SWJ{zye@O10Ftg9?yMH@-jb4b?kzIC zLX$bXE-kFRQ4Q)X`Q_TwKDj1QNM0nz z3o=g()xRpVpEfM+(M2%9#K?$-h}l)0O^t;)i{LB;RigKhO39RBnVq&|N)of2fvRuR zHJYtXPZofzbMq5fq|%ynAcJ^m;hK*pyEcMl@w7Ei%v=Xu!c=bMGyEt_Z8E%ib^O#S zM+Nn_sYb!pr4dZMF+;zs)))=+%2e?q&ENve*Q@YrQe`g#=SBtb+)z&mEDT=ybX4ix z&t|?6(4_jMNtzqW+RIf;n9pO*cH`33Hyc|-nh^bOISD#FsR#mE$Cx(#uB@D4G`p^u zDVHnLCEZe@pL#30zlmj_(2I#YEGX9qUXXjm+bT1&I9GhM0NiV6znsQ4d=5HV9Dyk@ zqLh!IftgAceJBmV)A4AY-qO_U3+bgv=^}TBbY`?z=Desmh%+O=v9mMDco~q;m`YVb zQQrZ2s-iYAHS?Oy&I zb2VUzcYS00a@Hw_tZ}*~xkj)=xtGx5Ab%aQ^4DQAHwrYUrkYaTd`ig&2XtWolhrWR zsZ1^YXji$or7uNICFyl3yncbS{j3(n734H8twXQ!ChNcfr3zYwMRv=p+lWPG?_cvru>R{|g*xD#EMuKZnYvwn2 zsD~yxSvAZ6jD_T!=h7^st~WlNoZpp9~)>tMFI-ss|E7ceUG3{xYkt{?LlEL zJwbtPJqcA~26MA(OVoYZ$tW8g$`wVC4Ry9JoxlL{>35~5_asMGwSdcITyWG$h4{v` zZwawJjaar6h=*UoI#pMg!2-IfA`qFt!<7B~wf-PXr@>rW)wKfE&NnR};zFk7bsMtD zET*9-X;?dFhK)_+bAPRWnVzUF0!gcS+I!`ceqynxbR-PwvT!pQC2WAR31x%1nJUL@y2VL`>PhOOIETqs-@vTNG7*N6Co1@i7E3IcUX#Ye%8u{@w59NNl9*?C+2nG_G-bSie4Tca0Cc&wpT zng@!oC62^vB0HAOq!GBJ^zy^vvCq!v0O>ssgW7l#eLN3mh|YYr=LB)fu*dwP==zTN=|%h6MPPWIe!>%;RF8MJUu*ES6|?(&1b_PhEbLBAN@L z22Tox@$eA0fno}Bn?7%_c1c!{#oK`tQ4hP>$2k%>7#{wG4(j2TqtW>ODO3YYA1I-w z-RLctM0BM8l4x)eHEuM!(=ljT8x7&|cT;!>KgEz>7NGg}c<=&-2Ez45HLpZ-!F0Zg zCZwfSpaY3`d{iD8y?7uh4=3WoBTYF~|C)tHWN{NGT4tW%fw8^)LvnO~bZAsO(A_`U zKeSicGZu?Q6W|hu2f8nYw_~__Y$V!ij}G-ld)-Q*s%fo=J@KKTXwRs9PZY9o4n{}2 zd%H)w?IWYziBUUEh21eW7`34;wIkL))IZYa>=}y=$NPsyZ6nd>MUIgJ{i8j7UiOk> z-Ted6UO)Hs#0Q55qN7m{tCJiZ93H*c%{{TPfdS2p*}Dh2N1z9yP^n(W*f7*2YL6xo z@q~XUIyy3%h;|RMmIipW`{TUnZfq#z(LFjg5>21Emb-g5?IH5nQk z+!IZBG;jYG#zbN|)pu zKeuwjpp5c<5Ag{x5by5gLCrjd-7X5?9*R{{h!1D?DzZ8T7ipx5rtYM z!OOL%hFu&^R`C8u*wRUDcUW7ZGihwLe8ihO);(ZTDA+7ftkjZQ!~F{x}h=g^3)`CurPYNmmNDL_U`FsGtvzNL9RrJC6ygP(fjDA#Kf-a zajjTpz|W!5>obli`m*6rYqkW~#SXDhtl(bhF`|g93CtuXzK4nhB}6sv$20|h zyejN7^!?H6mg#pWRUB-ucAbRMWs=Mf1O?@7-NiqL){bc^i1A?RolJ#$OlRxu{M&8Z4p?2ix34eh<;d^3XJ~ z`S$?0T^VNV%fWWA0LL_dWaL0Ckg;QiGK5+-hX z>78+PF5|+1#I3dM=yY<|+1?C2?PnCMYONeCiYjwUl_C$d;p}x8x*y@$Ukmf|Tc|NiyqFbP+0DoVvueWXNKDlQkk!dcB&uR444xxP3-#xLt4C zTkIa`9u9YQ?Cv-_yg9WcJXll}HN3Z4P}Ed55+3Xy4fAWT=?*s#9!SL`x(B2Di*V&p z`t?dpS;Lbzge9tj@`}C(P;9B!En^1HFbi2VCP6oz$`fy{|^C7E<+jRdZqHv>0wmjk*J4LnC9u!|?=8 zR$L9XQCK-ZG7b(RwV7hPd@C%{9>732jVb*wM(MIvekl_Ad|Qs&QpTCnJvy4`-vd}m zN>%_@OaeV2MhBuaM}-}yyEs*FaB#rKg$*OQNt9=WOhyKI5WT~~oO@=OfdqQ;)nmq< z+~N#%<0&YW)mmr5idm98aTsGz%RQ8p?)LaN;(sB9_Imp^Jv%2Zrp}E8VVv zXlzv8lYn*Sh85e#-5x#oqUfmChyWj?8;B3tpx2`ISogq4)HVdD+5z2&WILlM@uLCp zOoowJTociYJdJ(@*Fg`14zkz9LAo z({f?s(>7sBk%XO*Fg30mwx^ZJD(uY2DdzKQ7XjwfaK-pMkDq<+Cp^Wu7Vnh#n}FV$ zF9hP84AV~q*;1++UdKP$!FY9;a@)H{c5UC;*?H!6YOpw0*bqJnRR||xfu|~A{0I?C z9tjteqhYE*9d54qvT_$&YwfjxjXyTLwiakn&#pX4CacWfYOXhSAEG=e_6n_SxR?o} zVJ8}5G$LFnhVh2PKA45MOb)N!Bvlo;4jUlz|Bt;lkB_QI`^Bl#-KjpkB%Os$ryKAf zLJ|!LpfjVHaY03g83hN_aRz+3AqkKX(lH4FbKmz~1O(YZ_FWcP1l$m1kySQXL{Jn# zP!NzsP!JIiI&R+d4NU8?D6vZ`%w|6N{vxQ zA2G8)by^3TDV5r+x~sAv{k#u8D7$1aqU24aj)q2RlEkpb0VR1xr8vb-m+zWtcl=`K zw;YLh#KsJ+6c+kr>k%sz0cn_Dj zr<34h6jxkykoACRwJvxwO2yX023A0^CNBAhq1MYYEl+#1vjMruBdU5MTP-57i=KwM z_eK_1NzBedt-Z8C%a1;-kyUc!QtJ|lEIPtTK2o<46@+?Ic6r<|HHDaNRDor1TUBEd z+nAVB$4_>XDr>d5#tK4j6Sp&(hlcE&#Khbe+3g!%4Y?>-r6Y~<5@N*q{iewSsi;i{ zub4#=%G*1%?%1Y&4Q&YY#f3(*fA5NXcQpnR4Fz8m?X_E+>X`gBMfMAg2E|SlBL)fq z4c_z)TMgPg+iP?yEiDQ6l&Yp2uINk+rM8V-(3*{qEDEvyf)c4T8dmo} zn^qYst1?XX2Je2Q)s02*k)$aCt4bz5SVd_nLSSg}FGIPBo8PX4M&p^%ev+{k7nfF4 z6Gsq<(8XPb{kw|l@9tAnSV^wsTWzb*NY684 zDjnj)gz4QvSEbjSM!Pvd_h(KnwPwkDl}CvYH^WjAMM{CEL83}^HdQ5bkmymHtCH1s}URpT#C+5oXR(fx}!JFtW#lY zPOgq2i0b;w%KstOUZfZMpX%s2sq3)RA-PF}4G=N6hB!J`wMGUmP~AhasV3q!5z17B zbfuVfz;46{+{7O|qVcAc*y>T$jg)~0+do0$elVEWIM z8eOS;)B#^=2c~K~!O5s82?kXzBvJ%2tjML6rTI-BsH+zdhaY8sT+}Z=BtAv8I_mT5 zI~}aeOnRP(hmIjx{b6sUa4WEBD`GPHeG_|joQMRK~uRxlqf5I zhI%p*GkxaBqjj~PRzcLoEx`mGh#v6^ure>)M2PxzY7UGQB}!ptb)t^Z7INePN<&CS zLLf8T-FlZ7M}?$kh~0^XI6!2`(?TVNS8S?@S5}8e+BIw?F^0?NYv{s}8nxG)AoQ7+ zY_t{{r|7LhPPW8ovhl9HyVCs@KDmk^ZkUi$HrA$4xnogY6)*BDM581~)s%R8onvz^7?#J;&9WIJbzGQq$085*Pz=YjGSu3 z?HP|($;v30iD66X>W&TY_Y|ix&zxK%MK0&YQSJNDRHCD(g>`;Bs?p5rYO_AE(y6ru5gCp)-OW_5 z<(0xl+XvnBfel5jh&9$y4vL>C=+qBtusq$810sp>>?odYMKseUmEDOprdOJmn`v=U zwI&reB*~$9TJ32BuC}J?7Wd?8hh~zT&+~|VrMEmF%Tcylqo|~Lk9e^mCO!>w@vw>u zd!gaVGqM{cs9r@@DK-uID#b0!shtuOaz1Tjr>b$IE#Wn>T}Yl%t_hmAjC7N6J9t}$ z#jT|9O!A-MYFE@QU0i=8q=X73;}~Id=v9V!K@trch-Vr%OBdu@E!8HDY$=J5dbPD# zS)A33muH9+<&?3NUL#>ga`dd7R(%mi9;3D+DF?(7hT73?HFIfoU_HTIqyo?yK^qmy z{{C0Iz2;(+ZWW&2QG5mFVF`oudQj_jmYSnumy>};W2es1LishSC3Y<}${|hN>I^|b zshoN%DJf7%*e}1s88lBOrp8!eVn)R%aYR9LHTNYwige4WtogFUx~9caz<7dw#3@y9B*6+AeM+&L1fGv?O1@zhAXF^U;T9S=9tIeXBIK zu_h0m?G4sNqaV~v;_(S{4U*AIfwEc(sn(`y|u8WPa#S0Rym5gCWD5k zs>)9?D(IWiC2u0eU`1$9Xw>*A5>i0t?@S?ygty|-o9c?<7`7Q)M9?DvkU=27`ji%O8 zMcT+z2Py{WwWd0~k9BDOFs;rvwq^utOos{PN0U?yepPuWn9*JT!ZXjXFS)gPPiyis zIZuuuWn(pR;^l4QO2`tEUu9CdLOF^ux8nJ%fCg=IYEx9)Lr$NSiKZ@g7&iQuDFlig z3Q6_WPd?gGoFNLBat9r9O;bXs2r5UvP3=-NI{x(`vn2CPd9|7@KQX*!LB|rNh-ErG zI(3<*C<&9ta z5C6v9ZHe#CRcGOiyA40>@oXzE$9DauMN@an7A>0k#I8qqrKFrf5$3E;b!tk5kmjw9 zSIB)ul~!oKoYavg_DG+o+<{c2t|!!5wFoMOpi1#Kv11ynh0PJBdB8``RG>V~nKIeT z<(Nx)MlGgb>IoAwXAu63C~bn31t_(P*+G(NWkXyxDN^~3CF59PP3<^E)goD%_LS)- zNgkD91(6(0_~k49RVx*7^J!H@yu3s;SrPmu5`Ob*EA|q+nT}hT?#8W_q+^&@5TWWm z(cuxp3Q{IN$}lzHb!hAERdIWvoEkBuAgWIE`mnfxQLaMJd_l3Pv`*^AF~mG6v2nEw zA!Qn`kk;}0Kk|5GKlLGUj=>N$h{k$FG8ff7P;e#jQkiQpEhN;xm~V1Si$7W`F*S|% z|CE|XY<%<+Yd4uXo#pqMGiA9}pYPCYmuIv!sNSJ&n3bc@xW(?s)Ut)@bOirX5>kO9 zxf;sx>P)m*LZYhYs-3K>TReV~LwvGDJL~%K5)b+12&G!iEBv zH3;#V;|j0TafUvRLLz{;z6sWH(GmH~<^iLS3dJ_~u&pC4&yb3Wsgio>3P1WSy@uMY z6Ljh_GyF~EtQ>`HT<47)`A^zAT$PG7v-dNZZ8&iaz?|Lpnm6kvD?Q!j#kxIF!aW;0 zG?rQ7td;NB<23y!o0*v|C5K-rJFTOV`HQVphe)>7BFk;=jT&p|$e{fB44I>6imTGB ziehHcNFm12r>L?-O>KKidvz_Pk<bMVBXQF4bA@vsMM@%c;Y}P>$G1zVRpcbu6wd ztFR_Igg^%=vqW7EoA06Xge6J6g}QcLRz+8Mh+RAKY(uhrt)o!0K>JdP)USM-J>7!h zw?6~+T9Y8WkGfreerl!o$V);vF?=5xl;$-x8dT@bQDBt1s30LV>t>w|^oq@?ZAq1S zRh6}h6X_mT%)=ajDUU3l8|pJNoXLiWwe(m)i_ctK-RBw z!SYrqa`ov^O4ana_%n`XW6oh2@&mKvg&|e~Mb;Eqp_=uP)AePFlM0qdH6TwWPa)KH zIeQu+L(WPk4*LDHd9;h%J=%n}JqUqciRO-#PE*khSBof*s8u$#G9wpONal5{-I%A_ z6W4UCsq_jJ(6GBLxr+H)7L^r8EpD_nY4qES|s#c6aCg48j0eC05Z}s=bZml2Un}qEs!>LW(E}tx}wi2bmP&8&k}$|N^KIB(F~FTq(__ z8M%GRxt2#84n$CuM_)0`ZHVn$LAm8e{MvqE-jG_DB`#4N=J6?v_+H6ga4|MgK`u03xx3c!cW9|oOJEhnp>85>? zw8gy(xq0O)pQ!Lj1Eub#*QtIvKxX1_WJ$3rSNn3bW%qCMf77&qseBJ(IyokEOhEh0 z3ZG}>nMNvXvti49YN)^QN^@8s(h!?6#2&5Yb*SYNDX1b73x#t3BqFykFP^tT$^i7s zlqd2@{F3;DkW_(LW$keY$);k5OCd(+uyZwS+^S|wQVS;rHDafxIl0{Y9(#LQ5@iFj zrdbK2(r#D-UONe0c9if;Cr6dO`N3&5GzubEeY07JVU@KWZ|WCBh%!1dTw}L@EF878 zqutdsOO%Mbcpjs*qT}NC-#T$_$5oDI+`8 z{B5nDm8&^gSFfVJ8P2_7mZlH$Moz_HtH5EcT`EYRA5GebPD2f>LU~5wev2eo%;9cw zeNs&9Q(sX<%B_qvYwlO(2??dx8!E+zWXfqolLxk^`B%zxrn#Oc-DIRv|UePnZM|H2BNv*rm@8``% zd|Ho{Dk>mQ_cQv4id5aN;lu5oFdlBxu1$y5j~I_VasMN2A25J6?K-x>i`=B+FeFP~ zO!0`@3H@@Xf-r|3ZPV_FE^Qxg^JtfjZ60a!z~ezVRboo$b8OQndr3f+927eSjk2a_NLx9M9kko^b!{y9~-_ZD_8=v_dj!T zI(6>T{ApNoZ_DXa)cl`$t(*V-Y2kKjZfD$O^z<#fCKq!Q3l9P|<_uk^JrcKdQelAqRP{N5x6jGM8K3&r%V6NWND5@c8 zyNmb9H&4Ci_oyuGmS3$ljoefcFYZl;Hh#swzU7YsvRv7{tcgcl+9+sgk}#K&rCUX+ zYB}PBW{-GPbpfZzN4V(4J-b$&QWrS%uDI=2 zj&oZ3n;;KESQ24_>Vs+jP)>Gcc3$>ll;&TBD{Nx&>~3$KY5%&U&aF}AL}yI*Nl z9yn*5M%tBCWX;Y^NbF|U&3QL#H|NHFt$jaIUD7aNyST~9%{iE0fLPNuXuTZ z7@Cn^BX!j&eW2p2sL+;dn4p)3j&%qPjI^i9xf;~9+6Y)mB2h1VlAY5y`HwZ1Y-mU? zgl&6{jP_jH^S3If*cTRucwZ?yhqNm5$|P}B6!{vh)%=+`K2?)8)@*Z(ZV#zYQ^D{p zp}A)|`vDopXDXe{$xZ8I-kkijA+IK)FO}cmYSpT#IEf~=`gx^NN1AODLklD7oL~~! zFRE6B6tspp6gDaqIoh3*+RcTSP2>W%+EbS>k!Ag$DR~Zo9rk8*#IXaIJLpIzNR;|- ziFYlPWBG(|&Fo-w3!v#vOIG(9fjleVz4X&3qI*>zSHXMWw+1^=3A$$&X#rF|-18eGx- ztX#QEjD};SeNkobZaJA!B`T4eg`@k5jAr)fwHiTb3$Gc_5L1}qK7hWxdU6%eub%B7 z4Y-ObySp(CFAX$z{H5m!3CTHwW%#6x@*B!C5)~YJ%@-yWV<6qS+Fu=ig68XJzC20# z>C|Gxg1ZZ>&V^seODj%WlBfE}ANjRuLkhZ04DP~uDS z3^%pZn`*q;Tz(RwCH%uRW6ARe4eO3ZD?srIqgf4KnVQITsoXvc&RA&p>{%=J55KMY z)2LH>ho$DzC+3ssN`NY#*xfHSXWnF;&=Fe{yU~_^*t zxAlWhwr=-8n+JnMPG>D%#bqB>6?~+3aal=`@o**SK1o-$jzwV3Pn4I@JZ|#+Wz~w| zK3Q5>#6e1m%8NX$>8>14m#QM_S?T7SZhuKgE6=DSu{Ys5`qas6IlOUOZ(Op^Orj#m@MoG$Vpajo4r*orHmZClXm6eiz7b)@492_{e z&B?`aS~4*T4HnleD|?PMwX|a0S(i{J^S+&@PAe?*RM7~7 zJsmV`Et-Wlvzr4>yH{P&U8$9})Jj`14^Q>YIaEDO9T# z?9a1zC8gKcl|<}5tCO|s?rCcKR$y@`NT#n_@qXpze~0?QEOa+CAy+B6q+@kORL38WS9sCSiRJHPyMfahcv5wo?&;&zadDepW@7qQB5wS!RjIV%Z@rSZ*3Bw0 z#~luK99ar3LqbEccIbH3{+$4m#U^UCrZDl!8&s7vsi5%>30pkO^EqY&^wrkk8qs`dJ=rMhW|WKj-|IV1 z{ip-O_hLY2Dsc2!svM2v#Pzy{a>QF?SYZw8CQKtcnw@8;8y&=?jbjH(TB1Tp^b^^> zY-=-JfHGpPhO`yULcU1O+1oqnoVx%amr=8!66q&a^U6pTt+o$2R^`=vsrn(syqPLm z(TZuWQLY+on0YEu7l_GP*72_o$qzwB{f_qMv(5Dr1Tk|bp>r#B@A95yy&#N~MC#O1 zNn;61_oQ)$Ih0q^Tz3EIIKN!8d>YwptBydl1%o)0xNOic*S%@OVLwMLl3QDDwo@wV zCOAK_Lr1VG;!0}GJYW_pRn!KFdMTwuB!n>IZlgh-A(Uqbg;~~0gxb;JqvZwv$@>kia+U5FqB*&|^!1xJmy@_+2GFG@PZU9tnEx%kNo?&S zo|apvSX+YVCud33K`GicRwPF8;x`n%NyJOegUw3q?z`3-EG3BsDdVV6B6fO}W2cC6^9vnd zVm-dZg+VP;)>FZjs;05Yyq3 z9|<8$>{5|(LG$MKIF_;CVa-}wg_uvyIT`JonkVHQ>)^c!8aBmI0U4XBM5~-!pA=My zONlKzt?sF{a~64Yd>So3BxcHtV9@P&0p}WkZkUrHze!MwNk4@pZCUf#Y5`akJaO*3 zio|TOIP!1*P7WOf`N}CzotG8r11Ql3-R+%;W?i->Xp?10SE>d4jJm@39mk_Hoa#1d zuv!8U;Njv#MHl&XC{|QMUSgkejni7_5lvati$B%zRsWAkJ>twK{c@JNSgL$9EEnzB z`r*`|UsO3+M`=>MTklGmt_oY#DPK;K$eL_Ldt`(&cUx#r%X=ZG`Y!$sVYS+^y|v*XD6`TjNbuy>~Wp zab8!ft<<<*fj<3Ksg+d-IX&Z5iTtxj+(Bbz&L=N^qC%>#RPBncjc$iC{#jZXZ;8hI zYddtU4Z8Vp8Z?Ua`9dR84pf|TSE5CyQ@Gh|NrWrxg8Ld^|ihat*f1Zz(JnYR;I;nakJ z9nnfjRa0?ngWB57H8z5&d6KxC9v44y%g5<(jQNWsNV~QKlyHBAtJ@u2fePR`SGS(e zxo+>?3aD@u6&1UB-BAQQ=js&)D(-MSSMgg_3Y**y)5oxNX<3KSTEKit&^!Cvv-x6t@6_}GV$I*qf^ux=PMPiqGihsRJP*Q zLW3)BORFMXSLc^UwT1biKBctLE8E%xDmI4oE3MAYH`JYginEwZQZ-DpE=LT#msuJf zgf1>kcdCq4KUd_>W$JdJo+T>VvO~%&k6WK%&cTSYnte*0@5qyZ?^ipHp96zg0W9?$ZNZdd~5TLy~)wO#l#xMnTv=4 z!&Y=_G;fxs%d3E#Af$gu|}zkaW>5_+lGJC>JKVdQ#~onD>G-S^o3f*+f` zG*u9BdP4Qe_TEb|zBA+?A-8^=#h#n6-KjiYm3Z%oBiE3B%_|k1WF74ySe%f-a9DTNP@`GaHMr>RF8*geIIFJWE^Mr?aK9w$aDDT-XKPqoj zwr*z-F~l$;-*|{Nk6FTynxvMV##4_zlJBals^Y~`6{wqTFV(9}MW?s+F1NQl%gR9$ zrZvZ86e(!VpNZcu)3jMhN%v&2CswWpVx*Qtmx*-j7?QgQ=$Die$VqZ$-rH3wS*>!@ zRWBa8E33k1-7Z=csXKQ@_U(13h?V2}gfoR@6^=P9zhmYtYL2NbZ}Dr=Po#^@E8;wY zeFQs2&h%Q7KJu`ax**0%BSrKVRHbA@z1-A(n}n*@t*o!N93t(XQ8+Cr5H%sOG$k&rRC9NBql47UGz;KNc1l<>iU87VsqEEU%i6%rn$26uBDoR7OS*WVCsIs_6e!rp~@wm8Kv={1lL4&$??scmm&*U0w!-l6P=aad&}AL=J(hPQ%+4=BeWb#&uxyFqLqVjds5X(?!JV1MOk;~ z>%?%K43CQ~FY3GVjG8LkjWp9ma(4PiE@z2Lg7zliCn|e-DoADSo&S~!U*ldouTOMn zORUC)(YJ1u(_R1VWN5z{rITt&0jA5{|5jSWe@Nm?{3WUi|NYpB)6g<@VGRdE-fyht zal46St!g8Orfm9&pM*pdVasjuFQU>Fv~)jmmO<)Xy7m-(u~FoBeW<3^9|$`G^9@7dXx^WWxa?%EzP@k**%a?(}qkEWRs z2+-ZE<5p@$R`v|YPOg{*Nu2!5Gvu)j$7x{*6ZP|qnwp2<+NjpWcy<;@^tL_HI<$GT z{gZ7Tl$XPc+h!BAn5e7Y+dk6v@xL{;b?zEYoxbKZV}(Yb$l)BegF8d()@|~1V#tvs zT`OzZs#4`(LGW9tE46WJp)*+hPHIv^?&eaF0v$*jO$VuZrNOTRm)cva;@zt0^bide zt&3QLqKkbzj*&^%qMjb1DLUN29COLrR^pXn=;tz;8|?R4_AFja8Qtka8^!GLX;X7A zB1vH-*4nJOfD;#8ZmHiozEf!E#Y$~;Rm`fw=2205wL4;xwF=}tJb-j*125kT&Z=gX8)yi?#((g zFAFdq3o<>{rf-E}?N8IhSDv^LM)vUzzyGS~7qy9EXws!bsK7lbog)HP!*daNW zl~?qx_B-6|+pOg{+rHW_(#ffg_-VVAc*GsHS>^G>1Nzn}|5&FQYKa|jz0~v4Ttv`u zTI@DjZ$S-~M>rZ$y)Tk?E*aKKtTBIx-VPt>jIJiq$*Hf{@h(dbFhUnPv6I=tlH~qn^T@p%_*=L6G_bemif+K(VoP_m*SL5`%Z|$?>*K94@_#ZiC-PEYixwBiYAFHK$S##8eYz&*lo*udHfF_{J z=!~woLE7pe?YGg9e>IcgT;R>g&Bb6`U2dTEgzHVsn-AS$i@m%ASJa%_#rIy zt;p1W6v|nb@_6s=J@6&n;%f9?Y%K|xyJqvjHRXM}|EccNC5}(aWB)17{M0`4oPSH9 z^KE(RP;ZgC=tOF@rLExRI*S}IrnFAH$);3=kA zRu_tcwg`&rmeOhuQas&y_O9v?u{VsUAj;ow-#TIV`_)0YVpG+s=xo)6w(*LJgsCyHxLf1xIk`6_jw|e5QHf!Z z!on_eqnnvob1m6u

=wAEO2*ch<<5m^G}KC9#OF##H96%U0G%u}F*(&GmjYbgCVc|91T6ndC1T4k0c}Az(e_cJ|uPT_0nxd(k?l2F$<3}uMZ0{a4BU^&3EO?(S z*oG74S*&2GHAHE#+sR(q%C>}b>$f(mwsS+C*r8Wr*lKNTRRPuBP9^D!>{-fj4X@Jm6Fy(6ZwO8J{FllQ_UyfCH4TUfP2M#-ir#%za-!n!qZ_=*Y11n+WZ zWSU2I`UXVn5USfsOKvvW^p)4lqtf&!?F$~$o%(AlI4uoh(DZ;>J!M^4CF>n~f@1eV z|5q*b7RWVOQhY)esG6@XlBqRmEuUuBTGmVS4y035sU0v{ur>u9i_7E{@5S_6%B@tr zXEdB)*ET#e7`+p{jowA{GJ1^?B}69%5rPoC6D>sVWkeE07cE2^qKp!~*HJ?B(L0~q z&;7jb`>l1XS=Zm|oMX3RpV!{we~v`JZ%=vN_J3aZ`8}~Lg|^RrlUI45hcE)vw6W^P zSs0`)#+Lnyzp49jkNWpqp<$wSjqO>*tg1kBY`s14ThC!za(`fb&qwgoiRbeoJhQ*K z!gZ=1!o(e5nAF-OVklJEwN!ur#)E?>tVKdtoE2Uw>p^qoq!e>W_pNhp-UEXvMZ z#{}Sw7xqu(jWQ?VMFqR?HEql+2>%S!j-76l?3A9!90Tg)hKB<6i3>=@rXQd4tI%#= zb-xg(Geky!cXVm1;M5MHjAfXe()rv4iF}$V@FwML zMP5Bge{IF+Cmsi-T>~D$J_F9f=VCSGOjU-1zl`u3bXnX9%&e+p3Qdz8COo^$2lLi* zXubsgNGK(L_S$~2-=kFW#dFVO4yk?~@`m7n*vh|7FKfQP@UGx)c=|&(U8F&$wNLup zG1qy5bUUwc<+>ipUw>uGecMMN4k8C8{O)--kWmi;19o3OK)a9Ku}+1B7-1mi%GUF2l#NL5V7-@Wcf1^$y5qSt>F~qNRPzt z%J#=dbH6rS*pnm%rtyk>~b zM7P0W2^qD7RD<25BK2I?GpDaVPW$)MNv;v*pQ^SU`vspxR{67vwP+9l)e6;}-gH%D?R3@lnfg_dl3rN)^r0TlG^#bCWvgr=tjVV7= znYx7R*qpeW`u@0vG$T)0tzg8m#AKHrWD>3*y<5IhRFVg=H>g;1wyOLXnS*m8w3;Pa z4Sa3mAA=Nu)XI0(-X=BOA!UNsMT6HpgJBVJH6Lyo!hPgU)y~F`%jJ%~$hl`N1hX6k zgp9EW#~=wHRV2Q7$QMWgMN##c*zEQ{z)-ZP2Y0_EE zbqt7f^e&_(1BnZ9kY5i@ef{;8Np|gH+b7OOR5z=lPo?!}1fLWS1p~u}cwCj<$j^ci zpEZ_b*U#DU?#>&YJ=_MEL|hLU0bgJl%^RRgc4HtSXVK(i3x zFtG~w^923pUoxY%C-i9)44S&7XQRI~mddt#^y?G;VKP2Q>HTZ3)Ccf*zJrpM4vapx zOxl(b4px89<9=eW8s zxd=+-Q%j9EUOXzK{Rdh4Xnp>8{k$0ch!}l~7$TB(`wiZcbnZ_2yhmTFSQOLPG*2P_ z@Er0}d?unPt4AiT7Pj#cn4UC-{7nlMb26)|m=oK>c+=dVG+(ke2Avoq*zm^2gN}(g ziSL#C+3|mBtC3Sb!Z`=sOV?dNDEdA5`U^U~pnd+Ua`XG|xy@|OwFZpp%k~iwFZHL3 z<@FNffhB<|+G$y(XCJpzqK4LBw-`?^RgXKbu2WLmroD&VKNi0B zrpi^MzE~b!$f+MU{d7u`^hMtP!ey#$5^pYVa>#L@Z%feS?8PUP^!s~DX~EoNY2kng z`@OJ@19bSGQn^0Rek{&eS3-3J{N)ANxvpu4mEiP+^X)-&5Ek&>E6sV)1m!^3^w;a7 z^TBJDfMe+1&xg&Ve%DWs{58Q!*J{BTgU_msOMBqn9p5(r8C!q-r&2obz@7Ck6Y=}X zM`rK^T>I@S{DjK;0%u7w=q|U8Kl8G1@^;Th{ycNMjnu1K5qP22sCZp>FG=RL%=7W1 zyWZj5!KeU_<*lz+PdBR7WM{ZXKQ0u>M2E2K%<0vOI}{L8=%Vi8_=$`ttkJq>qoRvU z2uTswGOpmK3V6K#+n_NV)^cpX%RCZDm#BXGj*aTGw&u) zaVkD5WvNK1kNl*wL0SBiR{i!*0$Yn(H$GnpCA8|`I&8&K`zv9ZA(y_w9*xH?g$KWxcjA@F(k{s7=y)rE;!9z!GMH_XVXg6ewQd=$#=LWjbao z5B)C^Y7;E-8x7f|`evX>e_UsZDfUGEjCgzq+~}XtJ#(eGUP$ulERG9fjTU((t(g@0O5jX1aN2s08jHH}@$}mo z;&GkmgOdd1Xwo_Hb+0k@#S;SouQ}12JOwF1#~h?xSdVa9p)P$@9%WW2)wRz1tcOn< z1ybrY42x(sE#UZ{k6!I!9|juK#hz@;BIgnyKLnjJkT#WK*gA5!#RKcr@&o8^*AkN$&Z2sHHjY<`!kH z+`u@mud3*@pT`+})pEl0J?Hri=lLzw`HkSvz4-Zw!@{Y|+6pkvf#{{oC6=|2R3S^8 zS|sQf^?4k^gRS3-LWuV4ahyZzd$%>}Vd~*VR}h>$R3lDp69b<*f&Zn<2@gCrlwIAG zTZl3feC>qTLC{sV_cdPg_t3Qv@?kFlm+Y?0;to}LL!TpFqhzfKs~^3bR(0%$^{PrmO&1o9tdy5 zIl>nOLSwAm#roXgn1g~q0vN_2Wr$&fC`t;A?}qXBVa~0uH9&}j8nXpJ0FDF*f|N$} z(Z|%ktQm=(1aa764zh;oAzoP%2tD$_rN)lK<^VjL0|I7^_wU8EuxYhLHT2ifNx$;XOG8SEBRpH|-l;feoN}fFafy5I!HufY7m~LbFuUUEs`M z>|#N|P~aI$p6=ld!Hn(!kO;BIjK*?PUz@c8Spn5Tz96O%k5LkCiffXbp#})YE?Erw zN=H9eZP%(pvti&ea2aDW6dys1P(i7o$k60&G>ss0te~lrD3R8J9@%LT+1c)6bdzuU zD;Z*bwgWKycI8Y)2tfUJm0;Eu$*45NJpDG(`43^(GCsoE8V3y#n&@^_s<%c$*%XHo zJ;esQb69V0NCn_>WYk!3;2yw*0S&PhfCH2g(ubh1E{&ynI6>wGt*_$v zJ~WwxgTM{YG08rqHMAP=0*4P|>3R4FXC7o@#_U{H=;r^s+pM#KeZlnMXZ45jjr0=W znQTz(rt9)&?=`dZm8OO6gHG{<2zx)O>9&Nk4cNz5kCMHP4nV+!CD>WT2P#<&q* zy@bO5LqBWYYQhV^4AHLJ+XEe{`(G|7R%_zw>23q4>VmHI5_SMy0C*Q0icJK-PKpvF z*&uPt4B|xbqHx?O?yN9+t)WD@fHt5Gs5oRCL30rtyJ>==M>C*h-547IHvn_YAWVUR zjGMmh169cdkM8b+COmP&Z(;c>UWAQAKdeINBsd0^q}XuZpYVpQ3q;)ZNZSii{Ja$_^rWYgWtx_b$dMim$HqQ7`5mCn#_;qzdJkx zgou5xGKu*@t`%jdfK{Let48Rf9xm5jw1;ly__Sg1d3fx-kr|bsp=DLFg}sU)1HYPb2CSuKkp~>a zv?t98oA2pm2c6ta4VP)cJ zVH9CJ3CTk6q3o>1gs?O52Qe&x8~%+AfZ+%P1oa$XH_P~d&ZpPi)q=ouP!+HW!xi8P zas>{A{0RMXl80zU;05$PoakU#=y`}J!ql3fv$EF%15OuuZnK2x3~m6Jtu#C&*dcoe zq8aa1N$mD}0U0?=FAOF(D_5&DY4J8P`4ZxxY z#)BTn8?uJLMbV+y-0#U%mbpEAKwJ+nxXjIhJMWlMs5DrbGngUkTzo8V48y~ z0mh;Ep-u=-6cq|Q!lV?k845(mTR$Tc^Z^ccAV^Ru)?7ja)I@O)L!ListtNZ~BkUQK z%=`$F=WYM;+zE5U++*?m6-*qu1NH3*+S&wj$6)sW*GRoVLZq3PTHqppL8v3*oi$j9 zJQJ&hU=X|k@Cfxq@FD`NfnyfY=YpNb&u6GGo`w`6{u6ExIS4a^FzSu9B${ZoHP(mM z+7h=MWQ1`r>TQq_xoLV)rPO#TcoZtrYd;me2S=D*qkXg0j z%P{;OcQKUj_uww%`vdWA4ogCX!l|HGOBWPAD1&h8U?4RC0nic?aSl?FBqmt9Ik?zp zPk4yrLU?Oaqq)t!ZcCT!fc?Al{|}M7{IiaWSp;MW-)=cZKh;$-cOPE4qW;m-Jh;|^-(QTsH2dd2&RTxf zyW?Qgj%6@5#5k$F+ebjb@9ALx;qdff0EYaJC9Fk%s#KK>ahqWL7vTS<2UtdKcTC7& zs`ZKiO@QxS4c=DyKR=CVd!!qU^x zkd+`fn189$_t2MlTSLd|UPCk_}njV}@Iyd~qZmtm4`j&@b(V8NxMw%LRWh4pdO?@d$3FuKV2} z4a#f?e+aaRI)8BNo2>!>IRJVmLa0@s^Y$Td4$86Y+GSQD0w^zNHvZQ#mhs`(1OPB7 zJcKHw!*a<92QCQe@{jWKr4I}Ykbtv>Bq6viK7xCEE^E1B9Y$T?fw=am-L+iv4v&!n z^38(s(!2mBZomOQHTM0XrKJ!MaKQTj`DR})$Sx`r>mR%yFsuU+2Yi0l0Vca&fu?jK z-tY(!W)cko5LEd8u!)DrcmO;v#`|~J@_^+&oN(~e{eaiU} zGsA58;A%o(4eL)wHT_Fi76-0ZudiOF&YWw{EQ8WJ_8x3(@BnowP~AD8#M^q4kG|&& z8V~N=%Rc}*g^1G-SDYT*&jFkSMaw&x`L#c{vks8j%UhY&S2s0D`rD-!t26Z+vJGsxGOf=;7IPg#|mMhWwhy1OTNiyg8S z9mfgccA?0TFN%QxAt^Ch3Z36noq0^+4Zbx&W+US<@ zU*2M*%5?mZwH}Ex<|xX%=8c^I85JGIvwFUrsQeJS3g+65CPw z5U2i0%}D=ty1`-UH*c@FXBaM}rf9Zr*7g9A}5O^7G!mB)?c{S4E4C4y%|d0fx83^sR6^xNv?g4n38$yQNwPFBl{#p;9PDvI8QLfKuC(pHX9P0W9&Sq_%$f3kZ(KJv zjP!2S7zfIDeot`VKRv|S)rPo|nry7aQa4_`jI4Bf?2mo8gk^Mbq2GWPISFO-BYv^7 z=2}DO_CG0;80#TF^zPC8#u}VBAb@6G0amPgkJwxt;MN_Ojl#ik;9Ww+QIilk(jkLrdI2y*d~2%u!WeXn)2BDov$C`{ISn^y#Gh@}WY~QPAs8N_qA4Vk}=>_<6-6 zy`lSejE$(ZoS6h6a|niVo%Sv#Q*i$3o0qOBr^3GG3hQOiwupkY<<82aEvY1{_jK4M z@?)5@%$0A7{_fuewS6RH3(I$)dp`N*i z?~sYa&>$`hG6gVO7=bztO(KObf>f9p2n$hzg{Z*f$&m8H`Jr$*4B30HY>hdch=x2u zH9uCa)+A+BQX1`pP;_g!!qw7H&xNl~xs_{nm$zQL*Y~$UiHFJ~PJ1uOZ>ZtG6u`Sb z1QwD?#%(FuXW?3h9@B<$%i4P0wOg=kU~yJZTU4tS=Dz7?zM+_h5Q9P4l>&_-O@9m9;=A}miZNU=jJT1hD8&32~ zs-}XoCKRa(IUt`^eo(pjl^e)rRo5}(=r8LhqFM1tTM|j+u-izaMhzU|PWv6K&+?h&Q|0$3L2eFm z_sJ528N2jZZLCMKNyo|pB$q(ZKcB7cr*vy`qvJz%IRQ4@ToGSGDE7JjYgeDnm`Iap zUaXV8OxUHh^joZw+h6UC=A{4P%CW!Rwz8RY2Mh`$WDYyr#yyf#zlz z$^v6EuLA^=?YKy@DQHw9SKJtZacl!z)T$V^poJ%52wi7hgEp1N z?p<9TuNyiKI}GzC-N9=iMN!R|DW{W$PGg;k>6qdLSeR)kZS|a>i=F@)^Sh?k@p% z@z7;hd5t9`oXu z7DPA_^fo*fa>S5St2{*BB)^_aw}Kc2J6@zK%uNFmZI$C$`^aGzkvPg!BzU2!GV0Qr z9JCpsqK|UF0*a$^Rq5lBXJ~2y=um>MHlsu4TR#P=6&=Ln5~I-(A$Kyr+(MS5l$)sm zmq|!=h#YBC+_02dB(HL1XCwtRnKi}zCV;8b20+`!i25ZO_bt*%9Eq7Osf7{6P;^Qh zCd0JO`Y@WZKFuXjP=+~5Cg8o85RPy(5?>i?(8!_y)uhveQ75e^t72ZhdP9*kssNYa z_|_gm+D%PZb76U=Pg2u$!OT=#uuNV2n?;yAdWqv(f`cVg0pmJH-qpb^?~`^RiiD0M zX`GFawO51IbdZhkqg}FB91>VFgmctKJ2MS?!;7tekzH;9v8v1Ln}b1~guh}$3@Gep zF{~TZHz{wBDts^yoj{h*W;5X^{w6-!np1?lG8(T}f%#GH$M#jfN9seWNFQ1+{|T~> zlI)b1RNwzmfEt9Gix`Acq@D`2shO$3L`efCocCmfD_B5%9JX(s-ABa^vs6jW`K1m< zGUq>i?pP_7yWtq<7OABc)@T04mP8C$;ZcX7d`2#*uCUCRpiMzec4z*El+8Pj5*xgI8`)eVK;Ta+MArEy8iU<9~jj)k%-TNkNlkiipu<9lPV9hLq zr4H3f4%pJ^dG>F!Uctb11keM{NJY`Ro zg*E>_3Y>VB;Pk6f7fa#bsGw{RjtW*4+i}<>+9Gjq{tNNtKqqV|349uhI^(IPegWdc zvc#~&K1-C3-27Kc*1+y6&%~1cRJUTZ;Ftf?=8(3c1phMJdO!^84%(d986c+r>D*@K zUm?zROd0munf+G-tS#A@x1JdFap#S8%|D)RV1$=y9_uDH>l(VoEue)zN!7}M39O(<8ioTIw zha@q@m_x!I-_tPjc{e#sMj-7E&AgYjO|19Lcw(m z)hHa{@Sno`-%)Z?*MuzFl`DNaMSxMa6nAY6F-UueS+p~I%||4*as&BR0x+*3TuVA; zS8;+k^gpE>h<4CYM(&^DsfXC2UpEjAOb^rRyrWPDzmnMXz?1+y93uEZ2>vH*=wEQ` zNf2;`xQ3)4xKZ!$eu`Xm4<)XcHRk?bO8)@{*uN7YUu|nb$YfdpH==yTATm~1kD?lbap6@F zCuHl_gw%4Qmly-+0J;~9s|DWZ0IC zD^1KWH{@6YMd3y(LB4wR!{`4opA!|9+n31R^Vbhh&!5c_>&O^0JWE`Ks&GeJ9DukTFe_{LV ztN(G7Hn8Ptu&55qEOi|LeP;#*=JqS)BOICBo{bxr#5Ef63`-O%!0fLcf0Uq-NqCUv zZA)%Gc@ae(gdUg3qhyFY+Ee9j`hTxlQENw)(3xPW4?RP}?-S?UB%!Ee&!q%JE89K>MKQ#KSaj=Da90 zYizRE6iEzhHAmKaAn{{Ys{K&*#r<4Gcjq^Stt&>|7xxmPaW4tJQ2+9oK6!V`sGXmiTB57*uwLY4W7-f# zS;(Z%fIk4rBZS?Q(|JrCq|i@)w#ksF z^n5}3P+%p7_Ta|>M!te<(eyb5=OCwC@Y)GM9f6F4UDwMC1zVY0yT<=VlzR{I#^FPF zuvFvth$<~-(~idwda~PxSs%du+DeSmjrJ(M_`y1%PzA--p=&CVD?dbNNaTZbFdT$m z5{`IHv)an(=QX7kh|4Qx^aY59AB?-ex@cEJ{i=26)nJB!x0uA}2}!;w^`H+^_h7*vj1`Z_SMP&`Djh20B(=~nTD z6}}U^NPPOG_GtkB6amv1f#;Vj%PN|-ihpolCTMbvTo6|(As`O7R<75jQ{V_#fxcnO zQjIt*;QoEGxFpYKcJQs+-Di6c2K_@eUznw_Mv=;eT2QHf2*R*&}cIoM$1R>^_V|il@NQ>PZ0Z=Z8J5SvJ{@K z%&@epo={rTR-n}+67;)NeO=k*^J3l22eFxb7L+Qcu3 z48x?8HxcFsL~QS(paKEdcnjBgy3!xj2ro8_G;R?3BHVDe~ z3!aBg9BVl-{td?&U9@MG%v$x(lM&skNV{!T;T17q1FJ8$s`*MADp?mpT5U6@L?Fh) zzqXlA?`>Une_Z89H&+Z#Y+eTy3!XTBmp-Lgkv{EiGY>2|5NHj5#5<8c^0Iwe&ClI7 zIC`<7eG~sLzQO+Je;OSI`!S0cP|t+6DXQqjGC}mK@7Ia7T5QrH>{F%(HV?Itwp1@&6|`A_WH?bwYlww8=wY{qMmKPF%VN7 znFGXy8Z@k20;`E;0I1*SX>aC}Aspu`fv(%7SdqGht0ndM7iPCl8vk{q&M@a!r=4;Rau!$j zoqx|UhMu$~blr753HpGJa}?3Wlspk0GzOSP-qNVJ@qZ@nze>5Q-yajaYW*&E95-s+ zUu{t=Ir-Rn$Tsw?iHtDgFfQV2`3x%;G}GSxu+GHficl_A_QTz}UZBF%m#Z1TDu18i z;;Z6z_@quSYZ=3Nds?9`+xZ`pi~Tc0_PQ7T6ANd4t!EcYXRi+SMwjNlwEj@@cO52f z99bf6geQc=F_Ukjfz#O{By~3!l}R>%eyGYB+p89Y#g!AVvYiy*eZ}=OI0_zBb3*Ah zPlw*l2jqjUF_vbmPLw_|oWu;$?NIyElkq@T-P40)M>x*jw~3#;hlbRT_CB6i{-hSn zeL)hG%y~&e^)R5f^7rW}P(ydM*k8Xr=@b^XpoT4frKSK+*j@{7SCWp|_DH$I+u|=J zj=6iDPk+(_SIoO(&|G!pifJz}yemquuAEu+?J55Tl~xBWt$u3*SRIlzKMfZ5F)mNJ zdyb&oDX${*^X-}98omG#9`LYn8dk*xI$ouJqM~1Mbc-zZ?NQUa#cXqtUhsnK2mhtB zmSTj-gmcKptDb5MHVd~h-cgTlPkKDgm^T?_|(TW7Ebaq&g8WHrco?GW>4f5-$4bP`>ZCe%! z+2czs;wL$@wj~rY&%3qB4_C4)gG8^Jg}df++aY-G^-r&9%5+(_a}*|}lRHn!qV%>c zd(1z}mn-(AYJ@4nRJZN81%KE_)#B;TF7i>wo2DW8EKIrvvt}t`Vd}w`qxROn!}H~9 zgBktWU3v)hd{u1Sj=mZ0UQIB6bf1>L;i{>d$Zv*EjVMVTUnbVVDCKY2zpHATM47)r zDObN&baW&-_sd;OeUz;5taCMgkqv}M3wzB+T{?;>JT4|U| zt7=Y|jK71fg+>zyh$zRAU@#Q2nmy*fFL z0mtn2-XGOzt{hT&Q8=Td)}`n)ej zQ0>*1qu`p#%K)R2xk#(ui5t)BfAg?qnf}&2&myd&Nvz10wv|+N?>_J>(?ssP!X0}3 zyeiZzD2mhiwjjAb#GXM5`7XT6OJ#OkggEK0tIDzisp(e zpu@;lKka(cc0dh8xazilXK_eTM|j6UUvc!QfCFV}?aM`uj-%8lhM%hH6_y@uHDBFE z@*aIuO;X9Yf=J+s2Yi0_Qp4r-PX&IBh<9a)`#09f7xjG|2j__Gx@ojMpOpwYpnEuY zF_H&2KUxU2gfiN#1}B-VSa%BRhCN!6$1`;V++c@#~+x#d`Y#ry(Rm_sfU? zIM!L=(v&2LjLrtB*JAoIubeY?(Z!JUb(`TA^H14}Y|1HbH1-P;Rr3NLCK35<3ENG5 z04)-Iy8h;kO&>%q(j2i|`Wh`g3RfI22^^_?z~=7#lI9z&B=&?Iz6v?&oOujSu!5Td%Yr zahu+v{JE*RI}b&o>R6}jQGL2@&8~UUM(Zc<2C~KBJQsGyS1zS3JVK|fDEaJuAJdm+ z<-ZZ+m(9gGcXxuZMXGjo=X!SEs3x>F;`WhViTp3zJA(~NIqs$ zu8->u!}lzEvJYc$`epXB7H5~v^Ce{6-UG_;U?qC&8fiJ4aPJ_3_6#1?#)6!WpmZ)!r?L@OzS6WRi=N-4` z>5Qu0IxZhU6^j*x+mU)@wLUv4TK7+8a?a(> z(=R{q{blL&4*5|#E}W^Y-LI3w?aF#LPm#T(*KRSqF?jQJO~l2}6#AMseJh}3P=x)< z30ZgLhoUUz{g^s)o3%N%k{&Z8rhVyA0~}sWgYA#aBH2gqrSkw9ppm0e{VBERYXBve z9ZlrtHaY_l3wFEJn42$vw)tSVfT3m(^12lI}hF;0JPVRqs+#upucp4%h z5@DmOwBMEiTV#8)*$4S1h1++9>~}WLu}?~3fiVT-qr?3cOVIY(PhbbDn@v~v)PL;y zJ8lc;7s2*6_N=M!VJ+7d$*~ggC}feN2)Kt9ozh691oiiI)CfL<&CU;A!)Y`9}yDjZLPhexfKW+Ipr! zKDUTXR}a3)DcRf)E^~QB>-E&j-t2yOV)uNTF16%GTR?C{-+Iz*q=l>2Jei0U-t%uF z6_bavS>tk&%P?RTXnB|m+hi^wnQ16ESIoIY;UdfUpI}ORwb0 zp*iHbK=AA7W>>Nx)7ZGJ)A7h98m?XKXa$Tu=kmAJeW|`dg*xDrh%X(F({oPTW%DH@ zpFYh;7P24ko|az8n^xZiTiy%iTwa}VjVFidPRiYAC{?9JgEt?p&x3hn$voA6@H(Q; zQzLu*2z8_iNiegr(sAz&sx1A)Q!@ql$F7dTf98(&MriL08brjz5t=vqO;tL1dYvtb z`BYR?+$3;#CEqUMJ3nz3adK-=$hwVdF|$Y4=6BpXeB}>5v-!m@mGN50V?AlcL3?a- zuq7tLGB9VtL`xgC98lraeDV2Yke+s+8AkfKFhMRy#9YJ6=pzwFTG!HaYFsTGJEJJL zsL-Z|B*ms&jO^a1Mk-<^_zrGpa)nvbhI_Txc=(p9PiK7kFQ16?lY;R%Xlr4Ffn&>L z)qLT!E7)PSUUDKE;8;j0CZR(-PM7H13WSGgsX1;8<0msQOj*V~Xt( znGb1;Tq1#jwHhZyaIpkf^_l6HT4Zobv5EJOYEdJdk+osyEB2idL$ro&5#a}A<4JCG zoy}L617{CF>m;sr2vfhdes1k>&TF(ZW6^NnovPPQ13Ife!uffRU6FN;DiP#$M&~u< zNX*x-w5=D*^2)55&pRqcQlE)P#TZ?vCAy~tpL|F1?SF&|%4R}muR=D4r@8K_o|GO6 zi0b)2I$f209yWF1h9uX1{V{rdMUUJt3=T$TI(IPnYH!QwY34Ke6Hk9r~0F>o&)q==wvyG4)T+g@LgmWj#YU<6-Zu%Np4^+ z-sbJZXs1L|2sazgulE)8-48l3I7$}{eb%F;jS2Jp0N-`p1}m2ZLsu60Lq?b#>!Vfe zZ?Z*5j{@pqWaQU0$w%wBW%1uDac1mF6>@(Q@j5CZw8+kNalf7350Zd8m!zIJYN@}i zRoS@VlrES4yxI2{+LV?VZ}myf;Pc@)7vC{)Kc7;+&Qm*B6yri0vo7$-93Q29UC2?G z`MuRQQSxmf2X%6hK4*$inm!RtrZe8La4%Esu2x2DF`>GBc;FT`nfqp1`PhtWUzGKS zLRUnktGW##$*3*EoYa|n$2@(}CJ0A^JAZO7hhu-=m#HW92la6iOHc*;1`?)G zFDyS4Zj$9Wt$Mo5{F~gmQ9XsL*|X-^8tr!H)L%}7+c^tuMot^k!M&df)W`pJjjjIX zW2Nz^6&u>Qw;<}%Gni=2du?a#ma^zK)q z63K=&`pw%%(S!Pbq$yADP53x-rc|`6yq7WqiLnfV>xWJ(b=H<_m!02L8{F3^>9`0s zx6$OLEn&B&jH8qMA!a3y`r2RGj?Jfc@Q=mE6GpdN2PR%wwD(er7uwN4mibzuMy98#&i>?2C=Iur ze@Zi&4Euv>J3}9FZdHslZz;`#J%{M;r?PRj-smj0ahbf@j_F#YV|hOCnR9n5P5en> zo&QU@%Eh-!ftp6`xVeE1qi2dM`9(}40o$TW;xG}D@Cudia@zN(jp=un6XSGU$F!|y z({6E2!)n=uo^tf=oSnf2u7o2_%NJ$K*~^+M*Kh3kM}npSzt?wO`)%+XI~Hw?kwyI2 z#p2iba`=h!>9YXc>r}7i@~kNeR%VLHx9d%FvUeo~brs12ysyuCtFp9cOnfSiI$KF>TIgFdacGOpGbl^Fbc^iC} zVPcYOSLtHbcMz}tV;>!9(CWqC#|QPc3rgtRcF@mJctd(Roo3XRJRw2eWbD|uzQ}qQ z9PXdKgvce~GYL*(au^TV*<9BwB!|)W&7L=BEo$z`3ErM)lycoZj2Ig4FStt+@B1Fv3Y?>sDJ z4=x%-e-k1)?MZpzc%dkqo|CWw;bID=7Z(}Wt`IsEF%Az7RwdePACufUh5zD^dREmQ zGrb-fA=98fwEK0XN>uEoKxh3j2KD0A-$R-=k-=F>IaWo>m7Ax0n|?H0zm@!uOXdYp zW=DF8sxb}@jBS&zkE`Fd-;^vRj3FDwQ^Bt;w74D}g#zuZ_F`u5Pv#yw5Q~~t7uM%2 zRtHm_9UBH3Sn7Q8cHfqDZM@Efyb+m}e%{!Nsr+pUg=D?;8*bI_8;?Bc`?csmt51w` zmXR-)sVUcf8=TZDOUrz{y0{Cr`!@JF$a?M`?mKs0^-+wfC5U+IBV!)oq~q+Ntz@}U z6`0cIMNkuLZsj0W*I2XT>F;itBAJWxhn#6|(x0at>@WV&lCAhh-wvE-=cDC(S$hxH zV1MO(b;GFHcHex8(UpiwNuW*p*>k{?zMj1S6RV-8zH$PBw%u}!1eyZjDNjw@ zxpf7Uk_Vna<7yp|&uvSO#D{zsub%lXH!#|V<|OFjOE~><)Gch#7Kn)cM0MDkrPq~j zZU5NZ=&`w%v`DWbaU%mscQ|+djSfAHIBA}v^-n-aB_eP!-3yZFG@ z&wZ?}mF=kn4X9PJRbx!;=XJ7Vho97KVM9^9^8#lGKsD4^}8F8YEP9Ib(dB`Rw zXf`E;iUY$c8x-KngHCfqEbC&oNnsEMzvVEu%E1Z<6mw~R996bk*mM&$E%{7AZQbeaei;5V7eKQiN8=xgUI_buh*P#bYfFL&#pK}uQuU`l`Cg74ZUP~~+#o=2NCuc}L z9Md!x1eGAX*(De`;zO@Uewy?}eZfccQibXY0Gl#G^R0kEZz(VSh@yw0c?SoQ zS1vDrg8b<&78x#XpE5qG4gQTCWCge>AVl{zAlfNG_m-G9YLC&yUWniZ?7M}~^!9j! zl@u^|kKm>PKe4x&=l2WfLD2~2U!sOTvIxZ`a=z78#7j#M?@xvULT~>{yfT`du2U#p z;OVGw!ML8HTQ7K$E< zpF7|XetbdyerVI911o^BF_GB7FuIYwa3YtusxSc97$=CCajb%4*+CV~dr5XtzWTId zGJvMW?3bKkjiyepD*;(6XaQ*)Mj1fq_xCvQTJfjO0nN5Us1I`C?Y)^18)yW)DM(VmX`9O zj>!F^hVZ!I5{3>ZbOeSnVGACSqu7JUS_XAPjV~P0JaQP8YYrVdH}pQ?`D$}HH=uwmt!G=_CoYFehW2>2=;6{XIZjRl$+9mK=nrPX4ZZFrZ) z>N@z8$>Wehz+JZ{-RavYB104Zh_5Z-V@7X!qnh^;tW~(eYwReG{pD9Nw@7|5ZvxH^uXFlm!lV_Y=i5yI z>(XX4;32)paF4WdweqSO_7B+!GE}^&K_$WRE#ghTvs<$;_CmqnbB)iNC2ZZt9j*N!xBMECpL;vInVpOjQ{pokf3{S3XmVri z5(MraIir=61IJ?RI(>ol=r4H)94 zMEMqywR|!RrAm8^POiaiDR)*K3MnflZ7%Zz?-s$?fyTq3vJ=PFx~HI*XZnI+?Az?sKm2I!0w;K_ ztxkDflv-k3mrQruZTqV|-!XIkOuK~Gvp(f`QM3dan4CwEoDxYG{wB{QG&t^9c@30M ze1j2hIFCp6S<$V|6f<5gw&VM2X}8=tOZomuA{1u*mymBd#*;cYS)aYY#ibSq$L;fG zyd9=lzEh9I_s%}v3Q99d&wlyinJ)U_>btX#wf|tY*<)3@#?$vKRICgP+fWSNdLr-s z>4b*OfDk-@$xAf`qX~0&C{yZ?(ZS*1=F z=<{#0eX2`+zFD1tcw1zj*T_EKDo0_PIurie)mh*UISM<~*>LYt=b%4#t8>9UD$V-6 z>dVuG^(xB^5o7=lLgNpsOOfpnbs2b6 zT@D^oi?G-qS686YC)AbTNwpY0r_@z&pH^3cXVf)9WoJQ!om1DseI9&;T_6pC>qU80 z*(G&7Vq8{930*;TTQQQsVVI5A)C~x^u5LuE8)^l3Q>_GVsa4=@wHh_Kqi%vbPi#Vt zBz&8ZqCjlH9mJ2<;0gg$w&DsARJY*@Ar#wjg&1l(aD^c1JH^Y^YkZgJDDK})uNJU9 zQYQ9F`Pe5rY(FyZBo2U`#X+!(IE13Qf-37K)`Q)}VbKgyDQzfm1fjh^6;gQ=#q<{3 zI0nBy;u!q;isSGr72G_IyME#X?)oFw6Sx~7zOqwTZ$Ki$lT-o6IYr~bPA8&yMpoyn zq+D*qIV3Je;&VtmM4ZPR3GR8^4HFk|M?!l6cO%494GF~h9K#iDJ~K!^62;(Z(GFZA+JkFF z2aQCgBe-640yl`x;6~8}tPmvom7*)$RiYbMExLo7L=TO`q9?dT^a5){3Ak1C2Dgbm z+T(1y=nL);rSx~S4+^cHra}VxX_z^pKVt0@1Hk=aAb3DzUlP_ke2{ib%;$qOT7AmE za*Ykq==@`-8VS=#`Q7m-$Z>yjF;Pc9phx^7HrUV&aaE-E#W?ke!4 zs0L4oP2g#@Q-V%Gk+hQL=NCx-gE>AlE=4%JR0__k&ND2?*r>%Ab<6fv8 z1&g#}2q8HY5Istck#86oW?Iiq27Ej`?lXeR148Foh8c*X&l6VF`-L%sP>#m)J zAIaib-1XGX;f|#79PUcA^SC2AMOr81w;1_#LdX)WGwzn+ zt~2hIX~YCTbMmDUU4)u75YX(b5XjPTw%$!H&h)PUIAX?@||rj;U`B(xvGcWC{= zo!S6|?9v8;yR|_GCn+3^yS-W&xKAqw_iIDI1KLo8lN1g?o`s#&vEXWBk>fe!F&1~{wQ=AD5DPB+#v}bD zZ31Fn232-Nd;2}>yL=wMuKjSL&e%h2l1|psWStI$rs#AKG*zc9z%=kpHeDw>Ylcpz zJ2Q1U2%1H2uCUqQ57->!cte{D-qhxyq+8m2@V2%_9p>np(y`YN!az8baf1Y*AFYrrlbR$_gvPUE`{?5?lZ zX+$@GJ@t)XFTDaR(JOTt$ttjqUX4_J^-W-@zFDUc+yeI3Yv40L-wF=YOTj_` z$4O;6og$U%+u<`rr~TScofg|+`VOQWuI~g#=)1s?`fhNvz6UYK=zGDj`abxK)A#E% z4hO&qI@u5t^@DIv(hq@?^~2y4oi-g)^&@ak(~pAF^l|bLg3c`gw4XegQs<^^4#V{SvrTzYH$ZuOPSO`c-g+{>`_o@3S>} zlWTG$uY*5iHxO&LeiPiQ-vamPx554T9q@pjXK;2U5cv|lOp3yskXZ23tdA&1uL4R1a-es3{_8npUkX_TGE`~Jw zyBZ{d-3(fCyBj2wJq!}oo(73&FN01qOAPvstT*@`>x0a$OQE@;_l5hWUJBmAFknBX z_k;V6-XF|21{f+UFj~BCy~hfTtbs^9$Y6sFTD;4^a)S+#r4BXZ+HR2e50|BmkTn=7 zqmGgsZ8T%;j4{Z!y)hQ-V2lGh8souE#srkq*_enpU5rTv#MYP$|8B+~G8i2N?6gLB;}bu(1#>4yECq)d%fR8ra&UyP0vu_q1VnCi1(WDZQzrw0y@@j)Dl}&Q|HXC$g zyv2~)jv7NA5N}163yp2)p+&}a)MBx*16*S41eY4Sz-7j6aJjJuTw&}5R~q}kRmOgB zwQ&GkV;lt68i&C3#$j-S@xV8%7HqqrlhwS#pd*)^#u4Ljw#zsQ?lz8rdyM1YUgHF+ z{sBLUyuNr!R`|3W=rghjXJrqZ1HZt|OZNpSd>5sVT%zW%%LW~vUjeTg>{`;aQq0DK z#&zSL7^>ympgCjQ#N83xtr1VKqsA?oHO6h4HO3v9H3seLP8xY8W{r^#o;C`=Ge#Tm ztkD)cXB2|xjUw=ZQ4C%*+JTph_GUBGqXXPmjE>+{qZ4?|=nP&rx_~!~uHa3h8+gkg z4Sm}n4SmPx4xc=;2bgd61PjbwU>mc`^AOfRst@g~x|_7`>S5Bp ztEWl(u3jeXrbm&15%KYLYG3&rEE<{^r;EnpP`RZ;)vXmK7+I zEC-vJDy{W{OA%poRu4>7TgGKa!F6vQUV9A*mZV>Z$>hMRI8jF7DyDYF@caz>e> z!LjBTaGW_79B1KKw##Hc!FK1DY`i}Z5wGNU@TZh0Ri=?X9I*d5& ztRwJgZ+#hi^53vdR(RB;Ez>c{<5C1pNS;hY99pHbMSJirR4s90oG~6>}^qpU>jgPB&^@De%3Q*5)nFUl6E;~(kF1|rSM!Z z$x6Fu(i68!sB3@gG7=83uAs&Pt*h`EWL<-Muyq|Qvu=Rp)=hATbqgG7-KJS*-2sPN zc_?^V015A>s_)M&lbHCRr1>)|ve^^D`Y+bW_GZhTnIkz@_QpKP`H~AH7ov5Y?L}Z0dokG6UIKQrmxA3v zmG!Wf!QIn-QnmiddfV)&N36fG{W1L-W zy~4)ht{QcmAnP&F-h`S?vIi*OpWf>_h+8u(4Ox596RO(P7+w6|HW znr$JFHs6k+nrrVsTMD5)ZHzS2_%3U?t?ov?OYA-1QV{!5`*{TIwbt9(K4iJd-Vd&} z4}fdzgWy{G5V+3%O;=&HxYrN!aj`Tl{RfQs_Y}wxRWM7Dt`|s*(aOqW7co1 z4xAsicG&lwK#Cgs1X66ZPm(a&r$`v>(-tX%Ghj2WLP*#r>>P^QY16)TmrXm<-S&C- z?6FV6XRl5Bt9>@@!S>s<2RmS2Ac3?ml0e#*NFeRY;1T-@38eiyNX98!xr&<;_BE7w z(!P#*b>{qrb>22^!so1g6ZJZ0-;!cQ zw6kfgX)oD9vZGBW{he%@f}JJ1*z~<%SLxr)riymA<#|;Po9f$BChH|xBH3HAk7Qq) zM31_$y+hsD!J%&K=ukIya;O_SJEdrG7pEWC)meZgt((&y?(WV2u!l1c?CA^wdpTdo zx2>+QUmSn1Ek(Y}CR?*y7Bxh2s4btf%X-qIw7$-8BrSDLYRy;{A1~?

u-IN153Z*|9%bNCqff#FzNSUsSR9kxsrR!`1mI?=eK-0`w!CdkH3v}ui= zB+H#Fc?dIXmNNzA&UR>LI>(_0WOE&wN%Nd(;CyE~xWJ)#w9uIeE^=mpiyeB5wZ!?7 zuu5QFIFHOu%9|rgnkzZaCjC5Lmb5@}q2wZ2r^V8}1bMA+mVzstW#B4jIk?(c0j_aY zf@_^s;5uhDxZYUim=0>K+3iG0hn!vDVP`jZ#MuKLb@qbCoPFSN=Ut3@q5I%|+?;X_fTx{< z;2Gx-c-A=#o^y_X=bfW8)0|^y;zj2;c*!{dUUp7`R~$@WcGWqJ>2u9FLsQQ=OH&WT z)N{@u?Jeg#c-y%EpF7S)ntJXfFyFn5m<8??xZAi_5vQ&DAHo{I`8B(p`}6B)%MBaT zSGMw&P49o-mScCvrW4{khvX^WA?u~UAvtd2(3`kz9l0qgbV$sL9Qu&482lA$2ex-G zwP>%^QKsl5*;%rSWLL18gIPtouLt-G%qj}#C0XLoT&BHbdv`1L)*W2hs&;gbVKM0B z_I5Bs-9BJfx37Z*&Yh-4th+lE?BQjjssI{ygL@|3GO&>BA8;6-0_G%*_{ARaVLUP z-AUjycd`?)>FyM`XSh?rneH@jmOCA3XS*}NIbe#-b!WnVo;wS5oA1s>ss-*GxEH## z1zhCPHgvH&7jc%j^T4Ii zRXU`|t0b$DW~aLeX?D3UJz|w%fpev0a=;~v;GnzN5!MjQ8yB-3QgYGdHHdt{-3tF< z*bcjF8(hP&P4L*aRci!x2OevVnC(4vJ1SA+?LZO5-cGQc_nNRqL8W`@E{CQw)vu>V z1@-dEHDQf`X7|k94o!tUQch_*)7PUdMyaeM5F6nMulcz*gW=*ux3J;dQGl4^jX_gnfRKV<9 zkkvilwQ+?t8>-gR+Pb7!3K4e1D*}&t#o#fo9eCUuWC&|6w5_MKcS&b;fbVJV$0RPE z+7UPByiVYGuQRG~!7E4f1u(lkuZx=;Q@S+N-CQ~m>MnEW;Yv-{6RECwy};{U33$Wn z4c_wlfVaKA;2p0N%=7z!`F?-;@VUrW2H>u(KM*YR2f4Pn7{-SW%_6KNFe`j^)p*}p z3Ze6@G89zqLdDSAl|z$K$yO=x$yVv@4|AKbKK^jz)Yl&Ymii;Xe*P%;eQP;{&$mXq zbRsuKW-wNAoaA`fWfNQ)sEJ53$e#ob_9r99GJgtcwGw8%5ACh7F}|4Us%(Tm4IJrD zN5oP73~;nR)6KS4!`$$BTm5l1!EYt3H4tOJ*{r1RX1nwZVvg*!xh~1iJXz#?H^rv; z3y{Zje<8YkhQA1$=`V)QEPn|&+g}RK@t1*f{pH|1e+4+-UkNVoSAh%t)!-t34bmw;=6C zzs7}yzmD^*Zj~QwOWL#@m(N0BkyPxI+=Ud?K5gwb`McrX?C$}$_Hj-w$r{ z4uk7>5;N0&YLFxnkQShLD41!Vt zQTNs3E?Ji+lIT?YQU9d-j8zS@-e;c(YZIi}_fDa-)BY*+%NhSP+-LnW;5i>kll_LB zbLqRw^RC=%TtERAd^%RS=+iOGC7&$d%RW8gxZ*>*va9|j6u%kTGGMu%SX(d)0_QUF z>lj={37vu~aCZ)_f?YtBbq&bo?-r2F-#xen{~p10SK8;$&d~8UUDBtwQ12Rubii&S z|E*X)gXm7uFsN>p@3Dg9(k8i(*;*R3B_$FRg8hRca6nKD4h-6XgM#+p;GhFo7IXy5 zgHGU(pffl$=mHK4x`M-lp9^a{#5riv%}ZWL!sTDF5Rkdk)1yyWdU>=*jzm zU>*Dq2J68?!3OYfK<^413EE@%+J|Wnm>WI%inBtp(xYF4RprUY7D-obLZXwwX7E(7 z1w0efpgIRI6TM4;w#}o(ZoBMgTBy#;249d3z8LKEc6rtz%!%N^-5!1BxyPdiT6<-p zeIz`tyWf+de!!EjP8>`MJ%r@fg2UkT;0Su{MsO6o861Ppt>8F#J2>GrV|Rj+aOZ`m zyvJF7cp5AS*NZ1u8}M<~HavsS!jL|eC<^IAiQ@3A_XKMfo&(#5=RIK_p?>WYh8L28 zFM6~!yCf@kSqcSpSLg5wGU^gu1-pjS`Q1Y5_U<8ddykMhxo1e7+$*F`E(xjgdxzKH z?h}%|&^Nq}UOProIx@WPMpD>KDS@|S=iQdPBV{$urw>WeeF~E?G<(Xw&C&}j@v*AXS6+P(Wop(u* z$?UKUvvo4N_2C3$w?SsKQD#&jGpdvsRfQAL_Dh(KVQP{u2Wzs-V2V#FVyfgcpWaC# z>t;(h9Z74#8Q|7%Cb%t}1#S;#gFC`G;LdO^xGS6o?hfap3VXr@aPJMttlJkZgwOtP z5qKahcNhES6-?F;>$PoOg{c-|&9=>Jn2RCSZQHzonHXa2Ze`uXtP9npFiS3k%fXA` zV(?P90%5l>_dM)}aXrW3H~L~X0Ug(1?&^mfPJH_U}>}s>=$h(@t0K?AgeG?R$)-I z133(i{vxb45c(+EnT*sfpMKfHZpl61ULRJ1yzKXB8-BniljNXJOVlBs=KEov9w;0^ zDMO>9;IQZzI6OKIj)+cxBcqexsOS_pI{FkEd~&3nrg;;cp?MRXrFj#bqj?jZ2PZ`r zAVEb6zvxekyh};VE=&2oB6-y(35s7DZ$R&4_N&EQ)S{v!dJJ?C1_S zC&~*jf1>PU`^Bm+#3BS)A$W*Pb7Ncc1P3;+!>XCyQ1FU?&!DlT4JR1 z!Oi}tFL)p-4Uni`a5UomaepKl5IkdbSJ;UN){L-vDC~R$D@0@0BW+NivP;pRKw+1o z!Ej#z6?Qc$L&9rOIimMc_>kasWDZ63{|iQrY|C&2--*Z|$&1P4$dAcHDTvANXcLb> zXxn%s68BbEr&L6$e^4o zhhR#OF*&-$Q&B?qcpBI{a4i?EOFP0TtA}h62 zx|bp4$apz8DqaDOj#q+X;#J_-cr`dKUIUJg*Mbw`b>PH!Jvb@e08WnoB&>l7n-=RE zWwsTvAUZji9@B%Q88L13XM!r56<4BVgB3P6hRP6DnZg#tR#iY-r0PK0mz$E_-W*${SSwi79W-2v^o_5=}I z8Se#G#rwe3@qXmACZ;2>webPuw=O;iu8$9a8{)&@#`p+gR>VgUvobzLf*&6z!H-Xn z;KwIP@Z(b?`0;5H{P+wBetZ^bx5ekc?eTe%{rCdOeteN+KfXk=A72Ld#8<$*@zr2! zU=5=t9E`(j0qLsi;9p=6!AOd42F=*v_!f91z6~CY?|{eRys#NN9_NE6(2U2~$+#eV zoSll>fT!cO;F-7(JR27w&bgR$+xd8}kHSV!VV7dFIArXvu#_HWm*e(`bS3TpUX43~ z*Wym#^|&*5BkmHyCX2gHL97$e>UfmV-mmk0dBvQY0u{Q}n>BTZ)Fad#VhT?~y78d!~k<)LyBfaQ99P1N)?g zgMCvYz|z!6uwQBv*grKI9FQ6V4or;&2c^b=gHz)}+nRuRl?o<=^jEGXhGcq7lJ%V| zIYn}+5?;$epre=?H!&Xb9F?DKJFcvBBOOwie7ddoub!V$E5x$tVs&rrHoHG zGi7(o3Tg48585WA=!3S2Df)h6Qi?uno1B`BjHjmP&Dv=x`b=$lioUp*k)jXRW~Qjq zA_&z#U~|m6b3P~3xw6dVy}2HYY^Ff#h{MPXzP;ETh^ z8pzAIx&-N9p&&^)Uxp+@_!3T^#8z{DNa$QJY=dEUo`yis>o5#bww#K(r0^+o1 zm~gt10~1bHbJ*%o7ddQAsB1ZFZOC+u%~aTY<+JM$GYhtxu4KV>)7324Zo0^V?WSv4 zu-$Y$3$~kXWU=Z{H?!ELP`9#R#OZbxj5OWJf{~`XSuoOcFAGMR?q|VB(}OG+X?mCi zBTbL8V5I4B7K}7Kl?5YBzb^|$ntp#4j5NJT7K}9gfh-tl`h!{QV5mQo#SVpf(=0O9 z9)6Y`3E3mhvZGMnzu~?`aN9Qe4R=}=Pj1$yom~cAAV7nfx2bELktwYM`FxCz$ zXTn%NqMQw5FyY4ETUq`@d{gSTTK zYxKz2Y_y3YbBob7imV!=FpBK0Mo|7NYj4uaQ6o%xD`=G&7e}(0Rii!8R(;{D*c z%vx!Q{zo&%zpIS|WX=Hc4MUd$7&N zBf~O#l+7o=G1{JOv-vik2=@YevCUT6Y@@ADiXfOLO}?5$^QOlqN4l!O5XW>B$_4=ewvjFv~H$RRxB5CDjI|`JzbI z6qx0h>bio(YjLC-%Aff#$6bPU6p+aqsl%ORC}xDS9L0=uR$vH5IV&*)W1LlxqjAn^ z2-I}DV$Mr2gPHp* z+ZHh|8L_=Cjo3c-uxo8c^^dqaQ2nFsPE`Mxy9?Dn?(RnQPq=$f{gduqRR5H_57j^I z?nm{{xaVDV+0_mtVtGKIquqnl(e5GYX!ody^y}^|S2>K_Z@WhjbjL05)T3z0x7jhY zC*_CRNM4;hvHoN9qW#AdbXHZ;WTQ!R=VDEpEqog>j^f z_ln|3pWqe8kulM07f0qKuYDX@lf9!}WKZ#?du+bP7JF=k$2NGP1Cnp_IwE<6*9plh zz0OEp+zXX-&=J)j30G|!<=~!c&&nEe1Z{*Sn7Clo|`7r63y3uE)F%v6%)-Pt- zYM=Fwnf@0x0JWR!pYS6w#m6=p8(QpEkNXTe(Sfnfn{oBJ-*gaM%94N&Mx8KC%aEOb zsn0N2Ly#FF4UMTEhoK*zWy51;J`1CtSvG8YjFxSWikb5)jC&HvF)@kcSd@FiABXnb z^v9!DZut|?p4 ziNzdYCm?|#!=iX4WO)&56S1xlE06dr^yObj1wuc~kJ$1E6O(O^*uIDzjv&l!&qj;U?sE~u`ewvw z5xo@^#o7`?D~^{UTDy1|qP35gBU*=e1-hqW3=!!Ut1DwU6;~l@|9CZ`4v5k9Q)4zK zcGplpXzE%TFs`mcWnx8LkGjO3x&c**O?6|e>#?s^#JUj&YGtgOai~_sx`q6zW8H?p zZHjdVy}3Eo-8iMrkJ<8=?T^{TnB9q4af)?HvGNq(f&@cS<5Fxo7u=)|J^A~B@dtP( z`m+|_dg)9-`e z&T&qE2EaYV`3Hh|c?#bNUw@#&zadyKT;YLW;betJf<-eFo+EfXPvMUUmRe4~ZkXS< zU*T^Fc0X*?g{t69I;i|X!5$c>@}sE9gOgPLo?s8nPR|Dd zU|;TQ@h=PZm4z06NU*nAP#oM7KLXYt7x zf|ssa{87QaS#0xJxc!G-Hh)5}Z}qY1Z$-X5$mUN8_KzcNz8rzC%(D5bg8kDxn{UJI zw-?!b2d=NKu=!40n{TlB^Mbv$+2+3y?Dbw>o&?eT+%`{6W?AHucOJdZCx@JEY0e!>y_nZ+iHAFs{Gt@AIpM;J+*P`6z_{%P^l$f&agb_4%&^`~5_p&xH3MDtz8b zus`nhc@1v=?U2t8;rj0|VvZs9f53P-iTgiY^Z6%&{b&1tU&8I5dj&jKu>a~4@LRb3 z%gBJ=#`UkW0{(Ns{x&b*zYr{sR|Nb9Iyj$ihx+macF4y9z8sD={A|G6qs?vk<$%|~ zr;v9D`A#^BcxlMb!%@t!m%R>0J3cAo9eu&t^I0M90Y?XH_4oidI>H|0L*VGdt3y5- zj?VmG$S1tXvosh49qZcoT z_$oL`c$bK;hNCwxi}*%3`tUIk|BGOKVQcbF1uNzAB7On6^yAASo*xL_9~K#J18p*Z zZ;SY^1sllsM*Kgq1LB7w{#(HY^J5V&MT9bbGU5Z^DCZX8uT1CQU|p~cc%nqaacghe(yd3gNi%#E z&3L5Z`O4gZN=5S-7SH?9mWnFmR8?1RA`bx}#^6)}6@j!MZv&7eW_B9s_C>9Pv;Co?iINuJaOgr z;JH(k_dS^k1DvJ`$A$A8FxFG0R$Mt;A;vYg#(FOTy~rhho$xc#dJih(s;K zn^5Rt3v#Q@>Kv9;R_7j$Wu8^l(*5D#ID8=KqMl*345>&$a)xo`1x-W)@7yspQ&drd zD`X{xDOD7hsSsmNk&OHUWF#GY-)EBQR^^ODyVLP6mCw>v#ioE8Or-;W<4n? zbGMs4n;(r4qh5jiWr5^@t{=y9mMGMbUr>?f6QoR0EeQ}choQLK#WD@Xx*eG$bIK z;=hgfa>!cPZ`urDEh;n_w?cz(H=oQ|>t2apufz{w?rCRHCPcHa`PxxP?55;ZdCSs6jtfWsMBW z5(PopC;Skahq4KC;DDq}<2=uM<|923o>qGEDT#;CN>I*2&N1sGKsHZ0=y&-b4t%+FlNkpKJ6ai}8 z_%I!@Zh4yobx@#knQ?lFn~Gma(i#XA&6EjOxDJ^c_q(8p<3`FhHy^Wyv@&A4G(X=7-L!|hJIwkro zRdL;iS}r$RmP8+xL@jzLEwAvF59i(pb9zag^s-P~|0@;GzxT{$7+}x7`7CR_oL97{ z$neD5TRcED9UW?P^ z-{f)>npdgpBCZ;k}t_La3z$YfQWp85xdG+ z-b25J6;_Nc`GkL}c9$sn#y7qW=Ad?2f<(fpXgb8aTF*QB#A zqQ-5dQ9VN_P-qNZ(=ai9Uh(_~`Kgu#soXZH*7>QlKB`7`)ucn7yN{NO^bHisll+;S zwkbg#l%_ECA=2O`y-rmp>y&hS{9zGKJBmp~ysn_Q&(kzqfl2$}8bOYydUF|Vh5X6g z8h^MguE^ObW#kQk1dk^jP!Xnw;IXm2(*|AU7J~UEb=AYrK6lTvOlEIUW`9abPgWVK zMj&gwf+z7Sa+k@4ovyh>DJ(XLfAiuLf6^{12vwNcc0swUYB9%kt-Q(>ujNm=GPku! zQ_gwmXl2R5{T&^3%$JJh+-;yzvSCVzN50Up{?kOs2^0p9;`y8#~f2;`~ z!U9a`IL!8TDY?Q_(Jv>j?*7M`YRdZl-Es|mhr0N2s(t;MV5TB=#+DpQ4A?-&0lDoXXrbYReak^InvOXAO+a ztgT^^Th0L~IN#S)IZGZx92xtCsqvRk6;$J9n6)iz@dEiae*s$;wJd9USXSq8LX>6g zKyJsm5$|${xE&PPijTz8A88o-&2@sf zMT){&h(e1Osz?r~_QS2b7oYh^MA@~C-5b^mrpE1~*4Fx<{j#ZlW2}|P-KMncD5id- z)r&B~iqm!9A66yeMwWf|KW+}+qBK#QgdPZM{n68{G&*4*|AS#GQI{m0)uFIKon^Qb z-YOE_tULb?XKw-^Re9x$*WIf(tg2gJq9L|P+i0iamS~d6ENwC=Ivq3h=FJO3CR06C z-Fl}c6KCE`9{lrW-g_w8qhKf$hVJ>Pb|^PO|P^PSsg^WkC8)|JE#4z+g?&G?0;R7aZyu4N`wWJa6T#3+pl zNMyaPGx8l!cn>BgN7beX9Y2IGxBpG3p{L>YZ;1ozL?kuFlm>?7K~0k0aBIU@Gc?4Y zjWP&|qI>B$v~dIr`oBSHzE{iblEl{CC1qa)H}=;m72nKcVYNq?sO2My8{Qf)lv}T2 zD7OlRGP9&&Q1YNI<+e<Is}a;+F_9G$320ZyVXiI3+XAw8op+$4yb}3HNsb|JvD! z5fOh<6lOYFck^^2&lC=lT+%wxRG4MmDx{XwN!~0&K_#FIBi3yEUkKJLZ%f^FCT?{S z+;^CG*{L%cO8-(O-lB@ek`Y6pq`v`QaBm4)@P#vQGYH!Ti8W93i;SC28jZh9Em4$6 z`SiFfNW8lGSDIE;JxX%lC6xf0~K*xm4y^vzVG! zp#VrCAC1$@7K*t|5;R1kgocREUZlI{_1|F_-@){(C%#Zm9;Sn2Ku3N+x1H#WWV=io za0g{~sXRzTYHc(@RWn+cwiJmLH}~G$rrzA9VFVA-fwkLJUr?Y<65hD9EUYmahUPThSZa-Wx!h$9#abd|V* zVZDXq`@}F3rWBmyIY}A@HDYntur?L7M?_sxN0hlu&!Xi^UDmeqNI8 zCdG)9*4;-!c-@_4MIqG&3GG1npkDj*@hIk(m|7l^?k<72R z8rj`a*a(9_X0cCFVxM4z_A3%_G%;05&NBG}UbYxjz^gSmiD~7zc4=-MzWCLu5HVD?Ojq4~x!qEk91LEuKDS-M*>KYE&junl;%vCcXM^LO4S$b` zaa9fD1js>DnR0UAFRyJOt}?e)0uQ;jjEDS(F!7MvW%ppG+T*#ld%o|h;C+3+lzz^n zq~vp^WX&~4immLINqj2Pq>N*%IPIQW5KK&n3f1cT7*)x9s3I}>VT6_qU%~oC zD6i(#2V|snHh_2HTI~(EG`I>tdwT#^PP{8)aXZ_G$9@2Yivi1-t+I8Tq;D`L&=|ZI z!QXR{<_)ciyWrN8P);00?lZxPmzG=Ejif3@2Nl4LK2`IybV z!ivRzgcSCDVC-hrLI=tmN=S4@3-n*|Q%TA`Y;wv|l=769LW}zwAll6WD<(y4UlO#F zZ_>v3fy-FG5{80TrS={``JcEl7j@Ix878r-F4PnGCYaqjYzMP)hh$yRsGWGV*qu*D z!8X@7ZIB(7fcEzFVY~ekB)EUT21y6Q-oUyEwA-9kb8Cc3qKBC30o}AZYb3jNQ0hTh zWgfKQ3ltcr(O1R*994#=o(uz({~2@r3%=DPpuI{cw~EvnRZad{vrv`F3Om_J_*8e0&MMF^jP{#1`jFEoE|A&>o7I^&a z=5L2`tA*})H8b**{W~l*B$E1WZf&2QNm*MWrn>-dK#;#Vx|o~}>IT|I-L&bvVUR{M zFyM1oPS^+9SWCb3ug8x4N-*Ene%(5+p#p%+hCa4$3M7E%AOTR2ga8qKLg`A(igx*Mgcjhmgm(TH z`tvpPXAd!<@d{RII8th?OlM{&w+ChZH_G%ERBnOnk9aE&=>I^(`W8A`!{BR}h$J7A z<(Mlz&kiPOJy-cXc%5y``QJe#a=XO+N6|S^z@TnNsoRTPc^tP#SUw_3Ujb1nX~yb6jrU8= z19G~CkLDW4j5q@0%s26=dc!h7>h1@7{eTj=n+^kF32s1U@&+9FN^*%Q0UlJwNKko( zJrK&{!|s>9g+_m)_|>eXX8#p^eGwCif+7?JZ7ORD>YE0J)j>_t*PfInNz2T!rn;O? z)SA$XbfVUTR+u40$Ez2$CiI|$%L?KLGb=Ft*McM-i%|rI25fV%nuCiVzozF`P!8|h z`4u2x?Gjj{{^Dvdu{err!Nm3oTnSR{2;}w~DY@4u))s*Vh0K<4T{t-@D&=+vO0YwS z^y#N6nxCo&<%DXs?iZie(^BX**;V8+v1@F1m6KbfdfGNXfF+EK40nsGDmcIb-a|ap zZ_>GNQ1-4|NOG(l86k02@R}o;EukZ1qy&<6y?YW`Zz-t>vLwRCI85+HN#KUR@7bE* zjK9IvZ7z-SInP*_PH?!sIU(QTo$vct!`UIJgi}BEP|8Citp9!Ygl>t{U%jm9Q$P&) ze)%_aFrAVV1Qc57!BXTaUuzg^$}4G=uM53Qt9*UvVM(GP@KxG&9H@_QcP~j$LAEQI zVIBEltb$xlVuMh^zKs1sk*je9@3BFb9(QAB-S zigt0hL-McHSBhNs{*=S+4Ql#TR_G^RHKpjCgk-BY%IIRcb}w5P2kWFRVfsS!+JxpY z@hY!%q>hROKs@E$s;GZcUi7p*qiJ2G%3BM!-yiz2Tf<+_zoF_t@8R5hOUr+`S01@Wg59~ZoBlsUsSDNp{-!=@h|(HPy&BO7<|Ik4R@8r`#=is(By@tMr6bLe z5H(mq^;=aKnamv4Rtc3wRU`(DQblW3Yvqj8Yo^p%^*E(c6vg@<>+AFSfo@m$O+PIe zhN9j=!_n(P{RrV~r^rR9tEsJ~4z-1w1g?l`Oy&s~LmMf*He$opD!t8Y-bTINhF&MO zN3*jOyEW_pOIyR1uqI2di*UFI^MY2RE@TqbX*GsHI)atKFf^H37tu3aq7vd?T2{r? zx`0hQ8g*)lBrA&gRodLtesVjGk{$=-&30VWp9&*Nl^69O7%WXitMP4v=PXQf z7N)La%1-q4rB_9OXsw3By;}w0EcA?2p(?6R_tHB|;vO5EI>p*)I&~rbEAd~M{xe835No5q9`Z!%ZXEw#9lekEhk==ht%XYV5dx%WLZ)B(YdxEVhiziF=6=( zNu%T+VwyT5abcX1C`98mpenZ!X5OaWrVY_3(T;*ux=p{$xXl=%G~FiO7QQV^flz3s zL^G{@tJE@cx-5vnOu3M*ZLSUFX2?AG1riPUte6#S`|Mq$TIbAkuT_ z$W%39S2|O+Q?psa@Eo^TBTB;njZ^2+5Sc=7ste%xs{lO5MdwMXp=y5xbGMS zk=2~`HXR?lt{{}xzCMzkM?m~s#kIoz5OMhu(FW_N__H)818o;TDH-Omn1af6XN#g9NO#7flNOxxU?>pf3DRL!I&EP*)MuI^e+~Vs=z*uzdm%!9APSL`%z^anKp8G1YicQfW*48<4*nm5RZ% zeIFbT6)$=i=bJG`gc)O^mr)Rwj7>pFnxUqvR_j`*(8|^duwJAt*HRZ&@OF=2j}VN# z9*j`vB_2(^7)^ID8WLWKj}}dK2gO$sIpkI4>r-NuD(s?I#Rl+7x1?o`CuPa;bcTc( zZkdjz;EH{7UmyDQGEYtO)wsu|xZkJ?HGe~DDj!rn(#^O3ay27T`Wv>0PNx=7Iuv?^ zK{oy8K}uyHC-?wgSFr#`X#t4% z7OcavT}YmhL)jki59hzE#96Pz`z14d3_w5Njq@*s+nrhycYY2f+p+KB_K$e8B5z;o z4PqcX6|bFoTb5X`M)H9m`2@^6j>?@4(JH2LM`gB#nGXdYmpv$MS_-MtZ1eOX&81d3 zO7d=)YlX6&g*|i|(jGP`EA1n!NIVKU-($AZf39X<&eSi1vL#*5l7GEq^-@+u=W23Z zZGyE24GywmuMic_%ZWFl5D=jyN}ZRbfnlY#Ns6_!LLVUSgND7jdOxe!FVKpr+KX(? z-(8#UQhT5odQ&b8${nY^^rD~bbkvRllb{yA4x?@w>ES3z%?0Z%;wHvKJ(UYB1oa#J z{U*jDqUp1-(xOlkq+U0r$Xzs~F15K!4WUt|K6}7S5ArG{2T5O0&q=)_%Xg|u^tZ&7 zs9}go7a1S!CPb`nt`FtwyKUEA=v`KH+L@cJgLEvs=f-Za4pHo!h|#wEh^sdPooG=zuhG5JnBc`jNhjN6m z&6_feZQR^=15I8%%{~J-;p*3p$+c$GMYEG4t&lp#4CW_1h+4PBccQj6G1A4?`H53* zBo3SN>l+P=4;;1M}ihh~q+nd_}95t|=Z!14gN<=Yvg@@!`CS;^kx z4SkvEECgnM%ZtEaJh2beD<)JcrPmM8pUPE zVbeNdw$pS@mTBe(B}XfAT0w`CieybK2~UCfxYFff5r;i2N*y&du=HibEOSv#ywAjp zF_kFUV5#cSJEYIK_SGWm?C~sH>Fyf6ai`P62HsA7RtEu_8cC06G9CkD6h0H&T1Gx(~y? zSUPOnv?>XyEAde@&Wv|`lk}Ta(mIJ|LJ>3ZmdQ>?j+^j&FrE?NYc4L%R-jF`o6?&r z?B>lCQgUkr>=GWA6-Cn}Mk zK)H3zz=63RLLI?{1tED>)~H?;zifnt4SZ7{iUj3~Ej{enmVWl}}i2MsT-~8KGo;)nV-jH;L3^kvI%Do>1~?1MCoQ@e5EB z)=4_wn~-t`Umd3J$cPJ~ZVjo>LdY7m@iVWDy%F3cr+-3qlRuHHy%kNAxF8z#@c(8B#x zgK8DW{9K|zM)m*s50IerXeATSpn|wHD~KDj3#a9vwzkr}amoT4ZM>m9C7}K?YMAOUR4rQ~g*qv5CHf;dwxku*??;THXt@Imm)wf$$`MJ zl3R~>n)w-MZ6;cKnOlRT(vTR%ogq0jXkZ2!cr}R6M0}OUccf3dvY9zB_ca2xT82E@ zMNPKsj1&?l)gMW@ospJZ6n4IE<{>v3crW2 z9cghWd>&yZ(o&@G`v?~!Eqf{aA;NnjE!`Bph;VnLrH8@~Y838?wDeNA9pTKLizoXme(o#F~YA$S`JY76NC>$S`Je9Q-lvjS`Jb8IKqb_EpMPa#_~p_ z? z0L}M9+Rf2!FIwPe!He1)wY{jrQOAoGIa)*%Qo8I4 zyp#pW+>gGE=sFpEY9&W)>kMT7^-Pj#xI%#-iPQ~zKz@U1(obb74au+NZFdZBM^L{Y zTHMG0=%`3^#4@vdWZuNdplX>nBlBHH0-0kGXva45(PVmv{WbbGG*0_8LEyHOAopwX z=cMW96xW(*o)?&}2-Keo)PEkRzg4cPRJHe58#|z_qF_C**vS=PDL%&%-UoPsr1u|AC@H%68G z0MQE79Zk@zQSJ?YYlSt{gl&zrOND7gq1AxZU@J7Yt$ONZMT-n@+Z=;7`GMD5#TE}3 zpI2HdQmexfY^Vx$!AZWLNRD;ZbgXyjzxU|BbEbP~lX^v|Y^|Vq-6mI7s9GELGXZ^B zDN;XVfO5p}+l-<+oT3;`jXRv8?GGeea0*2ATNSO~&bKov-HKa< zR}?mbqr(Vc>%2+))OIyU+pZ!l^F9!3hip(639;N~?|kKzJ#QB5*06g8Wu#V0uu0u1 z>m;EQ17ov!t6e97JxQ)qG=j5B)^%~2;9gX@>ASfSr`Lr_%I}dCVsmcG%vT87c{4Ll z$$Vh``n-9Io!X>_GBjO}#%$~P%C5v?u`bH;-gFdj@oD_<>icS*;BhhWDux#OeyMSf zoWJe?1(pnoYt101UQ`Gll@Hhs;X~q5Z>_GrV8R!k&D|Mjv#CZ&<4=?|?)-tqCzLgA z|7RL6dW|nHZ~V!!#`pe##wV6FUi{BA-YqMH08500e4C2b&01~#);7H^)MgZ{c}ja* zI5HASmvF1`brj(iJ7cb?cB`7H>Add?o^5{(+xCktYmu8gHxHEO_E@~Bceua3MO&E0z#4zAu*Qc%wpUgOW|Cok z8N6P=vlCCnx*S(Do2L|M$PB&&9(8daFYS*B%`+EGDiS5?fhB%AhLufBj`8YJ*qWi! zUyvHyA*0BYvLXa)zib$qF+l+awCOBi-ui%!p8*dTrKTH2$epb!%z|3A2eizA3O2!= z8cQvTQg(ref`=Du^s`1$1*G5T(Vw$APpvh>A^;;06cpDHpojdx4E?MX3PU_jp=6;H z64|x%$AHygZIfq7q^FiDXq$*18z?dTL$`mU(bDU}9@rQj`?*VjpNR=%=9c5!lAgKE zS|4rP#~QBxNb8Y^3v61Y%P(yT4%#xhgqKaP32k}gF zM%rQ3msqK`|3Xf!4Pyq0{D#HO`iORx9{L!zO{(&V8B(vSuKv^vXa!)5$Sf`Z-sVjET^BgoNz?ofSoz&g=0AVx~!=7mfo2*_;W$$FRBH0Xf`H z)n1o^d-^IM4T(dBK$eEKY8ZmkAyJKWEwolq;8hM35|bsn*lM&^Qc=xMoI>ZX`nl9` z)i&*^t5e5Qs$QDfS|wh4(t5UrYfT+LK+M*vCXppA!c|QAor>0~=65Qj{Ou8%uGXr$ z5N)~q?fKi=YN)F!rc|^u-#Gksn$@4MKtbD%W;21`KYy^s6(A<}3lt&ZV30+?i^G`!QDrhVj`!uM@7@O zIxzZZzE#Ch)T-68^$7*CBj|jf<$Kc?jq-6J8ucHT`x$lU7|WGSBpIEKkm$H1)>S8W z@lQop?sTN(OavZ^5=&!ZA1{lwXHG}D66_-(u{@TL0%daU4CKkTWQ_nOsHIq)(u_@2 zCySp7`e9(x`w3t##@Z9o5U@ALnQI20e+GR1+Zh2PESpodV1A0&y+0dqv+-b)ZyiRTkUKJk__w^siVQTFTw6;O1 zjo*m(EZ-?ugkBcmkF`}P@OU0!{A&J=p4*_HByOa*pAbROJ@?^PvQUZ1_(aB)eZtFg zUt&zuGv7__sZ=uG?Z|((+h*~lU>#Mm?}o8bpK>Mm1?sKzA5qjc_g2bu3Pk~-K6$WG z&fEp1*ri}u*DE%Sl+r%q9^K|H!RR=o;`H6kcZZq)5di^Bk~yvTZ{7lrW8H6q zg*{O_u{I{cJ7hc26BS{H!s|Fa{k`Vzg$T4`XS<{Vb=pb1CZWqVclLW7x$O#?#ibS# z{1_d07(7NwB$lurhUE%|1FTJ)<(|>5qZ$t?J~DSRG9Oj|sGUoHT?{_)9t)e|I>Jp5 zb33A}*!b{O#m0w4v4<3N3vMia*ZjLs{&zj;A2@dN59It0a3Q(RDB|);@vS7N=KF2~ zqsmf0a8o-t^@h@2C9cIeHWIeGwIgwRrTDtGEqq}W#MdJ}TJa8;N8JPF+JU*Rzz$a_ zvBOoM?jy3$az0{b&*QjwlqZeG<8ZSy$ElAM1?Jra3Z7GNmGGDXdi|FqN%PAYJN-va zvH6c==FvV<)Cg<>tap^edQZqAA}ikaysPBDg)8@h^&W*v>z!91Z;Vp{E#n4P%Q(Iy zZKIp^xB@ZfM}e5*2q!M@Dmcgh9WqZmcpu$cC}t8J#2C?FAmSYNMVu!T`qlni$d4NL zdsEE!dZpW-&|h&W^f$Rkt45%p3&3DOe2)s^3nOC220C^!=Oeopl~AXoXQ%dThBJ*PxybYbmfPdf`t1HWRif9Th&?1%tLgmw3S(sef1QGmWCquYdc-$y$ z7le#Pmg>M;cdsrCn-v&IE@PtcN5tAw-wTPYu`Z#GqMbWgwjPV;BfUewm8-?*EnU-p?Q$>-A**bU1^42(E9O_#k!H44)BuKBfa*)!!&H9@99C_-%AK+uG2Az_>UOGF zC9#H#PGCDZ%6$mbsy$B7S~WInT)=M$CUNJ5w-O0*DIc3pQz-u} zVw!Cp9-N{@eS1qVfFu!QAB%&b`Bagk*W+lyYY-SrH#n(rao%CAy2rRb&ASj(;0v3g zMf`->_{oZ5Fz)bllPn}!SKZeX!#u%XbteqZwf9yPahHa)I*FQ=y{QmZ-UXhEDT=JK zMI``hXjP1iu$XU6%k4%5XpKNS^q){nTx~ZB)^VzW^Qx{?0sD@pcRl3=PAh_Sd`9tX zn#a2o>?;FvCo$@2N?^9|EH<2uE5r|#^tHCre63teeaOhzr6@Arj!_nE5|KDv(TI8t zfYURSCzL}SrJi?1C^W;b^8wXKe!z_3OilzXJ*#B?*qRr$Q)jvKtn!RKe#UG?lSQvM zV{g>1t*0!|n>k#ExSb3k>7E$p3Yozx>cV)g9C){%F}FO!_A{O@&v5*VdF2^JKVyD* zMvtGdpgg13&v-$R#Jw`5o`h_$uq5FfBrM_tUjkT+WmGZYkMlB~RPf)>uMQlT`)kTx zqIiaQD-_SU?ir(Y=ykat52^WpTEWdRk>GDHvfYBtq!dQEN_al3J+EO~s!n#FGz!PbT!3 zM0-`&ziC@f#NB6iX&I5EEF~wzLy4jbe-ZA&(D(53{KUOYy;`;|SmV{iUMBMF?hK7V zw?*w`K8_{+4zBFn?eUa&-8LkYd@?TOriobNt4eYrMIToS98bB_cZ5Cj*B;UDqaayr0*{at;1+@;hUc!Zm%3RMUs>(ml8l>SUf_haj@`a#Y86n^Yl zNX=3SbJVHvkQ50$6A#HG?@x=f^uL0ne?7EX!5bdoypaYZpkBkBrg=QX^EjyK3k=Za zsp6>{O$}9~{*UYltkIn0bE@Rqqy4G%EQtVf-3kO2?i>Aj;*3t8jXA=N=d&to>hLOZ zy1Gt0ikz;(wyvJmehnk;Kf7{^XNAvl%g=d2NmS&i6vE3XwX@xI9YeJBw2BCO4B?n- ze#@Rg3L<3&b(_wOz%+Rt%=AN{wThx^`v2l~dtLST_wk6JC-N%@s1CK7xwC^Zd4NY; zwx>uv5N6$!c$5+oZ^pWc)^U~2aGB)X3L!Y-GUNqShQBGTKD!I{V8jwFRAujl-c&wG z))5O~828xLoQh82P$9ik&2+`__B^>%m50Y*5l^W>|ybM0KFY^?a?+VzTL|ckb^c8uIc(y63?G)&BiE9cN8Bjz|6L5^gs=NN zp(Ren;59J4K((oKYJn<)fkDOhW0{$8@ur<(vq@L*5FF)F)Nq*hME zJRRL25F(n!%{+|-OnbqaqzZH2)Fg_DovEvUYMH0y(P`Qro#n+lj*5iCaj=4)pSpllkBOItlp~B%CHpqDXx5}iMYtN zGc6rx1ilU;@Z61d*oom&giljU#~WDY2OaQ?5vg$NVSo;NwBc5&U`?gPsbHnTR5jHt z<8k~VHPu6bOxrSXFHx{2tHWK_36$F5xULg+C@P`S7j5$j+;$eMCsl+Y zJ@`-L>r;7lShbT$3(c^T*jmXNE8=$@q68Ers?w_66AEihHv9hr{13c&ZX(QcfO8`S z(fJTWhh&5FU#35Z72&5h`Il1uFNqF%j(T@tE%pED?!P$|(SM||JdbID-l9!~V*=WC zPU1qWi{wr2DuZlQGrwCFs~4N{V$Aj&&82@w8|ima`Ugl!{y-ur9S+f3)x;A*`NrHa zGE;LvO+vegQt08B_Xc~K+PKRlG@(6A+V6HP{{}TVU@D%AOTX8`>?KFmx;As{wSf!NqVd;;Q55Pt^oqR&jtjeC!8$EicFCuZuu;*Pb1Tk2NA zv_jVl#yD!j7DL=@H%E1k8SYVBV~s-QCSq$-mw4OtDmu;hK*1tM!6IjF>Og(sl%uN0 zDev>)fAD$uF5|rai#YJtOIgdCtj!URwLDPDG;@Z}G#@Ol6L8>%$}`^ZfxY3gr8fya z^>9f7^QlKTL8a4?B1g-a);D>cuO+b0molwiqj`Q2{W+!t_Kt5cqlmYRKMLg=p~|9} zNPHa2Ix7F{p`TA;*-;YzbVxXvkJF*38~+Mc_Q%aX4&~MmSN5akABA%3U50i87}`}N zAoJx;os`?I77{Df+)n!WIF@t7&mPr=f0_c`xI%}A{Z8q5H8+ana;xL2ME2z$H1rql+s%&$Ag6O7h3>G>zGdWETWq#a|U87QTrkP!f-)Uxc9e$^n*;V-M zGPA4c7nUg54M>>I^|I?RO(zxK1aAY-_Z@5`Ls(UBK!H=7z)H~QP9cXcY)KUdPBwdm0KpvP;`m z>rvoRF#h#4{skESXXR-xAnkH_+CrpVDNkF3v_4K7=%yu$sx;htq_f72<2`{IbZa}h z^`Po^YdgBt9>mumK7!-@{KZQyj>ByFp|a!-FL`8;yk52TsBZG8vgCr7+)<~7l&0C2>ldC;8>akBg~f~ zhgI8E+&ro(lz0ukp;N`_$zv)T{}Wj@!f}zF-lW1Ki|r)eieh@kQG-U9r|S*G$1`Lx zzi(iEALj&o+QNp)kFbcfQJoQOC#x`3Fvj0)R^^}@D1)vjnB(-q$p}HN;%u<>U5>I+iWuo!6AR12wy+4fJPb}&EVL)a;H#5WV`L+sIJena?G?FmZ0b&92v__CHFa0Xw81%6clvGBowm$rHA|91!8)U6&*S&3ntdO? z@2c4k@cW*cT_)4-IW@bSXL-7cFF5fK&tQl%Xo%qoF~l=ycVQx0=Yj(7p}<@&0G!<%wv(fy!qumpdk=-54+@_{;dv#{cpm0s$_-p@ExUTk z532ZY=ncX<$X!r3sd#W1E!^JnLBvz;WIw=t1PMH{qE_LPQdM^H1U~{@NN1;_{utg9 z;8Eu*zX-XU$5)8u#Dxi*{84ed!loK|-YqtZQhNF;t$I*86k4PjvZ~&gULH?hhzOV0 zFUB(~;+dD?nU!(4?7Me)oUJ1kt7?U+{~0TA!ngkcgC+W_!yKvAU;`m`HrTj`7^e;J zdt5MHcp*WXq)5CM{DHj8jr8NZTHEoMxK~t&N;O_GrGkvUS`KJuDWFy5 zd3#FpR+s1PF3nq0p0|tg>b<bghVneSwBE+g=lS(EmFGF7 z^)`P#&#$+oJg-=q_uA+4e3)Cy^Y)h3+g6^}U0QGZ=kxq}JIeF+m*(v(&wITzZ&!KV zfzrI)<#`87^Y)bI9WKo)l;<5O&9lq%-YCs;R7gC2-kT(O407DOVtL-N(!9Opd2f~G zb(iNIEzRpG&+948>n+dgEzR3kp0}?wZ-05-@zT84RZsRw2w~_zNdgN)2h}q9=aA~j zKdVaRpBsr>U%*y=L-n-w$5l^jKTWG0`irqp zDOfW!Sk*WPh)4S~y@Z*ZP#5Zre4V|dIz?-BJb8zdzoVgz*i%QN6Ut5%HEDw)>|yq^ zdiE^y^0<;q?bSl*k5$`xJ8X^AQy&vg_>qW>(o-KXBlfO}_j-eHMWdjc2jvn^2 z1ar~V@zj1zavmSnBQ(t?c_{A_tF9aW%d@AB2w&;7?3B<$lff|0Dp<2MIPCec=dg!u ztY>Qg>{g2mgo6A&TyCz8XMWOgk0e|tQlVe3j^DDuyV#^jgnOSA!o3gVLsg~ud`9v= zpJ){;>O$9K86gUZhk*>m3KtD#Ylu(_!ZbicSF9U5Q6cSKS9on~SQV>qaY3;3sXGQx~eC@ec_#dEMish0m7) zCW9B(NgmSV{7-OwU97PAi!22FG1CRw%G(O-jT%_;^?iM4>LVWe-Eb}nJ;_fx@;~W? zvo(ik#|-*S|HKyWIpL&zdfk2eUA4OVW@L*MsQz)V@1MiI|0Iy9i3CE_=Z%5q>B%En zXr%k%$(pOd`sY$mdoF5!#7Lxj_WxjlF!ESFoPf zioE!2d{`HvFuZnAP;v+h2NStK8P_xbLE9N3yJ&QUx93lVpDJ6Jqa@$d;P{BE&~ou- zABZN4gb6O5;X(^8&Ip6$4$rCyinu-=S=b#mlnoj%^-sBpUe5_Mpt-x%E{Z0jc({C0Ez;bN zUoO(DwejS8n)~sA@r*(L4TPOPNJFBgz6`TTza33>Cj?J7Be5iwTo;#s=-31bsAyVu z7*S!%Eik_;spH#)IxaaL=aGCzw4c%ZyPHK?r*Qa6A>n(SegDwbrUW~?7D~G(f;n7- zIh;^3hl?saj4z{x^ z*t=xV*a|c@IY6x@J9#Q1WiO!E6i)s&_hA_$N+)*fQ>J_C_pDe}`ekv|z3Z470AB{+ z&v5*D5yylx<7geLYC-~jL!oKBL2oMDo|t47T({|hJ2WQj+#&J zKVf=5n-luG^@2vNQS99Di=a*}h02#4&jnCFWvFw%3~C2^d*seaBC3si{nd5`n3xETB^wvy77+z)6Dm*F2B@W z$}NB2<(9?Qh9Vt;oid7eU>S8ESbmc)XH21a2N)7z@9m3r1S<{y-#HF)$Ey=uj z7kKj#iszW<<U{LbVGj`A!u(&C|f84S)&UCt6$_ zCzR`?PFArCy#VN10NqdmbSd0HnrK_KH6gQ~Bg>fVy7udyO1D6j=3D z-%^{Q4UudZ5^}20CD@)W0bBI|*KXhsz0c6#%!p))8QNejoQMD`= z3WPVmEVmE2$AWImLO0&x_*X?-@VCS%omPW)7spFxeHQDw0uV0%#EFsuFQCB5FYLi? zweAXf@J>*D8>*iQ;@c5FT>@@9z?~^cn1_V3L5&@#@oq_h9VqZ#P+%ttoMQyMf&Clr z#5CdLXRf%5zh`C-%k4zH^Fh5`sP}#e;9aciC`s6jgb#xnyHVpp88vcg*u|jG9u)eh z1j-&j`B?MrXgB%F#|tX{8~ROL7~KJ1eVXevuH}}^5lv)(U*Rm2b&D5h5 z`Xmx_RROb1m=}`tH_LQ=@GRF0PHwSYDCU;wur^+**KS* zZEnuh%{i&d{86L4?w+_P^_=PDr}7mc^yb0R?72bqgCe_K$5^iw+0UE7U`AE&U`CYY z&nwCA5cv-=_#cY=`EEXc;bvPT4xIx$Pk=hOU%e4&t8}EnBL?_@k-ASD-I~~aqd0Y- zpSl&UWdtA0@z=OiN9phu1jBn+4DVrG74iVGUl5|qCGj4hb`VYp6s}J_A_ntFDX@h; zutCXowkI5|yVL^Ni~Q{52zE3#hI7B|@+3Mpbw$Zsn;a1hC9f!wVA@yc`0NaRt)qBs zJXZHvjzzjP4z67u)qUd+n&|8}_{4pzJZ(JE#_6sW3JosyyJL+9knuVWMSM2ZTCCgn z3?me&j-45hI;ks#^Z+M0fWK^)vRT*tIG69ogr@-H+N0wHeL{E7R(w>4l9QAyj<^ZB zq_~XHDV;x&r}o&a?@n!wha&vNpe=EJM5XIOxrMGcqAUEk*Abnu?@JG8Ro(8euUfp2 zgas78kSyZ4AuCH;)%MJQj(gbUp04V;u}Ap*>4oQ>=+~~Q=kPN{_`d1I10Lbkr`Il` zk40;yIgGzuD-FwTSJ~@KQG9CA{pKbuR;A4RGV3>ND}H~@n9uFI-!{5zg?!8^k{`uxu| zbzMnQy@}OmsfdQy#UiS?Y&U|LLCg59FT$?3A;0w=2ZEv_KL5#U9;h0C%TEXK#fVR4IA0eCx)k(lnORD|Mpww#byFC}TxNbb zioIxd6~xz%y4;VmbHqBU*>(5LoZ+Opb*?$JZ1X%0ynlw1+>MeP!#i_YS+S47=|3AJ zF=$Ha3bxpbG*`dJOib6I;H+`e9M=Wx3>^fXI1x6CO{_3|(^$Cr#>i&|;FsvuZgxJX zYpM`D(s$ZOcAD>0im5krev`!LOjA&jm%p=3E5clYu)0-Cd@A#1ND)#CYkpC4j;<+8 ze+$+T-B%PG(FKnQo94`tih88fg$Pk-2OrecCVb+!)(w+Hj@wt`k6|WV<^k*4FWpi3 z0q|Z!D2zf zt$R!IvMCImX%~j7p%To9O-PLeAEm_=kPF}ktnSDx(!F>`sWHOB}4{A`5RB$ z;v#qY-cl(d5k=@ z0`>94JovmWYc%d_!ieA)F~E$)bmjwD76eJ&sqg~h-FKnSgzt!aG_hVEBD2sxtQWp# zDhhUymeWa{(BF-Oon}?UC+(JZg#V2}#Tdd_)UN>$ci-xfoBa;_o`zK5sM1F4KL^ zbBXSGELg6q)aiQfym~3@lJ25#guhhh?;aPO)JnE-e~~BXdzc{PC)Svb_$b^7VylY7 z#kZ#I&Z5wTCDtN)o!M0s1wx?}e0E0N_42<{3=IX(1FUccl&mG4P9l3HGZf$E1Cb`_ zEy9m8I@>d(-qjV6c#IOy>MV*P5&83ZNU!%&s`TGoRRGcG(k!6X4zP#Ko%kO4?LEu@eWv1U<3YOfHPd?LE*r z*-$wBn|D3i-b-+b{LyY8qUPh~muR_F72o;Z6X))W%S&|F zO-L2uGL%%?gp!KCLRjP$vaiLVviiMHS!I}dXA;zDsGA}9%Y&~D~ z&z;xE-jY6RYV?G7*v`+t^gytB39r-F>8|NF)S7hY(tnvh>B#PZ*s(r1(_Yq-ALuyK zHUyjKW!i)vG84BkK&Y32P#1W=ZMrJ;BByRHI}|VA1l$skgk>NJujvr>aL(Dju=m&W zZ)hDvy`RHqNe|3%w|Vgh_fv|SHZ7~yMu|9$Yk6#l8fKFTrl9RwsC zRx^BZ?xZ%u;b{6n1FN_roP5x5T}JG3i%rI9x|<{6^jaO?t%T{3o3KuIzf)P@1S7Lg zx2A$>*$lV}fO~a7#S;#`28o1jw-ejY*B)8}CF|H{d$Q*>ix9R;c!HZ&`$0TOKxvo=ZtUL-bZUb(_tk^C!6(A zIQ5w>!^9$eT+g^fGj*KT`~XcmChTpIu}6_SI(v}e#nAy*1SsjnA?~}M1GNtT)qJLj zr!;u1Hlq3g&n0-JIGkTIS;6{gB(dO{#vOX<(};}7QKfOGAK7X)?(!oO36-z-v2Kd> z`LRiQ<8D9lwBESKkG!I1j{1@5bh7)A$$Dme5D7OH{K#9CjkX_|s%JL(ktup+14r(c z(j%%1%_FKM9vbqV;j`TuS?v{G$F8!hQGUXDk{nbEl4kF3@kU-u(x^u_~zWR8?M zA$gG*#HU^Mvu5j=V4-H|nP3U$=$T;EULp>Cgx`l3VantKd{xi9=@(o@C*3H&;2MgJ z^kb_j*5SwART>Za)pnVUhy2KX8t@}QEv;aU_G7P8-WWf&O3xhgtL@Y?9|qH@XFl*F z2dUDdex>L1jKDJC{@RB7>mKiOll-ag#BMWnN|FbL<-yW7_Gt$-IKX!3AQ({{I?Q|M z2H<{ogmJ$Z=Y6(O4q+4CTNGU65#mR}m3M!hi%h?zC*RUT`3muEQ+{o|T4rvtw(+o@ z+JKLYDSO%OxCo9i)J+U!Md&CA%|;j|+6Qzdo3$pcNJ&l}Ye@O)AiLJ)uS*@!VeqtVquR7^Hy+!ts zaY9NIA&>1f1>fYb+o;tLR*CjdGev3#MCh3AKCLL&pjVfeUg7-}iFQUfO&tSKi->QN z!SuT)E)9b_n9Td;uwgyvKGR_bZ#NV8Zrv-K?u~QdUKDmO^M{e_$DYyBJtVL8aO=l) zJm5$O0kYS0U-%|YE5hN(r&MsJBsmaW2i1j=5AjzciAIVpU-i9!6Lc&a;eQkCm+L;M zH!)dCOpy}LN{OjbVw#kAMoMf~hZr!-abO=t?93K5u~|(%8JC3xkL#W0n}Po@ja}CY zOR;J?Yv6c9v-ZWa``AlZk8o52`=gV*_Wk(4tjAOChYToV-_a|ojqsC-aED{yPnbIC z({MJSKdHCU{Ix)@g4x*T&PJBRmwy!}qp<9`l?=2AU9Amj7VE|?OC1*e#7H{@0olB-S_ zEGHKz8jd{-1dnV5j6YOirKI{5u&k}R7ySccYx{#^d&wQ!i^15!m6%mpzRK2cds`Jm zVE*JLtsmAAENq3lW>uBZH)+kP!j=5>yi?wWE!cyS4;wN>WEvE$9yROmi+liDK&8JL zMrsLPOr7S!KSJSpuP{$1us)r6hm||Yw3OOzggy@hCFggh7P0028GKP7{O6^hv_Aw& zV^GBcV`BhpF&iVF)m7@3CA?{?0b=zMUKL&4p%eQvQn8+Xi_p?5Yw4!fn*i<7uM zpS|nCY%M#}Oc&fiw5re^6@@VME^*l5nm42`cD!*vvGITx~?r%&lN4mUJX07RK%5@@ItHLRxkM)3AXrXHLAY;pQ@Uo zs=yL_FP6V;qVd_{e!5so^hKJaAFh=);u|LFekSv;vvhHQ$h=xzeaQ@|ZK3yIuNeL% zQ7{&j9dL!PF$P60)=9~NqQG~MM$-(E6m>VwA=n-k@bFmfGT+*Htbst1?pUCi>n#$7CARd#Mptnr zSQDexX$T#*bB`M4=b{jI_-*y!QgP?!^l#`*{2b~}U{|YalB)fE;r5nGkwWH=tv4b! zEszs$nLko;mm)2nd1)`miQ`E7EYfn>OIs)>-bUKxNXr#3ZIPTffwU`;mOd|Sv79)G zv_4!>UDV-3sX|Ck{J8NW-TT_h5$-2-QyE%M-Mbzabb8=I?h_qOtcRC6<$28T{CK`& zvL8=7H_j+nu9`SO)=P~ijx~}6pFdW4P2o;fxm%{go$gqBlo01a0=FwM&h&p zMZrRLACyYcs%5a3HL-#(U(-{t9y1bS zqwKQa(Wv#N>#{&730(7=t23=*@y^(faokhgv%G~Jd`h_22hZfHn4N86{l^*BTk+y_ zH!!pAz8Q(p(XQ+q%4`Zc6bfC^W!Ebf;J)WxvK$rmJgG?rUvv~wPaAx1sQa!Psqi^d z77I5eCU=LZ&-)I`8&D`c51TW*E#CexuW9}s$QK7CinV8 zTojq_YSc@UV5_Us_R;g5X}0yT z*8EE~dos>NKNhW>jI(J?BrHnTQZ}w04O!B(`b?*h>5EzK#MzYvUwGxO<*j{f@*#v> zpCJz#nxYwhSK{-9B`K;0hZ*U67(ZMd;vOk^Djp(q%S|NwoL~UEywW)SlTR5T@rl5K zHPOh^Dbtw8*9278!_1Hn@elro+B$)cBMkqOC+-`)K?8I5QpQ7uBvn+3Ju}i!waTi0 z6#i}>loFT3l8ZMA&l+yvIWI7m0@Dn;d72^Ro}ut`!!9&WH{@KGQLwEYVcXgnh7Ct* zzJaeRuEyWo41+ez`+kWcD1-X5%O$dls&gM63g`!69uI)6_F8j=T#WGL@C@#Y1XL zQqILg8pR$l6r-~0=i()d2FL!o@H+QxS0i~gj<+qE^^hKAw<^yXbY@Xv4fWe8Vu+p( zI@VEcV;$vaLsl!RzQa&^`BBOB!W9yr^%emP>s?GOFiIYn9OSxVxJUmkyiOLZXACtKASy|;8s{jig zGgPUv>K;0wsZ;rQ=2EPX{4g%Led{tZqohoSlzOsS7K{64IJL}>3T`_bPAy?NG|o^6 zk||W!bBsWRjeB8kg})oJ)QAmb-8%JkF}`I&6NSl2cHr@nNa>I;ZAeLtlw|aYin~3T zXex-|j*$H*y(Kc2niEoQF}6NV8?Lg7&7>9^o~_+t{xV8tDEPl82*)d{{>gujlRPDd zK%_7O?Q!C1#+VYZ9KuK(nZsj$%62~nNN_!ZU53qMtcuNnKa1v@Koe~1f|hSebymyb zTQ6Mdf`$YPoy6?J81+P}?^N z2%*qKL!*Vm6GNN|G&t+C82(<45d?hT&X8!`1i^gDP-rsm7HB&%j#C2mmYF*`@^|z& zv}c8lyi=cgnHh_j1`3XfyUzqB5sp+YXE>9 zBfyFcINRVGz@>^GPQ0Sji4$zBo?57=G(#*R_;?Wcj*t`ZQ6PS5^Tja4$0i=t8z&hA zMjJdhq+BEdd`IXbBEVYc0ukVv&}7~!-xbsRD9;|u`{>B|6`VjNelIOF0j-E=^Y zVXtp!M1udtbNox>`vsd z@CGCGpjT#%k-CWV#%GNDbwVO3SZfVFTt{f-@rN{`IP%A|p1%rx!UyWJym>(QQ;V2f zck$BRE>`D>xNoiO%^bzuiSwerJ{yP9aXQnnlyCv;ZMbvVptwt@CI~cf*vGKek=kg; z4pUK&aGcYI^TUhM60mPzxOJb0D|T{u;hBa)r0vJ0g$W}z8{Ao}AEe~of|J~2NYFbL z{loE19FF`_wwT(&7&MFVbb<_EF5$p9o{YQYg#-XQd*KnC6A_IDQDMS=J~|(qEox7U zyEY`^%%q;#o|!)n$Szo~@!}3H0a(NKGu~@}_R=$`6Hd(vHjgL5kCA-9kso3U1#bw@ zOFPjSPmb25;doF(EZ}B7vjr6&B4OY{gxF&68)akg7nXsI2r~@q)1Mm@4|4tHOz}SV zs!3=7OYTq2cpWzQb9k1&RetJMqh3I`fz>@tqv- zZz%s%+~u@<1`w%};%;-EPGM+cQJy-_V-}ivQ)( zcnf0$>VbtN0@6-!poB9&LHQ%b??g5r9M8P#JYFn4${Edar;`e?Q=TFuwGw|u280S=Q&LGO- z8=~c0DNJxu>-mEEMK0Tq5-_xFdS{kzm{%BZpgV!K1JBwsbRJw5_7UkT@q4etg^Tp` z2W$u+tV0OGwJ4R$M~FRAZ(MG`Ac2m}MFtyI zA$df9=7=c0Q1>%0NJSQ-h{{DCLJ?IIX%2!vZ+y{%w*-YX3M@rHr@%4MG2zPe;wf1xh9Wnk{FU_eu%E?L9$*#9Q&UT8KP)LzR>&F=s%^|rzEYd)d! zdH16ese^RfWwwOtDwC6L$24@;5QArfPMD{3wh zYy`oAP@H_-2u8!u*O9ro%ZlRo&y3WCQpccYc#~EywV%1yjXcl%5%#@?^=ZsiBp(rq zkv)AVJ_QpFjk*C7=vjNm!{)2{YES(@8+G?(cc^$*3MT;o^0JTD;;WGq-tDa;ZOU4t2;+0g`9KxE z6j1RM(R%7QuhTZ3`TLp2)T18%{pueE&al^nWscx8e+A1iti3uHoTWhbB#b!yRM*aY zJ1@>%Vj5O?Y&s)puE(&clTo)?Q71C8M3Dun{1*3h2lw^=a`qM^HgDRjU6!p~eCK~=E-pyfO~3DF3%oOP=FEQP%*9)!TcZo@kQ9k2A{ymgm3;bTx=?&4M#GmJc_oxiU)AWG|rY`a@ z8XK9m$uCLonk!R#-PTu*zwYy-_KB#KdTNam$T#(-cYB!`gjco6;m>CV2UyHt4n#WV zvu#0{K50oVC){W}1fjWnX=F}D7D^*c8M&`CGA|=LN+Sz0va>XDpN#A(jqH$-`%5D` zW#oa<$SxWAj?d!}5|S({^C&Ko4*Fce%4i~*e8!n-jHx$f0wa9G1}m? z9zunOeLbAiAMy2YN`KVX!zul{KB>vfCW?KQOZq9{QkV2!Lsv&3p;NMFe!wT@pILk6 zCA_qqqp5Mum(RiIIBlVy(#&m99g-yGAmb>=Wv8A?ck`?h;Vuf_mW+zyLp~w0+(_oK zk5)_*FSBDD&5Bf?`pndIq};&y9EE<0rOUR5dFsBv`{j9GaqYiZWeUM+(8!|d`&nO` z<(2HLFZq0MO&gwY0RNVp^v?%@#;kun=#1Z@BX`>X=ptBI{Uur9FB>^G%bcr&^{ayp z&V($C>8vmId=M7YFZtptg0!tXrDD0Aux?IzRC!BDl|_?p`g%3?WGUs@3ixeRMA$C+ z1HxDMFXcYPUG?WI1Af*QpQK_tJ?rBJ4PJwDequ1!d9Xs;b8Vmb z#2L^pZqQ+@%;%xpEa`sMCmn=R#&e-sJY}hL6ui$fKQ=Y&6osAU(d*X>TtW(vGu)b= ziq>3l%KiO3H#mLWH_W<9zTgwPQjJWAk5`$N?5MK3s?^l@S;o3lVg2Gayzy&p48TM+ zev?1iR9uwN$I~hnc zhzs1$m57+{9VRY9th9@al#K>l@!?;3T9^*UscB)zDez?g)FBdAef3v;j?5LS=XKt$ zM)6$zn76BYLAQvNom|4oiV5FThA9*utvamPec4*hiH}1j1R0Wli${}BxIDSCQ4w2jKzcG}cK@i=Zs3Lf?>xLn?|#C#Y0 zExJTrdt0wWXG>m*Q1-4W2ESOAHFj5JDIsg@!IJyfCsesV2G7a|`C3P_wWnS2d7blIU*+zF^|kmo8VjNcMaJ%;Fa5kWEF?7RFZv$gUzDG>co#tpAbP=A5|}X;ee8Gw z*Z^CQ&A8}`uXb_6?dwc0g3?~i$3A||;~ol$X6}<&DQPFOK-YPO+|7jO6CX?+Q;VT< zgkWl6C8(Y$nXMbV>->vQm;|Pw;w zR$k0O@Lo>fvGy#!$;9$$IR!guq%QmFVh2^>Gn*aa71h>c$}sqA0(6&Xu%K5rnO;3& zLs@s5mwoB?)bvHqF!9o479phm5)*Mo(83rTj$6E*zbLA-pJ!&a_Xys$4zPfE&Jrpu zUIe4s()tsjKBsbL@#4E853v`0*0nJvpXDfsC!(d#%p+agL%7$FUGS3KJh+dG3RsHU zSC~`H@dxryDQ&$GZ-b)r9Z@~Xg6xKh_SM<85NVJrtu6FGm1#Xen39DauG(*Y%_`2C zlJ^;*$}3sgb3VrT|AH^iCPS#R9XY96JF5O> zN0sAVc%nq?;a(^pf`PKirW7BNzKXXVI5#5jBXTXC_d&^Dmq)~-@`#weW>bx8Jr0o) z;z`BcpN8W8%vA2#jsu30x#mM2I$*R{J~&DRYlnRA%JQzs>EWchEBzd8VR(ffty;YQ z^#6qSH!R-&GoTWm%LWOz~tc%W|y_(|bU{^mw zbvDgVL2cNGJ#y`bN8~;;PkI-XBJnB?5@IOs;ierMzAuZsyl@vD<#Z6Z& zzYw_IzzegmAvVLV=N!~?W=Y05$oPy6Fc$!`N&w~pU^Ye%?&1?O!oB<+d>z9z13+_B zThnT;3ZDXuA^#}o**vvqV}Bm2GOM`}=XD*g@r22ZT-K_OYSUl^WKaUc4nQUS!pa%w$?sr4){Kd)N8c5|^8R7Y$H z+n!xQ1L^UE#q|yydvUd6?DWfPofmqfzF*ZdwO#ePJh10geE(%HcZrnoK~?e%SL|$s zBWrwEm96VvpK?98M2)wrE(_vVRsTAQr6{Krb-b#schre0k@p48i^xF^<(aTU`A5f6 zqLXe9tP%ZV{XaUI{;|W1&k8%RCYGu2hgl5{b9R`$i56-q@feXzrssqU@({ON?d6sI zzUs^(^wWxxj$KLZXUDj9$1YXn#rrbrW4ZOQ!unXL$_2hkj>2jnUZoaqf|p_JSKHwg z2(RH$jEi()iNo+*BMs8^3a^x5Hrl&XHO^F7F1#XZcmslFd3>$Pj&{}()sbGDSF0@h z8C&ibmOlGH+asTvTZwlP6&KU}#r7cdE`KZpiGOTLF~Oo20T>`t;fVYP%;$90I=WEl z<{@r=>}Yfs?uOk1lx4C}gS@EB18$dow7VNl0kNgMWcR=-^uWv9-IdH|E$1f2)~Qk< zc)eQmzrLzShBk1Qv0*3bg|k)g$s-tue0*M55gj?J(oD?_3yk!QhENz9z+BvyWbSiS zrZf?Z-d^M~1<wLfs?#mRait9lDxi|B6cp;aG$qg05iC<0n|NftA$zEJE?%R;eyAGXq=MgD zW__&W4>$}^;cEB^R{IqO_po-FzGf>whb-l1so*;|Otphj@(H1o48S}9b=xc-8$5v# z1i+mvnxuYd$y_7_D&f3Q=}_E`Y#5$6Hrq?uUAm;dXf0_NJ*0-?`1{>Fwb_gpoC?&p?02}s9RrC^p?DIiMx{_`_i&p7&fj|8O!yDoJ|YEoq031D2xsDZ3dvT zbiSsFMKKfO^n7fbu0Vx7FP!7C6h;+?O+#rHMtIGdc6l3O+^A|7&}L0#H59;vLp?}h zO{sFDC$j9iH%os(;dgPFFksaYVBn*=Gt>*t;n=OhjZ7F6N?g$xH>+&tyn8@z{C{1t znI~IymrMUS)lq0V zi@q7J8%^QZcwK4ffS3wzJJchra6@Us|C6+4D-Avb899w#a)M4VSel7Rx*40KQ|r>t zhHcw*rs}4y`hQ^uazxm+cpavcT1P&1GUT+p7j{;B3tc~wp=!G;zr-L`L{CV~9wdRU z%Eo8rYn3d0D33b_)tHa-b-(JS>V~KiwZ(j~a)2a&ML}^32D6W7wJRflHJ%K)X zDr`^A{xn0L?iC)>D?GMWcw9-iuEf2xNQV)!PA&{q>qhHlYBW|~m+JAl{fW=kPNV6P zn66tYVmt$<>#jJ>!iulnp$;Y(RRDogWx+sVRjX+WP$C1rMU&C>98bpQiQYN?pezYH zTou9Ke%0l1xf`Wbc+;2=uKFuFMbea>7-p`jsX{Pb_hh~I?03UJ3;>Nu;X>A!9L}=I z_-DgeV+wyO{5>_C9b`-c1RZo?ggJ~gctGvp&gdQ0a%c2{BwY7bXSRS@JXpeo9->2) z%MIgdY)+^S=7gGl)|oeEgj3Th@?Vdn##iQR`$)fvhkF)rq&J`YKy_v-N;q0_Fnt$i zC9j`_+}gv?Yk5z-!*B4r#G)zf)PIjS-sgjCQ5<1A4jJ2k^n;Q^7e{<(pUSp@#XClv zh0hmA+;7J2SLC(taU6pDMc<;^)CZ5LaODjxVHIkZKZ0Vj(b{AT7~++87meQZv#wzf zNX%KztR6e-@>DRdvt6~~r1O`PDl9Jzu+g|FH(!XF`NCsYT`pleY&#uS3-PP0I(kZl zwXE+K-8ytYl85yq68EEy`Sj8-nlRHVZHKbG{cWoBhykkfk!l%nTo&$S#PORr#{Ck{ z4khQ!Gkh2v!0P2hQ4%i&>t6~wSY7E+a8!HMT$=vDk4Y|W@~z)lKB7LJ8~D{udF05c zxth!#)eP0JT$b%HWO+EgU+w8I1UBf@$9523R_n;@R@pi=G0KOFwuR1djrhIqaV5Tw z`L^>M$BJ??=7I|C^ZTTOl2Ea{+U`0k(q$KUtfWXNJ7O-VY}ziT3TN&RVKu33N?xj< zbTH&4F5!DZtMaOl^7|z^!^G0W&Hxsp{+G_CUv{$3FZN)RukYiuJg$DMdOSY$B%a;! zF`AS^;YfE6E!>rYf#N`Sdr9CnI}m{xwl3Alrh%TZ`vxEBk$ArlSIUQJXpXDxA?Ye) zR#-5>k1PGDkDL{MDG=>foq3eVGni(}yXtaza~V6RI)>r(9=T_W-0`h^B`3P8pbwxK zA)EF=1<)X8o{nU&(Ec{&-IOMGNsBM@I1z0z;nk*Sm=3Cy+!xO(qC4k=i8{><6OUlb z3ulG<^L%Z0m5uffo%DeLm40+`9!2yWyRfGU;$Nfm%5KIdRJkg?0s&q0XjR`kK$NV6 zGI=V~Z^5G!_zlu-pIP6>Wwqo};w^ouFha;uvG))0q2}4Jye(!tsB@VpOt^#jIyXSn z8)9EpCDOCK&4T4>bT^k&+N8BibREYEuQ_X5O+K*^ z$}kYis?emY{0iHW-D~*}0?dMNG1>Mdbfvi9sqSq4Yp*&;bUJmeS|kC8GpQ!ee;lpH6SQ@3>gB|C#DjjI%=RpK8ABd@A-wJBQ-w<>;U zgHKmLxH_Q=XNLYktiZ+L;v(?sz4lJ-vdtoDxd!@kPjF<)FP5`1sI z*kyTBRsfsaf4LS8M~feN`QPMq1D_-kO+i@s{0uC@Y$-cN8ReI|AeJH0U=P=iMG_@G z{kgClkF4>0n8z24-e`aEqIWM==~I3e(cGBj(~O{!J!Sce9?O=8EstfyjhTXRw#D;_ zwE!3Wv>(^RcM)r`q6cg7OZtECxA>A6cY0&|wlCMQez&6f;r*w^nC7i})CEtj>_EFA z?-mZV8%8O{l5l2}($LButxCf#w{*GPKXXRS)jp6prRMIr?8=-}bH)qd%xSE+aelaa zeV8iqt@>{{n!Z)yIz3EHc#NTsj>YNI$X}rJa zRrMslSom$Psweryg4)hLS##-AYV8x{!tNsic)losKE=-$HoA`hFr@_GBLGab0Zs#8 zS_uF*ugwNH1Ayrz0A~O&!*98HT^astH?My#9)!LHy*t0n*gxZkTymXYql-7p=T-QR z1xtNN%@r~qt59IRjDlwQ!P+woyQrq$2*Nb0-{)jq_+(=)eE}=t5_*|3r7x@T*&&F! z&++4e4n{L!sFlq_SAx5$V|&b$%ZwM8M$YvY?_)2p^;SQg48Fxe9;(qE=4;PnW4bLA zA189=V%Jp%C@g9ee=F!fq_p$OjeA4v+Ff$mQzDz+XHKzAXdSQOg;=6W0~V|RVG_p* zaShTtk-5n!%V1K6PuGNkH)*kEEUh4Mdjw@1WNLKxPfhX`ah=ztmFq$=hrFOEa;vHC zZgzS!pSw>mywm*Xrv-j_RnD#6hE`vtPNWl6Y43;*!K05K7w|y=o{w)1vJRFXSJw@) zZCLuYV(#48Z#g3R-J>;f8Z3`tT)d|{&Z7NNJ_-6sn~;ikHTUu<`>xU zg>hkOfeCkBeZB+2N8noocs@p>SR*((s?f49@q^35S=fEIu-%FMS)MQ<*%6yclb-95 z^k!+&^L~$8itk@g3y>htS$05{!XH~5T5Umi6F`sC3|5y<`sh2UV z49nVNjaA`nqgTF8P|E5sF|ZdsSz`_2)0d=akI-y6$ZO?ZPoL`mI?0YdZ{!M zr{Z3%(OXCB$m+m~pCX391wSbxzsr%AJfZve!(P1dFGXx2-<5g>Fd2rBk8T-F0{Ll401rGU$Mxqrvv zuD0Kx(yk}C z*TOuY(TA}ntM%laOOcOhzn-|Z6b z)@I(=?2S)lGuW?`#%{4;$GM-nJ zil7y)XvMyoi{Iv}n%B8AH0;!7{B$|yz#D$+-D0)8{`+=y<_ma#W0pfO37h;7X)fO9 zy@@<(f9pQ)4iOY>@OPy6$#gg`c&min*z8w4F1I^35Ju&<_Lpd5x{sB zV&(m{;eTr$;!Xa}P##i-xGTPIz3B}Y;-ew?Qes`$*c6Vf3oDY=CtnPwUkV$G6c)L{ zS)$RGdO2KeIoo_xe(O%o(v3G+#Vxiz>|h!RZ7@VYKmHq3989`2y6;Bq z0sR)B1@%kJ(WO}z4__+J(OpoJf7>!i;@J>(C_@~t@H5)0VTbDp$7^AS`*)7l!w%0A zjvaol&*h%%WMhZkJXO18h&!LA2D}kYZwwoAUF{&PR?Mcbu_N4GK^;~%YzS9nXPBm` z@ix(i^qXPB+1{2lb`y1$^gQ-f*kPFERd^~&-c3E>w!j)avFCJme2Y`5m`wN?a8I~u zl&}{U+ZSh2I~nzNP<6`I z5eED4nb|5TMMAHvP)-4x=>JJ?O-K3K#JxRJbHe39ZeW40`rYEiiMpjTEMA<5N2#V^ zP%6HB_kbpsa~t3BZx2I}@a>+{d{^&j{=MqVOic6Le)~3jj~|BJ!`AKgGk#*l=lO{h zp9RRh#Ep1;53rn(U$0?lzj=P*xd|>k#~(ZDa%JZFk(f)*@~3t=@tJIZ3`35yJ}9%o zA{6ZYe~4=@&Z0Qhd}XCV3>5SfhVA&F?9L^MMcm}aQ|HB{dPs=U9{$pVr~ zyevEf{id7r;TgbY2>VFSzoKxYZS8~}9L0M7tmzh8P(F}lKt${PD|a{kE8 zPb4<*TmZjghqDMDEWw@y_Cq$nYyccC0hkSdBV1gKXn<6T=Z3 zQi3;TI7%uAYeOZ`qZFV?&#z49-9i~tFXK}X#pFZ`EDsh4IjVx_y>|eby#T4U!jFWP z9T2DHR_0-|FrVxv-CXo*ZR!WbMmp{74mb5f!`aIAsi0YZDyXE#D2Or(Le-*RvK&hx zz7)r(=V9oo5MRw3{0DTD@@wVnGTLt(2(!~mhvP%P+pYDWfC<5oaAH*@?^>%W3C~f+ z<1AXZ+4!XLH7WS9x-z`8yPN*7&s}j)SmGBxLIyx}=U!O@M9ZWp)FtUX;pL%UK^7z@ z0bC_tmNkEqhX- zT3Q9XAGRQ}(ttu~j$#w4nA2Ip-NiTTp<-BC*TFS9WBed2dWSmFeP>6yOFPnSb)=g+ zl6Pd;k&h_vDVLS2Y}0cu5%q}U#Y%@Wvfp4!-gEkct(7aFET5_!Y{TzFZ$H% z!$*4I2^(z)A7%I5-0gUHBp%!!gs|lYAv5+tNErm11UcJub+ziO?H;5!1AV9pjuQaa z*uXzc%6x2%AFt;p{Vum(`>hc8=Vl(4e05@HB?QT0J1Z6Deo(`@k{V*g3lX_Kq9gOd zt2`=EQs>=NXY(;OOGCxR*YXv_DI#)yP2)?66@KpptnW zH0Lw}2W7?MFENHQ79*L+hd+mt{DI^ozmAv2A|E)H`M?u`51b1=@SNXI{LMy%aQ&?Q zXO5UYS2sQi=jGutpXvzH>8>-J z?h2>&hbw+T05dDo2g1e$OQL|!l8Je`aWQOM3L77Xjmu%J_#gTAkZdm?W17EJsl^gigA@1anaR1oR zaPfxrM>KT8`d)urTouyqh7(7^`9?Q=#EyiO^n2kdmcpa(Aj|AtUOJ7poyV*f&e$*X zWK1slElGg)wt06aVoJtSTp!9GSyHwDdF#L5;N{{f20VJ zx)wH%?SV{#mnDAky}Wh9`u7Iv=Z*kd|30WIEnNjm-o;VH@_toDQv`-_kJEaEeegjA zijV+)1}MnrPQ40;x$Z%xF+39cAPk8AK@f{{#=eNK!!m4hM7{z3LEQg0x{WR!QH`GY zEYkv@%X-YLRA7l0-jqHJ*H$cU^wui)QWw8KpHBLGfkmHg`5{Wv4^IXa&)(CmJa5*j z;xB$4hX(0wKLle^EZ)&o>@+>)pwz&??N4K(FM>wH+<<8YF$+3APDS< zyh2i|Q*W0J&rz9f+J;T1<_ba7H!3AlqThO3`~qnf zz@r;^y%O7~2xVJ3-*kzobhx`4#SYg{?4wldvpSKbXLTnUSr~+%F(Q)XDBMdW^Obpy zp{B$NR4PRz79-DM-N`-8u?|ObDMn*t35rBk&vOibv$`^?hE@2*T^@Eiqu9JuF+^D2odSi>Tt z=1&noo`RKv6+q}4Jv0~?FhhwQ3oFvvJTBc(X5Jdh7R~8mQMnN?q z(r?wQrGH#5{U23lUdGZNt=WtFDa}WeNnCi;)fF1u58<-wX$_)5nuzWH5NO6nK4cwf*5gRCfi|L7zz>V3aO*X7RD@qs3z=8^czAf#4@1MR`Qg~} zbyRAcR=iVM4|aRJ9o~TO1Uvi+dq}jyuOd9j4!?%*WX(2+@)`=5VgtMmfT@~wOL9W? z&tFz&?k2!#8tnDf2ouq@PZ+I{hnz4DeWW6H&vwNajo=a3c5c($E{_*|m%Jx^q{7Uz zedp<#W#9Q^xM<(`LE7$W@us7>2=|W(_m+yB@rm;xC9|cX;RQWsT*3DXI!ZCF;`>NN z!&04vms3miB>lgof_8oSO`U=a>P&02DGAQ{iotdSXK7HghYXDQs6tVAlW1s{=-PFP zF>?aXYz^*S%NP$7W74PaWIBgqi6J(giX?4qnYqQbeZ+1168%5;$JDlapvF=%t+JYn zR?$y$1OKeKxMi^L_1B6by6nw7eLCFSRbXFlpnL^l?b&o0ZGVN-X_m^~BQ9J(k>+c- zC5x6GjJEm3)Z2<7UZCN2J;;LoTm7*e?DlgZXZophV!;ldc*Bxkp5mAA7@cFE8apYg znRKs_DhoC3(x6)GZ6rX$h6oQ!r%O35rmc`s%EPtW=`adsGd_~09uqWmvF1@d>;%FR z82=quQ+c&j(*pBHd{-e|SFjI~HG+?pcU!siHox(7gfF+wgkxvJ4vW*Z!iJZ3*=2h1 zs$-e%toVZBc+$F?a;4AVTFfJ^#kz1i&So6%rmbK#DxUnjzF0_ZbLfn73EB+|%zaZt!Efkksgo?VTrt)zd7&wB^Uks;- z|C*_^3kmpQU%>{mxrn|TQEtVFIgr_G)P#}giRbE&cFTubN(-AxWd1ube8Y- z=XgS7@^ZTG0j1pwbnP}-GeOptme4&D0LyHEJpfo<0cgfY%5@#m|pMtYX-+Mhxq25l09r*V2@@o{vT2DBN-IG zmp7;%&<3?qH_oV;m3qUD3gc#3W=BQCD&6QR&#clLwpSQ8$}-z48dmGI-Ko`j!y4LI z>gvCyq}JfvmeX+QDMT$WjwX$x9FfdPR3UEzoM6Xo8k{85bJG$hLb=$95G3$TV`>Ca z<+aXiY(zvcMk;Km@l24u>dyp;n@FtI2Xv-a>l+6U6HFCRU8*y497XKoS&$@@56^Qt zfDDIoW_#Nr?VWjc%w9_e3&QMdq1AibkL>{h_Qv!Gf>~omB%5vN0P!)6nIewDc~%-B zELqkO=-`os!^x2m>%2R`J$Mj}?LSjJIyL(OxJ#pMfLl-~!bs&6I@+J}V-}VRZJ4{@ zhgoQ((3CnYrWDHAuTgt^4~xvX^m#fnmb04nW-px+x=i5sLa9d0KucwBU@NX$fbQL* zKFtBGc)59jZ$s_ydA<$RaD5A>&qDR^9O#b8*EQu>4%Bt&mI$`YAon8$nks}AUq$9L zQL88GpLAp{_<=qb|Cpa94-rq`^^H}8h2ff}fr2d%9Uw&`!5P5dI#oJ-)Nl6lz*o({ z)~!?khtA?PUg@m zy?xjaH-qK`s0mOQ)wf_Z;R?nA_7blwnP%@W{g}g1@$bAV$KUrmDmwWSuq)a8DMi2M z$L|M}xW1UUqXH7%6#XGZ|2e*7mV0T@h3+CA@B|9aobV@Zx)S##?@RBksK1X5OjZ=J zR{Rs;t9SypzeoH-e3ZS>*UyVS2cJM?$e>a&0B(G(pc7*3)D#(XVch^}ts7c0wQkk- z#rE!Q_cyz{f7IRmOa8wd*Jjkqbt#%lCs5H!VW;&|zkDfL$^k$aKnC@-?_lxUlY^%b$#7nTGyO$xzbVSFGK21MZA)m&b$uUX8} z&blsMV09hB#3N97*h_*mdd;|E)M>-I3ifiN>KBGdr#i!ib$~ikX)nZ zY8C1awBBA3IAI>wsRXJoW)fqxDG> zKR@Y@ou++oq_SzL?v<;a2v0vTe|MC2T+XUXp6|j%t*Kfcd{(t~{MW1X;s57q^%zg{ zbE@?rSL-Q7iW2mvO8l{li6s3?pADL^kNwV}9iKzqpujk=)r=kUD{_SL!-jR{n6B}p zoAk%767oj1%7a~}s7%VuGY~%e!J3R@*FRX;SBw04dsYAIPaeX-r&3+ya9A}G`OCQ*=YOs z`*tbX_^61}Mu||7LJ2aHL=s*CYjwfir@OIFU)1m@ zNfc5LhjwY2oApoptZdIH+gaY;%yje3uZDL^2y!Ra>6u&pT=GRdbDRFG)d@gwmmf3b z{X42@ccsa`9W9ZXTmH~@u;nX@Xnueb8VapVgzs}fx+oDcV|@8F6fbikG;~1c(2l(7 ztDeTc2R2wu%a#xp(f=7;m?Dx?c?$PaBPa)T>Kb7k# zchP)G&JGga{D~}s5>G{9Peou8OPhbsN#8XMh9F$wa+-dv>BrPXMUUL z*NeM}M&ECJ^sD|>rC5@^yU}O$SjU zKi7y@)`o86@Mrjsr>0U?(vumlK@lyNNY|$w6u{_;^Mv#F|NGx>1;<1VDIY2)m0Q8F z5&yV|L-8u!@pR&*KNBJjSHECzV#MLD2nHub9G-MAI630*dNgfH#NneqQzH(Q{!EKF z{FLFT02ExHTg0ZOJ_+VxD?&=0V7PMexf-^4taz*dm3dvUtdm~3)%fmH*w&n)A);1Ex6xs9s+MgZ*1^THpS>LKQ1Lk$bH-x=&=YyA zq#!I$!Q+&a!mLt`*Na~)lA-K;SIqjMndw@Nc_<6**nperpAl#+Zs~Kg(@HdRX~Kck zGe16CQ*y~mm4uZ0X_{t!RyXCCJF*HK7l6XhId(GmRl%guqQ~gR&UhTM?~8kon^k8z zN~r-?4!#J4Ggw-~%?pNhnzX`$cd@%XjV(A+-x8;W$x12lkNqq;#*>NY0ifp##*5M6 z8Tk!;XjhI3GV=>!=R>p>JekQFHWNJ5>UJMMb1}bQJ^>fd0kY+d?$2y`qZH{xms2EQh+ziKl%JZFJMLKRr9ef`k+A zb0N$>F~z1Z!z98(Gf!)ldWkv+y>w+^jda_iP90mfP zZQP6LYr$bOQH5=D3SJF1^%FiA$7oIcG7~gAX{6RvA(F;v!Y)Bh?yQ-qTCTXswu{zT zT5TAuL`AJb(6Xsq{rU=bW-P4;7IMenHp0)gLUDMM3lP(#vagqZU6{tEHI39s1Xp;!$lgD*NK0(?#%QX-I$Z!N zacr1so@UI8n3gwy1=J=&m0qZssb?Y$Xd)p^FVNwd#`I==gU^e#$vC786hyYjw0IILCSO8=|o4-Qh(K1lf zsLO(Hn`?&`Aw16xFGM&SuoUcO^&ZpV8vYIbsSs5925j&Jobu&%Z-sHZJhQi=;c$g< zqC9iBq9IpdoGj1eDjLiR<5YRZtZ3M%8>h=N8}){Kg>j}lldovlq#OTFW)n^i3j)1e z0xv{GAOt1Ehs){z!H-e7_d%g6UvO1`&eD1Zafz;)1!pdGz6cg;CyLBO$e0|cxxrq(wo?&=Z)D)Y?tl~!8Q)T zL3S!WH{wie)hWfyH+mD>bzFkPw(HKsHu1SlcgDBsURXh36mtcNfh8#B2^4ex1PW8g zvr_Z8Mm|P2PoN{Bi~6@mMAZqgyU`cV3dHk(*ayTu$}AD%QzB+Wb9&Yb+2@PwbN)nj z6ui8oe2@yoMyTVK^iqtw6W|NB}a0ZP>)BweMt9r}-Ke!|?$l+XsgMwo* z&IyioiJ|t1g_L`NKr|l@sh@+Py*GwsbmDcp{ia>`@_^Ij_V%!Hvy5^ni#gW{0Z-uO z0xA`+t#`t&u;Et)@JRgExq)VzlwhbElsK zuZxtbfkF;pHRGfbX)4f&i>U)mE{=c57MAE=6p`r9ViUr*9ys!(w-sz)n<(PNJBnBv z={eDCkXmf7OUbOz62!}{(CRCdrb=S`S21Ii z!-ol=_{*9LZ^OCtGOefwx=fQdAR*Y|KsaW)l=u!+!2<)(fz2_CrNobWdD(6tPQdN? zy}b72&J7dUx*e>)9fagxYYpcZS>_sK3zQ7)0z>SFKuRI~Y((7uI@wU3an9N3{)GZ$ zc{)~bQLsqL;-CZ%k(13rQaEq0~C z1)b7Fm)7VtMtPt=VzTnc{jK-rrKKCw*zSgl^I^DVZ1hr{_UH)nh%|Z;qz;I$(VVyr zM^{NFkNA|ZmenR1CE;0j^HuKsKgiyPq|Psei$mF|vUL61EYnw2j;BJ3BeL^sq=HU^ zY$IWc9p8<3A=dUXi?x|F2K{LeoOo${(SBzBKyN>SiONa?-KE&Vy$N#t2vB_zbV z@QwTUugUv(O3tIrZPIEG^;mlw`;g58G*2Z{BXKpqr}$|6M!+3(du|BPNH6^pEqO&w zCtN<_Wc+Z>Oomzuv6nPwzW!n7Ak#Rga`y(f$6h5$fj&zTTzW}Fz{Qs9PK?bj9P|t2 zO4V#0;)e=~ztP{Q-qkW-h}U>mt$LV`1eysuU6JM?0bxWk(5T%-wSlUhNNM!R`Uf;N zA{kKW)Is?hJs7zGEW>?Jt^6}q@bij1l)}w@C^=qXU3*TW|O#2G~UE# zF1=ne-l87eprIEbj1q8r-JY?mt=L)DB_32aXgJ)BupU%#oJVAAyXK^sY48f)K(lda zgve8DSwv|bp!0k+n)LSO=3%13d1F>(d-Jf+4?r8J+|7}E*4ToBfygV>kwqYv-lV0L z)1Qqv_fcGItL9{Z3p!B_6DLZ>vW;239F-K?tSNMGM&$O0FlANXdsIrxrC-c_4w7kk@;p8)mc-L9aT2j9y|meO@Hoz6D$s#MH1ACK5X%GCFI#9bt08y}!$j-W|yn(ksN!=JAzU5y1#HTgtPJom|nspas*V zB_5!8^{Q6?fThA{76xGZyB9NpccTYdn;wAJHOL`qpE~Vk)s43UuqQlRS_$*i5EPvk zcF`Ddqj@JO&=64d?}v0px5sGHEAcwfh(K(orZhbu3=Fcr3yQtXv2D1f;~B_N0(vXa zcq)KSE0zyP=n8~q@m8`694TJQU=^0=sSt!PWzW?>gMN1aVyR=9=A zd}3`0hi>aN)5wV)tQkb}m#a!cK=Ui{fGPWnVlY(;teI72Wiv&#Li)5DWW4}WB5!L> z>NGAlA3=(AOYd;(;fk|N#sk8&_!n7UX(KO4lnKMwRjggWhEk9u%@RRIXD@Suxb}$c z(i|;fB^Ioe2u*ooA6mK=CTVH1W(SEdzNp5`7Q#A#(LsE!2GKGh^P4j|dq;F}1ccU@ zcWjg$xgU~|nxrFpr~{}du_P=LRC_eJN;r)wZzpi#+K^NVC;^p*QuAEY02W=>EY7L} z0h8fhv*2spS(&8}fEAp7>kg&YO#wngjVwc9M+@ z??hNf(1Kv>dE%$$R)Qt%;1Qwmlsip_33bnv5&pA^{#*;CR*OGt=+7o^YHh@%+;onC z9&o@z>-vzCq3~dJA99=dy81(Imt5V2{O!q)}}5J}k2+sEJ2 z*`80*M9W@^IHc_u8ck8H0IHS$)zZkl;41-{?+s}RNYFmF2}sBYS|_%|%aY;v+v?0W zVP3X`-#BPHdLy2QqaaUv4i$Q%bSmagH2h0e!8pIT9($MeM83D;reb$A@kMis)7WhIDeR+aN=ONsHPXI#o?uuIQ0TQ5Up*D?*lDo&=f>6@DI z33Dex3Z^-bOE(9EovIOm{;3^UrnC@u1s)t1jNfpxMo3+1iI;PJN(-eaP*k|8b+tEJ zhGUxo&CS*|VH3(z_<$;1$lcPKiza5fwPEnp$Ox~9s?=r1b5)i2^nk)k7l~KJ36$k| znq>N-0ClSCZCXP2J9g0$@;G+W5(=rV2|;x8Or_Ls5}Q5lsHk<+_i@%b;vHT(-z^Bl z*Sa8=GoCBSAiFEj2?T^!(_-x2iuyi6HvGrEy@bH=$DYVNA5~P*Atp&xy%8yTX-F&z zST(J%q=wi#NZ{}P0gf9)-6!%qa_0tO8BPjXihcILp`Xn7^E_cDb9#v1L&sW4&vDX} zAhvo~{WELYGfM*8aj%3e=ZKk>0G55XmX9wB;Id>Y!+!57%S79{;i@ zbnK;oLoW)wAtk*x!hePb7v|FI0_k&}1^oA{CzgxgEHIO?{@Lf^k-6~z9JG0<5m@81 z?8Q6|i+OesCM8)z<#1W2nI7R~Pfdr)%=$xR3gRF)lQ>*ff4IznAe}y+@}{2hf~%jy z*g>O+opf_g#mxvC0JV!5j$-DO)MGg6kqv@w_me%{6txV@CZ)+Ie*8=hNT!UwM zI^07GA#tg!kV`)uNFQ?pSN)|jSE>-nqmB+D zI(-~3@QM^_>GCDXVKW6=9c?a`Qu@UoIQF%@%SxeYKD z04wb96oglnWSD{st9YY%T$b1>X9%q#8mod|I+F}WC2buY<;pO9EX214;F^@8pNbZ; zMwc#X+Mf{9;_hJc_N#-q+>L{$3jI0X+_VQWF9sUj$$s2q-uC^qBp`3^%=9x(D1)Q&^jBK0 zq~ga%r3Xv;og# zlLM?OO@GbU{4(ObsDoi^{?5v z#v|A3yj0ml&S;Q?ePvjXZ`e>RfZABnrcEW^Z(7w#bd}Y2m4WuY#a(MrlSxHtf_>#x z9`4OSSU_g=1ENBy%T%{L0eq*{1Ub{>C^(?>Ld$c*9^MV!3dq=8`nFc;MIyc~2$vyS zEV7VGx1!ElgD^t-SD?7D4$itjg^!KIcbUNK^U65(x61ColCf5RCY}laB4^e=6>w!n zqbs)sVYuL*1#6a`>J9xu&Wb5f8Iv`p3ObPMiGQlM1plvFGpzwsPu|5&vvapa#a(RT zdRhJTG6%uIT%$cW>o7RC*t^1xAZ{XGcFQiF9yQa=uJnP3G$T8j$2!(orkJrq0U?zP zxEa<2-a}K1CTTHYrj;-*P@L`0M48w~%*L!}6&q4D(g!MNMJpAn6^E5JyO{QlNNcmx z=0vMlv~Fym&GMVEBH$PXIZ!TUZq!;mcQQ*AkJ)(UMXO*mODr7jUUm>Wzg7u`L_Ftz zr^r@??7Z1%6`PD-5`f-DF1|V7U}cy(B0&`&If`)eqg8n2NQ5}zoye3}NCA(7dM=D& zA&Bv<11kjjh4qYRP_c^nHljt*c5xM9MJ(O*Wx&Cg-qZ;Pc@zMa{7L$xF3ZH+A;P$t3FFQn zu0_Aeq~exlCV!PaNSx4{0XBUX-%dx28mpN2?tlwecQIDV`!#$eTn4Qn^EqLA`S^fBR8M(zlEh!Jr6b|`Y`Jfq3YXdJJZ^ms47beA$K z6;tVy%D0s@I4B_3(&D6zedKf`50|Y83D(VuiXARf;vYL5c^k`}XIijMf;GBjm3B+B z$B$1c{b{P@6I;sax0E@E4azfT!>0{JZgNWcU;hYolf6L?jmm#O01*gdI|EA7qhit} z9z{(3qZ9$=izY2GSz>ta;Iq;IppLlVNgaxSB|=PUK|p|0w>;-%Qr>gMaLNwu;9SY( zVDh(ghMnhW^F7(|%jKNFqRsK5UE@-?xu~CPAr)UohejGx-p&fa;(HrmSP_Lzog{fw z<0W4G3xX2x@rtMJ4Vv;4(Vvf)bP{D1xVHmX`pgII3%b3Y^4-d3pL@BOl~Hl`vnrY` zn)J(wV{Es^jNW2eA2{*hskOIrvrSlnr*va=wCK-qQvlx0DjvV%3aJRZ<__R{Enw#z zz-xO0KjOOY;YZh0j;*}!;bDg(__Gk!_*}$WDt#PuKy>oq7B@p3v_X12_a(LlinA`c zPU3wjYJv|jKB+KEu3p9d(@E6B2?tts=*Z~2J4wPHq z%TWnb+yb#yo)mO0_Bu^t3IAY6Fts))JqjP8*8JMh#WNx-XT2-w5*bwEb zk#s&#Qz_gh1&mjsLhJMX)>uAZT`v)O6E+VY+F*tjd0!V$7Vt^Fh!u-E=^hZh9{Q72}{~y6KH*iRq?eLCbX0`;nsQ zrhlle&ioL{y(P2jM9@jw5*^$M!q;c}AQKM)dHjGkKS}jtFO$iR0F3;_4(m$n5K9l9 z;;i@M%=?ZyR7kM(753&nh$yu+xu8pPxtFlCoIl~LcRE^zxa)?nl+{q!vBn=CgtSx^bTAuLJelOh%SqMWIvo8W zzch4+ANnLnZuDmvqS22(mDB}wLZ6}0KZ(E^{f)kGM|XFl2U=isaLiJHl*>0Ii!%60 zQ5GUnD3eO*-tcfK^L0K5*OyhW7z16h5pld5YQtUN5<_OAv#UYC0vbvVt8txg3QJbE7{yKmw!PSY0zx6zZ{r75IJpRi<5~~`v($FVY7zhpsGI4{ektj`+0Ra6xA^n*Tl`9FyI(px&$F}h z>p{z~W2ww%6p}xQySe*fcPrF+wrD3h?p-0*d86mZ-YkynNjb9ry*jfWLwBoWao-NY zx)7+qm~yjzOu5oDro1yB8&{4)ad$CsG9^x?#K}mU&WTNBJ&GG%W)*iz7Wd2Q%mEbF zT;>tinhhs)aEeDNU^Q=B^dVQ~q~37Kirb+W+Yxt4Z}`ZH+o>2k5cd&g)rhj*Y7!&M ziq_{MMaz2sgGksRMGZ4*NCB5Ne-8d9Yp4W}T(d0$lIKtmY`S)G^eJ`54pd0V=?*|<7A^#p@_~ZP0f`3o) z??-~9*%fILN_dNez!0#`@sgL!B%X2vvr%%3${vxSd04AZCbqRIy&w+&F)S|=jsY4y(ne;Z+gK&# zhrP6#-l}p?9v^J6W*dEye&fZb3=VYvMOflYp4F4*bZCwxPwUddS>Cu9#oB;GFP)10 zgf(BF8f1lWLAD(QznE+jL4>h_8r|3kgo+Zti!yVs3X(LTOzv=rYkJ~SYVN1B!(HPY zZX$QkVo)pg_5^V}C?n!~kjXN7cGLwCb+IJsqKLXw5_L%*gtm!B2^~#GpAwvB^d4~l zKd1ME16Jw^yI7Ie;h4lN`H~#G{)4ELVuOkvo+p<%g|9DK*~!bgwAXnlD!m^b#-g55 zR%GE0gN2(~hFd#2x*8ux5oK{1)5^+>tpFkgt$O zQPhkw@XHSgzSX$QOsntKX6h^$q?u(!4><%N zUFt+$3IqQ=Hnu8UKuvj+2D1r6G8YCJneP8;Rg6#eBaO;ZqoNveBaa?ZqfHf z5SCkd!)^M$hWOihL$_oMyY(dfe_c<%AE4jV`3QnHIC#Sf-sIp-D|m~8x2)i84&Fwv zp()fq)fB?ROpzdT6G1qSPMTjjU7A92Ag)`}<$a9dg5vbJLDS>?;`CT(PowuSjTV*l zlI2NOX9hvBtz@=5TL#wf+r2i)F9-(XLZn98Fs9F*+vE|kO+Hs9?db`#U3siBmKAuu z=Z3bVI8qnoNTsSXAEGfYlvJjjzsq3mB@}B7IntNVB};q9evH^{b>ZIfUiUp=m`{#a+; z^mwT& zu2;NRxEK)a-d)y9eJfp^nTYcCl+=7L^9nC1C1!&lz;Qc@w`^7zBU1Z^PDAOUaK843 z3u8A~Wi=7X!)DMA7yV&~Cov{Oe8QNJ(gGlw))tf(--`fE5N<#iO?|U;TymCqNff>zRW}9>OF7>w1mOu+=78glZuh+8T8An?S|=ZV|d@H5KA!CI)s5rVvtyv zNv1dVtqOledhHJv6hTJxt%`gG$UPIkv|tdpnp&m&uaTZ-C8qT42qhAYlo7Sg`67x? zW`}lhlndUqivTVO0E7fl7$oJb2&CuI*w=fC{Lkvlrz~>lF5sgl|yTTN1uWVSnZp{rWPu=~vBkqtq_mgZ>IL{c51Z zS)h>6xf)1%=cwF;g-_uH%PDpuVAJA4h{ckJ+Wgb&)` z8wek=!#5E=%ys)S+8;;u1&XZE(l?8|tV4-RSr{0_U74Np7IGf7!?zKB*A90h{GQFi zTnVISgN=UQ2Dt)|4@yYJRrJ({MW(G=(zHx<<}rxBmJHnSGRx^658OyN-8*43Mi*Te?!>&$=0{Ygm!t}u!IVJVp%A7uGE=7J}{Q{to`EsTlum4=UY;nO)j zC5T(qnQTqXax*zGm>#9zw^gZMQSM4LM=8TK>LQOX zS|T3C7uHwH;`6Hrp)w-sP%0eP%OINgcj(9!HMh#!I?XA`zNC6M_(I){90t z2)1D6<5PnyzkE{4FXz&eg6YF*d>YsIMj4*FSfJ2Mf8xo<+k)71ZW1@`@ubXV%hdSD zXzcZJ2VX#AZ6-_w*Um9hQPM5Z($J{rGL{{rI*#efufvo#*&0obj>guNJK0{$#9;EN zXnd^?6}UxAmdE#w{gQ$n;Xb@G-`i(0o)T-?*|-a`P3xN% zZO!H7us4;X#5&p!bLknu_;t4;KA!gAtVUa3emqP3lB@7b zg_eW;Z7&PlJC$1tbBtV=zoDZR7Uqa@YhkvQTfEnt@|wZF&CCMr7+r3=UVEy%hgZy} z%m0*D%zu`?jOGHVPeR5C^He~sdZW}YYO*@=%K0QE=3F6FQ{+)*`Ncnv-Sag zGG9njb74?EnJ*5Csku;2O~*9Cq6L`Z7vtJ`#EBhbI+p-Ckt8bCs>GiQ;z4glxyz{q zm~&ka#J}{kaIKb2O=E8uRCO)z4Up9N!TR|@M_pCkAU@$K zz1;260>k;GJsS)S!z9iI^Ql$^fAKGUAe7o4DuzZW6KkCdQy0pLF|8uzVwqJ6!BB)L zKj*_N1;a0};3+0AKSk=rGKgZL^Zf%pcd8Ah{|EnTwVZBOF|TKr49lEyr&|j|tW9xQ z5JnPc4~ehj#WALNH4p)rneWi44dI@!6g(*ybMk$GL(XadN^VGTr)Vc=LkBfSn z#zi~xkK_IkK!w!!D3`j#8ssby6fMA(&ZzOTBn4j@q^ZRNy)=k_)<#!W|}>iD7gtOuZB4I>skN6~rb8Zh25VjB<5rp%PRD@pdi;^MP^F-)1`Tq(*Tz zQDE{$hy!BIPpfo^8c_*AT8GTb;ibL8OYE@d(o!s#iCk;` zL`e|3v;S&A1&wew zrAx|Lt^juLmkIl%sO6vY6Z(5U{Xh7RT%VUvpOxh{54WluMs;9cx}hzU*iz0O#Lax6 zeoMJK)fQ@)P8$FscXH%*iku#5m=Q|sMdWsl+)I%&LJc!RiLK@AjvSF&DRO40;h9ik z2O_s|E5cc;V}ZZMkd+ z*2IshiU98~z~Pu4k5%~!${*Vubh2=i6%pU_UqZ;>{RKEKj+o(rm3xt3KJH}9$b%u@ z8&vERndkK$jb3Oq`i&mZi>&C40~I@2?nv)|v6MG095U8S?SO~! zx9*7EiRjI}aqI++Eewpk8l8vHj*Sf}O^>n144UF@x2MG#nP>aLn26@;l5*wu`!+*1sh_cy~OuOv5786Tc?!0X}Z4) zIdaAdTw9@41(xZ~rC$vSVOc`AQVek+JAGQR=*=x zh?_x(*=`8d#SW?pvgbqnB#zaxn(Kr8``5*r{VWD$p_FqKCT7^z1m{@Oqf`q-6}=X} z#xCr4@$~$y9DxY|emX~^Lsom%iuUw8h)~A7c59R`hgeV#mF5FMd1G+U`Zy%%`a3~0S-*x&Q6Tv}$g4Pecm`wMF3%-C3gyD6e$~&ANR6^qm{=dTkYVS6_kf@BWkLApnRhjsh zs!YzYU(j@qm92vp?#6rkZTAfIXW5tak;;;$eU+_#JGrmY*ECkOXoLapy>P%EETtVd zSF@dJy;&<~%St(_qUI9sRe}JA!u}w;NcbAO;oLnmfOcUVP`Ii6L)h*>gNY9-OWN*+ zorH4s*w-3J=EFY6MQy#me-E22c|RPa?i}z5BL!_g80xp{mFl0c5u06p^O#YuXauwA zY}k_i#+wdSCJt7*`SF=*t`oNA=CY~u*+wLG?Ak6C@uZ(Yq zAzdE&AYmhIFXPW*Hb*kc8@0X8NriKW=d_x%Y5S4h7{%DIbTJ;Ho zdMG|dY?Kh>of1xzuRf~yw;R((f$=eq$UM#>6Z-Y>=WvF(DVE{y>!YyajBN^&se%3t z`3=$MBS1L82P-6jnr9QK)3dRxK3Q0}o`wkcI4qw!W)tTs73mJ)G-{tJlstoyr+G@a zLBvtJOInDcU~0C(j$6-!iw?9I&RI^=*+ExjL^N;;8RvMi{Cj~4L10VP4c!ENLXTy) zoT)7H!NfRn(d4{(B^DsT2}wi+AQd;dpM-IU`+o{IeA0p+ zMQx569*+$+`)i;{#@dXr5;XBkc2x=K0i%mib0=hanx@br{VHobUppd7%?tXf!W2 z2DL0UnwL09ON{2FPI#%&-06fn!BLmPj$%c|u*_+g<~|FT2q)o=d6LmRwq5K3Z*&h8 zx2ZU5UCh$###x$Lf;6jTS~NA!U@aIcHC+?-CFdIrz2;M*UtvcrEe7KbM$FH)Ho_l*O z^V9PCdt?E8@iK^XY2$wt;l7HN{Y$ui1FOL-OAUJ<`qgPz{7aQ={C02|*3Ey~u#-Ehr$x(xM1X*m`t4fdkA!V;>OC$p4EG}AUf9q4 zZF_R6#`fTjh?@+2A8i_XG)?x%>8!wB#;A+lreA2drfr(|AiJL-te+8BgNmrmzn$Xu z_rh@6H5iW6;B;4-n)JILC|@|kqZBm*hf;r1a41Enj*w~_3k;f0Xhgc0aaK_*^{?&l zS~0NNPpifW!`ZXci^8)M?#f4LzF>*}M{K#yX$y8Ns8h_)u=B+oSjZg+IURt%^33oa z;Qo=o9oK3PX_%Fu1i#pT7jDhhuf)G!+IT93K}gE5q&paNXb|^M$ef`83(7t5ttI>j z}2m@p-8^DyY*3#tkDC;AAyP`buAJo-~vT|Wx zjzon;q5@PLt@$fT{HuKIMt*4EE2E^1P{+SSkCm!ki(6^l{#k0fIjZ7*_9-#fxc~lM z`do7SX+Th|lztBJ+*BCRWCE1yf906bBe zMzmoXvAituoG^lk76+llrA7`PGcOCNrA}_i@U{fVU}w}mB5PAEBoX*Omlfx6#jnVU zH^KZ#DN7!x$RpIAYYo`RnOX_pye;6s+=&K7i*{DUoNK@jQfiSAp&cQK2TEvOWbpZa zLvPC>BUe@elouJa8=7mR)*0As3>-`)S z91@6)i;RJ6OQv0ea>$($pPLyUSXJ~kOWPEI9*96F))8{o;$eJ=*3-BRvE8ObMV1bt zwMVp~yPHQf=k{xcob&$u#`Jkio{wpT713DD-O60T>SUaT-4^Xan$A^vRx+1R?{N+9 zNa{pcX}mFYrn26*pkD3bSw26wp6{m$KraL6`0E3`44_XI%6^8j6RuzOGnAdk2-II6 z0df0?7R7usN%PUBDJovgWNqp~Hb+gu;3)%pN^^9_XEJ^6q<=&HgVuZ|Zl*jWn!>GO z^%373waDl8ZOld4_;;Axt(TwHI|SEScta-Xi@?53YkU1y`tiZVkZxmhvF z@40*)29r>AZMVQz6n@IC@hpWe`5u32w#NRQwis$eXj^WH3ANX@5LIm&hTO(ss&ug( zNX(8Z@#kY*LYt*2$hNcQM4{y4TVdR#viDR@oEtSWQOpUqsq6!pW`&ysT6g0yVyJCo z$eb5V%#SMUGmuu~H+!=>oGjd9-^ou7QMDv7CgekZU~=tFFM$=_rJI@xw;3AS3py`&zTNGG`%?Tg3gIZ1pgyBJ9Y}ZBxpQSmLoD8)D|j z?MiZN&}W&;cs<$bN2q13&vM3hdGu-~2phtTmXqJ=PmT)}D!9e)4!&uaH+>jQDapq} zzUw8AXAlo@+KOmmWmG9{&B>vnkA!@%)n+ADMU_!QONG~97-(a9V6~Nn`d~;+J{l^4 z4YvKl2AgD)R)4|5;w&vtDh=6EWxto0z1Gz%9v)EKqYj{+OFrSjyu*um#EW@HG$#=c zNW_D}SF`xusBK!F!ydGQH}TMc!b)ql2E!(Nltw6~ZnG2TBWl}iy*XS1ku=ze4fh^B zL*GkdgRjKOweTI+91WIhA1LZxZ-p({qOG^0S0J=fAoQ0-2!UL?h+L2*nRqsqq2Ou? zvc*-AjJQZ0nA}^zNN*BIslYXCDB%j94hI%_8$3#szO263mb zy!n0kdA=2E4TvENz~DbPH^BaZguh~3`?l${&Sv|xmiY)4+`*Q$Q5^a5q1ag1ydpr| z82flnEF%h;YopRGbw}V}uiOvn?B=hNLaVinq$;d#3)Xi~p-RCg{ zri*X2w=f3G0fI}qgb{nIz&HFT#gffBvMeSUehpIS-aA{bG6yxU*!z77|Z+Tu{zpYC)R(8&;jdbfaIG zZUlmuRfd**scAzrz9*L05OqyAZj8c=pb&kq9Zv|}SXNe63M{t?EL;6$eu2;3ex^H{ z{sC$C#++C7B7^-al6=FQ%LVpHm9ByJl<19OFISB1C}}+tt|PIrqmDXrq<~b8sRPw% z8pU=gC$>x1hg4GpDZt(n`RP9iS5!VADzjxtX%&%>+|TnW$&dpvDLs_nvotCF3e%47 z7UempkSUvJQu-C9Bbt;p^Q6?F!B$oFI<~5Ke0WY&%`H)8qT~K1gX7P09ID<*I4b7F zt8-dT@R*ugqsF}j`ZQs2M)$Ir`Q0NXB|;1))9xVspnZ!_y-ME ze?Crq5~c`zFAx=mNh&Ph-gf-Hz5V~%fp7nO=+)>i)M$$t_`i;XGq*)YI4`NTM@QgF z+$Nbw>j7fp_(@+Atq8V68+~g0NUSM{;*CKyK0`%#JBMd-_(g>O!Jm97idFI=4Q~hj z!h4jQugo@$48_&Pks&qR7UIZ1G73)^H?bY8kT4uMF=Vy8LW`xT zGCq~#P*P#wKV+A;q4;qjGg8sS$cU1d6H<*qY(Hf((B|gQgv?LQ4XG9zh#Q!k#SJfK z5)YQZK8gV=b4M`a039WPewpnCO-5fPgdB*Y1jOth0LzMN`W1}bg8;ZADAmAHQ7f?{ zs4}1yJh~qihTF|y!@I3|r4s1WE9o6UfQHYc76d1U_$iL3IRFG0Zjy*@PUs(DkDG+} z38B(SsZt4WrRGzm0V(c~cu28IQ>B_ZVT@v{neKlqyMM9(g)Qz(3d!0nb2P-Pmbv_! z%xI{*hTDJ<_|!ZtaY0woq5_JTb2RP-h30BZ@j=PyO!2jv`+#st$bv=zY_t_6%InUj zHTT?qa)=5&!-epYge|2F1VG{4diRqXXyLt&9|X5wNMN13<))Gs(nuC z8+3-GQKZfgz>!$}ia?;0)&y@vv+lAO$%%R@p?%CWn< zoKK%_eO`m4L>%9%{%wi;C!k=kfPB!!>N+jlrj)-)Gt$qbZJ(XAtf*CX(6Zty$}U<~ z=t`EB6+XqHy{@jjPJ3Nlv7<`4r7AD*S;{mYs|bhUmtvs$D94OfXUNP&M>*Q8p@?)? z-02CKZ$)K978m@}%iylWa*_x7V$#}!B~l#U$5bdE18j4>#@iL7wAI*h>#{1OI=4`= z5R#@eL~cNmEh5+COFa;RG_pqMf!MBPGA<6>7? z1U)u&x0-Fr*swNW+4Uvtxa@Wx-=x3BlJdXUSJBscv1OFwgP_?>tEL;|NQ|zLNz&p6 zhX-e2^);~}#NN`l+?X1Xe>gC@$99~2;90M*X~SjOYqDmSlm`@mwir{RR*bFA+q~`7 zkPPDaK5jc-*IfHj1mckzS4DFXGi$QE4&X| z2pBsxX(A2WwyQAFb)s~Z=Oq|R=#lf%NcG0_Wtb+n+?i=N&rEo%@N%eb$S5_v8F2S_ zrlxnbf=RZ$p71_Tc)wOqGCkl)Kj;a+=LsM3gx~js4|~ENc)~|K;SY@Xdf}FLNCaI zam=h~g)BR{hwV|H(5|s#dXgvSZ>Dmg_+uFPwh}e<`T$`aZEVfwTSxntnhBJy^*OJ> zFm3yU&2W=ojjQ2Z#^hio{&-CWs5%oQL|dJh(2ze$3Mv_;x#;a^9b9o4Jd3KRdb`@P zB#6LZS&EIHr#5IZnl)__(!s>5hA*8B=}A6HmT51RDFU;*eNqe{D19)pei~u>L=A6r zA&>=BUopt6nGNA)l4&pFx1Q8mcJ~t~WsORM$|oL$kUTpxNXsY^zBKhbP|C^f>|OJ$BaWiD|UR*3XwLKUx$szA#@gPJ|8 zM4KNxAE^hI0r-A54QA44Nk8STaY6mr#!-`Uz>-B5Fas>;GCGt&a9DkQ4fZG z!Ugw*>V)zz1-NR~JQPY30_K5GDsCRu0Sm?<@q|tX4&Q?oR&OvqtENgADe$(%ME}Vb zz!|B7+Fz2x!87uaka;3>KkrQZ6u`aybqk$z3Z3La4>3mHk(8fZp!^|0B521ZKc)>h z6IXnDhe8yF87JKx^FtW((RzT!e7Y)H6fTM1k2W2SrcQ-Yr$dd0qrSxZQMCawD$UjH zaI_mWSYhlA%RCjb%+n$9T&D33f7=~UF=)D@UTJ8cJrok!?&x9kGBwa4%f!c5W}rcq ziO;LaKp!m=pO3(4XZG6{vRj_5x*p-i=sxN$cBochjLJR3YeG+IyPNumC-qdwNqy9l zdfH8WOvlTUuh7T*f%kpQUj7?0M&-9B@8U}us~3dHI2|X;Kb3kOg%A9!{J_s+syP?J z`DhMDwY6S_k8Gx+Br#ry(B>JNLWC_K)j4!A)ci0iePmEG3wh~e7qVJDjOx#`S+yLSRePfxR{HE#zfj>i618&FJ9%G#WLD=o z63udY;vy}}hVzMRIVV6zVHDVP1u9( zpgRL;NgvC?@{YVV07w1M;i%EQR}F%P#MRtenLxy$E?-Rn;=QK}*8$^o%!9C21xM4Y zmILR+PAISz7yVD_Snfiplll`s+0RN&3gQ!;pa)BYLjt6K+%Mi)W5v5_?0@jd0uzfW zo^mTrDoQ`?rax7be#T9oT$Fy+O`oD;Z}c5u!m?gn33j&Xebm!7mVmU?;4slq4#h#; zmhxq)?iy{Oaq${$If7HfX+=%KUW;gQy6&t6j>)w^JFNxIKwix7$cvde)|ApLC+&N9 z>+)I1ToJ;LeY(w}kl z_*k{&cod;le@jPMwl$C*87BENDx5X*A?b1_gfN?K{wyS|GLH`P^I$via9Cyeayb-V zUehWM6j<@-p)fi*TlW}fo}-shS6*j_Y(2E?8xvO9bofWn_=*}=DdY-o4B6c`HJH2- zQuQg)@;Mb;7_DxWu@g~B`05H@^^Xe)C!+)z*sQK`p2?01#~-GTb8OJp^f7gHMW}vQ z)+Az>=;Re916%wb8BTs2Erq|g_@ZchZB2Y#&Fv+!S!uA1-^usU+|Or$eLM@y)eHNM zjPAHoBA{Y}{8`8X>8x~~pJeKV#bO^4=IaI9BA8A)bkE{(K_Rm(44)^3cpnNcD#WiK zzF5D;0?=i7IKRYYwo{VX9wBDC1QeEfn5~m}RTs+4w265ltft3>aco{_Y8+5m#swcy zrLTD`z~BfyF+Qxar+FNuHJy&K*`U)=)!^ILGf`SDU@LREda#ZyCcJWCAJ6)Kul47= z3z5Nigv8QIfYsX*Pcm5l+6IHA2^a)>FDo&DG5}6dmg^;D{=g?bM-B$#YLBq{iaWP} z{$rJ$a>Eqs=w5w3%yh<|smb){-%t@;kKj!l+<;(=gBuYHaqw9LeH?ra!3zvw6M`SX zi)`GEEN_Se*~jGOnw5CsmaA9T0Ir@m8^vdWb!vHht3lrh#aekv@b^ES&)e}W_;@m- zd$3Hia|leO`a?_O(+0Gb)&?=(=_XO<`5GBj^?!uUq!ab&gP}X=L`(GVQgoXW^)pv* zccKB@8pmIBqCuU69geK)OLZUqFDt#_mVCbS{l5Iiefdj$`9MCe=kqt@^8@pF%Gegb zMP>h$eR;yp&d`6jF<+tMsPES7w}|dA=?I-bTZk-}YK)(YWO{7OC+DJIE9&C55@GAD zC!ddR0U%*CXQfUvO4+57!tR#Oo$E$>JR{;_xl|rd$5m`Go7S=qM0kIsJZK4(nCGKl zxp=Vp3DB<>`S7CtD{T5moMVNFbI}{2C4-CNt>U5>YL7%H(di&M zJ;CYpO@egX=TZVi-L9*;{|o!ZrK#;tqvoY(d^$BR4hfvZ%h6G=D3g^?L3mJNltT(n zb45f+3!=m)MT8an%{9aWrJqF=)vsKkO}bC%iz+3$liYcYT zlvS=^dnhIZ+oL4d#zJPT_6W8$EU)T>U~8Hjwi+jsXnQK0O->7wq)W^UE9pt7w6-AV zCPC1x3;tukCDJiFcnx`j6# zL^iPRW;gvUqk0laYM%t0@ToL^UR5`1R1cA;ST^^e<>A=pHSh&pc)7R>PVy_)PkyyH z`G=yz>zV53dCNH^Tvy#smVT{>MbJSEt%lMpdz7LCY0d3011{%@ax|Y3ARiK=r@XloJ_b@^r|=f6Nz5x z>EGn@KP~?EPon!9T#g?tIsP%?_=#ZNje5bR$g{N65XM<}TR9i`&vDvc30oSk%N@v^ zFdNF65kxRKBS;I%V?ITC)I%^mC(JfEC|B6z*rdZ=3!5pM6BZi0n>mSEi@!nSog0QA z-vUI-C6S%*u!{Ci6&f&wL_nSnRg<-!!snW7Z1rY8j|HaByU_bBwb=iy5x{_6%9$()kD(d8Im^(L(+vI*{Q>#5bd9Y znG|aAkr=wXiz)nlK9t!(FgnAmkU_sz%ML#XYEi$Jg)?y*r{JAobBE9$#WD=pxf*#% zo@GemR35Bl?qDsGg9XUW7t+aJh#0@r-cWnGlZ(GqgDmGh?YcwSSs2pJq9N@p3@LRE zQ8A=eVMsgWklOB$b_@Jq$I%_qM}NnVKJE_bQVi)^o~f~0$LV&1ch|5gEQWVA_JV7$ zZ(JMBrUsjrBI$LgP77h_G|XzDY`bMGQhON?lhIrij`zrOMXL^eezWGfaNFSSLA@5k zVWFLPCajF;)qjfB{$TU6OnjCT|57A=7OBP|rk&UrRw;FmNWCJ9KF383k=Pfh#*S+s zL8YRE{kkypZ{OB~0e@g0`-C7ac|Oc`BgIy8D~HK_1t^$aS6OZVitRW+r z7T#&N;Fn~<_X-8Qq}j(Sq#wybhlnrvG+B=0YLXXY_*a+?qayE%!I#I*9Bvo(1aXmB zSRt;1d^(`Jeq+H;E1h!;a2l@h59;#$Yy5BxQZw-nAdj|E;0OY1DDWWyYx#SAPj_vA zGXLR8e^Js!B)#u00>;Wkz!S6xSc%W@ut!{fz_Qf6emcUk;_IJl!s&%D{)UWqg~dF6 zN3IuaLLJqMY(gCaz_LNV!zR>|5>|Q}a69e+^-(dX6JH2wf&*$hK%Mk}`nVX>sV@XI z(E;@$K%LeLFNn_Qfq*b!!E^hMap!SX_p5>6KhUSOp)!7yc`Vga$t$GP3?ntoXxSNV zdb5(f3l6$n9h7*pvc$NFW&pMvGO*=JplMenm4pS8rk!p%X$|O{4x<)Odyx8qFR?k2 z^W5m)P@_&$)cE2mZk!sO?rn77?dm{o6t^qaG*vKG^a%*#A3$6)j=83l{tcN#v`D`M zI$ZG3;o^1Z@F`Ui}5Qj~J7tmnt3lUD5r7 z>@9>WfvW++DOWjcsr}`6x>XT9ZRaAe-Q~lxVR39BFA!__@FOw2Ah#1QCc$9nb zd$Z`6$nT(&^N^h8CQ*v-~i1tKyJN9BVfbtf(^gS zI!>$8{ye8zHaHC zHFt=0U20<`!qQaKIEKInxh*>|JdZPpA_b6J*=iPESi0jnzA&y+Fs`qm;&{)vKFQ9o`F$Mi5z&zWzC?-8EEoJs+!Ra?!odn3=ZH7% zIFX}ow!G;UJ7+!_OWdNu0u!rQIsQ`)=JS|(C;qqisK7lQj=F}ACPh47mN54Adwp3? zF{i-YhHdtsmz0R1w9fk~|z&qjO!xTu_mC4bZu*b>SEy(pq0Pnuu3d2?&Zo>o= z!6R=wfp=d_`~sUq}jey6tz#hB=Y{uJoSE(g6nf zte@m6RoC#EusNp7v3OB-`8_6P#{EPm6h6_Z_(XT%6P*_+?3U(7u*@!cDe|9K_$hmnaptcdI2=TI2d{4DdRzhz#2}U+{?B zzJNI~*7ma=JMt6pMhnS!Pt{)@qyGU)z8j8zOq2m5BaR{`Q9HuQc!T3-WX-cUu~s@G&g+*+azQ?g7Uchl ztFwrsNVz8BsQ8}?*A1Djrtijqvxm9XMPR@E>t5zMFPZBTY{f5ukuESJT@2&0_EWIa z(-G-*o=<~$Oz<=zaV4w_@6IMZ!`NL6!_kEW>ZIhDr!#{-eV(%iT)in?Dks3HRdn|YSudn@zIFC&T--5 zK}$Sc4u#b>cx`|uD2aAWfsw*$!5bplo|qCNenG=Xp#3wL<;^duG_|NwJ6GDt5;;Z# zGA-t@#y1Nx`5};FSEOK$FB>63mfqm~Y#!5u<3W5XV^Q9s5e}BZy4oGt!~~5RdjkKB z!+(!!iWq8}S`C(d3w<7|!Is-2TH+B+Nwim~!y6#EZk8zwVqP{ep8f~AxuS~gYSW`y z;?d%YB88i!iciXlZ!uZ}*j!e<4{!5i&50Ti>Mmrf^(A=5CovM)J%wyvy>dXs zy?RaUwFP!KjQEg6-vB|(l zGfllB&E_6ys-8?Tnh94o*Af z`Noe&TqAZ@aO~7qBaPU(3ywK4*DA?8F!wBgI1zza5@M(3jYixGr8gSQ&l>SPTKZX| z`8gxLS4%%9+ z)j@-AEGyx+TytYeYPM!>Hmam?Io0^eSMew{6N9;nc`?N|O!+ucBF%=))~tFp@dCSi zJXMsMm#L@e>-3lZgY*2hPF&9y-j?yT{rs4c=!mh_d13+o`ZDIi7|QZ(V9~?cIzpB& z&j+i~@BSrRk1vYh!R%tf%}|!aa9?~TQlxl&HsYL@EtS*re3HqtLF{wxa9?(Zdp-?! zhnoI(oHBT(jw>W?{pGg^x8&K>f5dlHCOTv457fc%luXM5AlW0_r;pbg$Lk7VN{b0K zSQRBSmA;7Md(ahB3p9v-bAcAWTqU(|JAhRllD*=uWmCw1s5WbMXl8Gf*(J>zQ#83O zra(ossO=P$9$ky)1l0O%>X+)jhQ`|r9F_iARzMqbJ!t0c{!cR-&`59Ys=XVg|MsF>YpS`qvoS~c?NydD4|vL|8gw0Ne73lbJSa} zJV49P1_l)%P$-dFtija6Hp9otC8exQ33Y(hzyvDY8p!qECEOl9W~lTbmBD)SLjy4J zj%7T*FYv@}y-M2B7Ht9TngZJVe+`##?78OPco=8l2e1mTI(k>B73DQ^Rb#T+0qhub?H&5-t7+F)xTn*jfQlDl#skipw;M)fks)HnbSg zG+su@rV$XTj40csu`1&-O{y}k(}brJsoP1c`e1OkE5!#FFE2HrhHPwH`ccEHc)mH@J;!o6a4{}lY>JzbKr{)vd z7F1w-t#q!Tu}3ctxP{R!sCb8*w?d38QjDS$D*DwGzAj^+1o&jFCr@^_>Er0F6OH;| z$&aJH+v1C&g;wBxovTeCMAvAO1ey)K6n>#W*uY$)L070=2|In7SSv4lh@DL0CX<+n z$`cX&w`G%0YYL(6sB#uslG>cC#Q8KHn5*3b6!n+NQwFv|ST7A^4qmQFP0!{03VkwX zv*`e8dRdi2EurWssD{_X_;{umjt|vIwaipfxoxW9Zp>F;l|8KxUyb;5!`Vizli%Ya z`W_#^p^oABaDT;s5BH~P*@ycpnD%EHa6tNN5)V_vxq&6M&4t0S$+cK{rCu@OQ)-cu ziBA>Zkd#?4CFIB5YB|bGtA!YOS`1gN=`}+{?Zj(_57KIzAGW4wA3?$@oBIJ~hFJ!@ zyy6b}&ulBPzeG*^nQFe{OC{Rd61_R;4}N+reb_TRG6o5%()+~ZizQ0}PwNPdXso(g;5EknA{^GCZ2Y9R;@92)0__iuv`DtrinoZ2-v^dc^!9 z9un6N(F;tzZA89Tm@JD898Vk!u%?;w+Tb^hzfszL)16Jesww&bDA@`%s+J=GD?TaG za;P6y*BAR_)|(=0bB&ud&dJ&-vbGFxvwr1d?Gjm23R$;1S+9$%bA`74!pX{tthSh& zRYzIzj@oR3A{p=hc-Z?|e>l&ygbo*K4s`256*<13))q=*yQ!b?h1Y>1VfE(X7a}e! zn_&$qhP9|RCty7RSc|WR5nQofu6DuX7)+fD22)ymNiD3R0Zu}N-rGwWMnnIuHxfrA zYi`#PZ)h<2ES{=w*W&ApE`B4Hcv%$WH#?NBRtq<~tSh&jgxAtq(Z+kCz|a7VY->O& z%FS048ci8h^+^=nGC)Rs`ok2R;zUdI;S@dR)b+DYb6ZSC1FX~BQ7fZC)@feoL`zwx zd9f2MW1Z${4&-vyY2KsDXoag-*N+wJ`jrK4xYC#Jch{Bts4Mx|SMnRKE}<13V3*J;53psp<2_txsXOD10bW+?j`u%$M>N0#ytpvl7~tiF z@ow;PLm3b7iduKPodK@k0j{6{{!u<(@g$GsqQ3m(7UU4)$3r)5K-U!KNgGA?=!m-CiP|+KQrnl8-v%2sSy>CZG62h_PZCVClxMH0 zBuk69rQ6{a@yX%b-lpG-Y~J}W`H^#cm4J()TaD_0SL5mw7M4|3b75G`*25UO>lTe1 z&I`;}8`a-K)2n}H)4va-qk5M|ima*aGE4wIs%JbTT{ocXDp3>qyxxfR*Zw~3HwA5W zf!uUDn#0Qy`4Evdjydca`CW*W;{nOW*O82kKP_Yz4s1IO9F7lUWp0|>8oJGGG*QE@ z{x%JE_3=2Qp*y`@6YAYvJRR0!7Vg$nK3v_V-A_~94nivVnud$RZCdpQJpV4^o$XQ9 zexv?cM)e*=%7Q6WRn&Khv#sVD2HQ`><>DPqN$3*`$@25+8znFYc)0@{gCFo_N(OyK zP;)bJ{7(VKmF4TDEdQV4+wP8AuF5m*A>%)H)SaF0%g5grQRjwD`MmFc=JWrY&u`)` zFXu&WZT4FzSMC|B*J-RyaFG>ELN?xE<5G(amy!5#gWC$h?KQB7v65?4p~hBYl{9r0 z-%txr&Z`XQ9l{%F;-&ry-jK-|4HuDid}FOFZ^xhI@~aKMfYmfX=dr@DH%`z?N)%;{ zyCm8umqbfwNi>$pi*;dK|*mRYQuo-Qo0c43M2j8S;nwF?E-7YbxiV1uUs zL)hr9^`Dg(EF}zn1Kn){=YKqhe%M!lMT!)SifsQ0DcG@5rB z^(E<@M)NMC-k;uOG-r+aU^;6wTSk3p+A^ALqrNO{8_hYRzC4{Xn%{z%dwtNfj<=)cb&&mB| zuVzjQN?XXAcxz&@y-(cgq3Y3!dM{cTlu1@&}&xWxoYIUg&mILqoMmvl=mHoziT6 zvt6&4TVf+1@dhQ2Y9)H5s#tu#U;Q;FHx+0f(VNXdTGzE{n3DyNHX0SIuNAIg{eCr0 zP>m&`hOJkB$Hk}!8?0zY$P~4Zk~~QW{2kX+X{r?<~yST6}zz_Qy{~GpH&#=alAHsC&(* z5Pp@Leqte=IFV+CL-hMT{T-&iy_)%2kbhte?*pyzC*`=+V+r_Oc84VoD)ul7XPe&D ztZd`EnlHUaL!z{-ctn04dz{bXbw+idfDjQ!uLy$SyNS~o9rHv0E@5y*$Q&BLoQb=? zqS(%&*e*j}r9$f{%dN065F*73wc-OHlp_M=0(P*Kk^B&I9XDIAgjx51YHh16B3+Zl ziaRtI$v&o`Wq2I2@#jkeK%e4q4H`rgIHtkDHN)CoE8lSEl4p3ta)vYs$k{JCJO5K! z@;wc}-{S3ewIq`m)IE-{=r*MOz%`j!D|wdd_i&MK$$|Q^=(Rat7dAYo$(L%ysizzn zs(6ko_VS|h`x3F2Yf(Vbpg=w$V)khDN(=Fg%hfcR7O`^jyavsgw^;<-B(W4WAw~a) z4#`h6<_entP}z43S0#C$CZy2r!hE+6^W7dp^*iK#*K-`Y*Aw1n6mHP>d%_1ibq~4% z?0G4`mXQE^8}jHq&xjr(0tSPBWSP=cY?pA2BIBp7il1R$L+=~@V6g0`Z2vIut?L>* zbZo%-f6UK2)$}DzSU>z!yHmaOkl{Fl&zhHEyeKd*dBsos64)A3)c;goh$+6ilx;Dk zB&KYSDgG+u#TXp9A2y(>_awXKa`cl@G<>h(fPq+_9kaKEc*5W(#i_lawXBhE!y9WZ z`F@28TT3EpCk|Dp*OdNhQRy6dPZREI)B>hlV(ha46=w)+d0s?uyGb90xUrXq4d}f=eUAjo z$v}$5OBiQ0C{rhUe1<#mr$YY?kI|{z2;qV-YhKc}}hIHNNN&ECVX_pMB*A=H- zHvGaZqlF6r%3#skt=lwH$|9eUd=CbV)OE!26{;O9+vu@owp@2_t?yR(rPZLd4bU37 zIkj9bq_rn{40VL@1H40SD#~bFuB!}%+T(Z47XvL915V^Zpyh%S;g5hOeGwJ)L+wcQ zq?j+q-0C9Xl^8$}SzoWEmG!ByYD_}%)MksQf*F$pauA+HX6?^G%3gUYKUY?kXvpUYyed0A`)SnL3n zeR(59}hNe_dtH63JOsX*9*)z5TAtj*eXYz;`M?$ z#VT}o0;-SmH1c>AzTcvC?i5|DbEoRm=qyxtq6#WY_W}NN-Q5RF*LfchaF*<}!>IoQ z-e64Ekt;VC8J$;9(g##}8s$yV<902UY;t0ruJaT=C_PhWD8m@aY&1Q-%F&jhQevrq zis>2LgqX((T#mra(A}HWDyW(8b1BMutJXE>MYRfe;98P<|62)NEan_%Ro#F{tGQ|V zysv&(+L6I_W~%O)PK7ZeCkbk)H`3QGYKv-Ft&$|6O_FeyZa!W)SkjC=AQJGwkbn~z zh5z(+Fu71y81NH8VTn|3rYPk^0Jd1|`aqa(DXDkewJr^@xXPkfa-L2pGxaG#-Po2@ zC#_;%vjah9N}-SDUH-6ezgh*+#>x=1#^S@21-gRwb{16x0<=9_?u`6w^ujS#+Mxr$ zQ&pHP{uBVHt&;)SWSAclVr|aRRfA=KA#%7YxSXgkxeD^|rzz@CPi<bbf! zgp=Y%ok?cCJ_7cOQNEW6K-t(YH!aYCc=KGHDyQdSjHeKRgJpm0y*W;Hfxxo`hu%85 zN-XgS*4vVpvvslBSA})Nsmz4GK<5|>CS!7$0K(h!I^x!>!;RVaJGF9hA(*>sB}ocN zduk;aI0+tyt9e1+SS=-(GOem47zjSg=LJ84ZyaVIvB9XXn2+6b{jmAO6aZu=7U{k= zy*DRsJ9F~36TBmoA?}1lmR7&{Mod5>9Nw*Ml^C@K5{q>;@-4|wdnu#vX)>FZ*%(Vv5TOV-rNphCm>ZcCOOY8}# zP#d6JSD!?Q?ak+LBRZoBbL$^sQP@)sE4O*+UQr-{RD5ILpP9_R-;iJY{#pn5thq$T za|CywIIUC1%mLkgt~N+kJK(9dw743W3flf${wBfY4Cf;`qz9cH5Pf~(ia1E`MX#m&8sTB{F_e*mX+TGl8u3xNqFGx znj&PCWwwi?c9oTVI;sM`@``($qr4S*R#r%_k->H~y%s@AT7_SVuhyxL6fxK69qDz5 zP>Q*hB2Vine#DL}q~IkxKj>be^PBhNGfct-RRLABYOd7laOw*+il*nP5KlY@r>oB6 z@|8M2E*B5zSL#SG_ZwfYP9DT4afI6gI;#uqtrYE1y9l=&$En^zB4T+3Kk!(k4;M?) ztY0YPE+W}eUQx>jkpQ&Z_gVPgC`FxBAj5yvOAE zc@5_0&O&@G;$0ZxGTMDN?TmfS3(Ymo3(e(_Y0Ih#OP1wTMJKWwFl(TW!rSUVf)WuGe9Jdb^Kp%<$qxB9(mJ2l_r!$i*Z+EV%Wv+y?;9h2ap$kU@wY9lciuht-n)n0dEZZe zaaZ~QrR=)JelXlL@4WB(|MH7_S|0etz5k+Ad=W4uc-<0zPvPWiNlcwo?B~6o_PSPfA(%$A@J7yb)Vf|`s_d9 zecx|JAiVCgvfppMlf*m&~|#@KT^ehrMBv$Mv?+;~F0UNDR0 z5nP?`$~jJHWME)zZLNQ8xZko@1_sZZIWrI+8W z&X=6LRa9xMEn5}md{>vpHJjOFTB_KlV42Mf=%!xKi%xl9uzwI4Wium&WfwH(f>tV- z`HV*F2Cny&b5`csnzpL<8Kzdwog3IfBpN60nEHj$thKDG2~)qW9#==V=moZmLde-I zMK9EusS$O5#wrYyY%5#IpiTy|62$c+#G+JLHuL42ZeOSruNAE|SH#vX;bd%GbFA$c zwLLmFwc<)eThT8}ToGrRYoVj&pgolp^>nW~6dxRNwV=&&1NS)F+rju%jejb zbocJv$HOuPIDp{uPtP#%JOAH(fioEJ{|Te_Q-*1+l}zo1Zl|n0Z$roqHrXBaFv6|P{!imjwVBWFw#g$;9Qp`}8Las7J z-3eXM9X!;vf`T(-UepCfY-?HzLWFdyEj)87PTnks$MXVFvv1t%a1qO$_lspqib(%8F1OirnPdf7s)#Lvb7ROl__96RnjVD zJsVABE4C=+c^%`OEr(JiTVJI_Y4T8Jt#Y0af~ibSLr0ns5%Mv2f%slf8RuQCHBY;pp}}A?->nR}2HaRWVIUV9m+o^sJI{>Vg(X zi2~T2!q_?Eo()^bkyO5bHSgxD;x4>e2)LV^(p$go#Dq2O-k7`9lFFd{i?&H=Z?5Y0 zCEYCP_Ac_u;@su!yE{?^-O;dxXj{^KWydOoGp1G61@|y|g>n=7Ohj`WEt7NO7RXv8 z*k|IPtWeG(+bPmDM4F0FN35dQ5p~++Eyk=u$kq!M%45| zR<&smNgu(8SpnpRarcX0N6AC=dE}!!@sZU*Ix1nxG-)R0@0?c5T7`K$ZE&+fq)4!q zbv{apkb5#w%m$6Tsn6%1(VL33W`I${78JcV>w=_W(a5hv$u`~4DyHLZ1gs)OWfwYa zt8P2mKaUtZ6eA(O2{VB zOEwjdZkPv_3L;} z6VZZHOK7QVUSSL91^>l~$*C*BnkN4Fh0(c%LoT0QRb2DL06XS3CgIFx;x$t8BfejLr`a48`Lo2bL>&6XhNKgBRn- z_KT61Gn$E#8gp7XxmK(x5T@dHPr{;qJYB&WYRGsMf{msb6Kq?{$u`>V#M|x?twGg1 z3$m78E_OMtM^#cwc*R;f;NppXr;^+BZ2fUqG#N}AjPF`L>*N&6&f|@tnOA8Q$m#i& zoYPp_yd;*v*}3H8+{FBRa4b19dwF7EA~=;yP0Y1$%RxRxp-xKD--FMqmZehVFjttj#K~2 zRBRgssA(Vqz{eF*CdHScA7nEMC4GF!kb!lk?4JPEOXA%dPc- zvHN=5N&QYYu~AL8C!!(s$Tbf%)Vea3XRzem@Qw52{8TN)V<|3*0f5BW*ZTu?Uj*V=Mdm>kV_%%Yv6u` z2b#Dq%6-k;7vr#lOFOxL7nfQ%-@{=qhgKfk&x38;e}MZB^3Wk3KFsBgP$x3%;^CvA z9w~GTxYElT`?)f}l{i-hxiZ9+VXhqKffHOg$(2()bebz?xH7_(vs^jHmGfM=z?BEM z@*r16xpI-qV_X^MfeEf8xPOu>mpDvuWtuAwak$Kt84g#tlH|%PS03id99QPKvcQ!^ zt~|n(t6X`ME01yIajrbUl_$Bf#FdY5NO4GW(73Y9A;Xm{2c3h#VTD7EL!QG^9IkON zITSb)IanM@9G>Q2b0~9gI8->Sa#-VVox=^TJi~p@a`~fN{uqbnINU@pd;;NlgclG# ziSQ!Arx0F3_%v|obNKu`!WR&}i0~B%y^Mj_d4)HB9rno_UIQL{9S^>N@J)no0pIyu zeBMHM1K~}Cw-COE@O^~05#B-g0m2Uv-bEol#^+BE-UA+ZADl*5FA)9`;ja*WiSXA5e}nM12!Dt0_Xz)h@Q(<;M)(Z^dhTC<@A^03Emd5uBix42 zQQukdr3P%GA&)cTKFZjvcAU-1{S)nE2`aGa{qxbXxq(&Nmh9(-Ku0_TXv!`(QExfTIh6p64s-beAK8}_z_8epFU`+-3lgd);`gJ-5x#tyM|#zO2csnkI_ z93jF^nsqUDly#GiJ&YY=y@b+7LqB6pY=CBQ8U`8L&4w6$F%Kgd$j2EAvJ;G*j6_Z` zcAA}GY=oU9%yYo}=kYuG0xHR&H`(F7}&l!VEgBRwJiWUun6qnBf!+Fzz#hsvG&I# zcKC6Lbvz-lBTq`Kb4g-dACcJ6CuG*0l37n$X2&#Oz01J*GQj$?zy@?+aYJH*D-s*Z z0UOTC?D$g>J8=!zNmF8{3KE)y_DC}9^#L1!HXnnt3?LYitMN$;2|y?utj3qxrrX#h zbU&>F)p&J^O;Vf?^Kdn;voyrh#dz8uPh0VHDV}b{|BtdZ@yqQ+R3Cusb9wq@+2@}{ z9%=_mJ|b0(t{5gu)lhpso_rc`nLNifR(qN;Q@g81!LV0jm}S-Ed4vmWeYGd%cTZMh zoahSc$uc0{fGqpNBh~oTp5Ml+Mtjm>(fxQHUynb|dhYbZj};)T>XHeG3QMj+TTl|* z1#>mmJ_&(HbnQChGWh(bP`$3)VT3+}c(sG&I%w#u8a=VyB((>%#4#N)Zd8q#GYA`^UmqhQtlLLc4DLV z%aC{uV#g4FBXJRGYJ3%zea20=B|Lvz%&W#k@)KCG!s%nIdOJ6O7Bo=QPNO$nMmOQk zMmqLk47)_{%g_rOw-({jM(?YTcpe~z-DI5>#tV@AB)|q{PDGXhe1A2bZeMy4){RfW z^os!LQ^`?j>8dadWhj2hA3Wkk&@gk zf6A%%L!gSfmHZ}9L3OY@NGL!5yXbjLe;sab96+JS5qG8DKZaW_H(Ft8P|}ecT5dbg zUVtH4YW~IDyFpo0DPd`^!z7%WnpwLpe+zp*ux|sCV(dG>Y}W-$$illTE?3^=$=#F*aVSGL4PX7=|hu7ao{0LwGy>?0n#3YS1)I_`ST+aA7fOkmAXT#8{S`mi%}N<#MnoAwGoDB?foam(4#T zj}xx26KVfTXj&Ty>2^=kZaY8hMG@OqCpNY9?N`(RA~^37Xs%LjaIkHhgGto=FcNkd z?*iU5LL<`c9wrAjecmPRpe}49KSov$yO&TGqNvoYrvyr~@Hp}6%=K0`Feq=5UiZI6 ztHxmw{Y@Q&2xNH3=d~UTE=DZz6OeLU^`b?i>c<`JJ<_qAu0CHi-i2N|TsQBaxxX6k zpqGgww4j#>qZ?P9RKdNp8jOWVqd_(CKENeMtQdXFGq!b)z{wTbKH=t&@IS_R=yUmP6~w4@CL) zc?PS}f@Xk;oHQ~n6q3ZM@$f@pI-3Y$b$#}6YeF*eg2aiHq ct_}D5chD!v{r-5+f8W1EJ&4%<3y$L_<50C{3jhEB literal 0 HcmV?d00001 diff --git a/lib/flowplayer/flowplayer.audio-3.2.8.swf b/lib/flowplayer/flowplayer.audio-3.2.8.swf deleted file mode 100644 index df9692ae59134c7ee7cf0402c8efdc9285dcba55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4117 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 Te|GM>=l@|mgxvoJgdUr>{ag#O diff --git a/lib/flowplayer/flowplayer.controls-3.2.13.swf b/lib/flowplayer/flowplayer.controls-3.2.13.swf new file mode 100644 index 0000000000000000000000000000000000000000..61e95d478a43fd05e30b9400c8c529c87363e71e GIT binary patch literal 38336 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 literal 0 HcmV?d00001 diff --git a/lib/flowplayer/flowplayer.controls-3.2.9.swf b/lib/flowplayer/flowplayer.controls-3.2.9.swf deleted file mode 100644 index a3d34c7bfcb9113e567ad27feaf9d6bbe5d06822..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37903 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 0 && video.height > 0) { - var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf', width: video.width, height: video.height}; + var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', width: video.width, height: video.height}; } else { - var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf'; + var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf'; } flowplayer(video.id, src, { plugins: {controls: controls}, @@ -1856,17 +1856,17 @@ M.util.load_flowplayer = function() { controls.height = 25; controls.time = true; } - flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf', { - plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.8.swf'}}, + flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', { + plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.10.swf'}}, clip: {url: audio.fileurl, provider: "audio", autoPlay: false} }); } } - if (M.cfg.jsrev == -10) { - var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.8.min.js'; + if (M.cfg.jsrev == -1) { + var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.11.js'; } else { - var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.8.min.js&rev=' + M.cfg.jsrev; + var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.11.min.js&rev=' + M.cfg.jsrev; } var fileref = document.createElement('script'); fileref.setAttribute('type','text/javascript'); diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 48811300c00..4012a3c7a96 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -88,7 +88,7 @@ flowplayer Flowplayer GPL - 3.2.9 + 3.2.14 3 From 0747dabad8791d571a3c961df022af8fe234e790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sun, 2 Sep 2012 15:36:25 +0200 Subject: [PATCH 46/96] MDL-35186 import phpmailer 5.2.1 --- lib/phpmailer/README | 15 +- lib/phpmailer/README_MOODLE.txt | 2 +- lib/phpmailer/changelog.txt | 13 + lib/phpmailer/class.phpmailer.php | 658 +++++++++++++++++++---------- lib/phpmailer/class.smtp.php | 30 +- lib/phpmailer/moodle_phpmailer.php | 37 +- lib/thirdpartylibs.xml | 2 +- 7 files changed, 496 insertions(+), 261 deletions(-) diff --git a/lib/phpmailer/README b/lib/phpmailer/README index 8d48dc05f52..f66012d40e8 100644 --- a/lib/phpmailer/README +++ b/lib/phpmailer/README @@ -1,14 +1,21 @@ /******************************************************************* -* The http://phpmailer.codeworxtech.com/ website now carries a few * -* advertisements through the Google Adsense network. Please visit * -* the advertiser sites and help us offset some of our costs. * -* Thanks .... * +* http://code.google.com/a/apache-extras.org/p/phpmailer/ * ********************************************************************/ PHPMailer Full Featured Email Transfer Class for PHP ========================================== +Version 5.2.1 (January 16, 2012) + +Patch release (see changelog.txt). + +Version 5.2.0 (July 19, 2011) + +With the release of this version, PHPMailer has moved to Apache +Extras: + http://code.google.com/a/apache-extras.org/p/phpmailer/ + Version 5.0.0 (April 02, 2009) With the release of this version, we are initiating a new version numbering diff --git a/lib/phpmailer/README_MOODLE.txt b/lib/phpmailer/README_MOODLE.txt index 57d158c95da..107d288e9df 100644 --- a/lib/phpmailer/README_MOODLE.txt +++ b/lib/phpmailer/README_MOODLE.txt @@ -1,4 +1,4 @@ -Description of PHPMailer 5.1 library import into Moodle +Description of PHPMailer 5.2.1 library import into Moodle We now use a vanilla version of phpmailer and do our customisations in a subclass. diff --git a/lib/phpmailer/changelog.txt b/lib/phpmailer/changelog.txt index a5c0cb57d06..e2c982db4f3 100644 --- a/lib/phpmailer/changelog.txt +++ b/lib/phpmailer/changelog.txt @@ -3,6 +3,19 @@ ChangeLog NOTE: THIS VERSION OF PHPMAILER IS DESIGNED FOR PHP5/PHP6. IT WILL NOT WORK WITH PHP4. +Version 5.2.1 (January 16, 2012) +* Closed several bugs +* Performance improvements +* MsgHTML() now returns the message as required. +* New method: GetSentMIMEMessage() (returns full copy of sent message) + +Version 5.2 (July 19, 2011) +* protected MIME body and header +* better DKIM DNS Resource Record support +* better aly handling +* htmlfilter class added to extras +* moved to Apache Extras + Version 5.1 (October 20, 2009) * fixed filename issue with AddStringAttachment (thanks to Tony) * fixed "SingleTo" property, now works with Senmail, Qmail, and SMTP in diff --git a/lib/phpmailer/class.phpmailer.php b/lib/phpmailer/class.phpmailer.php index 430cbc9ab81..af089d59789 100644 --- a/lib/phpmailer/class.phpmailer.php +++ b/lib/phpmailer/class.phpmailer.php @@ -2,15 +2,15 @@ /*~ class.phpmailer.php .---------------------------------------------------------------------------. | Software: PHPMailer - PHP email class | -| Version: 5.1 | -| Contact: via sourceforge.net support pages (also www.worxware.com) | -| Info: http://phpmailer.sourceforge.net | -| Support: http://sourceforge.net/projects/phpmailer/ | +| Version: 5.2.1 | +| Site: https://code.google.com/a/apache-extras.org/p/phpmailer/ | | ------------------------------------------------------------------------- | -| Admin: Andy Prevost (project admininistrator) | +| Admin: Jim Jagielski (project admininistrator) | | Authors: Andy Prevost (codeworxtech) codeworxtech@users.sourceforge.net | | : Marcus Bointon (coolbru) coolbru@users.sourceforge.net | +| : Jim Jagielski (jimjag) jimjag@gmail.com | | Founder: Brent R. Matzelle (original founder) | +| Copyright (c) 2010-2012, Jim Jagielski. All Rights Reserved. | | Copyright (c) 2004-2009, Andy Prevost. All Rights Reserved. | | Copyright (c) 2001-2003, Brent R. Matzelle | | ------------------------------------------------------------------------- | @@ -19,11 +19,6 @@ | This program is distributed in the hope that it will be useful - WITHOUT | | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | | FITNESS FOR A PARTICULAR PURPOSE. | -| ------------------------------------------------------------------------- | -| We offer a number of paid services (www.worxware.com): | -| - Web Hosting on highly optimized fast and secure servers | -| - Technology Consulting | -| - Oursourcing (highly qualified programmers and graphic designers) | '---------------------------------------------------------------------------' */ @@ -33,8 +28,10 @@ * @package PHPMailer * @author Andy Prevost * @author Marcus Bointon + * @author Jim Jagielski + * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost - * @version $Id$ + * @version $Id: class.phpmailer.php 450 2010-06-23 16:46:33Z coolbru $ * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License */ @@ -118,6 +115,27 @@ class PHPMailer { */ public $AltBody = ''; + /** + * Stores the complete compiled MIME message body. + * @var string + * @access protected + */ + protected $MIMEBody = ''; + + /** + * Stores the complete compiled MIME message headers. + * @var string + * @access protected + */ + protected $MIMEHeader = ''; + + /** + * Stores the complete sent MIME message (Body and Headers) + * @var string + * @access protected + */ + protected $SentMIMEMessage = ''; + /** * Sets word wrapping on the body of the message to a given number of * characters. @@ -269,6 +287,12 @@ class PHPMailer { */ public $DKIM_identity = ''; + /** + * Used with DKIM DNS Resource Record + * @var string + */ + public $DKIM_passphrase = ''; + /** * Used with DKIM DNS Resource Record * optional, in format of email address 'you@yourdomain.com' @@ -300,28 +324,34 @@ class PHPMailer { * Sets the PHPMailer Version number * @var string */ - public $Version = '5.1'; + public $Version = '5.2.1'; + + /** + * What to use in the X-Mailer header + * @var string + */ + public $XMailer = ''; ///////////////////////////////////////////////// // PROPERTIES, PRIVATE AND PROTECTED ///////////////////////////////////////////////// - private $smtp = NULL; - private $to = array(); - private $cc = array(); - private $bcc = array(); - private $ReplyTo = array(); - private $all_recipients = array(); - private $attachment = array(); - private $CustomHeader = array(); - private $message_type = ''; - private $boundary = array(); - protected $language = array(); - private $error_count = 0; - private $sign_cert_file = ""; - private $sign_key_file = ""; - private $sign_key_pass = ""; - private $exceptions = false; + protected $smtp = NULL; + protected $to = array(); + protected $cc = array(); + protected $bcc = array(); + protected $ReplyTo = array(); + protected $all_recipients = array(); + protected $attachment = array(); + protected $CustomHeader = array(); + protected $message_type = ''; + protected $boundary = array(); + protected $language = array(); + protected $error_count = 0; + protected $sign_cert_file = ''; + protected $sign_key_file = ''; + protected $sign_key_pass = ''; + protected $exceptions = false; ///////////////////////////////////////////////// // CONSTANTS @@ -437,7 +467,7 @@ class PHPMailer { * @return boolean */ public function AddReplyTo($address, $name = '') { - return $this->AddAnAddress('ReplyTo', $address, $name); + return $this->AddAnAddress('Reply-To', $address, $name); } /** @@ -447,11 +477,17 @@ class PHPMailer { * @param string $address The email address to send to * @param string $name * @return boolean true on success, false if address already used or invalid in some way - * @access private + * @access protected */ - private function AddAnAddress($kind, $address, $name = '') { - if (!preg_match('/^(to|cc|bcc|ReplyTo)$/', $kind)) { - echo 'Invalid recipient array: ' . kind; + protected function AddAnAddress($kind, $address, $name = '') { + if (!preg_match('/^(to|cc|bcc|Reply-To)$/', $kind)) { + $this->SetError($this->Lang('Invalid recipient array').': '.$kind); + if ($this->exceptions) { + throw new phpmailerException('Invalid recipient array: ' . $kind); + } + if ($this->SMTPDebug) { + echo $this->Lang('Invalid recipient array').': '.$kind; + } return false; } $address = trim($address); @@ -461,10 +497,12 @@ class PHPMailer { if ($this->exceptions) { throw new phpmailerException($this->Lang('invalid_address').': '.$address); } - echo $this->Lang('invalid_address').': '.$address; + if ($this->SMTPDebug) { + echo $this->Lang('invalid_address').': '.$address; + } return false; } - if ($kind != 'ReplyTo') { + if ($kind != 'Reply-To') { if (!isset($this->all_recipients[strtolower($address)])) { array_push($this->$kind, array($address, $name)); $this->all_recipients[strtolower($address)] = true; @@ -485,7 +523,7 @@ class PHPMailer { * @param string $name * @return boolean */ - public function SetFrom($address, $name = '',$auto=1) { + public function SetFrom($address, $name = '', $auto = 1) { $address = trim($address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim if (!self::ValidateAddress($address)) { @@ -493,14 +531,16 @@ class PHPMailer { if ($this->exceptions) { throw new phpmailerException($this->Lang('invalid_address').': '.$address); } - echo $this->Lang('invalid_address').': '.$address; + if ($this->SMTPDebug) { + echo $this->Lang('invalid_address').': '.$address; + } return false; } $this->From = $address; $this->FromName = $name; if ($auto) { if (empty($this->ReplyTo)) { - $this->AddAnAddress('ReplyTo', $address, $name); + $this->AddAnAddress('Reply-To', $address, $name); } if (empty($this->Sender)) { $this->Sender = $address; @@ -544,6 +584,21 @@ class PHPMailer { */ public function Send() { try { + if(!$this->PreSend()) return false; + return $this->PostSend(); + } catch (phpmailerException $e) { + $this->SentMIMEMessage = ''; + $this->SetError($e->getMessage()); + if ($this->exceptions) { + throw $e; + } + return false; + } + } + + protected function PreSend() { + try { + $mailHeader = ""; if ((count($this->to) + count($this->cc) + count($this->bcc)) < 1) { throw new phpmailerException($this->Lang('provide_address'), self::STOP_CRITICAL); } @@ -555,27 +610,58 @@ class PHPMailer { $this->error_count = 0; // reset errors $this->SetMessageType(); - $header = $this->CreateHeader(); - $body = $this->CreateBody(); - + //Refuse to send an empty message if (empty($this->Body)) { throw new phpmailerException($this->Lang('empty_message'), self::STOP_CRITICAL); } - // digitally sign with DKIM if enabled - if ($this->DKIM_domain && $this->DKIM_private) { - $header_dkim = $this->DKIM_Add($header,$this->Subject,$body); - $header = str_replace("\r\n","\n",$header_dkim) . $header; + $this->MIMEHeader = $this->CreateHeader(); + $this->MIMEBody = $this->CreateBody(); + + // To capture the complete message when using mail(), create + // an extra header list which CreateHeader() doesn't fold in + if ($this->Mailer == 'mail') { + if (count($this->to) > 0) { + $mailHeader .= $this->AddrAppend("To", $this->to); + } else { + $mailHeader .= $this->HeaderLine("To", "undisclosed-recipients:;"); + } + $mailHeader .= $this->HeaderLine('Subject', $this->EncodeHeader($this->SecureHeader(trim($this->Subject)))); + // if(count($this->cc) > 0) { + // $mailHeader .= $this->AddrAppend("Cc", $this->cc); + // } } + // digitally sign with DKIM if enabled + if ($this->DKIM_domain && $this->DKIM_private) { + $header_dkim = $this->DKIM_Add($this->MIMEHeader, $this->EncodeHeader($this->SecureHeader($this->Subject)), $this->MIMEBody); + $this->MIMEHeader = str_replace("\r\n", "\n", $header_dkim) . $this->MIMEHeader; + } + + $this->SentMIMEMessage = sprintf("%s%s\r\n\r\n%s",$this->MIMEHeader,$mailHeader,$this->MIMEBody); + return true; + + } catch (phpmailerException $e) { + $this->SetError($e->getMessage()); + if ($this->exceptions) { + throw $e; + } + return false; + } + } + + protected function PostSend() { + try { // Choose the mailer and send through it switch($this->Mailer) { case 'sendmail': - return $this->SendmailSend($header, $body); + return $this->SendmailSend($this->MIMEHeader, $this->MIMEBody); case 'smtp': - return $this->SmtpSend($header, $body); + return $this->SmtpSend($this->MIMEHeader, $this->MIMEBody); + case 'mail': + return $this->MailSend($this->MIMEHeader, $this->MIMEBody); default: - return $this->MailSend($header, $body); + return $this->MailSend($this->MIMEHeader, $this->MIMEBody); } } catch (phpmailerException $e) { @@ -583,7 +669,9 @@ class PHPMailer { if ($this->exceptions) { throw $e; } - echo $e->getMessage()."\n"; + if ($this->SMTPDebug) { + echo $e->getMessage()."\n"; + } return false; } } @@ -612,7 +700,7 @@ class PHPMailer { $result = pclose($mail); // implement call back function if it exists $isSent = ($result == 0) ? 1 : 0; - $this->doCallback($isSent,$val,$this->cc,$this->bcc,$this->Subject,$body); + $this->doCallback($isSent, $val, $this->cc, $this->bcc, $this->Subject, $body); if($result != 0) { throw new phpmailerException($this->Lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } @@ -626,7 +714,7 @@ class PHPMailer { $result = pclose($mail); // implement call back function if it exists $isSent = ($result == 0) ? 1 : 0; - $this->doCallback($isSent,$this->to,$this->cc,$this->bcc,$this->Subject,$body); + $this->doCallback($isSent, $this->to, $this->cc, $this->bcc, $this->Subject, $body); if($result != 0) { throw new phpmailerException($this->Lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } @@ -648,8 +736,12 @@ class PHPMailer { } $to = implode(', ', $toArr); - $params = sprintf("-oi -f %s", $this->Sender); - if ($this->Sender != '' && strlen(ini_get('safe_mode'))< 1) { + if (empty($this->Sender)) { + $params = "-oi "; + } else { + $params = sprintf("-oi -f %s", $this->Sender); + } + if ($this->Sender != '' and !ini_get('safe_mode')) { $old_from = ini_get('sendmail_from'); ini_set('sendmail_from', $this->Sender); if ($this->SingleTo === true && count($toArr) > 1) { @@ -657,13 +749,13 @@ class PHPMailer { $rt = @mail($val, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params); // implement call back function if it exists $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent,$val,$this->cc,$this->bcc,$this->Subject,$body); + $this->doCallback($isSent, $val, $this->cc, $this->bcc, $this->Subject, $body); } } else { $rt = @mail($to, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params); // implement call back function if it exists $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent,$to,$this->cc,$this->bcc,$this->Subject,$body); + $this->doCallback($isSent, $to, $this->cc, $this->bcc, $this->Subject, $body); } } else { if ($this->SingleTo === true && count($toArr) > 1) { @@ -671,13 +763,13 @@ class PHPMailer { $rt = @mail($val, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params); // implement call back function if it exists $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent,$val,$this->cc,$this->bcc,$this->Subject,$body); + $this->doCallback($isSent, $val, $this->cc, $this->bcc, $this->Subject, $body); } } else { - $rt = @mail($to, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header); + $rt = @mail($to, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params); // implement call back function if it exists $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent,$to,$this->cc,$this->bcc,$this->Subject,$body); + $this->doCallback($isSent, $to, $this->cc, $this->bcc, $this->Subject, $body); } } if (isset($old_from)) { @@ -716,11 +808,11 @@ class PHPMailer { $bad_rcpt[] = $to[0]; // implement call back function if it exists $isSent = 0; - $this->doCallback($isSent,$to[0],'','',$this->Subject,$body); + $this->doCallback($isSent, $to[0], '', '', $this->Subject, $body); } else { // implement call back function if it exists $isSent = 1; - $this->doCallback($isSent,$to[0],'','',$this->Subject,$body); + $this->doCallback($isSent, $to[0], '', '', $this->Subject, $body); } } foreach($this->cc as $cc) { @@ -728,11 +820,11 @@ class PHPMailer { $bad_rcpt[] = $cc[0]; // implement call back function if it exists $isSent = 0; - $this->doCallback($isSent,'',$cc[0],'',$this->Subject,$body); + $this->doCallback($isSent, '', $cc[0], '', $this->Subject, $body); } else { // implement call back function if it exists $isSent = 1; - $this->doCallback($isSent,'',$cc[0],'',$this->Subject,$body); + $this->doCallback($isSent, '', $cc[0], '', $this->Subject, $body); } } foreach($this->bcc as $bcc) { @@ -740,11 +832,11 @@ class PHPMailer { $bad_rcpt[] = $bcc[0]; // implement call back function if it exists $isSent = 0; - $this->doCallback($isSent,'','',$bcc[0],$this->Subject,$body); + $this->doCallback($isSent, '', '', $bcc[0], $this->Subject, $body); } else { // implement call back function if it exists $isSent = 1; - $this->doCallback($isSent,'','',$bcc[0],$this->Subject,$body); + $this->doCallback($isSent, '', '', $bcc[0], $this->Subject, $body); } } @@ -822,7 +914,9 @@ class PHPMailer { } } catch (phpmailerException $e) { $this->smtp->Reset(); - throw $e; + if ($this->exceptions) { + throw $e; + } } return true; } @@ -942,7 +1036,7 @@ class PHPMailer { $line = explode($this->LE, $message); $message = ''; - for ($i=0 ;$i < count($line); $i++) { + for ($i = 0 ;$i < count($line); $i++) { $line_part = explode(' ', $line[$i]); $buf = ''; for ($e = 0; $emessage_type) { case 'alt': - case 'alt_attachments': + case 'alt_inline': + case 'alt_attach': + case 'alt_inline_attach': $this->AltBody = $this->WrapText($this->AltBody, $this->WordWrap); break; default: @@ -1077,12 +1173,13 @@ class PHPMailer { $uniq_id = md5(uniqid(time())); $this->boundary[1] = 'b1_' . $uniq_id; $this->boundary[2] = 'b2_' . $uniq_id; + $this->boundary[3] = 'b3_' . $uniq_id; $result .= $this->HeaderLine('Date', self::RFCDate()); if($this->Sender == '') { - $result .= $this->HeaderLine('Return-Path', trim($this->SecureHeader($this->From))); // Moodle modification + $result .= $this->HeaderLine('Return-Path', trim($this->From)); } else { - $result .= $this->HeaderLine('Return-Path', trim($this->SecureHeader($this->Sender))); // Moodle modification + $result .= $this->HeaderLine('Return-Path', trim($this->Sender)); } // To be created automatically by mail() @@ -1098,7 +1195,7 @@ class PHPMailer { $result .= $this->HeaderLine('To', 'undisclosed-recipients:;'); } } - } + } $from = array(); $from[0][0] = trim($this->From); @@ -1116,7 +1213,7 @@ class PHPMailer { } if(count($this->ReplyTo) > 0) { - $result .= $this->AddrAppend('Reply-to', $this->ReplyTo); + $result .= $this->AddrAppend('Reply-To', $this->ReplyTo); } // mail() sets the subject itself @@ -1125,12 +1222,16 @@ class PHPMailer { } if($this->MessageID != '') { - $result .= $this->HeaderLine('Message-ID',$this->MessageID); + $result .= $this->HeaderLine('Message-ID', $this->MessageID); } else { $result .= sprintf("Message-ID: <%s@%s>%s", $uniq_id, $this->ServerHostname(), $this->LE); } $result .= $this->HeaderLine('X-Priority', $this->Priority); - $result .= $this->HeaderLine('X-Mailer', 'PHPMailer '.$this->Version.' (phpmailer.worxware.com)'); + if($this->XMailer) { + $result .= $this->HeaderLine('X-Mailer', $this->XMailer); + } else { + $result .= $this->HeaderLine('X-Mailer', 'PHPMailer '.$this->Version.' (http://code.google.com/a/apache-extras.org/p/phpmailer/)'); + } if($this->ConfirmReadingTo != '') { $result .= $this->HeaderLine('Disposition-Notification-To', '<' . trim($this->ConfirmReadingTo) . '>'); @@ -1158,18 +1259,21 @@ class PHPMailer { switch($this->message_type) { case 'plain': $result .= $this->HeaderLine('Content-Transfer-Encoding', $this->Encoding); - $result .= sprintf("Content-Type: %s; charset=\"%s\"", $this->ContentType, $this->CharSet); + $result .= $this->TextLine('Content-Type: '.$this->ContentType.'; charset="'.$this->CharSet.'"'); break; - case 'attachments': - case 'alt_attachments': - if($this->InlineImageExists()){ - $result .= sprintf("Content-Type: %s;%s\ttype=\"text/html\";%s\tboundary=\"%s\"%s", 'multipart/related', $this->LE, $this->LE, $this->boundary[1], $this->LE); - } else { - $result .= $this->HeaderLine('Content-Type', 'multipart/mixed;'); - $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"'); - } + case 'inline': + $result .= $this->HeaderLine('Content-Type', 'multipart/related;'); + $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + case 'attach': + case 'inline_attach': + case 'alt_attach': + case 'alt_inline_attach': + $result .= $this->HeaderLine('Content-Type', 'multipart/mixed;'); + $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"'); break; case 'alt': + case 'alt_inline': $result .= $this->HeaderLine('Content-Type', 'multipart/alternative;'); $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"'); break; @@ -1182,6 +1286,16 @@ class PHPMailer { return $result; } + /** + * Returns the MIME message (headers and body). Only really valid post PreSend(). + * @access public + * @return string + */ + public function GetSentMIMEMessage() { + return $this->SentMIMEMessage; + } + + /** * Assembles the message body. Returns an empty string on failure. * @access public @@ -1197,6 +1311,33 @@ class PHPMailer { $this->SetWordWrap(); switch($this->message_type) { + case 'plain': + $body .= $this->EncodeString($this->Body, $this->Encoding); + break; + case 'inline': + $body .= $this->GetBoundary($this->boundary[1], '', '', ''); + $body .= $this->EncodeString($this->Body, $this->Encoding); + $body .= $this->LE.$this->LE; + $body .= $this->AttachAll("inline", $this->boundary[1]); + break; + case 'attach': + $body .= $this->GetBoundary($this->boundary[1], '', '', ''); + $body .= $this->EncodeString($this->Body, $this->Encoding); + $body .= $this->LE.$this->LE; + $body .= $this->AttachAll("attachment", $this->boundary[1]); + break; + case 'inline_attach': + $body .= $this->TextLine("--" . $this->boundary[1]); + $body .= $this->HeaderLine('Content-Type', 'multipart/related;'); + $body .= $this->TextLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->GetBoundary($this->boundary[2], '', '', ''); + $body .= $this->EncodeString($this->Body, $this->Encoding); + $body .= $this->LE.$this->LE; + $body .= $this->AttachAll("inline", $this->boundary[2]); + $body .= $this->LE; + $body .= $this->AttachAll("attachment", $this->boundary[1]); + break; case 'alt': $body .= $this->GetBoundary($this->boundary[1], '', 'text/plain', ''); $body .= $this->EncodeString($this->AltBody, $this->Encoding); @@ -1206,26 +1347,56 @@ class PHPMailer { $body .= $this->LE.$this->LE; $body .= $this->EndBoundary($this->boundary[1]); break; - case 'plain': - $body .= $this->EncodeString($this->Body, $this->Encoding); - break; - case 'attachments': - $body .= $this->GetBoundary($this->boundary[1], '', '', ''); - $body .= $this->EncodeString($this->Body, $this->Encoding); - $body .= $this->LE; - $body .= $this->AttachAll(); - break; - case 'alt_attachments': - $body .= sprintf("--%s%s", $this->boundary[1], $this->LE); - $body .= sprintf("Content-Type: %s;%s" . "\tboundary=\"%s\"%s", 'multipart/alternative', $this->LE, $this->boundary[2], $this->LE.$this->LE); - $body .= $this->GetBoundary($this->boundary[2], '', 'text/plain', '') . $this->LE; // Create text body + case 'alt_inline': + $body .= $this->GetBoundary($this->boundary[1], '', 'text/plain', ''); $body .= $this->EncodeString($this->AltBody, $this->Encoding); $body .= $this->LE.$this->LE; - $body .= $this->GetBoundary($this->boundary[2], '', 'text/html', '') . $this->LE; // Create the HTML body + $body .= $this->TextLine("--" . $this->boundary[1]); + $body .= $this->HeaderLine('Content-Type', 'multipart/related;'); + $body .= $this->TextLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->GetBoundary($this->boundary[2], '', 'text/html', ''); + $body .= $this->EncodeString($this->Body, $this->Encoding); + $body .= $this->LE.$this->LE; + $body .= $this->AttachAll("inline", $this->boundary[2]); + $body .= $this->LE; + $body .= $this->EndBoundary($this->boundary[1]); + break; + case 'alt_attach': + $body .= $this->TextLine("--" . $this->boundary[1]); + $body .= $this->HeaderLine('Content-Type', 'multipart/alternative;'); + $body .= $this->TextLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->GetBoundary($this->boundary[2], '', 'text/plain', ''); + $body .= $this->EncodeString($this->AltBody, $this->Encoding); + $body .= $this->LE.$this->LE; + $body .= $this->GetBoundary($this->boundary[2], '', 'text/html', ''); $body .= $this->EncodeString($this->Body, $this->Encoding); $body .= $this->LE.$this->LE; $body .= $this->EndBoundary($this->boundary[2]); - $body .= $this->AttachAll(); + $body .= $this->LE; + $body .= $this->AttachAll("attachment", $this->boundary[1]); + break; + case 'alt_inline_attach': + $body .= $this->TextLine("--" . $this->boundary[1]); + $body .= $this->HeaderLine('Content-Type', 'multipart/alternative;'); + $body .= $this->TextLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->GetBoundary($this->boundary[2], '', 'text/plain', ''); + $body .= $this->EncodeString($this->AltBody, $this->Encoding); + $body .= $this->LE.$this->LE; + $body .= $this->TextLine("--" . $this->boundary[2]); + $body .= $this->HeaderLine('Content-Type', 'multipart/related;'); + $body .= $this->TextLine("\tboundary=\"" . $this->boundary[3] . '"'); + $body .= $this->LE; + $body .= $this->GetBoundary($this->boundary[3], '', 'text/html', ''); + $body .= $this->EncodeString($this->Body, $this->Encoding); + $body .= $this->LE.$this->LE; + $body .= $this->AttachAll("inline", $this->boundary[3]); + $body .= $this->LE; + $body .= $this->EndBoundary($this->boundary[2]); + $body .= $this->LE; + $body .= $this->AttachAll("attachment", $this->boundary[1]); break; } @@ -1238,8 +1409,8 @@ class PHPMailer { $signed = tempnam("", "signed"); if (@openssl_pkcs7_sign($file, $signed, "file://".$this->sign_cert_file, array("file://".$this->sign_key_file, $this->sign_key_pass), NULL)) { @unlink($file); - @unlink($signed); $body = file_get_contents($signed); + @unlink($signed); } else { @unlink($file); @unlink($signed); @@ -1258,9 +1429,10 @@ class PHPMailer { /** * Returns the start of a message boundary. - * @access private + * @access protected + * @return string */ - private function GetBoundary($boundary, $charSet, $contentType, $encoding) { + protected function GetBoundary($boundary, $charSet, $contentType, $encoding) { $result = ''; if($charSet == '') { $charSet = $this->CharSet; @@ -1272,7 +1444,7 @@ class PHPMailer { $encoding = $this->Encoding; } $result .= $this->TextLine('--' . $boundary); - $result .= sprintf("Content-Type: %s; charset = \"%s\"", $contentType, $charSet); + $result .= sprintf("Content-Type: %s; charset=\"%s\"", $contentType, $charSet); $result .= $this->LE; $result .= $this->HeaderLine('Content-Transfer-Encoding', $encoding); $result .= $this->LE; @@ -1282,31 +1454,25 @@ class PHPMailer { /** * Returns the end of a message boundary. - * @access private + * @access protected + * @return string */ - private function EndBoundary($boundary) { + protected function EndBoundary($boundary) { return $this->LE . '--' . $boundary . '--' . $this->LE; } /** * Sets the message type. - * @access private + * @access protected * @return void */ - private function SetMessageType() { - if(count($this->attachment) < 1 && strlen($this->AltBody) < 1) { - $this->message_type = 'plain'; - } else { - if(count($this->attachment) > 0) { - $this->message_type = 'attachments'; - } - if(strlen($this->AltBody) > 0 && count($this->attachment) < 1) { - $this->message_type = 'alt'; - } - if(strlen($this->AltBody) > 0 && count($this->attachment) > 0) { - $this->message_type = 'alt_attachments'; - } - } + protected function SetMessageType() { + $this->message_type = array(); + if($this->AlternativeExists()) $this->message_type[] = "alt"; + if($this->InlineImageExists()) $this->message_type[] = "inline"; + if($this->AttachmentExists()) $this->message_type[] = "attach"; + $this->message_type = implode("_", $this->message_type); + if($this->message_type == "") $this->message_type = "plain"; } /** @@ -1367,7 +1533,9 @@ class PHPMailer { if ($this->exceptions) { throw $e; } - echo $e->getMessage()."\n"; + if ($this->SMTPDebug) { + echo $e->getMessage()."\n"; + } if ( $e->getCode() == self::STOP_CRITICAL ) { return false; } @@ -1386,10 +1554,10 @@ class PHPMailer { /** * Attaches all fs, string, and binary attachments to the message. * Returns an empty string on failure. - * @access private + * @access protected * @return string */ - private function AttachAll() { + protected function AttachAll($disposition_type, $boundary) { // Return text of body $mime = array(); $cidUniq = array(); @@ -1397,54 +1565,58 @@ class PHPMailer { // Add all attachments foreach ($this->attachment as $attachment) { - // Check for string attachment - $bString = $attachment[5]; - if ($bString) { - $string = $attachment[0]; - } else { - $path = $attachment[0]; - } - - if (in_array($attachment[0], $incl)) { continue; } - $filename = $attachment[1]; - $name = $attachment[2]; - $encoding = $attachment[3]; - $type = $attachment[4]; - $disposition = $attachment[6]; - $cid = $attachment[7]; - $incl[] = $attachment[0]; - if ( $disposition == 'inline' && isset($cidUniq[$cid]) ) { continue; } - $cidUniq[$cid] = true; - - $mime[] = sprintf("--%s%s", $this->boundary[1], $this->LE); - $mime[] = sprintf("Content-Type: %s; name=\"%s\"%s", $type, $this->EncodeHeader($this->SecureHeader($name)), $this->LE); - $mime[] = sprintf("Content-Transfer-Encoding: %s%s", $encoding, $this->LE); - - if($disposition == 'inline') { - $mime[] = sprintf("Content-ID: <%s>%s", $cid, $this->LE); - } - - $mime[] = sprintf("Content-Disposition: %s; filename=\"%s\"%s", $disposition, $this->EncodeHeader($this->SecureHeader($name)), $this->LE.$this->LE); - - // Encode as string attachment - if($bString) { - $mime[] = $this->EncodeString($string, $encoding); - if($this->IsError()) { - return ''; + // CHECK IF IT IS A VALID DISPOSITION_FILTER + if($attachment[6] == $disposition_type) { + // Check for string attachment + $bString = $attachment[5]; + if ($bString) { + $string = $attachment[0]; + } else { + $path = $attachment[0]; } - $mime[] = $this->LE.$this->LE; - } else { - $mime[] = $this->EncodeFile($path, $encoding); - if($this->IsError()) { - return ''; + + $inclhash = md5(serialize($attachment)); + if (in_array($inclhash, $incl)) { continue; } + $incl[] = $inclhash; + $filename = $attachment[1]; + $name = $attachment[2]; + $encoding = $attachment[3]; + $type = $attachment[4]; + $disposition = $attachment[6]; + $cid = $attachment[7]; + if ( $disposition == 'inline' && isset($cidUniq[$cid]) ) { continue; } + $cidUniq[$cid] = true; + + $mime[] = sprintf("--%s%s", $boundary, $this->LE); + $mime[] = sprintf("Content-Type: %s; name=\"%s\"%s", $type, $this->EncodeHeader($this->SecureHeader($name)), $this->LE); + $mime[] = sprintf("Content-Transfer-Encoding: %s%s", $encoding, $this->LE); + + if($disposition == 'inline') { + $mime[] = sprintf("Content-ID: <%s>%s", $cid, $this->LE); + } + + $mime[] = sprintf("Content-Disposition: %s; filename=\"%s\"%s", $disposition, $this->EncodeHeader($this->SecureHeader($name)), $this->LE.$this->LE); + + // Encode as string attachment + if($bString) { + $mime[] = $this->EncodeString($string, $encoding); + if($this->IsError()) { + return ''; + } + $mime[] = $this->LE.$this->LE; + } else { + $mime[] = $this->EncodeFile($path, $encoding); + if($this->IsError()) { + return ''; + } + $mime[] = $this->LE.$this->LE; } - $mime[] = $this->LE.$this->LE; } } - $mime[] = sprintf("--%s--%s", $this->boundary[1], $this->LE); + $mime[] = sprintf("--%s--%s", $boundary, $this->LE); - return join('', $mime); + return implode("", $mime); } /** @@ -1453,10 +1625,10 @@ class PHPMailer { * @param string $path The full path to the file * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' * @see EncodeFile() - * @access private + * @access protected * @return string */ - private function EncodeFile($path, $encoding = 'base64') { + protected function EncodeFile($path, $encoding = 'base64') { try { if (!is_readable($path)) { throw new phpmailerException($this->Lang('file_open') . $path, self::STOP_CONTINUE); @@ -1466,13 +1638,23 @@ class PHPMailer { return false; } } - if (PHP_VERSION < 6) { - $magic_quotes = get_magic_quotes_runtime(); - set_magic_quotes_runtime(0); - } + $magic_quotes = get_magic_quotes_runtime(); + if ($magic_quotes) { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + set_magic_quotes_runtime(0); + } else { + ini_set('magic_quotes_runtime', 0); + } + } $file_buffer = file_get_contents($path); $file_buffer = $this->EncodeString($file_buffer, $encoding); - if (PHP_VERSION < 6) { set_magic_quotes_runtime($magic_quotes); } + if ($magic_quotes) { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + set_magic_quotes_runtime($magic_quotes); + } else { + ini_set('magic_quotes_runtime', $magic_quotes); + } + } return $file_buffer; } catch (Exception $e) { $this->SetError($e->getMessage()); @@ -1488,7 +1670,7 @@ class PHPMailer { * @access public * @return string */ - public function EncodeString ($str, $encoding = 'base64') { + public function EncodeString($str, $encoding = 'base64') { $encoded = ''; switch(strtolower($encoding)) { case 'base64': @@ -1637,7 +1819,7 @@ class PHPMailer { * @return string */ public function EncodeQPphp( $input = '', $line_max = 76, $space_conv = false) { - $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'); + $hex = array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'); $lines = preg_split('/(?:\r\n|\r|\n)/', $input); $eol = "\r\n"; $escape = '='; @@ -1718,7 +1900,7 @@ class PHPMailer { * @access public * @return string */ - public function EncodeQ ($str, $position = 'text') { + public function EncodeQ($str, $position = 'text') { // There should not be any EOL in the string $encoded = preg_replace('/[\r\n]*/', '', $str); @@ -1733,7 +1915,7 @@ class PHPMailer { // Replace every high ascii, control =, ? and _ characters //TODO using /e (equivalent to eval()) is probably not a good idea $encoded = preg_replace('/([\000-\011\013\014\016-\037\075\077\137\177-\377])/e', - "'='.sprintf('%02X', ord('\\1'))", $encoded); + "'='.sprintf('%02X', ord(stripslashes('\\1')))", $encoded); break; } @@ -1807,6 +1989,20 @@ class PHPMailer { return true; } + public function AddStringEmbeddedImage($string, $cid, $filename = '', $encoding = 'base64', $type = 'application/octet-stream') { + // Append to $attachment array + $this->attachment[] = array( + 0 => $string, + 1 => $filename, + 2 => basename($filename), + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => 'inline', + 7 => $cid + ); + } + /** * Returns true if an inline attachment is present. * @access public @@ -1821,6 +2017,19 @@ class PHPMailer { return false; } + public function AttachmentExists() { + foreach($this->attachment as $attachment) { + if ($attachment[6] == 'attachment') { + return true; + } + } + return false; + } + + public function AlternativeExists() { + return strlen($this->AltBody)>0; + } + ///////////////////////////////////////////////// // CLASS METHODS, MESSAGE RESET ///////////////////////////////////////////////// @@ -1933,10 +2142,10 @@ class PHPMailer { /** * Returns the server hostname or 'localhost.localdomain' if unknown. - * @access private + * @access protected * @return string */ - private function ServerHostname() { + protected function ServerHostname() { if (!empty($this->Hostname)) { $result = $this->Hostname; } elseif (isset($_SERVER['SERVER_NAME'])) { @@ -1950,10 +2159,10 @@ class PHPMailer { /** * Returns a message in the appropriate language. - * @access private + * @access protected * @return string */ - private function Lang($key) { + protected function Lang($key) { if(count($this->language) < 1) { $this->SetLanguage('en'); // set the default language } @@ -1976,10 +2185,10 @@ class PHPMailer { /** * Changes every end of line from CR or LF to CRLF. - * @access private + * @access public * @return string */ - private function FixEOL($str) { + public function FixEOL($str) { $str = str_replace("\r\n", "\n", $str); $str = str_replace("\r", "\n", $str); $str = str_replace("\n", $this->LE, $str); @@ -2001,34 +2210,37 @@ class PHPMailer { * @return $message */ public function MsgHTML($message, $basedir = '') { - preg_match_all("/(src|background)=\"(.*)\"/Ui", $message, $images); + preg_match_all("/(src|background)=[\"'](.*)[\"']/Ui", $message, $images); if(isset($images[2])) { foreach($images[2] as $i => $url) { // do not change urls for absolute images (thanks to corvuscorax) - if (!preg_match('#^[A-z]+://#',$url)) { + if (!preg_match('#^[A-z]+://#', $url)) { $filename = basename($url); $directory = dirname($url); - ($directory == '.')?$directory='':''; + ($directory == '.') ? $directory='': ''; $cid = 'cid:' . md5($filename); $ext = pathinfo($filename, PATHINFO_EXTENSION); $mimeType = self::_mime_types($ext); - if ( strlen($basedir) > 1 && substr($basedir,-1) != '/') { $basedir .= '/'; } - if ( strlen($directory) > 1 && substr($directory,-1) != '/') { $directory .= '/'; } - if ( $this->AddEmbeddedImage($basedir.$directory.$filename, md5($filename), $filename, 'base64',$mimeType) ) { - $message = preg_replace("/".$images[1][$i]."=\"".preg_quote($url, '/')."\"/Ui", $images[1][$i]."=\"".$cid."\"", $message); + if ( strlen($basedir) > 1 && substr($basedir, -1) != '/') { $basedir .= '/'; } + if ( strlen($directory) > 1 && substr($directory, -1) != '/') { $directory .= '/'; } + if ( $this->AddEmbeddedImage($basedir.$directory.$filename, md5($filename), $filename, 'base64', $mimeType) ) { + $message = preg_replace("/".$images[1][$i]."=[\"']".preg_quote($url, '/')."[\"']/Ui", $images[1][$i]."=\"".$cid."\"", $message); } } } } $this->IsHTML(true); $this->Body = $message; - $textMsg = trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/s','',$message))); - if (!empty($textMsg) && empty($this->AltBody)) { - $this->AltBody = html_entity_decode($textMsg); - } + if (empty($this->AltBody)) { + $textMsg = trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/s', '', $message))); + if (!empty($textMsg)) { + $this->AltBody = html_entity_decode($textMsg, ENT_QUOTES, $this->CharSet); + } + } if (empty($this->AltBody)) { $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . "\n\n"; } + return $message; } /** @@ -2192,14 +2404,14 @@ class PHPMailer { * @param string $key_pass Password for private key */ public function DKIM_QP($txt) { - $tmp=""; - $line=""; - for ($i=0;$iDKIM_private); - if ($this->DKIM_passphrase!='') { - $privKey = openssl_pkey_get_private($privKeyStr,$this->DKIM_passphrase); + if ($this->DKIM_passphrase != '') { + $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); } else { $privKey = $privKeyStr; } @@ -2230,15 +2442,15 @@ class PHPMailer { * @param string $s Header */ public function DKIM_HeaderC($s) { - $s=preg_replace("/\r\n\s+/"," ",$s); - $lines=explode("\r\n",$s); - foreach ($lines as $key=>$line) { - list($heading,$value)=explode(":",$line,2); - $heading=strtolower($heading); - $value=preg_replace("/\s+/"," ",$value) ; // Compress useless spaces - $lines[$key]=$heading.":".trim($value) ; // Don't forget to remove WSP around the value + $s = preg_replace("/\r\n\s+/", " ", $s); + $lines = explode("\r\n", $s); + foreach ($lines as $key => $line) { + list($heading, $value) = explode(":", $line, 2); + $heading = strtolower($heading); + $value = preg_replace("/\s+/", " ", $value) ; // Compress useless spaces + $lines[$key] = $heading.":".trim($value) ; // Don't forget to remove WSP around the value } - $s=implode("\r\n",$lines); + $s = implode("\r\n", $lines); return $s; } @@ -2251,11 +2463,11 @@ class PHPMailer { public function DKIM_BodyC($body) { if ($body == '') return "\r\n"; // stabilize line endings - $body=str_replace("\r\n","\n",$body); - $body=str_replace("\n","\r\n",$body); + $body = str_replace("\r\n", "\n", $body); + $body = str_replace("\n", "\r\n", $body); // END stabilize line endings - while (substr($body,strlen($body)-4,4) == "\r\n\r\n") { - $body=substr($body,0,strlen($body)-2); + while (substr($body, strlen($body) - 4, 4) == "\r\n\r\n") { + $body = substr($body, 0, strlen($body) - 2); } return $body; } @@ -2268,23 +2480,23 @@ class PHPMailer { * @param string $subject Subject * @param string $body Body */ - public function DKIM_Add($headers_line,$subject,$body) { + public function DKIM_Add($headers_line, $subject, $body) { $DKIMsignatureType = 'rsa-sha1'; // Signature & hash algorithms $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body $DKIMquery = 'dns/txt'; // Query method $DKIMtime = time() ; // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) $subject_header = "Subject: $subject"; - $headers = explode("\r\n",$headers_line); + $headers = explode($this->LE, $headers_line); foreach($headers as $header) { - if (strpos($header,'From:') === 0) { - $from_header=$header; - } elseif (strpos($header,'To:') === 0) { - $to_header=$header; + if (strpos($header, 'From:') === 0) { + $from_header = $header; + } elseif (strpos($header, 'To:') === 0) { + $to_header = $header; } } - $from = str_replace('|','=7C',$this->DKIM_QP($from_header)); - $to = str_replace('|','=7C',$this->DKIM_QP($to_header)); - $subject = str_replace('|','=7C',$this->DKIM_QP($subject_header)) ; // Copied header fields (dkim-quoted-printable + $from = str_replace('|', '=7C', $this->DKIM_QP($from_header)); + $to = str_replace('|', '=7C', $this->DKIM_QP($to_header)); + $subject = str_replace('|', '=7C', $this->DKIM_QP($subject_header)) ; // Copied header fields (dkim-quoted-printable $body = $this->DKIM_BodyC($body); $DKIMlen = strlen($body) ; // Length of body $DKIMb64 = base64_encode(pack("H*", sha1($body))) ; // Base64 of packed binary SHA-1 hash of body @@ -2303,10 +2515,10 @@ class PHPMailer { return "X-PHPMAILER-DKIM: phpmailer.worxware.com\r\n".$dkimhdrs.$signed."\r\n"; } - protected function doCallback($isSent,$to,$cc,$bcc,$subject,$body) { + protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body) { if (!empty($this->action_function) && function_exists($this->action_function)) { - $params = array($isSent,$to,$cc,$bcc,$subject,$body); - call_user_func_array($this->action_function,$params); + $params = array($isSent, $to, $cc, $bcc, $subject, $body); + call_user_func_array($this->action_function, $params); } } } diff --git a/lib/phpmailer/class.smtp.php b/lib/phpmailer/class.smtp.php index 6d3f24eff85..6977bffad14 100644 --- a/lib/phpmailer/class.smtp.php +++ b/lib/phpmailer/class.smtp.php @@ -2,15 +2,15 @@ /*~ class.smtp.php .---------------------------------------------------------------------------. | Software: PHPMailer - PHP email class | -| Version: 5.1 | -| Contact: via sourceforge.net support pages (also www.codeworxtech.com) | -| Info: http://phpmailer.sourceforge.net | -| Support: http://sourceforge.net/projects/phpmailer/ | +| Version: 5.2.1 | +| Site: https://code.google.com/a/apache-extras.org/p/phpmailer/ | | ------------------------------------------------------------------------- | -| Admin: Andy Prevost (project admininistrator) | +| Admin: Jim Jagielski (project admininistrator) | | Authors: Andy Prevost (codeworxtech) codeworxtech@users.sourceforge.net | | : Marcus Bointon (coolbru) coolbru@users.sourceforge.net | +| : Jim Jagielski (jimjag) jimjag@gmail.com | | Founder: Brent R. Matzelle (original founder) | +| Copyright (c) 2010-2012, Jim Jagielski. All Rights Reserved. | | Copyright (c) 2004-2009, Andy Prevost. All Rights Reserved. | | Copyright (c) 2001-2003, Brent R. Matzelle | | ------------------------------------------------------------------------- | @@ -19,11 +19,6 @@ | This program is distributed in the hope that it will be useful - WITHOUT | | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | | FITNESS FOR A PARTICULAR PURPOSE. | -| ------------------------------------------------------------------------- | -| We offer a number of paid services (www.codeworxtech.com): | -| - Web Hosting on highly optimized fast and secure servers | -| - Technology Consulting | -| - Oursourcing (highly qualified programmers and graphic designers) | '---------------------------------------------------------------------------' */ @@ -34,8 +29,10 @@ * @author Andy Prevost * @author Marcus Bointon * @copyright 2004 - 2008 Andy Prevost + * @author Jim Jagielski + * @copyright 2010 - 2012 Jim Jagielski * @license http://www.gnu.org/copyleft/lesser.html Distributed under the Lesser General Public License (LGPL) - * @version $Id$ + * @version $Id: class.smtp.php 450 2010-06-23 16:46:33Z coolbru $ */ /** @@ -71,6 +68,12 @@ class SMTP { */ public $do_verp = false; + /** + * Sets the SMTP PHPMailer Version number + * @var string + */ + public $Version = '5.2.1'; + ///////////////////////////////////////////////// // PROPERTIES, PRIVATE AND PROTECTED ///////////////////////////////////////////////// @@ -794,7 +797,8 @@ class SMTP { */ private function get_lines() { $data = ""; - while($str = @fgets($this->smtp_conn,515)) { + while(!feof($this->smtp_conn)) { + $str = @fgets($this->smtp_conn,515); if($this->do_debug >= 4) { echo "SMTP -> get_lines(): \$data was \"$data\"" . $this->CRLF . '
'; echo "SMTP -> get_lines(): \$str is \"$str\"" . $this->CRLF . '
'; @@ -811,4 +815,4 @@ class SMTP { } -?> \ No newline at end of file +?> diff --git a/lib/phpmailer/moodle_phpmailer.php b/lib/phpmailer/moodle_phpmailer.php index debfb6b7676..03d88eeb46c 100644 --- a/lib/phpmailer/moodle_phpmailer.php +++ b/lib/phpmailer/moodle_phpmailer.php @@ -1,31 +1,30 @@ . /** - * Moodle - Modular Object-Oriented Dynamic Learning Environment - * http://moodle.org - * Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * Customised version of phpmailer for Moodle * * @package moodle * @subpackage lib * @author Dan Poltawski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * - * Customised version of phpmailer for Moodle */ +defined('MOODLE_INTERNAL') || die(); + // PLEASE NOTE: we use the phpmailer class _unmodified_ // through the joys of OO. Distros are free to use their stock // version of this file. @@ -73,7 +72,7 @@ class moodle_phpmailer extends PHPMailer { * Use internal moodles own textlib to encode mimeheaders. * Fall back to phpmailers inbuilt functions if not */ - public function EncodeHeader ($str, $position = 'text') { + public function EncodeHeader($str, $position = 'text') { $encoded = textlib::encode_mimeheader($str, $this->CharSet); if ($encoded !== false) { $encoded = str_replace("\n", $this->LE, $encoded); diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 48811300c00..58ed03c9f22 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -165,7 +165,7 @@ phpmailer PHPMailer LGPL - 5.1 + 5.2.1 2.1
From 77a1dfd629ce69d17108a41bf4033830dd12f440 Mon Sep 17 00:00:00 2001 From: Mary Evans Date: Sun, 2 Sep 2012 14:58:23 +0100 Subject: [PATCH 47/96] MDL-35180 theme_afterburner: fixes IE9 compatibility view bug --- theme/afterburner/style/afterburner_styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/theme/afterburner/style/afterburner_styles.css b/theme/afterburner/style/afterburner_styles.css index 326e7d542f7..990fe49ecf3 100644 --- a/theme/afterburner/style/afterburner_styles.css +++ b/theme/afterburner/style/afterburner_styles.css @@ -209,6 +209,10 @@ select, input, button { background-color: #34637f; color: #fff; } +.ie7 select { /* fixes compatibility view */ + background-color: #eee; + color: #036; +} #loginbtn, input, button, select { cursor: pointer; margin-left: 5px; From d6ed2191d7deae02a2c345ef585adcf93397d224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sun, 2 Sep 2012 15:58:25 +0200 Subject: [PATCH 48/96] MDL-35188 import tcpdf 5.9.181 --- lib/tcpdf/2dbarcodes.php | 12 +- lib/tcpdf/CHANGELOG.TXT | 97 +++- lib/tcpdf/README.TXT | 6 +- lib/tcpdf/barcodes.php | 12 +- lib/tcpdf/composer.json | 38 ++ lib/tcpdf/config/tcpdf_config.php | 0 lib/tcpdf/qrcode.php | 22 +- lib/tcpdf/readme_moodle.txt | 4 +- lib/tcpdf/tcpdf.php | 734 +++++++++++++++++++++++------- lib/tcpdf/tcpdf_parser.php | 35 +- lib/thirdpartylibs.xml | 2 +- 11 files changed, 747 insertions(+), 215 deletions(-) create mode 100644 lib/tcpdf/composer.json mode change 100644 => 100755 lib/tcpdf/config/tcpdf_config.php diff --git a/lib/tcpdf/2dbarcodes.php b/lib/tcpdf/2dbarcodes.php index e04b3073ce1..6490dfa7eaf 100644 --- a/lib/tcpdf/2dbarcodes.php +++ b/lib/tcpdf/2dbarcodes.php @@ -1,9 +1,9 @@ barcode_array['bcode'][$r][$c] == 1) { // draw a single barcode cell if ($imagick) { - $bar->rectangle($x, $y, ($x + $w), ($y + $h)); + $bar->rectangle($x, $y, ($x + $w - 1), ($y + $h - 1)); } else { - imagefilledrectangle($png, $x, $y, ($x + $w), ($y + $h), $fgcol); + imagefilledrectangle($png, $x, $y, ($x + $w - 1), ($y + $h - 1), $fgcol); } } $x += $w; diff --git a/lib/tcpdf/CHANGELOG.TXT b/lib/tcpdf/CHANGELOG.TXT index 23a14f9991d..3b87c8c356d 100644 --- a/lib/tcpdf/CHANGELOG.TXT +++ b/lib/tcpdf/CHANGELOG.TXT @@ -1,3 +1,98 @@ +5.9.181 (2012-08-31) + - composer.json file was added. + - Bug item #3563369 "Cached images are not unlinked some time" was fixed. + +5.9.180 (2012-08-22) + - Bug item #3560493 "Problems with nested cells in HTML" was fixed. + +5.9.179 (2012-08-04) + - SVG 'use' tag was fixed for 'circle' and 'ellipse' shift problem. + - Alpha status is now correctly stored and restored by getGraphicVars() and SetGraphicVars() methods. + +5.9.178 (2012-08-02) + - SVG 'use' tag was fixed for 'circle' and 'ellipse'. + +5.9.177 (2012-08-02) + - An additional control on annotations was fixed. + +5.9.176 (2012-07-25) + - A bug related to stroke width was fixed. + - A problem related to font spacing in HTML was fixed. + +5.9.175 (2012-07-25) + - The problem of missing letter on hyphen break was fixed. + +5.9.174 (2012-07-25) + - The problem of wrong filename when downloading PDF from an Android device was fixed. + - The method setHeaderData() was extended to set text and line color for header (see example n. 1). + - The method setFooterData() was added to set text and line color for footer (see example n. 1). + - The methods setTextShadow() and getTextShadow() were added to set text shadows (see example n. 1). + - The GetCharWidth() method was fixed for negative character spacing. + - A 'none' border mode is now correctly recognized. + - Break on hyphen problem was fixed. + +5.9.173 (2012-07-23) + - Some additional control wher added on barcode methods. + - The option CURLOPT_FOLLOWLOCATION on Image method is now disabled if PHP safe_mode is on or open_basedir is set. + - Method Bookmark() was extended to include X parameter. + - Method setDestination() was extended to include X parameter. + - A problem with Thai language was fixed. + +5.9.172 (2012-07-02) + - A PNG color profile issue was fixed. + +5.9.171 (2012-07-01) + - Some SVG rendering problems were fixed. + +5.9.170 (2012-06-27) + - Bug #3538227 "Numerous errors inserting shared images" was fixed. + +5.9.169 (2012-06-25) + - Some SVG rendering problems were fixed. + +5.9.168 (2012-06-22) + - Thai language rendering was fixed. + +5.9.167 (2012-06-22) + - Thai language rendering was fixed and improved. + - Method isCharDefined() was improved. + - Protected method replaceChar() was added. + - Font "kerning" word was corrected to "tracking". + +5.9.166 (2012-06-21) + - Array to string conversion on file_id creation was fixed. + - Thai language rendering was fixed (thanks to Atsawin Chaowanakritsanakul). + +5.9.165 (2012-06-07) + - Some HTML form related bugs were fixed. + +5.9.164 (2012-06-06) + - A bug introduced on the latest release was fixed. + +5.9.163 (2012-06-05) + - Method getGDgamma() was changed. + - Rendering performances of PNG images with alpha channel were improved. + +5.9.162 (2012-05-11) + - A bug related to long text on TD cells was fixed. + +5.9.161 (2012-05-09) + - A bug on XREF table was fixed (Bug ID: 3525051). + - Deprecated Imagick:clone was replaced. + - Method objclone() was fixed for PHP4. + +5.9.160 (2012-05-03) + - A bug on tcpdf_parser.php was fixed. + +5.9.159 (2012-04-30) + - Barcode classes were updated to fix PNG export Bug (ID: 3522291). + +5.9.158 (2012-04-22) + - Some SVG-related bugs were fixed. + +5.9.157 (2012-04-16) + - Some SVG-related bugs were fixed. + 5.9.156 (2012-04-10) - Bug item #3515885 "TOC and booklet: left and right page exchanged". - SetAutoPageBreak(false) now works also in multicolumn mode. @@ -567,7 +662,7 @@ - The problem of blank page for nobr table higher than a single page was fixed. 5.9.000 (2010-10-06) - - Support for text stretching and spacing (kerning) was added, see example n. 63 and methods setFontStretching(), getFontStretching(), setFontSpacing(), getFontSpacing(). + - Support for text stretching and spacing (tracking) was added, see example n. 63 and methods setFontStretching(), getFontStretching(), setFontSpacing(), getFontSpacing(). - Support for CSS properties 'font-stretch' and 'letter-spacing' was added (see example n. 63). - The cMargin state was replaced by cell_padding array that can be set/get using setCellPadding() and getCellPadding() methods. - Methods getCellPaddings() and setCellPaddings() were added to fine tune cell paddings (see example n. 5). diff --git a/lib/tcpdf/README.TXT b/lib/tcpdf/README.TXT index 83520bb1272..1d0fc015deb 100644 --- a/lib/tcpdf/README.TXT +++ b/lib/tcpdf/README.TXT @@ -8,8 +8,8 @@ http://sourceforge.net/donate/index.php?group_id=128076 ------------------------------------------------------------ Name: TCPDF -Version: 5.9.156 -Release date: 2012-04-10 +Version: 5.9.181 +Release date: 2012-08-31 Author: Nicola Asuni Copyright (c) 2002-2012: @@ -47,7 +47,7 @@ Main Features: * no-write page regions; * bookmarks, named destinations and table of content; * text hyphenation; - * text stretching and spacing (tracking/kerning); + * text stretching and spacing (tracking); * automatic page break, line break and text alignments including justification; * automatic page numbering and page groups; * move and delete pages; diff --git a/lib/tcpdf/barcodes.php b/lib/tcpdf/barcodes.php index 4027aa7109a..c6c0172e4a3 100644 --- a/lib/tcpdf/barcodes.php +++ b/lib/tcpdf/barcodes.php @@ -1,9 +1,9 @@ * @package com.tecnick.tcpdf - * @version 1.0.023 + * @version 1.0.024 * @author Nicola Asuni */ class TCPDFBarcode { @@ -201,9 +201,9 @@ class TCPDFBarcode { $y = round(($v['p'] * $h / $this->barcode_array['maxh']), 3); // draw a vertical bar if ($imagick) { - $bar->rectangle($x, $y, ($x + $bw), ($y + $bh)); + $bar->rectangle($x, $y, ($x + $bw - 1), ($y + $bh - 1)); } else { - imagefilledrectangle($png, $x, $y, ($x + $bw), ($y + $bh), $fgcol); + imagefilledrectangle($png, $x, $y, ($x + $bw - 1), ($y + $bh - 1), $fgcol); } } $x += $bw; diff --git a/lib/tcpdf/composer.json b/lib/tcpdf/composer.json new file mode 100644 index 00000000000..4cbb538103d --- /dev/null +++ b/lib/tcpdf/composer.json @@ -0,0 +1,38 @@ +{ + "name": "tcpdf/tcpdf", + "version": "5.9.181", + "homepage": "http://www.tcpdf.org/", + "type": "library", + "description": "TCPDF is a PHP class for generating PDF files on-the-fly without requiring external extensions.", + "keywords": ["pdf"], + "license": "LGPLv3", + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "homepage": "http://nicolaasuni.tecnick.com" + } + ], + "require": { + "php": ">5.2" + }, + "autoload": { + "classmap": [ + "fonts", + "config/lang", + "config", + "2dbarcodes.php", + "barcodes.php", + "datamatrix.php", + "encodings_maps.php", + "htmlcolors.php", + "pdf417.php", + "qrcode.php", + "spotcolors.php", + "tcpdf.php", + "tcpdf_filters.php", + "tcpdf_parser.php", + "unicode_data.php" + ] + } +} diff --git a/lib/tcpdf/config/tcpdf_config.php b/lib/tcpdf/config/tcpdf_config.php old mode 100644 new mode 100755 diff --git a/lib/tcpdf/qrcode.php b/lib/tcpdf/qrcode.php index 1fab6b58a77..93b05cbe0a2 100644 --- a/lib/tcpdf/qrcode.php +++ b/lib/tcpdf/qrcode.php @@ -1,9 +1,9 @@ dataStr) > 0) { - if ($this->dataStr == '') { - return 0; - } $mode = $this->identifyMode(0); switch ($mode) { case QR_MODE_NM: { @@ -1476,6 +1474,7 @@ class QRcode { } $this->dataStr = substr($this->dataStr, $length); } + return 0; } /** @@ -2028,7 +2027,7 @@ class QRcode { if ($ver > $this->version) { $this->version = $ver; } - for (;;) { + while (true) { $cbs = $this->createBitStream($items); $items = $cbs[0]; $bits = $cbs[1]; @@ -2315,17 +2314,18 @@ class QRcode { /** * Return a version number that satisfies the input code length. - * @param $size (int) input code length (byte) + * @param $size (int) input code length (bytes) * @param $level (int) error correction level * @return int version number */ protected function getMinimumVersion($size, $level) { - for ($i=1; $i <= QRSPEC_VERSION_MAX; ++$i) { - $words = $this->capacity[$i][QRCAP_WORDS] - $this->capacity[$i][QRCAP_EC][$level]; + for ($i = 1; $i <= QRSPEC_VERSION_MAX; ++$i) { + $words = ($this->capacity[$i][QRCAP_WORDS] - $this->capacity[$i][QRCAP_EC][$level]); if ($words >= $size) { return $i; } } + // the size of input data is greater than QR capacity, try to lover the error correction mode return -1; } diff --git a/lib/tcpdf/readme_moodle.txt b/lib/tcpdf/readme_moodle.txt index 1b6d92480dd..e953af35e22 100644 --- a/lib/tcpdf/readme_moodle.txt +++ b/lib/tcpdf/readme_moodle.txt @@ -1,5 +1,5 @@ -Description of TCPDF library import 5.9.156 -=================================== +Description of TCPDF library import 5.9.181 +=========================================== * delete cache/ doc/ examples/ config/tcpdf_config_alt.php config/lang/ images/ * remove all fonts but the core ones (courier.php, helveticabi.php, helveticab.php, helveticai.php, helvetica.php, symbol.php, timesbi.php, diff --git a/lib/tcpdf/tcpdf.php b/lib/tcpdf/tcpdf.php index 7c18096a52a..c5109bf4a68 100644 --- a/lib/tcpdf/tcpdf.php +++ b/lib/tcpdf/tcpdf.php @@ -1,9 +1,9 @@ no-write page regions; *

  • bookmarks, named destinations and table of content;
  • *
  • text hyphenation;
  • - *
  • text stretching and spacing (tracking/kerning);
  • + *
  • text stretching and spacing (tracking);
  • *
  • automatic page break, line break and text alignments including justification;
  • *
  • automatic page numbering and page groups;
  • *
  • move and delete pages;
  • @@ -137,7 +138,7 @@ * Tools to encode your unicode fonts are on fonts/utils directory.

    * @package com.tecnick.tcpdf * @author Nicola Asuni - * @version 5.9.156 + * @version 5.9.181 */ // Main configuration file. Define the K_TCPDF_EXTERNAL_CONFIG constant to skip this file. @@ -149,7 +150,7 @@ require_once(dirname(__FILE__).'/config/tcpdf_config.php'); * TCPDF project (http://www.tcpdf.org) has been originally derived in 2002 from the Public Domain FPDF class by Olivier Plathey (http://www.fpdf.org), but now is almost entirely rewritten.
    * @package com.tecnick.tcpdf * @brief PHP class for generating PDF documents without requiring external extensions. - * @version 5.9.156 + * @version 5.9.181 * @author Nicola Asuni - info@tecnick.com */ class TCPDF { @@ -160,7 +161,7 @@ class TCPDF { * Current TCPDF version. * @private */ - private $tcpdf_version = '5.9.156'; + private $tcpdf_version = '5.9.181'; // Protected properties @@ -180,7 +181,13 @@ class TCPDF { * Array of object offsets. * @protected */ - protected $offsets; + protected $offsets = array(); + + /** + * Array of object IDs for each page. + * @protected + */ + protected $pageobjects = array(); /** * Buffer holding in-memory PDF. @@ -352,6 +359,12 @@ class TCPDF { */ protected $images = array(); + /** + * Array of cached files. + * @protected + */ + protected $cached_files = array(); + /** * Array of Annotations in pages. * @protected @@ -705,6 +718,41 @@ class TCPDF { */ protected $header_string = ''; + /** + * Color for header text (RGB array). + * @since 5.9.174 (2012-07-25) + * @protected + */ + protected $header_text_color = array(0,0,0); + + /** + * Color for header line (RGB array). + * @since 5.9.174 (2012-07-25) + * @protected + */ + protected $header_line_color = array(0,0,0); + + /** + * Color for footer text (RGB array). + * @since 5.9.174 (2012-07-25) + * @protected + */ + protected $footer_text_color = array(0,0,0); + + /** + * Color for footer line (RGB array). + * @since 5.9.174 (2012-07-25) + * @protected + */ + protected $footer_line_color = array(0,0,0); + + /** + * Text shadow data array. + * @since 5.9.174 (2012-07-25) + * @protected + */ + protected $txtshadow = array('enabled'=>false, 'depth_w'=>0, 'depth_h'=>0, 'color'=>false, 'opacity'=>1, 'blend_mode'=>'Normal'); + /** * Default number of columns for html table. * @protected @@ -1590,7 +1638,7 @@ class TCPDF { protected $font_stretching = 100; /** - * Increases or decreases the space between characters in a text by the specified amount (tracking/kerning). + * Increases or decreases the space between characters in a text by the specified amount (tracking). * @protected * @since 5.9.000 (2010-09-29) */ @@ -1873,6 +1921,13 @@ class TCPDF { */ protected $tcpdflink = true; + /** + * Cache array for computed GD gamma values. + * @protected + * @since 5.9.1632 (2012-06-05) + */ + protected $gdgammacache = array(); + //------------------------------------------------------------ // METHODS //------------------------------------------------------------ @@ -2006,6 +2061,7 @@ class TCPDF { $this->strokecolor = array('R' => 0, 'G' => 0, 'B' => 0); $this->bgcolor = array('R' => 255, 'G' => 255, 'B' => 255); $this->extgstates = array(); + $this->setTextShadow(); // user's rights $this->sign = false; $this->ur['enabled'] = false; @@ -2037,7 +2093,8 @@ class TCPDF { } $this->default_form_prop = array('lineWidth'=>1, 'borderStyle'=>'solid', 'fillColor'=>array(255, 255, 255), 'strokeColor'=>array(128, 128, 128)); // set file ID for trailer - $this->file_id = md5($this->getRandomSeed('TCPDF'.$orientation.$unit.$format.$encoding)); + $serformat = (is_array($format) ? serialize($format) : $format); + $this->file_id = md5($this->getRandomSeed('TCPDF'.$orientation.$unit.$serformat.$encoding)); // set document creation and modification timestamp $this->doc_creation_timestamp = time(); $this->doc_modification_timestamp = $this->doc_creation_timestamp; @@ -3059,8 +3116,8 @@ class TCPDF { // swap X and Y coordinates (change page orientation) $this->swapPageBoxCoordinates($this->page); } - $this->w = $this->wPt / $this->k; - $this->h = $this->hPt / $this->k; + $this->w = ($this->wPt / $this->k); + $this->h = ($this->hPt / $this->k); if ($this->empty_string($autopagebreak)) { if (isset($this->AutoPageBreak)) { $autopagebreak = $this->AutoPageBreak; @@ -4083,13 +4140,28 @@ class TCPDF { * @param $lw (string) header image logo width in mm * @param $ht (string) string to print as title on document header * @param $hs (string) string to print on document header + * @param $tc (array) RGB array color for text. + * @param $lc (array) RGB array color for line. * @public */ - public function setHeaderData($ln='', $lw=0, $ht='', $hs='') { + public function setHeaderData($ln='', $lw=0, $ht='', $hs='', $tc=array(0,0,0), $lc=array(0,0,0)) { $this->header_logo = $ln; $this->header_logo_width = $lw; $this->header_title = $ht; $this->header_string = $hs; + $this->header_text_color = $tc; + $this->header_line_color = $lc; + } + + /** + * Set footer data. + * @param $tc (array) RGB array color for text. + * @param $lc (array) RGB array color for line. + * @public + */ + public function setFooterData($tc=array(0,0,0), $lc=array(0,0,0)) { + $this->footer_text_color = $tc; + $this->footer_line_color = $lc; } /** @@ -4105,6 +4177,8 @@ class TCPDF { $ret['logo_width'] = $this->header_logo_width; $ret['title'] = $this->header_title; $ret['string'] = $this->header_string; + $ret['text_color'] = $this->header_text_color; + $ret['line_color'] = $this->header_line_color; return $ret; } @@ -4238,7 +4312,7 @@ class TCPDF { $header_x = $this->original_lMargin + ($headerdata['logo_width'] * 1.1); } $cw = $this->w - $this->original_lMargin - $this->original_rMargin - ($headerdata['logo_width'] * 1.1); - $this->SetTextColor(0, 0, 0); + $this->SetTextColorArray($this->header_text_color); // header title $this->SetFont($headerfont[0], 'B', $headerfont[2] + 1); $this->SetX($header_x); @@ -4248,7 +4322,7 @@ class TCPDF { $this->SetX($header_x); $this->MultiCell($cw, $cell_height, $headerdata['string'], 0, '', 0, 1, '', '', true, 0, false, true, 0, 'T', false); // print an ending header line - $this->SetLineStyle(array('width' => 0.85 / $this->k, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => array(0, 0, 0))); + $this->SetLineStyle(array('width' => 0.85 / $this->k, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => $headerdata['line_color'])); $this->SetY((2.835 / $this->k) + max($imgy, $this->y)); if ($this->rtl) { $this->SetX($this->original_rMargin); @@ -4284,10 +4358,10 @@ class TCPDF { */ public function Footer() { $cur_y = $this->y; - $this->SetTextColor(0, 0, 0); + $this->SetTextColorArray($this->footer_text_color); //set style for cell border - $line_width = 0.85 / $this->k; - $this->SetLineStyle(array('width' => $line_width, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => array(0, 0, 0))); + $line_width = (0.85 / $this->k); + $this->SetLineStyle(array('width' => $line_width, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => $this->footer_line_color)); //print document barcode $barcode = $this->getBarcode(); if (!empty($barcode)) { @@ -4919,9 +4993,9 @@ class TCPDF { } /** - * Returns the length of the char in user unit for the current font considering current stretching and spacing (tracking/kerning). + * Returns the length of the char in user unit for the current font considering current stretching and spacing (tracking). * @param $char (int) The char code whose length is to be returned - * @param $notlast (boolean) set to false for the latest character on string, true otherwise (default) + * @param $notlast (boolean) If false ignore the font-spacing. * @return float char width * @author Nicola Asuni * @public @@ -4930,7 +5004,7 @@ class TCPDF { public function GetCharWidth($char, $notlast=true) { // get raw width $chw = $this->getRawCharWidth($char); - if (($this->font_spacing != 0) AND $notlast) { + if (($this->font_spacing < 0) OR (($this->font_spacing > 0) AND $notlast)) { // increase/decrease font spacing $chw += $this->font_spacing; } @@ -5400,7 +5474,7 @@ class TCPDF { } /** - * Return the font ascent value + * Return the font ascent value. * @param $font (string) font name * @param $style (string) font style * @param $size (float) The size (in points) @@ -5421,7 +5495,7 @@ class TCPDF { } /** - * Return the font descent value + * Return true in the character is present in the specified font. * @param $char (mixed) Character to check (integer value or string) * @param $font (string) Font name (family name). * @param $style (string) Font style. @@ -5436,6 +5510,9 @@ class TCPDF { $char = $char[0]; } if ($this->empty_string($font)) { + if ($this->empty_string($style)) { + return (isset($this->CurrentFont['cw'][intval($char)])); + } $font = $this->FontFamily; } $fontdata = $this->AddFont($font, $style); @@ -5828,6 +5905,35 @@ class TCPDF { } } $this->checkPageBreak($h + $this->cell_margin['T'] + $this->cell_margin['B']); + // apply text shadow if enabled + if ($this->txtshadow['enabled']) { + // save data + $x = $this->x; + $y = $this->y; + $bc = $this->bgcolor; + $fc = $this->fgcolor; + $sc = $this->strokecolor; + $alpha = $this->alpha; + // print shadow + $this->x += $this->txtshadow['depth_w']; + $this->y += $this->txtshadow['depth_h']; + $this->SetFillColorArray($this->txtshadow['color']); + $this->SetTextColorArray($this->txtshadow['color']); + $this->SetDrawColorArray($this->txtshadow['color']); + if ($this->txtshadow['opacity'] != $alpha['CA']) { + $this->setAlpha($this->txtshadow['opacity'], $this->txtshadow['blend_mode']); + } + $this->_out($this->getCellCode($w, $h, $txt, $border, $ln, $align, $fill, $link, $stretch, true, $calign, $valign)); + //restore data + $this->x = $x; + $this->y = $y; + $this->SetFillColorArray($bc); + $this->SetTextColorArray($fc); + $this->SetDrawColorArray($sc); + if ($this->txtshadow['opacity'] != $alpha['CA']) { + $this->setAlpha($alpha['CA'], $alpha['BM'], $alpha['ca'], $alpha['AIS']); + } + } $this->_out($this->getCellCode($w, $h, $txt, $border, $ln, $align, $fill, $link, $stretch, true, $calign, $valign)); $this->cell_padding = $prev_cell_padding; $this->cell_margin = $prev_cell_margin; @@ -6023,34 +6129,97 @@ class TCPDF { } else { $unicode = $this->UTF8StringToArray($txt); // array of UTF-8 unicode values $unicode = $this->utf8Bidi($unicode, '', $this->tmprtl); + // replace thai chars (if any) if (defined('K_THAI_TOPCHARS') AND (K_THAI_TOPCHARS == true)) { - // ---- Fix for bug #2977340 "Incorrect Thai characters position arrangement" ---- - // NOTE: this doesn't work with HTML justification - // Symbols that could overlap on the font top (only works in LTR) - $topchar = array(3611, 3613, 3615, 3650, 3651, 3652); // chars that extends on top - $topsym = array(3633, 3636, 3637, 3638, 3639, 3655, 3656, 3657, 3658, 3659, 3660, 3661, 3662); // symbols with top position - $numchars = count($unicode); // number of chars - $unik = 0; - $uniblock = array(); - $uniblock[$unik] = array(); - $uniblock[$unik][] = $unicode[0]; - // resolve overlapping conflicts by splitting the string in several parts - for ($i = 1; $i < $numchars; ++$i) { - // check if symbols overlaps at top - if (in_array($unicode[$i], $topsym) AND (in_array($unicode[($i - 1)], $topsym) OR in_array($unicode[($i - 1)], $topchar))) { - // move symbols to another array - ++$unik; - $uniblock[$unik] = array(); - $uniblock[$unik][] = $unicode[$i]; - ++$unik; - $uniblock[$unik] = array(); - $unicode[$i] = 0x200b; // Unicode Character 'ZERO WIDTH SPACE' (DEC:8203, U+200B) + // number of chars + $numchars = count($unicode); + // po pla, for far, for fan + $longtail = array(0x0e1b, 0x0e1d, 0x0e1f); + // do chada, to patak + $lowtail = array(0x0e0e, 0x0e0f); + // mai hun arkad, sara i, sara ii, sara ue, sara uee + $upvowel = array(0x0e31, 0x0e34, 0x0e35, 0x0e36, 0x0e37); + // mai ek, mai tho, mai tri, mai chattawa, karan + $tonemark = array(0x0e48, 0x0e49, 0x0e4a, 0x0e4b, 0x0e4c); + // sara u, sara uu, pinthu + $lowvowel = array(0x0e38, 0x0e39, 0x0e3a); + $output = array(); + for ($i = 0; $i < $numchars; $i++) { + if (($unicode[$i] >= 0x0e00) && ($unicode[$i] <= 0x0e5b)) { + $ch0 = $unicode[$i]; + $ch1 = ($i > 0) ? $unicode[($i - 1)] : 0; + $ch2 = ($i > 1) ? $unicode[($i - 2)] : 0; + $chn = ($i < ($numchars - 1)) ? $unicode[($i + 1)] : 0; + if (in_array($ch0, $tonemark)) { + if ($chn == 0x0e33) { + // sara um + if (in_array($ch1, $longtail)) { + // tonemark at upper left + $output[] = $this->replaceChar($ch0, (0xf713 + $ch0 - 0x0e48)); + } else { + // tonemark at upper right (normal position) + $output[] = $ch0; + } + } elseif (in_array($ch1, $longtail) OR (in_array($ch2, $longtail) AND in_array($ch1, $lowvowel))) { + // tonemark at lower left + $output[] = $this->replaceChar($ch0, (0xf705 + $ch0 - 0x0e48)); + } elseif (in_array($ch1, $upvowel)) { + if (in_array($ch2, $longtail)) { + // tonemark at upper left + $output[] = $this->replaceChar($ch0, (0xf713 + $ch0 - 0x0e48)); + } else { + // tonemark at upper right (normal position) + $output[] = $ch0; + } + } else { + // tonemark at lower right + $output[] = $this->replaceChar($ch0, (0xf70a + $ch0 - 0x0e48)); + } + } elseif (($ch0 == 0x0e33) AND (in_array($ch1, $longtail) OR (in_array($ch2, $longtail) AND in_array($ch1, $tonemark)))) { + // add lower left nikhahit and sara aa + if ($this->isCharDefined(0xf711) AND $this->isCharDefined(0x0e32)) { + $output[] = 0xf711; + $this->CurrentFont['subsetchars'][0xf711] = true; + $output[] = 0x0e32; + $this->CurrentFont['subsetchars'][0x0e32] = true; + } else { + $output[] = $ch0; + } + } elseif (in_array($ch1, $longtail)) { + if ($ch0 == 0x0e31) { + // lower left mai hun arkad + $output[] = $this->replaceChar($ch0, 0xf710); + } elseif (in_array($ch0, $upvowel)) { + // lower left + $output[] = $this->replaceChar($ch0, (0xf701 + $ch0 - 0x0e34)); + } elseif ($ch0 == 0x0e47) { + // lower left mai tai koo + $output[] = $this->replaceChar($ch0, 0xf712); + } else { + // normal character + $output[] = $ch0; + } + } elseif (in_array($ch1, $lowtail) AND in_array($ch0, $lowvowel)) { + // lower vowel + $output[] = $this->replaceChar($ch0, (0xf718 + $ch0 - 0x0e38)); + } elseif (($ch0 == 0x0e0d) AND in_array($chn, $lowvowel)) { + // yo ying without lower part + $output[] = $this->replaceChar($ch0, 0xf70f); + } elseif (($ch0 == 0x0e10) AND in_array($chn, $lowvowel)) { + // tho santan without lower part + $output[] = $this->replaceChar($ch0, 0xf700); + } else { + $output[] = $ch0; + } } else { - $uniblock[$unik][] = $unicode[$i]; + // non-thai character + $output[] = $unicode[$i]; } } - // ---- END OF Fix for bug #2977340 - } + $unicode = $output; + // update font subsetchars + $this->setFontSubBuffer($this->CurrentFont['fontkey'], 'subsetchars', $this->CurrentFont['subsetchars']); + } // end of K_THAI_TOPCHARS $txt2 = $this->arrUTF8ToUTF16BE($unicode, false); } } @@ -6094,7 +6263,7 @@ class TCPDF { $s .= 'q '.$this->TextColor.' '; } // rendering mode - $s .= sprintf('BT %d Tr %F w ET ', $this->textrendermode, $this->textstrokewidth); + $s .= sprintf('BT %d Tr %F w ET ', $this->textrendermode, ($this->textstrokewidth * $this->k)); // count number of spaces $ns = substr_count($txt, chr(32)); // Justification @@ -6250,6 +6419,25 @@ class TCPDF { return $rs; } + /** + * Replace a char if is defined on the current font. + * @param $oldchar (int) Integer code (unicode) of the character to replace. + * @param $newchar (int) Integer code (unicode) of the new character. + * @return int the replaced char or the old char in case the new char i not defined + * @protected + * @since 5.9.167 (2012-06-22) + */ + protected function replaceChar($oldchar, $newchar) { + if ($this->isCharDefined($newchar)) { + // add the new char on the subset list + $this->CurrentFont['subsetchars'][$newchar] = true; + // return the new character + return $newchar; + } + // return the old char + return $oldchar; + } + /** * Returns the code to draw the cell border * @param $x (float) X coordinate. @@ -7069,7 +7257,7 @@ class TCPDF { $w = $this->w - $this->rMargin - $this->x; } // max column width - $wmax = $w - $wadj; + $wmax = ($w - $wadj); if (!$firstline) { $wmax -= ($this->cell_padding['L'] + $this->cell_padding['R']); } @@ -7155,18 +7343,27 @@ class TCPDF { $this->rMargin += $margin['R']; } $w = $this->getRemainingWidth(); - $wmax = $w - $this->cell_padding['L'] - $this->cell_padding['R']; + $wmax = ($w - $this->cell_padding['L'] - $this->cell_padding['R']); } else { // 160 is the non-breaking space. // 173 is SHY (Soft Hypen). // \p{Z} or \p{Separator}: any kind of Unicode whitespace or invisible separator. // \p{Lo} or \p{Other_Letter}: a Unicode letter or ideograph that does not have lowercase and uppercase variants. // \p{Lo} is needed because Chinese characters are packed next to each other without spaces in between. - if (($c != 160) AND (($c == 173) OR preg_match($this->re_spaces, $this->unichr($c)))) { + if (($c != 160) + AND (($c == 173) + OR preg_match($this->re_spaces, $this->unichr($c)) + OR (($c == 45) + AND ($i < ($nb - 1)) + AND @preg_match('/[\p{L}]/'.$this->re_space['m'], $this->unichr($pc)) + AND @preg_match('/[\p{L}]/'.$this->re_space['m'], $this->unichr($chars[($i + 1)])) + ) + ) + ) { // update last blank space position $sep = $i; // check if is a SHY - if ($c == 173) { + if (($c == 173) OR ($c == 45)) { $shy = true; if ($pc == 45) { $tmp_shy_replacement_width = 0; @@ -7191,8 +7388,8 @@ class TCPDF { // we have reached the end of column if ($sep == -1) { // check if the line was already started - if (($this->rtl AND ($this->x <= ($this->w - $this->rMargin - $chrwidth))) - OR ((!$this->rtl) AND ($this->x >= ($this->lMargin + $chrwidth)))) { + if (($this->rtl AND ($this->x <= ($this->w - $this->rMargin - $this->cell_padding['R'] - $margin['R'] - $chrwidth))) + OR ((!$this->rtl) AND ($this->x >= ($this->lMargin + $this->cell_padding['L'] + $margin['L'] + $chrwidth)))) { // print a void cell and go to next line $this->Cell($w, $h, '', 0, 1); $linebreak = true; @@ -7255,9 +7452,9 @@ class TCPDF { $linew = $this->GetArrStringWidth($tmparr); unset($tmparr); if ($this->rtl) { - $this->endlinex = $startx - $linew; + $this->endlinex = ($startx - $linew); } else { - $this->endlinex = $startx + $linew; + $this->endlinex = ($startx + $linew); } $w = $linew; $tmpcellpadding = $this->cell_padding; @@ -7320,6 +7517,9 @@ class TCPDF { $this->Cell($w, $h, $shy_char_left.$tmpstr.$shy_char_right, 0, 1, $align, $fill, $link, $stretch); unset($tmpstr); if ($firstline) { + if ($chars[$sep] == 45) { + $endspace += 1; + } // return the remaining text $this->cell_padding = $tmpcellpadding; return ($this->UniArrSubString($uchars, ($sep + $endspace))); @@ -7327,7 +7527,7 @@ class TCPDF { $i = $sep; $sep = -1; $shy = false; - $j = ($i+1); + $j = ($i + 1); } } // account for margin changes @@ -7655,22 +7855,21 @@ class TCPDF { } // check page for no-write regions and adapt page margins if necessary list($x, $y) = $this->checkPageRegions($h, $x, $y); - $cached_file = false; // true when the file is cached $exurl = ''; // external streams // check if we are passing an image as file or string if ($file[0] === '@') { // image from string $imgdata = substr($file, 1); - $file = K_PATH_CACHE.'img_'.md5($imgdata); + $file = $this->getObjFilename('img'); $fp = fopen($file, 'w'); fwrite($fp, $imgdata); fclose($fp); unset($imgdata); - $cached_file = true; $imsize = @getimagesize($file); if ($imsize === FALSE) { unlink($file); - $cached_file = false; + } else { + $this->cached_files[] = $file; } } else { // image file if ($file{0} === '*') { @@ -7697,7 +7896,9 @@ class TCPDF { curl_setopt($cs, CURLOPT_BINARYTRANSFER, true); curl_setopt($cs, CURLOPT_FAILONERROR, true); curl_setopt($cs, CURLOPT_RETURNTRANSFER, true); - curl_setopt($cs, CURLOPT_FOLLOWLOCATION, true); + if ((ini_get('open_basedir') == '') AND (ini_get('safe_mode') == 'Off')) { + curl_setopt($cs, CURLOPT_FOLLOWLOCATION, true); + } curl_setopt($cs, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($cs, CURLOPT_TIMEOUT, 30); curl_setopt($cs, CURLOPT_SSL_VERIFYPEER, false); @@ -7707,16 +7908,16 @@ class TCPDF { curl_close($cs); if ($imgdata !== FALSE) { // copy image to cache - $file = K_PATH_CACHE.'img_'.md5($imgdata); + $file = $this->getObjFilename('img'); $fp = fopen($file, 'w'); fwrite($fp, $imgdata); fclose($fp); unset($imgdata); - $cached_file = true; $imsize = @getimagesize($file); if ($imsize === FALSE) { unlink($file); - $cached_file = false; + } else { + $this->cached_files[] = $file; } } } elseif (($w > 0) AND ($h > 0)) { @@ -7738,7 +7939,7 @@ class TCPDF { } } // file hash - $filehash = md5($file); + $filehash = md5($this->file_id.$file); // get original image width and height in pixels list($pixw, $pixh) = $imsize; // calculate image width and height on document @@ -7976,10 +8177,6 @@ class TCPDF { // add image to document $this->setImageBuffer($file, $info); } - if ($cached_file) { - // remove cached file - unlink($file); - } // set alignment $this->img_rb_y = $y + $h; // set alignment @@ -8320,11 +8517,13 @@ class TCPDF { $data .= $this->rfread($f, $n); fread($f, 4); } elseif ($type == 'iCCP') { - // skip profile name and null separator + // skip profile name $len = 0; while ((ord(fread($f, 1)) > 0) AND ($len < 80)) { ++$len; } + // skip null separator + fread($f, 1); // get compression method if (ord(fread($f, 1)) != 0) { //$this->Error('Unknown filter method: '.$file); @@ -8396,7 +8595,7 @@ class TCPDF { */ protected function ImagePngAlpha($file, $x, $y, $wpx, $hpx, $w, $h, $type, $link, $align, $resize, $dpi, $palign, $filehash='') { if (empty($filehash)) { - $filehash = md5($file); + $filehash = md5($this->file_id.$file); } // create temp image file (without alpha channel) $tempfile_plain = K_PATH_CACHE.'mskp_'.$filehash; @@ -8407,7 +8606,7 @@ class TCPDF { $img = new Imagick(); $img->readImage($file); // clone image object - $imga = $img->clone(); + $imga = $this->objclone($img); // extract alpha channel $img->separateImageChannel(8); // 8 = (imagick::CHANNEL_ALPHA | imagick::CHANNEL_OPACITY | imagick::CHANNEL_MATTE); $img->negateImage(true); @@ -8429,9 +8628,7 @@ class TCPDF { for ($xpx = 0; $xpx < $wpx; ++$xpx) { for ($ypx = 0; $ypx < $hpx; ++$ypx) { $color = imagecolorat($img, $xpx, $ypx); - $alpha = ($color >> 24); // shifts off the first 24 bits (where 8x3 are used for each color), and returns the remaining 7 allocated bits (commonly used for alpha) - $alpha = (((127 - $alpha) / 127) * 255); // GD alpha is only 7 bit (0 -> 127) - $alpha = $this->getGDgamma($alpha); // correct gamma + $alpha = $this->getGDgamma($color); // correct gamma imagesetpixel($imgalpha, $xpx, $ypx, $alpha); } } @@ -8455,13 +8652,27 @@ class TCPDF { } /** - * Correct the gamma value to be used with GD library - * @param $v (float) the gamma value to be corrected + * Get the GD-corrected PNG gamma value from alpha color + * @param $c (int) alpha color * @protected * @since 4.3.007 (2008-12-04) */ - protected function getGDgamma($v) { - return (pow(($v / 255), 2.2) * 255); + protected function getGDgamma($c) { + if (!isset($this->gdgammacache["'".$c."'"])) { + // shifts off the first 24 bits (where 8x3 are used for each color), + // and returns the remaining 7 allocated bits (commonly used for alpha) + $alpha = ($c >> 24); + // GD alpha is only 7 bit (0 -> 127) + $alpha = (((127 - $alpha) / 127) * 255); + // correct gamma + $this->gdgammacache["'".$c."'"] = (pow(($alpha / 255), 2.2) * 255); + // store the latest values on cache to improve performances + if (count($this->gdgammacache) > 8) { + // remove one element from the cache array + array_shift($this->gdgammacache); + } + } + return $this->gdgammacache["'".$c."'"]; } /** @@ -8736,7 +8947,7 @@ class TCPDF { header('Pragma: public'); header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); // Date in the past header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); - header('Content-Disposition: inline; filename="'.basename($name).'";'); + header('Content-Disposition: inline; filename="'.basename($name).'"'); $this->sendOutputData($this->getBuffer(), $this->bufferlen); } else { echo $this->getBuffer(); @@ -8767,7 +8978,7 @@ class TCPDF { header('Content-Type: application/pdf'); } // use the Content-Disposition header to supply a recommended filename - header('Content-Disposition: attachment; filename="'.basename($name).'";'); + header('Content-Disposition: attachment; filename="'.basename($name).'"'); header('Content-Transfer-Encoding: binary'); $this->sendOutputData($this->getBuffer(), $this->bufferlen); break; @@ -8794,7 +9005,7 @@ class TCPDF { header('Pragma: public'); header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); // Date in the past header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); - header('Content-Disposition: inline; filename="'.basename($name).'";'); + header('Content-Disposition: inline; filename="'.basename($name).'"'); $this->sendOutputData(file_get_contents($name), filesize($name)); } elseif ($dest == 'FD') { // send headers to browser @@ -8819,7 +9030,7 @@ class TCPDF { header('Content-Type: application/pdf'); } // use the Content-Disposition header to supply a recommended filename - header('Content-Disposition: attachment; filename="'.basename($name).'";'); + header('Content-Disposition: attachment; filename="'.basename($name).'"'); header('Content-Transfer-Encoding: binary'); $this->sendOutputData(file_get_contents($name), filesize($name)); } @@ -8865,6 +9076,7 @@ class TCPDF { AND ($val != 'bufferlen') AND ($val != 'buffer') AND ($val != 'diskcache') + AND ($val != 'cached_files') AND ($val != 'sign') AND ($val != 'signature_data') AND ($val != 'signature_max_length') @@ -8875,6 +9087,15 @@ class TCPDF { } } } + if (isset($this->cached_files) AND !empty($this->cached_files)) { + // remove cached files + foreach ($this->cached_files as $cachefile) { + if (is_file($cachefile)) { + unlink($cachefile); + } + } + unset($this->cached_files); + } } /** @@ -9430,7 +9651,7 @@ class TCPDF { if (isset($pl['opt']['be']) AND (is_array($pl['opt']['be']))) { $annots .= ' /BE <<'; $bstyles = array('S', 'C'); - if (isset($pl['opt']['be']['s']) AND in_array($pl['opt']['be']['s'], $markups)) { + if (isset($pl['opt']['be']['s']) AND in_array($pl['opt']['be']['s'], $bstyles)) { $annots .= ' /S /'.$pl['opt']['bs']['s']; } else { $annots .= ' /S /S'; @@ -9509,9 +9730,11 @@ class TCPDF { $annots .= ' /A <
    _datastring($this->unhtmlentities($pl['txt']), $annot_obj_id).'>>'; } else { // internal link - $l = $this->links[$pl['txt']]; - if (isset($this->page_obj_id[($l[0])])) { - $annots .= sprintf(' /Dest [%u 0 R /XYZ 0 %F null]', $this->page_obj_id[($l[0])], ($this->pagedim[$l[0]]['h'] - ($l[1] * $this->k))); + if (isset($this->links[$pl['txt']])) { + $l = $this->links[$pl['txt']]; + if (isset($this->page_obj_id[($l[0])])) { + $annots .= sprintf(' /Dest [%u 0 R /XYZ 0 %F null]', $this->page_obj_id[($l[0])], ($this->pagedim[$l[0]]['h'] - ($l[1] * $this->k))); + } } } $hmodes = array('N', 'I', 'O', 'P'); @@ -12855,11 +13078,14 @@ class TCPDF { $this->_out('xref'); $this->_out('0 '.($this->n + 1)); $this->_out('0000000000 65535 f '); + $freegen = ($this->n + 2); for ($i=1; $i <= $this->n; ++$i) { if (!isset($this->offsets[$i]) AND ($i > 1)) { - $this->offsets[$i] = $this->offsets[($i - 1)]; + $this->_out(sprintf('0000000000 %05d f ', $freegen)); + ++$freegen; + } else { + $this->_out(sprintf('%010d 00000 n ', $this->offsets[$i])); } - $this->_out(sprintf('%010d 00000 n ', $this->offsets[$i])); } // TRAILER $out = 'trailer'."\n"; @@ -12899,6 +13125,7 @@ class TCPDF { */ protected function _beginpage($orientation='', $format='') { ++$this->page; + $this->pageobjects[$this->page] = array(); $this->setPageBuffer($this->page, ''); // initialize array for graphics tranformation positions inside a page buffer $this->transfmrk[$this->page] = array(); @@ -12967,6 +13194,7 @@ class TCPDF { $objid = $this->n; } $this->offsets[$objid] = $this->bufferlen; + $this->pageobjects[$this->page][] = $objid; return $objid.' 0 obj'; } @@ -15012,7 +15240,7 @@ class TCPDF { * @since 2.1.000 (2008-01-08) */ protected function _outPoint($x, $y) { - $this->_out(sprintf('%F %F m', $x * $this->k, ($this->h - $y) * $this->k)); + $this->_out(sprintf('%F %F m', ($x * $this->k), (($this->h - $y) * $this->k))); } /** @@ -15024,7 +15252,7 @@ class TCPDF { * @since 2.1.000 (2008-01-08) */ protected function _outLine($x, $y) { - $this->_out(sprintf('%F %F l', $x * $this->k, ($this->h - $y) * $this->k)); + $this->_out(sprintf('%F %F l', ($x * $this->k), (($this->h - $y) * $this->k))); } /** @@ -16404,12 +16632,13 @@ class TCPDF { * @param $name (string) Destination name. * @param $y (float) Y position in user units of the destiantion on the selected page (default = -1 = current position; 0 = page start;). * @param $page (int) Target page number (leave empty for current page). + * @param $x (float) X position in user units of the destiantion on the selected page (default = -1 = current position;). * @return (string) Stripped named destination identifier or false in case of error. * @public * @author Christian Deligant, Nicola Asuni * @since 5.9.097 (2011-06-23) */ - public function setDestination($name, $y=-1, $page='') { + public function setDestination($name, $y=-1, $page='', $x=-1) { // remove unsupported characters $name = $this->encodeNameObject($name); if ($this->empty_string($name)) { @@ -16417,6 +16646,17 @@ class TCPDF { } if ($y == -1) { $y = $this->GetY(); + } elseif ($y < 0) { + $y = 0; + } elseif ($y > $this->h) { + $y = $this->h; + } + if ($x == -1) { + $x = $this->GetX(); + } elseif ($x < 0) { + $x = 0; + } elseif ($x > $this->w) { + $x = $this->w; } if (empty($page)) { $page = $this->PageNo(); @@ -16424,7 +16664,7 @@ class TCPDF { return; } } - $this->dests[$name] = array('y' => $y, 'p' => $page); + $this->dests[$name] = array('x' => $x, 'y' => $y, 'p' => $page); return $name; } @@ -16452,7 +16692,7 @@ class TCPDF { $this->n_dests = $this->_newobj(); $out = ' <<'; foreach($this->dests as $name => $o) { - $out .= ' /'.$name.' '.sprintf('[%u 0 R /XYZ 0 %F null]', $this->page_obj_id[($o['p'])], ($this->pagedim[$o['p']]['h'] - ($o['y'] * $this->k))); + $out .= ' /'.$name.' '.sprintf('[%u 0 R /XYZ %F %F null]', $this->page_obj_id[($o['p'])], ($o['x'] * $this->k), ($this->pagedim[$o['p']]['h'] - ($o['y'] * $this->k))); } $out .= ' >>'; $out .= "\n".'endobj'; @@ -16481,11 +16721,12 @@ class TCPDF { * @param $page (int) Target page number (leave empty for current page). * @param $style (string) Font style: B = Bold, I = Italic, BI = Bold + Italic. * @param $color (array) RGB color array (values from 0 to 255). + * @param $x (float) X position in user units of the bookmark on the selected page (default = -1 = current position;). * @public * @author Olivier Plathey, Nicola Asuni * @since 2.1.002 (2008-02-12) */ - public function Bookmark($txt, $level=0, $y=-1, $page='', $style='', $color=array(0,0,0)) { + public function Bookmark($txt, $level=0, $y=-1, $page='', $style='', $color=array(0,0,0), $x=-1) { if ($level < 0) { $level = 0; } @@ -16500,6 +16741,17 @@ class TCPDF { } if ($y == -1) { $y = $this->GetY(); + } elseif ($y < 0) { + $y = 0; + } elseif ($y > $this->h) { + $y = $this->h; + } + if ($x == -1) { + $x = $this->GetX(); + } elseif ($x < 0) { + $x = 0; + } elseif ($x > $this->w) { + $x = $this->w; } if (empty($page)) { $page = $this->PageNo(); @@ -16507,7 +16759,7 @@ class TCPDF { return; } } - $this->outlines[] = array('t' => $txt, 'l' => $level, 'y' => $y, 'p' => $page, 's' => strtoupper($style), 'c' => $color); + $this->outlines[] = array('t' => $txt, 'l' => $level, 'x' => $x, 'y' => $y, 'p' => $page, 's' => strtoupper($style), 'c' => $color); } /** @@ -16590,7 +16842,7 @@ class TCPDF { $out .= ' /Last '.($n + $o['last']).' 0 R'; } if (isset($this->page_obj_id[($o['p'])])) { - $out .= ' '.sprintf('/Dest [%u 0 R /XYZ 0 %F null]', $this->page_obj_id[($o['p'])], ($this->pagedim[$o['p']]['h'] - ($o['y'] * $this->k))); + $out .= ' '.sprintf('/Dest [%u 0 R /XYZ %F %F null]', $this->page_obj_id[($o['p'])], ($o['x'] * $this->k), ($this->pagedim[$o['p']]['h'] - ($o['y'] * $this->k))); } // set font style $style = 0; @@ -19169,7 +19421,7 @@ class TCPDF { */ public function PieSectorXY($xc, $yc, $rx, $ry, $a, $b, $style='FD', $cw=false, $o=0, $nc=2) { if ($this->rtl) { - $xc = $this->w - $xc; + $xc = ($this->w - $xc); } $op = $this->getPathPaintOperator($style); if ($op == 'f') { @@ -19177,8 +19429,8 @@ class TCPDF { } if ($cw) { $d = $b; - $b = 360 - $a + $o; - $a = 360 - $d + $o; + $b = (360 - $a + $o); + $a = (360 - $d + $o); } else { $b += $o; $a += $o; @@ -19578,7 +19830,7 @@ class TCPDF { // create new barcode object $barcodeobj = new TCPDFBarcode($code, $type); $arrcode = $barcodeobj->getBarcodeArray(); - if ($arrcode === false) { + if (($arrcode === false) OR empty($arrcode) OR ($arrcode['maxw'] == 0)) { $this->Error('Error in 1D barcode string'); } // set default values @@ -19949,7 +20201,7 @@ class TCPDF { // create new barcode object $barcodeobj = new TCPDF2DBarcode($code, $type); $arrcode = $barcodeobj->getBarcodeArray(); - if (($arrcode === false) OR empty($arrcode)) { + if (($arrcode === false) OR empty($arrcode) OR !isset($arrcode['num_rows']) OR ($arrcode['num_rows'] == 0) OR !isset($arrcode['num_cols']) OR ($arrcode['num_cols'] == 0)) { $this->Error('Error in 2D barcode string'); } // set default values @@ -20004,6 +20256,9 @@ class TCPDF { // module width and height $mw = $style['module_width']; $mh = $style['module_height']; + if (($mw == 0) OR ($mh == 0)) { + $this->Error('Error in 2D barcode string'); + } // get max dimensions if ($this->rtl) { $maxw = $x - $this->lMargin; @@ -20886,7 +21141,7 @@ class TCPDF { /** * Returns the letter-spacing value from CSS value * @param $spacing (string) letter-spacing value - * @param $parent (float) font spacing (tracking/kerning) value of the parent element + * @param $parent (float) font spacing (tracking) value of the parent element * @return float quantity to increases or decreases the space between characters in a text. * @protected * @since 5.9.000 (2010-10-02) @@ -21499,7 +21754,7 @@ class TCPDF { } if (isset($dom[$key]['style']['border-style'])) { $brd_styles = preg_split('/[\s]+/', trim($dom[$key]['style']['border-style'])); - if (isset($brd_styles[3])) { + if (isset($brd_styles[3]) AND ($brd_styles[3]!='none')) { $dom[$key]['border']['L']['cap'] = 'square'; $dom[$key]['border']['L']['join'] = 'miter'; $dom[$key]['border']['L']['dash'] = $this->getCSSBorderDashStyle($brd_styles[3]); @@ -21834,7 +22089,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $curfontascent = $this->getFontAscent($curfontname, $curfontstyle, $curfontsize); $curfontdescent = $this->getFontDescent($curfontname, $curfontstyle, $curfontsize); $curfontstretcing = $this->font_stretching; - $curfontkerning = $this->font_spacing; + $curfonttracking = $this->font_spacing; $this->newline = true; $newline = true; $startlinepage = $this->page; @@ -21973,7 +22228,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this_method_vars['curfontascent'] = $curfontascent; $this_method_vars['curfontdescent'] = $curfontdescent; $this_method_vars['curfontstretcing'] = $curfontstretcing; - $this_method_vars['curfontkerning'] = $curfontkerning; + $this_method_vars['curfonttracking'] = $curfonttracking; $this_method_vars['minstartliney'] = $minstartliney; $this_method_vars['maxbottomliney'] = $maxbottomliney; $this_method_vars['yshift'] = $yshift; @@ -23051,9 +23306,9 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: } else { $wadj = 0; // space to leave for block continuity if ($this->rtl) { - $cwa = $this->x - $this->lMargin; + $cwa = ($this->x - $this->lMargin); } else { - $cwa = $this->w - $this->rMargin - $this->x; + $cwa = ($this->w - $this->rMargin - $this->x); } if (($strlinelen < $cwa) AND (isset($dom[($key + 1)])) AND ($dom[($key + 1)]['tag']) AND (!$dom[($key + 1)]['block'])) { // check the next text blocks for continuity @@ -23143,6 +23398,14 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: } } else { $loop = 0; + // add the positive font spacing of the last character (if any) + if ($this->font_spacing > 0) { + if ($this->rtl) { + $this->x -= $this->font_spacing; + } else { + $this->x += $this->font_spacing; + } + } } } ++$key; @@ -23740,7 +24003,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $value = $tag['attribute']['value']; } if (isset($tag['attribute']['maxlength']) AND !$this->empty_string($tag['attribute']['maxlength'])) { - $opt['maxlen'] = intval($tag['attribute']['value']); + $opt['maxlen'] = intval($tag['attribute']['maxlength']); } $h = $this->FontSize * $this->cell_height_ratio; if (isset($tag['attribute']['size']) AND !$this->empty_string($tag['attribute']['size'])) { @@ -23786,14 +24049,23 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: break; } case 'checkbox': { + if (!isset($value)) { + break; + } $this->CheckBox($name, $w, $checked, $prop, $opt, $value, '', '', false); break; } case 'radio': { + if (!isset($value)) { + break; + } $this->RadioButton($name, $w, $prop, $opt, $value, $checked, '', '', false); break; } case 'submit': { + if (!isset($value)) { + $value = 'submit'; + } $w = $this->GetStringWidth($value) * 1.5; $h *= 1.6; $prop = array('lineWidth'=>1, 'borderStyle'=>'beveled', 'fillColor'=>array(196, 196, 196), 'strokeColor'=>array(255, 255, 255)); @@ -23810,6 +24082,9 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: break; } case 'reset': { + if (!isset($value)) { + $value = 'reset'; + } $w = $this->GetStringWidth($value) * 1.5; $h *= 1.6; $prop = array('lineWidth'=>1, 'borderStyle'=>'beveled', 'fillColor'=>array(196, 196, 196), 'strokeColor'=>array(255, 255, 255)); @@ -23855,6 +24130,9 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: break; } case 'button': { + if (!isset($value)) { + $value = ' '; + } $w = $this->GetStringWidth($value) * 1.5; $h *= 1.6; $prop = array('lineWidth'=>1, 'borderStyle'=>'beveled', 'fillColor'=>array(196, 196, 196), 'strokeColor'=>array(255, 255, 255)); @@ -24328,13 +24606,10 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: if (end($this->transfmrk[$this->page]) !== false) { $pagemarkkey = key($this->transfmrk[$this->page]); $pagemark = $this->transfmrk[$this->page][$pagemarkkey]; - $this->transfmrk[$this->page][$pagemarkkey] += $offsetlen; } elseif ($this->InFooter) { $pagemark = $this->footerpos[$this->page]; - $this->footerpos[$this->page] += $offsetlen; } else { $pagemark = $this->intmrk[$this->page]; - $this->intmrk[$this->page] += $offsetlen; } $pagebuff = $this->getPageBuffer($this->page); $pstart = substr($pagebuff, 0, $pagemark); @@ -24738,17 +25013,14 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: if (end($this->transfmrk[$this->page]) !== false) { $pagemarkkey = key($this->transfmrk[$this->page]); $pagemark = $this->transfmrk[$this->page][$pagemarkkey]; - $this->transfmrk[$this->page][$pagemarkkey] += $offsetlen; } elseif ($this->InFooter) { $pagemark = $this->footerpos[$this->page]; - $this->footerpos[$this->page] += $offsetlen; } else { $pagemark = $this->intmrk[$this->page]; - $this->intmrk[$this->page] += $offsetlen; } $pagebuff = $this->getPageBuffer($this->page); - $pstart = substr($pagebuff, 0, $this->bordermrk[$this->page]); - $pend = substr($pagebuff, $this->bordermrk[$this->page]); + $pstart = substr($pagebuff, 0, $pagemark); + $pend = substr($pagebuff, $pagemark); $this->setPageBuffer($this->page, $pstart.$ccode.$pend); $this->bordermrk[$this->page] += $offsetlen; $this->cntmrk[$this->page] += $offsetlen; @@ -25278,6 +25550,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 'cell_height_ratio' => $this->cell_height_ratio, 'font_stretching' => $this->font_stretching, 'font_spacing' => $this->font_spacing, + 'alpha' => $this->alpha, // extended 'lasth' => $this->lasth, 'tMargin' => $this->tMargin, @@ -25337,6 +25610,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->cell_height_ratio = $gvars['cell_height_ratio']; $this->font_stretching = $gvars['font_stretching']; $this->font_spacing = $gvars['font_spacing']; + $this->alpha = $gvars['alpha']; if ($extended) { // restore extended values $this->lasth = $gvars['lasth']; @@ -25651,6 +25925,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $tmpintmrk = $this->intmrk[$frompage]; $tmpbordermrk = $this->bordermrk[$frompage]; $tmpcntmrk = $this->cntmrk[$frompage]; + $tmppageobjects = $this->pageobjects[$frompage]; if (isset($this->footerpos[$frompage])) { $tmpfooterpos = $this->footerpos[$frompage]; } @@ -25686,6 +25961,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->intmrk[$i] = $this->intmrk[$j]; $this->bordermrk[$i] = $this->bordermrk[$j]; $this->cntmrk[$i] = $this->cntmrk[$j]; + $this->pageobjects[$i] = $this->pageobjects[$j]; if (isset($this->footerpos[$j])) { $this->footerpos[$i] = $this->footerpos[$j]; } elseif (isset($this->footerpos[$i])) { @@ -25720,6 +25996,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->intmrk[$topage] = $tmpintmrk; $this->bordermrk[$topage] = $tmpbordermrk; $this->cntmrk[$topage] = $tmpcntmrk; + $this->pageobjects[$topage] = $tmppageobjects; if (isset($tmpfooterpos)) { $this->footerpos[$topage] = $tmpfooterpos; } elseif (isset($this->footerpos[$topage])) { @@ -25807,6 +26084,12 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: unset($this->intmrk[$page]); unset($this->bordermrk[$page]); unset($this->cntmrk[$page]); + foreach ($this->pageobjects[$page] as $oid) { + if (isset($this->offsets[$oid])){ + unset($this->offsets[$oid]); + } + } + unset($this->pageobjects[$page]); if (isset($this->footerpos[$page])) { unset($this->footerpos[$page]); } @@ -25841,6 +26124,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->intmrk[$i] = $this->intmrk[$j]; $this->bordermrk[$i] = $this->bordermrk[$j]; $this->cntmrk[$i] = $this->cntmrk[$j]; + $this->pageobjects[$i] = $this->pageobjects[$j]; if (isset($this->footerpos[$j])) { $this->footerpos[$i] = $this->footerpos[$j]; } elseif (isset($this->footerpos[$i])) { @@ -25881,6 +26165,12 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: unset($this->intmrk[$this->numpages]); unset($this->bordermrk[$this->numpages]); unset($this->cntmrk[$this->numpages]); + foreach ($this->pageobjects[$this->numpages] as $oid) { + if (isset($this->offsets[$oid])){ + unset($this->offsets[$oid]); + } + } + unset($this->pageobjects[$this->numpages]); if (isset($this->footerpos[$this->numpages])) { unset($this->footerpos[$this->numpages]); } @@ -25982,6 +26272,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->intmrk[$this->page] = $this->intmrk[$page]; $this->bordermrk[$this->page] = $this->bordermrk[$page]; $this->cntmrk[$this->page] = $this->cntmrk[$page]; + $this->pageobjects[$this->page] = $this->pageobjects[$page]; $this->pageopen[$this->page] = false; if (isset($this->footerpos[$page])) { $this->footerpos[$this->page] = $this->footerpos[$page]; @@ -26007,7 +26298,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $tmpoutlines = $this->outlines; foreach ($tmpoutlines as $key => $outline) { if ($outline['p'] == $page) { - $this->outlines[] = array('t' => $outline['t'], 'l' => $outline['l'], 'y' => $outline['y'], 'p' => $this->page, 's' => $outline['s'], 'c' => $outline['c']); + $this->outlines[] = array('t' => $outline['t'], 'l' => $outline['l'], 'x' => $outline['x'], 'y' => $outline['y'], 'p' => $this->page, 's' => $outline['s'], 'c' => $outline['c']); } } // copy links @@ -26695,7 +26986,59 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: } } $this->textrendermode = $textrendermode; - $this->textstrokewidth = $stroke * $this->k; + $this->textstrokewidth = $stroke; + } + + /** + * Set parameters for drop shadow effect for text. + * @param $params (array) Array of parameters: enabled (boolean) set to true to enable shadow; depth_w (float) shadow width in user units; depth_h (float) shadow height in user units; color (array) shadow color or false to use the stroke color; opacity (float) Alpha value: real value from 0 (transparent) to 1 (opaque); blend_mode (string) blend mode, one of the following: Normal, Multiply, Screen, Overlay, Darken, Lighten, ColorDodge, ColorBurn, HardLight, SoftLight, Difference, Exclusion, Hue, Saturation, Color, Luminosity. + * @since 5.9.174 (2012-07-25) + * @public + */ + public function setTextShadow($params=array('enabled'=>false, 'depth_w'=>0, 'depth_h'=>0, 'color'=>false, 'opacity'=>1, 'blend_mode'=>'Normal')) { + if (isset($params['enabled'])) { + $this->txtshadow['enabled'] = $params['enabled']?true:false; + } else { + $this->txtshadow['enabled'] = false; + } + if (isset($params['depth_w'])) { + $this->txtshadow['depth_w'] = floatval($params['depth_w']); + } else { + $this->txtshadow['depth_w'] = 0; + } + if (isset($params['depth_h'])) { + $this->txtshadow['depth_h'] = floatval($params['depth_h']); + } else { + $this->txtshadow['depth_h'] = 0; + } + if (isset($params['color']) AND ($params['color'] !== false) AND is_array($params['color'])) { + $this->txtshadow['color'] = $params['color']; + } else { + $this->txtshadow['color'] = $this->strokecolor; + } + if (isset($params['opacity'])) { + $this->txtshadow['opacity'] = min(1, max(0, floatval($params['opacity']))); + } else { + $this->txtshadow['opacity'] = 1; + } + if (isset($params['blend_mode']) AND in_array($params['blend_mode'], array('Normal', 'Multiply', 'Screen', 'Overlay', 'Darken', 'Lighten', 'ColorDodge', 'ColorBurn', 'HardLight', 'SoftLight', 'Difference', 'Exclusion', 'Hue', 'Saturation', 'Color', 'Luminosity'))) { + $this->txtshadow['blend_mode'] = $params['blend_mode']; + } else { + $this->txtshadow['blend_mode'] = 'Normal'; + } + if ((($this->txtshadow['depth_w'] == 0) AND ($this->txtshadow['depth_h'] == 0)) OR ($this->txtshadow['opacity'] == 0)) { + $this->txtshadow['enabled'] = false; + } + } + + /** + * Return the text shadow parameters array. + * @return Array of parameters. + * @since 5.9.174 (2012-07-25) + * @public + */ + public function getTextShadow() { + return $this->txtshadow; } /** @@ -27337,7 +27680,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: /** * Get the amount to increase or decrease the space between characters in a text. - * @return int font spacing (tracking/kerning) value + * @return int font spacing (tracking) value * @author Nicola Asuni * @public * @since 5.9.000 (2010-09-29) @@ -27741,7 +28084,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: // scale and translate $e = $ox * $this->k * (1 - $svgscale_x); $f = ($this->h - $oy) * $this->k * (1 - $svgscale_y); - $this->_out(sprintf('%F %F %F %F %F %F cm', $svgscale_x, 0, 0, $svgscale_y, $e + $svgoffset_x, $f + $svgoffset_y)); + $this->_out(sprintf('%F %F %F %F %F %F cm', $svgscale_x, 0, 0, $svgscale_y, ($e + $svgoffset_x), ($f + $svgoffset_y))); // creates a new XML parser to be used by the other XML functions $this->parser = xml_parser_create('UTF-8'); // the following function allows to use parser inside object @@ -28089,10 +28432,10 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $gradient['coords'][3] += $y; } // calculate percentages - $gradient['coords'][0] = ($gradient['coords'][0] - $x) / $w; - $gradient['coords'][1] = ($gradient['coords'][1] - $y) / $h; - $gradient['coords'][2] = ($gradient['coords'][2] - $x) / $w; - $gradient['coords'][3] = ($gradient['coords'][3] - $y) / $h; + $gradient['coords'][0] = (($gradient['coords'][0] - $x) / $w); + $gradient['coords'][1] = (($gradient['coords'][1] - $y) / $h); + $gradient['coords'][2] = (($gradient['coords'][2] - $x) / $w); + $gradient['coords'][3] = (($gradient['coords'][3] - $y) / $h); if (isset($gradient['coords'][4])) { $gradient['coords'][4] /= $w; } @@ -28121,9 +28464,9 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: if ($gradient['type'] == 3) { // circular gradient $cy = $this->h - $y - ($gradient['coords'][1] * ($w + $h)); - $this->_out(sprintf('%F 0 0 %F %F %F cm', $w*$this->k, $w*$this->k, $x*$this->k, $cy*$this->k)); + $this->_out(sprintf('%F 0 0 %F %F %F cm', ($w * $this->k), ($w * $this->k), ($x * $this->k), ($cy * $this->k))); } else { - $this->_out(sprintf('%F 0 0 %F %F %F cm', $w*$this->k, $h*$this->k, $x*$this->k, ($this->h-($y+$h))*$this->k)); + $this->_out(sprintf('%F 0 0 %F %F %F cm', ($w * $this->k), ($h * $this->k), ($x * $this->k), (($this->h - ($y + $h)) * $this->k))); } if (count($gradient['stops']) > 1) { $this->Gradient($gradient['type'], $gradient['coords'], $gradient['stops'], array(), false); @@ -28331,6 +28674,8 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: } else { $this->_outLine($x, $y); } + $x0 = $x; + $y0 = $y; } $xmin = min($xmin, $x); $ymin = min($ymin, $y); @@ -28352,6 +28697,8 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $y = $cp + $yoffset; if ((abs($x0 - $x) >= $minlen) OR (abs($y0 - $y) >= $minlen)) { $this->_outLine($x, $y); + $x0 = $x; + $y0 = $y; } $xmin = min($xmin, $x); $ymin = min($ymin, $y); @@ -28370,6 +28717,8 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $x = $cp + $xoffset; if ((abs($x0 - $x) >= $minlen) OR (abs($y0 - $y) >= $minlen)) { $this->_outLine($x, $y); + $x0 = $x; + $y0 = $y; } $xmin = min($xmin, $x); $xmax = max($xmax, $x); @@ -28384,6 +28733,8 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $y = $cp + $yoffset; if ((abs($x0 - $x) >= $minlen) OR (abs($y0 - $y) >= $minlen)) { $this->_outLine($x, $y); + $x0 = $x; + $y0 = $y; } $ymin = min($ymin, $y); $ymax = max($ymax, $y); @@ -28661,6 +29012,10 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: // get styling properties $prev_svgstyle = $this->svgstyles[(count($this->svgstyles) - 1)]; // previous style $svgstyle = $this->svgstyles[0]; // set default style + if ($clipping AND !isset($attribs['fill']) AND (!isset($attribs['style']) OR (!preg_match('/[;\"\s]{1}fill[\s]*:[\s]*([^;\"]*)/si', $attribs['style'], $attrval)))) { + // default fill attribute for clipping + $attribs['fill'] = 'none'; + } if (isset($attribs['style']) AND !$this->empty_string($attribs['style'])) { // fix style for regular expression $attribs['style'] = ';'.$attribs['style']; @@ -28693,7 +29048,8 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: if (!empty($ctm)) { $tm = $ctm; } else { - $tm = $this->svgstyles[(count($this->svgstyles) - 1)]['transfmatrix']; + //$tm = $this->svgstyles[(count($this->svgstyles) - 1)]['transfmatrix']; + $tm = array(1,0,0,1,0,0); } if (isset($attribs['transform']) AND !empty($attribs['transform'])) { $tm = $this->getTransformationMatrixProduct($tm, $this->getSVGTransformMatrix($attribs['transform'])); @@ -28732,6 +29088,7 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: // group together related graphics elements array_push($this->svgstyles, $svgstyle); $this->StartTransform(); + $this->SVGTransform($tm); $this->setSVGStyles($svgstyle, $prev_svgstyle); break; } @@ -28752,15 +29109,19 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->svggradients[$this->svggradientid]['gradientUnits'] = 'objectBoundingBox'; } //$attribs['spreadMethod'] - $x1 = (isset($attribs['x1'])?$attribs['x1']:'0%'); - $y1 = (isset($attribs['y1'])?$attribs['y1']:'0%'); - $x2 = (isset($attribs['x2'])?$attribs['x2']:'100%'); - $y2 = (isset($attribs['y2'])?$attribs['y2']:'0%'); - if (substr($x1, -1) != '%') { - $this->svggradients[$this->svggradientid]['mode'] = 'measure'; - } else { + if (((!isset($attribs['x1'])) AND (!isset($attribs['y1'])) AND (!isset($attribs['x2'])) AND (!isset($attribs['y2']))) + OR ((isset($attribs['x1']) AND (substr($attribs['x1'], -1) == '%')) + OR (isset($attribs['y1']) AND (substr($attribs['y1'], -1) == '%')) + OR (isset($attribs['x2']) AND (substr($attribs['x2'], -1) == '%')) + OR (isset($attribs['y2']) AND (substr($attribs['y2'], -1) == '%')))) { $this->svggradients[$this->svggradientid]['mode'] = 'percentage'; + } else { + $this->svggradients[$this->svggradientid]['mode'] = 'measure'; } + $x1 = (isset($attribs['x1'])?$attribs['x1']:'0'); + $y1 = (isset($attribs['y1'])?$attribs['y1']:'0'); + $x2 = (isset($attribs['x2'])?$attribs['x2']:'100'); + $y2 = (isset($attribs['y2'])?$attribs['y2']:'0'); if (isset($attribs['gradientTransform'])) { $this->svggradients[$this->svggradientid]['gradientTransform'] = $this->getSVGTransformMatrix($attribs['gradientTransform']); } @@ -28788,16 +29149,18 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->svggradients[$this->svggradientid]['gradientUnits'] = 'objectBoundingBox'; } //$attribs['spreadMethod'] - $cx = (isset($attribs['cx'])?$attribs['cx']:0.5); - $cy = (isset($attribs['cy'])?$attribs['cy']:0.5); - $fx = (isset($attribs['fx'])?$attribs['fx']:$cx); - $fy = (isset($attribs['fy'])?$attribs['fy']:$cy); - $r = (isset($attribs['r'])?$attribs['r']:0.5); - if (isset($attribs['cx']) AND (substr($attribs['cx'], -1) != '%')) { - $this->svggradients[$this->svggradientid]['mode'] = 'measure'; - } else { + if (((!isset($attribs['cx'])) AND (!isset($attribs['cy']))) + OR ((isset($attribs['cx']) AND (substr($attribs['cx'], -1) == '%')) + OR (isset($attribs['cy']) AND (substr($attribs['cy'], -1) == '%')) )) { $this->svggradients[$this->svggradientid]['mode'] = 'percentage'; + } else { + $this->svggradients[$this->svggradientid]['mode'] = 'measure'; } + $cx = (isset($attribs['cx']) ? $attribs['cx'] : 0.5); + $cy = (isset($attribs['cy']) ? $attribs['cy'] : 0.5); + $fx = (isset($attribs['fx']) ? $attribs['fx'] : $cx); + $fy = (isset($attribs['fy']) ? $attribs['fy'] : $cy); + $r = (isset($attribs['r']) ? $attribs['r'] : 0.5); if (isset($attribs['gradientTransform'])) { $this->svggradients[$this->svggradientid]['gradientTransform'] = $this->getSVGTransformMatrix($attribs['gradientTransform']); } @@ -28876,12 +29239,12 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: if ($invisible) { break; } - $cx = (isset($attribs['cx'])?$this->getHTMLUnitToUnits($attribs['cx'], 0, $this->svgunit, false):0); - $cy = (isset($attribs['cy'])?$this->getHTMLUnitToUnits($attribs['cy'], 0, $this->svgunit, false):0); - $r = (isset($attribs['r'])?$this->getHTMLUnitToUnits($attribs['r'], 0, $this->svgunit, false):0); - $x = $cx - $r; - $y = $cy - $r; - $w = 2 * $r; + $r = (isset($attribs['r']) ? $this->getHTMLUnitToUnits($attribs['r'], 0, $this->svgunit, false) : 0); + $cx = (isset($attribs['cx']) ? $this->getHTMLUnitToUnits($attribs['cx'], 0, $this->svgunit, false) : (isset($attribs['x']) ? $this->getHTMLUnitToUnits($attribs['x'], 0, $this->svgunit, false) : 0)); + $cy = (isset($attribs['cy']) ? $this->getHTMLUnitToUnits($attribs['cy'], 0, $this->svgunit, false) : (isset($attribs['y']) ? $this->getHTMLUnitToUnits($attribs['y'], 0, $this->svgunit, false) : 0)); + $x = ($cx - $r); + $y = ($cy - $r); + $w = (2 * $r); $h = $w; if ($clipping) { $this->SVGTransform($tm); @@ -28901,14 +29264,14 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: if ($invisible) { break; } - $cx = (isset($attribs['cx'])?$this->getHTMLUnitToUnits($attribs['cx'], 0, $this->svgunit, false):0); - $cy = (isset($attribs['cy'])?$this->getHTMLUnitToUnits($attribs['cy'], 0, $this->svgunit, false):0); - $rx = (isset($attribs['rx'])?$this->getHTMLUnitToUnits($attribs['rx'], 0, $this->svgunit, false):0); - $ry = (isset($attribs['ry'])?$this->getHTMLUnitToUnits($attribs['ry'], 0, $this->svgunit, false):0); - $x = $cx - $rx; - $y = $cy - $ry; - $w = 2 * $rx; - $h = 2 * $ry; + $rx = (isset($attribs['rx']) ? $this->getHTMLUnitToUnits($attribs['rx'], 0, $this->svgunit, false) : 0); + $ry = (isset($attribs['ry']) ? $this->getHTMLUnitToUnits($attribs['ry'], 0, $this->svgunit, false) : 0); + $cx = (isset($attribs['cx']) ? $this->getHTMLUnitToUnits($attribs['cx'], 0, $this->svgunit, false) : (isset($attribs['x']) ? $this->getHTMLUnitToUnits($attribs['x'], 0, $this->svgunit, false) : 0)); + $cy = (isset($attribs['cy']) ? $this->getHTMLUnitToUnits($attribs['cy'], 0, $this->svgunit, false) : (isset($attribs['y']) ? $this->getHTMLUnitToUnits($attribs['y'], 0, $this->svgunit, false) : 0)); + $x = ($cx - $rx); + $y = ($cy - $ry); + $w = (2 * $rx); + $h = (2 * $ry); if ($clipping) { $this->SVGTransform($tm); $this->Ellipse($cx, $cy, $rx, $ry, 0, 0, 360, 'CNZ', array(), array(), 8); @@ -28981,7 +29344,9 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $this->StartTransform(); $this->SVGTransform($tm); $obstyle = $this->setSVGStyles($svgstyle, $prev_svgstyle, $x, $y, $w, $h, 'PolyLine', array($p, 'CNZ')); - $this->PolyLine($p, 'D', array(), array()); + if (!empty($obstyle)) { + $this->PolyLine($p, $obstyle, array(), array()); + } $this->StopTransform(); } else { // polygon if ($clipping) { @@ -29050,14 +29415,26 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: // text case 'text': case 'tspan': { + // only basic support - advanced features must be implemented $this->svgtextmode['invisible'] = $invisible; if ($invisible) { break; } array_push($this->svgstyles, $svgstyle); - // only basic support - advanced features must be implemented - $x = (isset($attribs['x'])?$this->getHTMLUnitToUnits($attribs['x'], 0, $this->svgunit, false):$this->x); - $y = (isset($attribs['y'])?$this->getHTMLUnitToUnits($attribs['y'], 0, $this->svgunit, false):$this->y); + if (isset($attribs['x'])) { + $x = $this->getHTMLUnitToUnits($attribs['x'], 0, $this->svgunit, false); + } elseif ($name == 'tspan') { + $x = $this->x; + } else { + $x = 0; + } + if (isset($attribs['y'])) { + $y = $this->getHTMLUnitToUnits($attribs['y'], 0, $this->svgunit, false); + } elseif ($name == 'tspan') { + $y = $this->y; + } else { + $y = 0; + } $svgstyle['text-color'] = $svgstyle['fill']; $this->svgtext = ''; if (isset($svgstyle['text-anchor'])) { @@ -29088,16 +29465,19 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: } // use case 'use': { - if (isset($attribs['xlink:href'])) { - $use = $this->svgdefs[substr($attribs['xlink:href'], 1)]; - if (isset($attribs['xlink:href'])) { - unset($attribs['xlink:href']); + if (isset($attribs['xlink:href']) AND !empty($attribs['xlink:href'])) { + $svgdefid = substr($attribs['xlink:href'], 1); + if (isset($this->svgdefs[$svgdefid])) { + $use = $this->svgdefs[$svgdefid]; + if (isset($attribs['xlink:href'])) { + unset($attribs['xlink:href']); + } + if (isset($attribs['id'])) { + unset($attribs['id']); + } + $attribs = array_merge($attribs, $use['attribs']); + $this->startSVGElementHandler($parser, $use['name'], $attribs); } - if (isset($attribs['id'])) { - unset($attribs['id']); - } - $attribs = array_merge($use['attribs'], $attribs); - $this->startSVGElementHandler($parser, $use['name'], $use['attribs']); } break; } @@ -29162,7 +29542,17 @@ Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: $textrendermode = $this->textrendermode; $textstrokewidth = $this->textstrokewidth; $this->setTextRenderingMode($this->svgtextmode['stroke'], true, false); + if ($name == 'text') { + // store current coordinates + $tmpx = $this->x; + $tmpy = $this->y; + } $this->Cell($textlen, 0, $text, 0, 0, '', false, '', 0, false, 'L', 'T'); + if ($name == 'text') { + // restore coordinates + $this->x = $tmpx; + $this->y = $tmpy; + } // restore previous rendering mode $this->textrendermode = $textrendermode; $this->textstrokewidth = $textstrokewidth; diff --git a/lib/tcpdf/tcpdf_parser.php b/lib/tcpdf/tcpdf_parser.php index 122676034d3..f17359f2134 100644 --- a/lib/tcpdf/tcpdf_parser.php +++ b/lib/tcpdf/tcpdf_parser.php @@ -1,9 +1,9 @@ * @package com.tecnick.tcpdf * @author Nicola Asuni - * @version 1.0.000 + * @version 1.0.001 */ // include class for decoding filters @@ -48,7 +48,7 @@ require_once(dirname(__FILE__).'/tcpdf_filters.php'); * This is a PHP class for parsing PDF documents.
    * @package com.tecnick.tcpdf * @brief This is a PHP class for parsing PDF documents.. - * @version 1.0.000 + * @version 1.0.001 * @author Nicola Asuni - info@tecnick.com */ class TCPDF_PARSER { @@ -127,20 +127,29 @@ class TCPDF_PARSER { * @since 1.0.000 (2011-05-24) */ protected function getXrefData($offset=0, $xref=array()) { - // find last startxref - if (preg_match_all('/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i', $this->pdfdata, $matches, PREG_SET_ORDER, $offset) == 0) { - $this->Error('Unable to find startxref'); + if ($offset == 0) { + // find last startxref + if (preg_match_all('/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i', $this->pdfdata, $matches, PREG_SET_ORDER, $offset) == 0) { + $this->Error('Unable to find startxref'); + } + $matches = array_pop($matches); + $startxref = $matches[1]; + } else { + // get the first xref at the specified offset + if (preg_match('/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $offset) == 0) { + $this->Error('Unable to find startxref'); + } + $startxref = $matches[1][0]; } - $matches = array_pop($matches); - $startxref = $matches[1]; // check xref position if (strpos($this->pdfdata, 'xref', $startxref) != $startxref) { $this->Error('Unable to find xref'); } // extract xref data (object indexes and offsets) - $offset = $startxref + 5; + $xoffset = $startxref + 5; // initialize object number $obj_num = 0; + $offset = $xoffset; while (preg_match('/^([0-9]+)[\s]([0-9]+)[\s]?([nf]?)/im', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { $offset = (strlen($matches[0][0]) + $matches[0][1]); if ($matches[3][0] == 'n') { @@ -162,7 +171,7 @@ class TCPDF_PARSER { } } // get trailer data - if (preg_match('/trailer[\s]*<<(.*)>>[\s]*[\r\n]+startxref[\s]*[\r\n]+/isU', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { + if (preg_match('/trailer[\s]*<<(.*)>>[\s]*[\r\n]+startxref[\s]*[\r\n]+/isU', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $xoffset) > 0) { $trailer_data = $matches[1][0]; if (!isset($xref['trailer'])) { // get only the last updated version @@ -188,7 +197,7 @@ class TCPDF_PARSER { } if (preg_match('/Prev[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) { // get previous xref - $xref = getXrefData(substr($this->pdfdata, 0, $startxref), intval($matches[1]), $xref); + $xref = $this->getXrefData(intval($matches[1]), $xref); } } else { $this->Error('Unable to find trailer'); @@ -399,7 +408,7 @@ class TCPDF_PARSER { $offset = $element[2]; // decode stream using stream's dictionary information if ($decoding AND ($element[0] == 'stream') AND (isset($objdata[($i - 1)][0])) AND ($objdata[($i - 1)][0] == '<<')) { - $element[3] = $this->decodeStream($objdata[($i - 1)][1], $element[1]); + $element[3] = $this->decodeStream($objdata[($i - 1)][1], substr($element[1], 1)); } $objdata[$i] = $element; ++$i; diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 48811300c00..43a932fe4c2 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -179,7 +179,7 @@ tcpdf TCPDF LGPL - 5.9.156 + 5.9.181 3 From 1290a9e2a18775b90b33080ebd2f11a44146f7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Sun, 2 Sep 2012 16:10:57 +0200 Subject: [PATCH 49/96] MDL-35189 import typo3 4.7.4 --- lib/thirdpartylibs.xml | 2 +- lib/typo3/class.t3lib_cs.php | 48 ++-- lib/typo3/class.t3lib_div.php | 309 ++++++++----------------- lib/typo3/class.t3lib_l10n_locales.php | 7 +- lib/typo3/readme_moodle.txt | 4 +- 5 files changed, 132 insertions(+), 238 deletions(-) diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 48811300c00..1d966d7ce1e 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -186,7 +186,7 @@ typo3 Typo3 GPL - 4.6.8 + 4.7.4 2.0+ diff --git a/lib/typo3/class.t3lib_cs.php b/lib/typo3/class.t3lib_cs.php index 75273e2fb53..2bb2cc742f4 100644 --- a/lib/typo3/class.t3lib_cs.php +++ b/lib/typo3/class.t3lib_cs.php @@ -194,6 +194,7 @@ class t3lib_cs { // mapping of iso-639-1 language codes to script names var $lang_to_script = array( // iso-639-1 language codes, see http://www.loc.gov/standards/iso639-2/php/code_list.php + 'af' => 'west_european', //Afrikaans 'ar' => 'arabic', 'bg' => 'cyrillic', // Bulgarian 'bs' => 'east_european', // Bosnian @@ -244,6 +245,7 @@ class t3lib_cs { 'zh' => 'chinese', // MS language codes, see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/html/_crt_language_strings.asp // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wceinternational5/html/wce50conLanguageIdentifiersandLocales.asp + 'afk'=> 'west_european', // Afrikaans 'ara' => 'arabic', 'bgr' => 'cyrillic', // Bulgarian 'cat' => 'west_european', // Catalan @@ -304,6 +306,7 @@ class t3lib_cs { 'trk' => 'turkish', 'ukr' => 'cyrillic', // Ukrainian // English language names + 'afrikaans' => 'west_european', 'albanian' => 'albanian', 'arabic' => 'arabic', 'basque' => 'west_european', @@ -412,6 +415,7 @@ class t3lib_cs { // TYPO3 specific: Array with the system charsets used for each system language in TYPO3: // Empty values means "iso-8859-1" var $charSetArray = array( + 'af' => '', 'ar' => 'iso-8859-6', 'ba' => 'iso-8859-2', 'bg' => 'windows-1251', @@ -481,7 +485,7 @@ class t3lib_cs { // TYPO3 specific: Array with the iso names used for each system language in TYPO3: // Missing keys means: same as TYPO3 - // @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - use t3lib_l10n_Locales::getIsoMapping() + // @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - use t3lib_l10n_Locales::getIsoMapping() var $isoArray = array( 'ba' => 'bs', 'br' => 'pt_BR', @@ -568,7 +572,7 @@ class t3lib_cs { if (TYPO3_OS == 'WIN') { $cs = $this->script_to_charset_windows[$script] ? $this->script_to_charset_windows[$script] : 'windows-1252'; } else { - $cs = $this->script_to_charset_unix[$script] ? $this->script_to_charset_unix[$script] : 'iso-8859-1'; + $cs = $this->script_to_charset_unix[$script] ? $this->script_to_charset_unix[$script] : 'utf-8'; } return $cs; @@ -810,26 +814,32 @@ class t3lib_cs { * @param boolean If set, then all string-HTML entities (like & or £ will be converted as well) * @return string Output string */ - function entities_to_utf8($str, $alsoStdHtmlEnt = 0) { + function entities_to_utf8($str, $alsoStdHtmlEnt = FALSE) { if ($alsoStdHtmlEnt) { - $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES)); // Getting them in iso-8859-1 - but thats ok since this is observed below. + $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_COMPAT, 'UTF-8')); } $token = md5(microtime()); $parts = explode($token, preg_replace('/(&([#[:alnum:]]*);)/', $token . '${2}' . $token, $str)); foreach ($parts as $k => $v) { - if ($k % 2) { - if (substr($v, 0, 1) == '#') { // Dec or hex entities: - if (substr($v, 1, 1) == 'x') { - $parts[$k] = $this->UnumberToChar(hexdec(substr($v, 2))); - } else { - $parts[$k] = $this->UnumberToChar(substr($v, 1)); - } - } elseif ($alsoStdHtmlEnt && $trans_tbl['&' . $v . ';']) { // Other entities: - $parts[$k] = $this->utf8_encode($trans_tbl['&' . $v . ';'], 'iso-8859-1'); - } else { // No conversion: - $parts[$k] = '&' . $v . ';'; + // only take every second element + if ($k % 2 === 0) { + continue; + } + + $position = 0; + if (substr($v, $position, 1) == '#') { // Dec or hex entities: + $position++; + if (substr($v, $position, 1) == 'x') { + $v = hexdec(substr($v, ++$position)); + } else { + $v = substr($v, $position); } + $parts[$k] = $this->UnumberToChar($v); + } elseif ($alsoStdHtmlEnt && isset($trans_tbl['&' . $v . ';'])) { // Other entities: + $parts[$k] = $trans_tbl['&' . $v . ';']; + } else { // No conversion: + $parts[$k] = '&' . $v . ';'; } } @@ -1696,9 +1706,9 @@ class t3lib_cs { /** * Converts special chars (like æøåÆØÅ, umlauts etc) to ascii equivalents (usually double-bytes, like æ => ae etc.) * - * @param string Character set of string - * @param string Input string to convert - * @return string The converted string + * @param string $charset Character set of string + * @param string $string Input string to convert + * @return string The converted string */ function specCharsToASCII($charset, $string) { if ($charset == 'utf-8') { @@ -2342,4 +2352,4 @@ if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLA include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_cs.php']); } -?> \ No newline at end of file +?> diff --git a/lib/typo3/class.t3lib_div.php b/lib/typo3/class.t3lib_div.php index 6a6c241e0ee..1d7ddb28f39 100644 --- a/lib/typo3/class.t3lib_div.php +++ b/lib/typo3/class.t3lib_div.php @@ -375,7 +375,7 @@ final class t3lib_div { } else { // this case should not happen $csConvObj = self::makeInstance('t3lib_cs'); - return $csConvObj->crop('iso-8859-1', $string, $chars, $appendString); + return $csConvObj->crop('utf-8', $string, $chars, $appendString); } } @@ -386,7 +386,7 @@ final class t3lib_div { * @param string $newlineChar The string to implode the broken lines with (default/typically \n) * @param integer $lineWidth The line width * @return string reformatted text - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - Use t3lib_utility_Mail::breakLinesForEmail() + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - Use t3lib_utility_Mail::breakLinesForEmail() */ public static function breakLinesForEmail($str, $newlineChar = LF, $lineWidth = 76) { self::logDeprecatedFunction(); @@ -856,7 +856,7 @@ final class t3lib_div { * @param integer $max Higher limit * @param integer $zeroValue Default value if input is FALSE. * @return integer The input value forced into the boundaries of $min and $max - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - Use t3lib_utility_Math::forceIntegerInRange() instead + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - Use t3lib_utility_Math::forceIntegerInRange() instead */ public static function intInRange($theInt, $min, $max = 2000000000, $zeroValue = 0) { self::logDeprecatedFunction(); @@ -868,7 +868,7 @@ final class t3lib_div { * * @param integer $theInt Integer string to process * @return integer - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - Use t3lib_utility_Math::convertToPositiveInteger() instead + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - Use t3lib_utility_Math::convertToPositiveInteger() instead */ public static function intval_positive($theInt) { self::logDeprecatedFunction(); @@ -880,7 +880,7 @@ final class t3lib_div { * * @param string $verNumberStr Version number on format x.x.x * @return integer Integer version of version number (where each part can count to 999) - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.9 - Use t3lib_utility_VersionNumber::convertVersionNumberToInteger() instead + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.1 - Use t3lib_utility_VersionNumber::convertVersionNumberToInteger() instead */ public static function int_from_ver($verNumberStr) { // Deprecation log is activated only for TYPO3 4.7 and above @@ -1066,25 +1066,12 @@ final class t3lib_div { return self::modifyHTMLColor($color, $all, $all, $all); } - /** - * Removes comma (if present) in the end of string - * - * @param string $string String from which the comma in the end (if any) will be removed. - * @return string - * @deprecated since TYPO3 4.5, will be removed in TYPO3 4.7 - Use rtrim() directly - */ - public static function rm_endcomma($string) { - self::logDeprecatedFunction(); - - return rtrim($string, ','); - } - /** * Tests if the input can be interpreted as integer. * * @param mixed $var Any input variable to test * @return boolean Returns TRUE if string is an integer - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - Use t3lib_utility_Math::canBeInterpretedAsInteger() instead + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - Use t3lib_utility_Math::canBeInterpretedAsInteger() instead */ public static function testInt($var) { self::logDeprecatedFunction(); @@ -1177,7 +1164,7 @@ final class t3lib_div { * @param string $string Input string, eg "123 + 456 / 789 - 4" * @return integer Calculated value. Or error string. * @see calcParenthesis() - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - Use t3lib_utility_Math::calculateWithPriorityToAdditionAndSubtraction() instead + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - Use t3lib_utility_Math::calculateWithPriorityToAdditionAndSubtraction() instead */ public static function calcPriority($string) { self::logDeprecatedFunction(); @@ -1191,7 +1178,7 @@ final class t3lib_div { * @param string $string Input string, eg "(123 + 456) / 789 - 4" * @return integer Calculated value. Or error string. * @see calcPriority(), tslib_cObj::stdWrap() - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - Use t3lib_utility_Math::calculateWithParentheses() instead + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - Use t3lib_utility_Math::calculateWithParentheses() instead */ public static function calcParenthesis($string) { self::logDeprecatedFunction(); @@ -1272,7 +1259,10 @@ final class t3lib_div { if (strlen($email) > 320) { return FALSE; } - return (filter_var($email, FILTER_VALIDATE_EMAIL) !== FALSE); + require_once(PATH_typo3 . 'contrib/idna/idna_convert.class.php'); + $IDN = new idna_convert(array('idn_version' => 2008)); + + return (filter_var($IDN->encode($email), FILTER_VALIDATE_EMAIL) !== FALSE); } /** @@ -1522,7 +1512,10 @@ final class t3lib_div { * @return boolean Whether the given URL is valid */ public static function isValidUrl($url) { - return (filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED) !== FALSE); + require_once(PATH_typo3 . 'contrib/idna/idna_convert.class.php'); + $IDN = new idna_convert(array('idn_version' => 2008)); + + return (filter_var($IDN->encode($url), FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED) !== FALSE); } @@ -1862,28 +1855,30 @@ final class t3lib_div { * @param array $arr1 Second array, overruling the first array * @param boolean $notAddKeys If set, keys that are NOT found in $arr0 (first array) will not be set. Thus only existing value can/will be overruled from second array. * @param boolean $includeEmptyValues If set, values from $arr1 will overrule if they are empty or zero. Default: TRUE + * @param boolean $enableUnsetFeature If set, special values "__UNSET" can be used in the second array in order to unset array keys in the resulting array. * @return array Resulting array where $arr1 values has overruled $arr0 values */ - public static function array_merge_recursive_overrule(array $arr0, array $arr1, $notAddKeys = FALSE, $includeEmptyValues = TRUE) { + public static function array_merge_recursive_overrule(array $arr0, array $arr1, $notAddKeys = FALSE, $includeEmptyValues = TRUE, $enableUnsetFeature = TRUE) { foreach ($arr1 as $key => $val) { if (is_array($arr0[$key])) { if (is_array($arr1[$key])) { - $arr0[$key] = self::array_merge_recursive_overrule($arr0[$key], $arr1[$key], $notAddKeys, $includeEmptyValues); + $arr0[$key] = self::array_merge_recursive_overrule( + $arr0[$key], + $arr1[$key], + $notAddKeys, + $includeEmptyValues, + $enableUnsetFeature + ); } - } else { - if ($notAddKeys) { - if (isset($arr0[$key])) { - if ($includeEmptyValues || $val) { - $arr0[$key] = $val; - } - } - } else { - if ($includeEmptyValues || $val) { - $arr0[$key] = $val; - } + } elseif (!$notAddKeys || isset($arr0[$key])) { + if ($enableUnsetFeature && $val === '__UNSET') { + unset($arr0[$key]); + } elseif ($includeEmptyValues || $val) { + $arr0[$key] = $val; } } } + reset($arr0); return $arr0; } @@ -2198,16 +2193,8 @@ final class t3lib_div { */ public static function array2xml_cs(array $array, $docTag = 'phparray', array $options = array(), $charset = '') { - // Figure out charset if not given explicitly: - if (!$charset) { - if ($GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset']) { // First priority: forceCharset! If set, this will be authoritative! - $charset = $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset']; - } elseif (is_object($GLOBALS['LANG'])) { - $charset = $GLOBALS['LANG']->charSet; // If "LANG" is around, that will hold the current charset - } else { - $charset = 'iso-8859-1'; // THIS is just a hopeful guess! - } - } + // Set default charset unless explicitly specified + $charset = $charset ? $charset : 'utf-8'; // Return XML: return '' . LF . @@ -2406,7 +2393,7 @@ final class t3lib_div { // default output charset is UTF-8, only ASCII, ISO-8859-1 and UTF-8 are supported!!! $match = array(); preg_match('/^[[:space:]]*<\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $match); - $theCharset = $match[1] ? $match[1] : ($GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] ? $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] : 'iso-8859-1'); + $theCharset = $match[1] ? $match[1] : 'utf-8'; xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $theCharset); // us-ascii / utf-8 / iso-8859-1 // Parse content: @@ -2742,10 +2729,10 @@ final class t3lib_div { ) ); - $content = file_get_contents($url, FALSE, $ctx); + $content = @file_get_contents($url, FALSE, $ctx); if ($content === FALSE && isset($report)) { - $report['error'] = -1; + $report['error'] = -1; $report['message'] = 'Couldn\'t get URL: ' . implode(LF, $http_response_header); } } else { @@ -2753,10 +2740,10 @@ final class t3lib_div { $report['lib'] = 'file'; } - $content = file_get_contents($url); + $content = @file_get_contents($url); if ($content === FALSE && isset($report)) { - $report['error'] = -1; + $report['error'] = -1; $report['message'] = 'Couldn\'t get URL: ' . implode(LF, $http_response_header); } } @@ -2989,7 +2976,7 @@ final class t3lib_div { $result = @mkdir($fullDirectoryPath, $permissionMask, TRUE); if (!$result) { - throw new \RuntimeException('Could not create directory!', 1170251400); + throw new \RuntimeException('Could not create directory "' . $fullDirectoryPath . '"!', 1170251400); } } return $firstCreatedPath; @@ -3090,7 +3077,7 @@ final class t3lib_div { $sortarray[$key] = filemtime($path . '/' . $entry); } elseif ($order) { - $sortarray[$key] = $entry; + $sortarray[$key] = strtolower($entry); } } } @@ -3341,99 +3328,6 @@ final class t3lib_div { return $fullName; } - - /************************* - * - * DEBUG helper FUNCTIONS - * - *************************/ - - /* Deprecated since 4.5, use t3lib_utility_Debug */ - - - /** - * Returns a string with a list of ascii-values for the first $characters characters in $string - * - * @param string $string String to show ASCII value for - * @param integer $characters Number of characters to show - * @return string The string with ASCII values in separated by a space char. - * @deprecated since TYPO3 4.5 - Use t3lib_utility_Debug::ordinalValue instead - */ - public static function debug_ordvalue($string, $characters = 100) { - self::logDeprecatedFunction(); - return t3lib_utility_Debug::ordinalValue($string, $characters); - } - - /** - * Returns HTML-code, which is a visual representation of a multidimensional array - * use t3lib_div::print_array() in order to print an array - * Returns FALSE if $array_in is not an array - * - * @param mixed $array_in Array to view - * @return string HTML output - * @deprecated since TYPO3 4.5 - Use t3lib_utility_Debug::viewArray instead - */ - public static function view_array($array_in) { - self::logDeprecatedFunction(); - return t3lib_utility_Debug::viewArray($array_in); - } - - /** - * Prints an array - * - * @param mixed $array_in Array to print visually (in a table). - * @return void - * @see view_array() - * @deprecated since TYPO3 4.5 - Use t3lib_utility_Debug::printArray instead - */ - public static function print_array($array_in) { - self::logDeprecatedFunction(); - t3lib_utility_Debug::printArray($array_in); - } - - /** - * Makes debug output - * Prints $var in bold between two vertical lines - * If not $var the word 'debug' is printed - * If $var is an array, the array is printed by t3lib_div::print_array() - * - * @param mixed $var Variable to print - * @param string $header The header. - * @param string $group Group for the debug console - * @return void - * @deprecated since TYPO3 4.5 - Use t3lib_utility_Debug::debug instead - */ - public static function debug($var = '', $header = '', $group = 'Debug') { - self::logDeprecatedFunction(); - t3lib_utility_Debug::debug($var, $header, $group); - } - - /** - * Displays the "path" of the function call stack in a string, using debug_backtrace - * - * @return string - * @deprecated since TYPO3 4.5 - Use t3lib_utility_Debug::debugTrail instead - */ - public static function debug_trail() { - self::logDeprecatedFunction(); - return t3lib_utility_Debug::debugTrail(); - } - - /** - * Displays an array as rows in a table. Useful to debug output like an array of database records. - * - * @param mixed $rows Array of arrays with similar keys - * @param string $header Table header - * @param boolean $returnHTML If TRUE, will return content instead of echo'ing out. - * @return mixed Outputs to browser or returns an HTML string if $returnHTML is TRUE - * @deprecated since TYPO3 4.5 - Use t3lib_utility_Debug::debugRows instead - */ - public static function debugRows($rows, $header = '', $returnHTML = FALSE) { - self::logDeprecatedFunction(); - return t3lib_utility_Debug::debugRows($rows, $header, $returnHTML); - } - - /************************* * * SYSTEM INFORMATION @@ -3750,7 +3644,7 @@ final class t3lib_div { if ($proxySSL == '*') { $proxySSL = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']; } - if (self::cmpIP($_SERVER['REMOTE_ADDR'], $proxySSL)) { + if (self::cmpIP(self::getIndpEnv('REMOTE_ADDR'), $proxySSL)) { $retVal = TRUE; } else { $retVal = $_SERVER['SSL_SESSION_ID'] || !strcasecmp($_SERVER['HTTPS'], 'on') || !strcmp($_SERVER['HTTPS'], '1') ? TRUE : FALSE; // see http://bugs.typo3.org/view.php?id=3909 @@ -3959,7 +3853,7 @@ final class t3lib_div { * So it's compatible with the UNIX style path strings valid for TYPO3 internally. * * @param string $theFile File path to evaluate - * @return boolean TRUE, $theFile is allowed path string + * @return boolean TRUE, $theFile is allowed path string, FALSE otherwise * @see http://php.net/manual/en/security.filesystem.nullbytes.php * @todo Possible improvement: Should it rawurldecode the string first to check if any of these characters is encoded? */ @@ -3967,6 +3861,8 @@ final class t3lib_div { if (strpos($theFile, '//') === FALSE && strpos($theFile, '\\') === FALSE && !preg_match('#(?:^\.\.|/\.\./|[[:cntrl:]])#u', $theFile)) { return TRUE; } + + return FALSE; } /** @@ -4175,18 +4071,15 @@ final class t3lib_div { * @param string $addQueryParams Query-parameters: "&xxx=yyy&zzz=uuu" * @return array Array with key/value pairs of query-parameters WITHOUT a certain list of variable names (like id, type, no_cache etc.) and WITH a variable, encryptionKey, specific for this server/installation * @see tslib_fe::makeCacheHash(), tslib_cObj::typoLink(), t3lib_div::calculateCHash() + * @deprecated since TYPO3 4.7 - will be removed in TYPO3 6.1 - use t3lib_cacheHash instead */ public static function cHashParams($addQueryParams) { + t3lib_div::logDeprecatedFunction(); $params = explode('&', substr($addQueryParams, 1)); // Splitting parameters up + /* @var $cacheHash t3lib_cacheHash */ + $cacheHash = t3lib_div::makeInstance('t3lib_cacheHash'); + $pA = $cacheHash->getRelevantParameters($addQueryParams); - // Make array: - $pA = array(); - foreach ($params as $theP) { - $pKV = explode('=', $theP); // Splitting single param by '=' sign - if (!self::inList('id,type,no_cache,cHash,MP,ftu', $pKV[0]) && !preg_match('/TSFE_ADMIN_PANEL\[.*?\]/', $pKV[0])) { - $pA[rawurldecode($pKV[0])] = (string) rawurldecode($pKV[1]); - } - } // Hook: Allows to manipulate the parameters which are taken to build the chash: if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['cHashParamsHook'])) { $cHashParamsHook =& $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['cHashParamsHook']; @@ -4202,9 +4095,6 @@ final class t3lib_div { } } } - // Finish and sort parameters array by keys: - $pA['encryptionKey'] = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']; - ksort($pA); return $pA; } @@ -4215,11 +4105,13 @@ final class t3lib_div { * @param string $addQueryParams Query-parameters: "&xxx=yyy&zzz=uuu" * @return string Hash of all the values * @see t3lib_div::cHashParams(), t3lib_div::calculateCHash() + * @deprecated since TYPO3 4.7 - will be removed in TYPO3 6.1 - use t3lib_cacheHash instead */ public static function generateCHash($addQueryParams) { - $cHashParams = self::cHashParams($addQueryParams); - $cHash = self::calculateCHash($cHashParams); - return $cHash; + t3lib_div::logDeprecatedFunction(); + /* @var $cacheHash t3lib_cacheHash */ + $cacheHash = t3lib_div::makeInstance('t3lib_cacheHash'); + return $cacheHash->generateForParameters($addQueryParams); } /** @@ -4227,10 +4119,13 @@ final class t3lib_div { * * @param array $params Array of key-value pairs * @return string Hash of all the values + * @deprecated since TYPO3 4.7 - will be removed in TYPO3 6.1 - use t3lib_cacheHash instead */ public static function calculateCHash($params) { - $cHash = md5(serialize($params)); - return $cHash; + t3lib_div::logDeprecatedFunction(); + /* @var $cacheHash t3lib_cacheHash */ + $cacheHash = t3lib_div::makeInstance('t3lib_cacheHash'); + return $cacheHash->calculateCacheHash($params); } /** @@ -4282,7 +4177,7 @@ final class t3lib_div { * @param string $langKey TYPO3 language key, eg. "dk" or "de" or "default" * @param string $charset Character set (optional) * @return array LOCAL_LANG array in return. - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - use t3lib_l10n_parser_Llphp::getParsedData() from now on + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - use t3lib_l10n_parser_Llphp::getParsedData() from now on */ public static function readLLPHPfile($fileRef, $langKey, $charset = '') { t3lib_div::logDeprecatedFunction(); @@ -4298,14 +4193,11 @@ final class t3lib_div { if (@is_file($fileRef) && $langKey) { // Set charsets: - $sourceCharset = $csConvObj->parse_charset($csConvObj->charSetArray[$langKey] ? $csConvObj->charSetArray[$langKey] : 'iso-8859-1'); + $sourceCharset = $csConvObj->parse_charset($csConvObj->charSetArray[$langKey] ? $csConvObj->charSetArray[$langKey] : 'utf-8'); if ($charset) { $targetCharset = $csConvObj->parse_charset($charset); - } elseif ($GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset']) { - // when forceCharset is set, we store ALL labels in this charset!!! - $targetCharset = $csConvObj->parse_charset($GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset']); } else { - $targetCharset = $csConvObj->parse_charset($csConvObj->charSetArray[$langKey] ? $csConvObj->charSetArray[$langKey] : 'iso-8859-1'); + $targetCharset = 'utf-8'; } // Cache file name: @@ -4328,9 +4220,9 @@ final class t3lib_div { // converting the default language (English) // this needs to be done for a few accented loan words and extension names - if (is_array($LOCAL_LANG['default']) && $targetCharset != 'iso-8859-1') { + if (is_array($LOCAL_LANG['default']) && $targetCharset != 'utf-8') { foreach ($LOCAL_LANG['default'] as &$labelValue) { - $labelValue = $csConvObj->conv($labelValue, 'iso-8859-1', $targetCharset); + $labelValue = $csConvObj->conv($labelValue, 'utf-8', $targetCharset); } unset($labelValue); } @@ -4369,7 +4261,7 @@ final class t3lib_div { * @param string $langKey TYPO3 language key, eg. "dk" or "de" or "default" * @param string $charset Character set (optional) * @return array LOCAL_LANG array in return. - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 - use t3lib_l10n_parser_Llxml::getParsedData() from now on + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 - use t3lib_l10n_parser_Llxml::getParsedData() from now on */ public static function readLLXMLfile($fileRef, $langKey, $charset = '') { t3lib_div::logDeprecatedFunction(); @@ -4388,11 +4280,8 @@ final class t3lib_div { // Set charset: if ($charset) { $targetCharset = $csConvObj->parse_charset($charset); - } elseif ($GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset']) { - // when forceCharset is set, we store ALL labels in this charset!!! - $targetCharset = $csConvObj->parse_charset($GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset']); } else { - $targetCharset = $csConvObj->parse_charset($csConvObj->charSetArray[$langKey] ? $csConvObj->charSetArray[$langKey] : 'iso-8859-1'); + $targetCharset = 'utf-8'; } // Cache file name: @@ -5177,7 +5066,7 @@ final class t3lib_div { /** * Simple substitute for the PHP function mail() which allows you to specify encoding and character set * The fifth parameter ($encoding) will allow you to specify 'base64' encryption for the output (set $encoding=base64) - * Further the output has the charset set to ISO-8859-1 by default. + * Further the output has the charset set to UTF-8 by default. * * @param string $email Email address to send to. (see PHP function mail()) * @param string $subject Subject line, non-encoded. (see PHP function mail()) @@ -5190,7 +5079,7 @@ final class t3lib_div { */ public static function plainMailEncoded($email, $subject, $message, $headers = '', $encoding = 'quoted-printable', $charset = '', $dontEncodeHeader = FALSE) { if (!$charset) { - $charset = $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] ? $GLOBALS['TYPO3_CONF_VARS']['BE']['forceCharset'] : 'ISO-8859-1'; + $charset = 'utf-8'; } $email = self::normalizeMailAddress($email); @@ -5305,7 +5194,7 @@ final class t3lib_div { * @param string $charset Charset used for encoding * @return string The encoded string */ - public static function encodeHeader($line, $enc = 'quoted-printable', $charset = 'iso-8859-1') { + public static function encodeHeader($line, $enc = 'quoted-printable', $charset = 'utf-8') { // Avoid problems if "###" is found in $line (would conflict with the placeholder which is used below) if (strpos($line, '###') !== FALSE) { return $line; @@ -5361,30 +5250,32 @@ final class t3lib_div { * @see makeRedirectUrl() */ public static function substUrlsInPlainText($message, $urlmode = '76', $index_script_url = '') { - // Substitute URLs with shorter links: - foreach (array('http', 'https') as $protocol) { - $urlSplit = explode($protocol . '://', $message); - foreach ($urlSplit as $c => &$v) { - if ($c) { - $newParts = preg_split('/\s|[<>"{}|\\\^`()\']/', $v, 2); - $newURL = $protocol . '://' . $newParts[0]; + $lengthLimit = FALSE; - switch ((string) $urlmode) { - case 'all': - $newURL = self::makeRedirectUrl($newURL, 0, $index_script_url); - break; - case '76': - $newURL = self::makeRedirectUrl($newURL, 76, $index_script_url); - break; - } - $v = $newURL . substr($v, strlen($newParts[0])); - } - } - unset($v); - $message = implode('', $urlSplit); + switch ((string) $urlmode) { + case '': + $lengthLimit = FALSE; + break; + case 'all': + $lengthLimit = 0; + break; + case '76': + default: + $lengthLimit = (int) $urlmode; } - return $message; + if ($lengthLimit === FALSE) { + // no processing + $messageSubstituted = $message; + } else { + $messageSubstituted = preg_replace( + '/(http|https):\/\/.+(?=[\]\.\?]*([\! \'"()<>]+|$))/eiU', + 'self::makeRedirectUrl("\\0",' . $lengthLimit . ',"' . $index_script_url . '")', + $message + ); + } + + return $messageSubstituted; } /** @@ -5775,25 +5666,15 @@ final class t3lib_div { /** - * Quotes a string for usage as JS parameter. Depends whether the value is - * used in script tags (it doesn't need/must not get htmlspecialchar'ed in - * this case). + * Quotes a string for usage as JS parameter. * * @param string $value the string to encode, may be empty - * @param boolean $withinCData - * whether the escaped data is expected to be used as CDATA and thus - * does not need to be htmlspecialchared * * @return string the encoded value already quoted (with single quotes), * will not be empty */ - static public function quoteJSvalue($value, $withinCData = FALSE) { - $escapedValue = addcslashes( - $value, '\'' . '"' . '\\' . TAB . LF . CR - ); - if (!$withinCData) { - $escapedValue = htmlspecialchars($escapedValue); - } + public static function quoteJSvalue($value) { + $escapedValue = t3lib_div::makeInstance('t3lib_codec_JavaScriptEncoder')->encode($value); return '\'' . $escapedValue . '\''; } diff --git a/lib/typo3/class.t3lib_l10n_locales.php b/lib/typo3/class.t3lib_l10n_locales.php index 160f0ffddaa..535bc5ccad2 100644 --- a/lib/typo3/class.t3lib_l10n_locales.php +++ b/lib/typo3/class.t3lib_l10n_locales.php @@ -47,6 +47,7 @@ class t3lib_l10n_Locales implements t3lib_Singleton { */ protected $languages = array( 'default' => 'English', + 'af' => 'Afrikaans', 'ar' => 'Arabic', 'bs' => 'Bosnian', 'bg' => 'Bulgarian', @@ -101,7 +102,7 @@ class t3lib_l10n_Locales implements t3lib_Singleton { /** * Supported TYPO3 locales - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 * @var array */ protected $locales = array(); @@ -176,12 +177,12 @@ class t3lib_l10n_Locales implements t3lib_Singleton { } /** - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 */ $instance->locales = array_keys($instance->languages); /** - * @deprecated since TYPO3 4.6, will be removed in TYPO3 4.8 + * @deprecated since TYPO3 4.6, will be removed in TYPO3 6.0 */ define('TYPO3_languages', implode('|', $instance->getLocales())); } diff --git a/lib/typo3/readme_moodle.txt b/lib/typo3/readme_moodle.txt index 7f390fdeb97..d45a1835b7b 100644 --- a/lib/typo3/readme_moodle.txt +++ b/lib/typo3/readme_moodle.txt @@ -1,10 +1,12 @@ -Description of Typo3 libraries (v 4.6.8) import into Moodle +Description of Typo3 libraries (v 4.7.4) import into Moodle Changes: none skodak, stronk7 +Previous changes: + 25 June 2010 - Martin D (4.3.0RC1) I renamed getURL to getUrl since it was being called that way everywhere. I added a check to avoid notices on lib/typo3/class.t3lib_cs.php line 976 From cc21d0ff6ea6ea0ef4520bf105211306d8c5c5c0 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Mon, 3 Sep 2012 11:41:07 +1200 Subject: [PATCH 50/96] MDL-26822 user: Minor improvements post integration --- user/profile.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/user/profile.php b/user/profile.php index cb612601539..92f55cf4515 100644 --- a/user/profile.php +++ b/user/profile.php @@ -258,11 +258,11 @@ if (isset($identityfields['address']) && $user->address) { } if (isset($identityfields['phone1']) && $user->phone1) { - print_row(get_string("phone").":", "$user->phone1"); + print_row(get_string("phone").":", "$user->phone1"); } if (isset($identityfields['phone2']) && $user->phone2) { - print_row(get_string("phone2").":", "$user->phone2"); + print_row(get_string("phone2").":", "$user->phone2"); } if (isset($identityfields['institution']) && $user->institution) { @@ -273,16 +273,14 @@ if (isset($identityfields['department']) && $user->department) { print_row(get_string("department").":", "$user->department"); } -if (isset($identityfields['idnumber']) && $user->idnumber) { - print_row(get_string("idnumber").":", "$user->idnumber"); +if (isset($identityfields['idnumber']) && $user->idnumber) { + print_row(get_string("idnumber").":", "$user->idnumber"); } -if (($currentuser +if (isset($identityfields['email']) and ($currentuser or $user->maildisplay == 1 or has_capability('moodle/course:useremail', $context) - or ($user->maildisplay == 2 and enrol_sharing_course($user, $USER))) - and isset($identityfields['email'])) { - + or ($user->maildisplay == 2 and enrol_sharing_course($user, $USER)))) { print_row(get_string("email").":", obfuscate_mailto($user->email, '')); } From 9e795179ad2574c0e06b8f1763372abb6debaa60 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Fri, 8 Jun 2012 14:10:16 +0800 Subject: [PATCH 51/96] MDL-31295: Allow granting individual extension dates to students for mod_assign --- .../backup/moodle2/backup_assign_stepslib.php | 5 +- .../moodle2/restore_assign_stepslib.php | 11 + mod/assign/db/access.php | 12 + mod/assign/db/install.xml | 15 +- mod/assign/db/upgrade.php | 39 +++ mod/assign/extensionform.php | 102 ++++++++ .../file/lang/en/assignfeedback_file.php | 1 + mod/assign/gradingbatchoperationsform.php | 3 + mod/assign/gradingtable.php | 84 +++++-- mod/assign/lang/en/assign.php | 24 +- mod/assign/lib.php | 21 +- mod/assign/locallib.php | 230 +++++++++++++++--- mod/assign/mod_form.php | 17 +- mod/assign/renderable.php | 20 +- mod/assign/renderer.php | 99 ++++++-- mod/assign/styles.css | 4 + mod/assign/upgradelib.php | 6 +- mod/assign/version.php | 2 +- 18 files changed, 601 insertions(+), 94 deletions(-) mode change 100644 => 100755 mod/assign/db/install.xml create mode 100644 mod/assign/extensionform.php diff --git a/mod/assign/backup/moodle2/backup_assign_stepslib.php b/mod/assign/backup/moodle2/backup_assign_stepslib.php index 06a8fdc4d9b..9c2bfc8f3d0 100644 --- a/mod/assign/backup/moodle2/backup_assign_stepslib.php +++ b/mod/assign/backup/moodle2/backup_assign_stepslib.php @@ -48,11 +48,11 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st 'intro', 'introformat', 'alwaysshowdescription', - 'preventlatesubmissions', 'submissiondrafts', 'sendnotifications', 'sendlatenotifications', 'duedate', + 'cutoffdate', 'allowsubmissionsfromdate', 'grade', 'timemodified', @@ -75,7 +75,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st 'grader', 'grade', 'locked', - 'mailed')); + 'mailed', + 'extensionduedate')); $pluginconfigs = new backup_nested_element('plugin_configs'); diff --git a/mod/assign/backup/moodle2/restore_assign_stepslib.php b/mod/assign/backup/moodle2/restore_assign_stepslib.php index 6ad67254480..50100cb19a7 100644 --- a/mod/assign/backup/moodle2/restore_assign_stepslib.php +++ b/mod/assign/backup/moodle2/restore_assign_stepslib.php @@ -74,6 +74,16 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ $data->allowsubmissionsfromdate = $this->apply_date_offset($data->allowsubmissionsfromdate); $data->duedate = $this->apply_date_offset($data->duedate); + if (!isset($data->cutoffdate)) { + $data->cutoffdate = 0; + } + + if (!empty($data->preventlatesubmissions)) { + $data->cutoffdate = $data->duedate; + } else { + $data->cutoffdate = $this->apply_date_offset($data->cutoffdate); + } + $newitemid = $DB->insert_record('assign', $data); @@ -121,6 +131,7 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ $data->timecreated = $this->apply_date_offset($data->timecreated); $data->userid = $this->get_mappingid('user', $data->userid); $data->grader = $this->get_mappingid('user', $data->grader); + $data->extensionduedate = $this->apply_date_offset($data->extensionduedate); $newitemid = $DB->insert_record('assign_grades', $data); diff --git a/mod/assign/db/access.php b/mod/assign/db/access.php index 9f8cde73d5e..f3e7ee16a2b 100644 --- a/mod/assign/db/access.php +++ b/mod/assign/db/access.php @@ -81,6 +81,18 @@ $capabilities = array( ), 'clonepermissionsfrom' => 'moodle/course:manageactivities' ), + + 'mod/assign:grantextension' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'gradereport/grader:view' + ), + ); diff --git a/mod/assign/db/install.xml b/mod/assign/db/install.xml old mode 100644 new mode 100755 index 213a62b6655..2c354d5130d --- a/mod/assign/db/install.xml +++ b/mod/assign/db/install.xml @@ -1,5 +1,5 @@ - @@ -12,9 +12,8 @@ - - - + + @@ -22,7 +21,8 @@ - + + @@ -58,7 +58,8 @@ - + + @@ -89,4 +90,4 @@ - + \ No newline at end of file diff --git a/mod/assign/db/upgrade.php b/mod/assign/db/upgrade.php index b80a60eb74a..07a91a8f8f4 100644 --- a/mod/assign/db/upgrade.php +++ b/mod/assign/db/upgrade.php @@ -80,6 +80,45 @@ function xmldb_assign_upgrade($oldversion) { upgrade_mod_savepoint(true, 2012081600, 'assign'); } + // Individual extension dates support. + if ($oldversion < 2012082100) { + + // Define field sendlatenotifications to be added to assign. + $table = new xmldb_table('assign'); + $field = new xmldb_field('cutoffdate', XMLDB_TYPE_INTEGER, '10', null, + XMLDB_NOTNULL, null, '0', 'completionsubmit'); + + // Conditionally launch add field sendlatenotifications. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + // If prevent late is on - set cutoffdate to due date. + + // Now remove the preventlatesubmissions column. + $field = new xmldb_field('preventlatesubmissions', XMLDB_TYPE_INTEGER, '2', null, + XMLDB_NOTNULL, null, '0', 'nosubmissions'); + if ($dbman->field_exists($table, $field)) { + // Set the cutoffdate to the duedate if preventlatesubmissions was enabled. + $sql = 'UPDATE {assign} SET cutoffdate = duedate WHERE preventlatesubmissions = 1'; + $DB->execute($sql); + + $dbman->drop_field($table, $field); + } + + // Define field extensionduedate to be added to assign_grades + $table = new xmldb_table('assign_grades'); + $field = new xmldb_field('extensionduedate', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'mailed'); + + // Conditionally launch add field extensionduedate + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Assign savepoint reached. + upgrade_mod_savepoint(true, 2012082100, 'assign'); + } + + return true; } diff --git a/mod/assign/extensionform.php b/mod/assign/extensionform.php new file mode 100644 index 00000000000..f838638adfc --- /dev/null +++ b/mod/assign/extensionform.php @@ -0,0 +1,102 @@ +. + +/** + * This file contains the forms to create and edit an instance of this module + * + * @package mod_assign + * @copyright 2012 NetSpot {@link http://www.netspot.com.au} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); + + +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + +/** + * Assignment extension dates form + * + * @package mod_assign + * @copyright 2012 NetSpot {@link http://www.netspot.com.au} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_assign_extension_form extends moodleform { + /** @var array $instance - The data passed to this form */ + private $instance; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $mform = $this->_form; + + list($coursemoduleid, $userid, $batchusers, $instance, $data) = $this->_customdata; + // Instance variable is used by the form validation function. + $this->instance = $instance; + + if ($batchusers) { + $listusersmessage = get_string('grantextensionforusers', 'assign', count(explode(',', $batchusers))); + $mform->addElement('static', 'applytoselectedusers', '', $listusersmessage); + } + if ($instance->allowsubmissionsfromdate) { + $mform->addElement('static', 'allowsubmissionsfromdate', get_string('allowsubmissionsfromdate', 'assign'), + userdate($instance->allowsubmissionsfromdate)); + } + if ($instance->duedate) { + $mform->addElement('static', 'duedate', get_string('duedate', 'assign'), userdate($instance->duedate)); + $finaldate = $instance->duedate; + } + if ($instance->cutoffdate) { + $mform->addElement('static', 'cutoffdate', get_string('cutoffdate', 'assign'), userdate($instance->cutoffdate)); + $finaldate = $instance->cutoffdate; + } + $mform->addElement('date_time_selector', 'extensionduedate', + get_string('extensionduedate', 'assign'), array('optional'=>true)); + $mform->setDefault('extensionduedate', $finaldate); + $mform->addElement('hidden', 'id', $coursemoduleid); + $mform->addElement('hidden', 'userid', $userid); + $mform->addElement('hidden', 'selectedusers', $batchusers); + $mform->addElement('hidden', 'action', 'saveextension'); + $this->add_action_buttons(true, get_string('savechanges', 'assign')); + + if ($data) { + $this->set_data($data); + } + } + + /** + * Perform validation on the extension form + * @param array $data + * @param array $files + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + if ($this->instance->duedate && $data['extensionduedate']) { + if ($this->instance->duedate > $data['extensionduedate']) { + $errors['extensionduedate'] = get_string('extensionnotafterduedate', 'assign'); + } + } + if ($this->instance->allowsubmissionsfromdate && $data['extensionduedate']) { + if ($this->instance->allowsubmissionsfromdate > $data['extensionduedate']) { + $errors['extensionduedate'] = get_string('extensionnotafterfromdate', 'assign'); + } + } + + return $errors; + } +} diff --git a/mod/assign/feedback/file/lang/en/assignfeedback_file.php b/mod/assign/feedback/file/lang/en/assignfeedback_file.php index e62e6df8fe1..ca017e9dcc2 100644 --- a/mod/assign/feedback/file/lang/en/assignfeedback_file.php +++ b/mod/assign/feedback/file/lang/en/assignfeedback_file.php @@ -23,6 +23,7 @@ */ $string['configmaxbytes'] = 'Maximum file size'; +$string['countfiles'] = '{$a} files'; $string['default'] = 'Enabled by default'; $string['default_help'] = 'If set, this feedback method will be enabled by default for all new assignments.'; $string['enabled'] = 'File feedback'; diff --git a/mod/assign/gradingbatchoperationsform.php b/mod/assign/gradingbatchoperationsform.php index 2ae00a2d4a6..fa6a18e353d 100644 --- a/mod/assign/gradingbatchoperationsform.php +++ b/mod/assign/gradingbatchoperationsform.php @@ -52,6 +52,9 @@ class mod_assign_grading_batch_operations_form extends moodleform { if ($instance['submissiondrafts']) { $options['reverttodraft'] = get_string('reverttodraft', 'assign'); } + if ($instance['duedate']) { + $options['grantextension'] = get_string('grantextension', 'assign'); + } $mform->addElement('hidden', 'action', 'batchgradingoperation'); $mform->addElement('hidden', 'id', $instance['cm']); $mform->addElement('hidden', 'selectedusers', '', array('class'=>'selectedusers')); diff --git a/mod/assign/gradingtable.php b/mod/assign/gradingtable.php index 40bed663228..745d5db2182 100644 --- a/mod/assign/gradingtable.php +++ b/mod/assign/gradingtable.php @@ -50,6 +50,8 @@ class assign_grading_table extends table_sql implements renderable { private $tablemaxrows = 10000; /** @var boolean $quickgrading */ private $quickgrading = false; + /** @var boolean $hasgrantextension - Only do the capability check once for the entire table */ + private $hasgrantextension = false; /** * overridden constructor keeps a reference to the assignment class that is displaying this table @@ -88,9 +90,19 @@ class assign_grading_table extends table_sql implements renderable { $params['assignmentid1'] = (int)$this->assignment->get_instance()->id; $params['assignmentid2'] = (int)$this->assignment->get_instance()->id; - $fields = user_picture::fields('u') . ', u.id as userid, '; - $fields .= 's.status as status, s.id as submissionid, s.timecreated as firstsubmission, s.timemodified as timesubmitted, '; - $fields .= 'g.id as gradeid, g.grade as grade, g.timemodified as timemarked, g.timecreated as firstmarked, g.mailed as mailed, g.locked as locked'; + $fields = user_picture::fields('u') . ', '; + $fields .= 'u.id as userid, '; + $fields .= 's.status as status, '; + $fields .= 's.id as submissionid, '; + $fields .= 's.timecreated as firstsubmission, '; + $fields .= 's.timemodified as timesubmitted, '; + $fields .= 'g.id as gradeid, '; + $fields .= 'g.grade as grade, '; + $fields .= 'g.timemodified as timemarked, '; + $fields .= 'g.timecreated as firstmarked, '; + $fields .= 'g.mailed as mailed, '; + $fields .= 'g.locked as locked, '; + $fields .= 'g.extensionduedate as extensionduedate'; $from = '{user} u LEFT JOIN {assign_submission} s ON u.id = s.userid AND s.assignment = :assignmentid1' . ' LEFT JOIN {assign_grades} g ON u.id = g.userid AND g.assignment = :assignmentid2'; @@ -177,6 +189,7 @@ class assign_grading_table extends table_sql implements renderable { // load the grading info for all users $this->gradinginfo = grade_get_grades($this->assignment->get_course()->id, 'mod', 'assign', $this->assignment->get_instance()->id, $users); + $this->hasgrantextension = has_capability('mod/assign:grantextension', $this->assignment->get_context()); if (!empty($CFG->enableoutcomes) && !empty($this->gradinginfo->outcomes)) { $columns[] = 'outcomes'; @@ -409,9 +422,14 @@ class assign_grading_table extends table_sql implements renderable { if ($this->assignment->is_any_submission_plugin_enabled()) { - $o .= $this->output->container(get_string('submissionstatus_' . $row->status, 'assign'), array('class'=>'submissionstatus' .$row->status)); + $o .= $this->output->container(get_string('submissionstatus_' . $row->status, 'assign'), + array('class'=>'submissionstatus' .$row->status)); if ($this->assignment->get_instance()->duedate && $row->timesubmitted > $this->assignment->get_instance()->duedate) { - $o .= $this->output->container(get_string('submittedlateshort', 'assign', format_time($row->timesubmitted - $this->assignment->get_instance()->duedate)), 'latesubmission'); + if (!$row->extensionduedate || $row->timesubmitted > $row->extensionduedate) { + $latemessage = get_string('submittedlateshort', 'assign', + format_time($row->timesubmitted - $this->assignment->get_instance()->duedate)); + $o .= $this->output->container($latemessage, 'latesubmission'); + } } if ($row->locked) { $o .= $this->output->container(get_string('submissionslockedshort', 'assign'), 'lockedsubmission'); @@ -419,6 +437,19 @@ class assign_grading_table extends table_sql implements renderable { if ($row->grade !== NULL && $row->grade >= 0) { $o .= $this->output->container(get_string('graded', 'assign'), 'submissiongraded'); } + if (!$row->timesubmitted) { + $now = time(); + $due = $this->assignment->get_instance()->duedate; + if ($row->extensionduedate) { + $due = $row->extensionduedate; + } + if ($due && ($now > $due)) { + $o .= $this->output->container(get_string('overdue', 'assign', format_time($now - $due)), 'overduesubmission'); + } + } + if ($row->extensionduedate) { + $o .= $this->output->container(get_string('userextensiondate', 'assign', userdate($row->extensionduedate)), 'extensiondate'); + } } return $o; @@ -450,23 +481,42 @@ class assign_grading_table extends table_sql implements renderable { } $actions[$url->out(false)] = $description; - if (!$row->status || $row->status == ASSIGN_SUBMISSION_STATUS_DRAFT || !$this->assignment->get_instance()->submissiondrafts) { - if (!$row->locked) { + // Hide for offline assignments. + if ($this->assignment->is_any_submission_plugin_enabled()) { + if (!$row->status || + $row->status == ASSIGN_SUBMISSION_STATUS_DRAFT || + !$this->assignment->get_instance()->submissiondrafts) { + + if (!$row->locked) { + $url = new moodle_url('/mod/assign/view.php', array('id' => $this->assignment->get_course_module()->id, + 'userid'=>$row->id, + 'action'=>'lock', + 'sesskey'=>sesskey(), + 'page'=>$this->currpage)); + $description = get_string('preventsubmissionsshort', 'assign'); + $actions[$url->out(false)] = $description; + } else { + $url = new moodle_url('/mod/assign/view.php', array('id' => $this->assignment->get_course_module()->id, + 'userid'=>$row->id, + 'action'=>'unlock', + 'sesskey'=>sesskey(), + 'page'=>$this->currpage)); + $description = get_string('allowsubmissionsshort', 'assign'); + $actions[$url->out(false)] = $description; + } + } + + if (($this->assignment->get_instance()->duedate || + $this->assignment->get_instance()->cutoffdate) && + $this->hasgrantextension) { $url = new moodle_url('/mod/assign/view.php', array('id' => $this->assignment->get_course_module()->id, 'userid'=>$row->id, - 'action'=>'lock', + 'action'=>'grantextension', 'sesskey'=>sesskey(), 'page'=>$this->currpage)); - $description = get_string('preventsubmissionsshort', 'assign'); - $actions[$url->out(false)] = $description; - } else { - $url = new moodle_url('/mod/assign/view.php', array('id' => $this->assignment->get_course_module()->id, - 'userid'=>$row->id, - 'action'=>'unlock', - 'sesskey'=>sesskey(), - 'page'=>$this->currpage)); - $description = get_string('allowsubmissionsshort', 'assign'); + $description = get_string('grantextension', 'assign'); $actions[$url->out(false)] = $description; + } } if ($row->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED && $this->assignment->get_instance()->submissiondrafts) { diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php index 29fbc833c51..4e1746795ba 100644 --- a/mod/assign/lang/en/assign.php +++ b/mod/assign/lang/en/assign.php @@ -35,6 +35,7 @@ $string['alwaysshowdescription_help'] = 'If disabled, the Assignment Description $string['assign:addinstance'] = 'Add a new assignment'; $string['assign:exportownsubmission'] = 'Export own submission'; $string['assign:grade'] = 'Grade assignment'; +$string['assign:grantextension'] = 'Grant extension'; $string['assign:submit'] = 'Submit assignment'; $string['assign:view'] = 'View assignment'; $string['assignfeedback'] = 'Feedback plugin'; @@ -60,6 +61,7 @@ $string['availability'] = 'Availability'; $string['backtoassignment'] = 'Back to assignment'; $string['batchoperationsdescription'] = 'With selected...'; $string['batchoperationconfirmlock'] = 'Lock all selected submissions?'; +$string['batchoperationconfirmgrantextension'] = 'Grant an extension to all selected submissions?'; $string['batchoperationconfirmunlock'] = 'Unlock all selected submissions?'; $string['batchoperationconfirmreverttodraft'] = 'Revert selected submissions to draft?'; $string['batchoperationlock'] = 'lock submissions'; @@ -78,6 +80,10 @@ $string['couldnotcreatecoursemodule'] = 'Could not create course module.'; $string['couldnotcreatenewassignmentinstance'] = 'Could not create new assignment instance.'; $string['couldnotfindassignmenttoupgrade'] = 'Could not find old assignment instance to upgrade.'; $string['currentgrade'] = 'Current grade in gradebook'; +$string['cutoffdate'] = 'Cut-off date'; +$string['cutoffdate_help'] = 'If set, the assignment will not accept submissions after this date without an extension.'; +$string['cutoffdatevalidation'] = 'Cut-off date must be after the due date.'; +$string['cutoffdatefromdatevalidation'] = 'Cut-off date must be after the allow submissions from date.'; $string['defaultplugins'] = 'Default assignment settings'; $string['defaultplugins_help'] = 'These settings define the defaults for all new assignments.'; $string['deletepluginareyousure'] = 'Delete assignment plugin {$a}: are you sure?'; @@ -87,12 +93,15 @@ $string['description'] = 'Description'; $string['downloadall'] = 'Download all submissions'; $string['download all submissions'] = 'Download all submissions in a zip file.'; $string['duedate'] = 'Due date'; -$string['duedate_help'] = 'This is when the assignment is due. If late submissions are allowed, any assignments submitted after this date are marked as late.'; +$string['duedate_help'] = 'This is when the assignment is due. Submissions will still be allowed after this date but any assignments submitted after this date are marked as late. To prevent submissions after a certain date - set the assignment cut off date.'; $string['duedateno'] = 'No due date'; $string['duedatereached'] = 'The due date for this assignment has now passed'; $string['duedatevalidation'] = 'Due date must be after the allow submissions from date.'; $string['editsubmission'] = 'Edit my submission'; $string['editaction'] = 'Actions...'; +$string['extensionduedate'] = 'Extension due date'; +$string['extensionnotafterduedate'] = 'Extension date must be after the due date'; +$string['extensionnotafterfromdate'] = 'Extension date must be after the allow submissions from date'; $string['gradersubmissionupdatedtext'] = '{$a->username} has updated their assignment submission for \'{$a->assignment}\' at {$a->timeupdated} @@ -103,6 +112,8 @@ $string['gradersubmissionupdatedhtml'] = '{$a->username} has updated their assig for \'{$a->assignment}\' at {$a->timeupdated}

    It is available on the web site.'; $string['gradersubmissionupdatedsmall'] = '{$a->username} has updated their submission for assignment {$a->assignment}.'; +$string['grantextension'] = 'Grant extension'; +$string['grantextensionforusers'] = 'Grant extension for {$a} students'; $string['enabled'] = 'Enabled'; $string['errornosubmissions'] = 'There are no submissions to download'; $string['errorquickgradingvsadvancedgrading'] = 'The grades were not saved because this assignment is currently using advanced grading'; @@ -148,6 +159,8 @@ $string['invalidgradeforscale'] = 'The grade supplied was not valid for the curr $string['invalidfloatforgrade'] = 'The grade provided could not be understood: {$a}'; $string['lastmodifiedsubmission'] = 'Last modified (submission)'; $string['lastmodifiedgrade'] = 'Last modified (grade)'; +$string['latesubmissions'] = 'Late submissions'; +$string['latesubmissionsaccepted'] = 'Only student(s) having been granted extension can still submit the assignment'; $string['locksubmissionforstudent'] = 'Prevent any more submissions for student: (id={$a->id}, fullname={$a->fullname}).'; $string['locksubmissions'] = 'Lock submissions'; $string['manageassignfeedbackplugins'] = 'Manage assignment feedback plugins'; @@ -165,9 +178,12 @@ $string['mysubmission'] = 'My submission: '; $string['newsubmissions'] = 'Assignments submitted'; $string['nofiles'] = 'No files. '; $string['nograde'] = 'No grade. '; +$string['nolatesubmissions'] = 'No late submissions accepted. '; $string['noonlinesubmissions'] = 'This assignment does not require you to submit anything online'; $string['nosavebutnext'] = 'Next'; $string['nosubmission'] = 'Nothing has been submitted for this assignment'; +$string['nosubmissionsacceptedafter'] = 'No submissions accepted after '; +$string['nomoresubmissionsaccepted'] = 'No more submissions accepted'; $string['notgraded'] = 'Not graded'; $string['notgradedyet'] = 'Not graded yet'; $string['notsubmittedyet'] = 'Not submitted yet'; @@ -178,14 +194,13 @@ $string['numberofparticipants'] = 'Participants'; $string['numberofsubmittedassignments'] = 'Submitted'; $string['numberofsubmissionsneedgrading'] = 'Needs grading'; $string['offline'] = 'No online submissions required'; +$string['open'] = 'Open'; $string['overdue'] = 'Assignment is overdue by: {$a}'; $string['outlinegrade'] = 'Grade: {$a}'; $string['page-mod-assign-x'] = 'Any assignment module page'; $string['page-mod-assign-view'] = 'Assignment module main and submission page'; $string['pluginadministration'] = 'Assignment administration'; $string['pluginname'] = 'Assignment'; -$string['preventlatesubmissions'] = 'Prevent late submissions'; -$string['preventlatesubmissions_help'] = 'If enabled, students will not be able submit after the Due Date. If disabled, students will be able to submit assignments after the due date.'; $string['preventsubmissions'] = 'Prevent the user from making any more submissions to this assignment.'; $string['preventsubmissionsshort'] = 'Prevent submission changes'; $string['previous'] = 'Previous'; @@ -213,6 +228,8 @@ $string['settings'] = 'Assignment settings'; $string['showrecentsubmissions'] = 'Show recent submissions'; $string['submissiondrafts'] = 'Require students click submit button'; $string['submissiondrafts_help'] = 'If enabled, students will have to click a Submit button to declare their submission as final. This allows students to keep a draft version of the submission on the system. If this setting is changed from "No" to "Yes" after students have already submitted those submissions will be regarded as final.'; +$string['submissioneditable'] = 'Student can edit this submission'; +$string['submissionnoteditable'] = 'Student cannot edit this submission'; $string['submissionnotready'] = 'This assignment is not ready to submit:'; $string['submissionplugins'] = 'Submission plugins'; $string['submissionreceipts'] = 'Send submission receipts'; @@ -258,6 +275,7 @@ $string['unlocksubmissions'] = 'Unlock submissions'; $string['updategrade'] = 'Update grade'; $string['updatetable'] = 'Save and update table'; $string['upgradenotimplemented'] = 'Upgrade not implemented in plugin ({$a->type} {$a->subtype})'; +$string['userextensiondate'] = 'Extension granted until: {$a}'; $string['viewfeedback'] = 'View feedback'; $string['viewfeedbackforuser'] = 'View feedback for user: {$a}'; $string['viewfullgradingpage'] = 'Open the full grading page to provide feedback'; diff --git a/mod/assign/lib.php b/mod/assign/lib.php index 18a1b11659c..c981e228213 100644 --- a/mod/assign/lib.php +++ b/mod/assign/lib.php @@ -216,9 +216,14 @@ function assign_print_overview($courses, &$htmlarray) { $time = time(); $isopen = false; if ($assignment->duedate) { - $isopen = $assignment->allowsubmissionsfromdate <= $time; - if ($assignment->preventlatesubmissions) { - $isopen = ($isopen && $time <= $assignment->duedate); + $duedate = false; + if ($assignment->cutoffdate) { + $duedate = $assignment->cutoffdate; + } + if ($duedate) { + $isopen = ($assignment->allowsubmissionsfromdate <= $time && $time <= $duedate); + } else { + $isopen = ($assignment->allowsubmissionsfromdate <= $time); } } if ($isopen) { @@ -232,6 +237,9 @@ function assign_print_overview($courses, &$htmlarray) { } $strduedate = get_string('duedate', 'assign'); + $strcutoffdate = get_string('nosubmissionsacceptedafter', 'assign'); + $strnolatesubmissions = get_string('nolatesubmissions', 'assign'); + $strduedateno = get_string('duedateno', 'assign'); $strduedateno = get_string('duedateno', 'assign'); $strgraded = get_string('graded', 'assign'); $strnotgradedyet = get_string('notgradedyet', 'assign'); @@ -279,6 +287,13 @@ function assign_print_overview($courses, &$htmlarray) { } else { $str .= '
    '.$strduedateno.'
    '; } + if ($assignment->cutoffdate) { + if ($assignment->cutoffdate == $assignment->duedate) { + $str .= '
    '.$strnolatesubmissions.'
    '; + } else { + $str .= '
    '.$strcutoffdate.': '.userdate($assignment->cutoffdate).'
    '; + } + } $context = context_module::instance($assignment->coursemodule); if (has_capability('mod/assign:grade', $context)) { diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 455afd1fadf..6297087bb68 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -303,25 +303,24 @@ class assign { if ($this->process_save_submission($mform)) { $action = 'view'; } - } else if ($action == 'lock') { + } else if ($action == 'lock') { $this->process_lock(); $action = 'grading'; - } else if ($action == 'reverttodraft') { + } else if ($action == 'reverttodraft') { $this->process_revert_to_draft(); $action = 'grading'; - } else if ($action == 'unlock') { + } else if ($action == 'unlock') { $this->process_unlock(); $action = 'grading'; - } else if ($action == 'confirmsubmit') { + } else if ($action == 'confirmsubmit') { $action = 'submit'; if ($this->process_submit_for_grading($mform)) { $action = 'view'; } // save and show next button - } else if ($action == 'batchgradingoperation') { - $this->process_batch_grading_operation(); - $action = 'grading'; - } else if ($action == 'submitgrade') { + } else if ($action == 'batchgradingoperation') { + $action = $this->process_batch_grading_operation(); + } else if ($action == 'submitgrade') { if (optional_param('saveandshownext', null, PARAM_ALPHA)) { //save and show next $action = 'grade'; @@ -343,12 +342,17 @@ class assign { //cancel button $action = 'grading'; } - }else if ($action == 'quickgrade') { + } else if ($action == 'quickgrade') { $message = $this->process_save_quick_grades(); $action = 'quickgradingresult'; - }else if ($action == 'saveoptions') { + } else if ($action == 'saveoptions') { $this->process_save_grading_options(); $action = 'grading'; + } else if ($action == 'saveextension') { + $action = 'grantextension'; + if ($this->process_save_extension($mform)) { + $action = 'grading'; + } } $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT)); @@ -378,6 +382,8 @@ class assign { $o .= $this->download_submissions(); } else if ($action == 'submit') { $o .= $this->check_submit_for_grading($mform); + } else if ($action == 'grantextension') { + $o .= $this->view_grant_extension($mform); } else { $o .= $this->view_submission_page(); } @@ -409,12 +415,12 @@ class assign { $update->intro = $formdata->intro; $update->introformat = $formdata->introformat; $update->alwaysshowdescription = $formdata->alwaysshowdescription; - $update->preventlatesubmissions = $formdata->preventlatesubmissions; $update->submissiondrafts = $formdata->submissiondrafts; $update->requiresubmissionstatement = $formdata->requiresubmissionstatement; $update->sendnotifications = $formdata->sendnotifications; $update->sendlatenotifications = $formdata->sendlatenotifications; $update->duedate = $formdata->duedate; + $update->cutoffdate = $formdata->cutoffdate; $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; $update->completionsubmit = !empty($formdata->completionsubmit); @@ -629,12 +635,12 @@ class assign { $update->intro = $formdata->intro; $update->introformat = $formdata->introformat; $update->alwaysshowdescription = $formdata->alwaysshowdescription; - $update->preventlatesubmissions = $formdata->preventlatesubmissions; $update->submissiondrafts = $formdata->submissiondrafts; $update->requiresubmissionstatement = $formdata->requiresubmissionstatement; $update->sendnotifications = $formdata->sendnotifications; $update->sendlatenotifications = $formdata->sendlatenotifications; $update->duedate = $formdata->duedate; + $update->cutoffdate = $formdata->cutoffdate; $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; $update->completionsubmit = !empty($formdata->completionsubmit); @@ -1270,6 +1276,56 @@ class assign { return $result; } + /** + * View the grant extension date page + * + * Uses url parameters 'userid' + * or from parameter 'selectedusers' + * @param moodleform $mform - Used for validation of the submitted data + * @return string + */ + private function view_grant_extension($mform) { + global $DB, $CFG; + require_once($CFG->dirroot . '/mod/assign/extensionform.php'); + + $o = ''; + $batchusers = optional_param('selectedusers', '', PARAM_TEXT); + $data = new stdClass(); + $data->extensionduedate = null; + $userid = 0; + if (!$batchusers) { + $userid = required_param('userid', PARAM_INT); + + $grade = $this->get_user_grade($userid, false); + + $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); + + if ($grade) { + $data->extensionduedate = $grade->extensionduedate; + } + $data->userid = $userid; + } else { + $data->batchusers = $batchusers; + } + $o .= $this->output->render(new assign_header($this->get_instance(), + $this->get_context(), + $this->show_intro(), + $this->get_course_module()->id, + get_string('grantextension', 'assign'))); + + if (!$mform) { + $mform = new mod_assign_extension_form(null, array($this->get_course_module()->id, + $userid, + $batchusers, + $this->get_instance(), + $data)); + } + $o .= $this->output->render(new assign_form('extensionform', $mform)); + $o .= $this->view_footer(); + return $o; + } + + /** * display the submission that is used by a plugin * Uses url parameters 'sid', 'gid' and 'plugin' @@ -1652,6 +1708,15 @@ class assign { $grade = $this->get_user_grade($userid, false); if ($this->can_view_submission($userid)) { $gradelocked = ($grade && $grade->locked) || $this->grading_disabled($userid); + $extensionduedate = null; + if ($grade) { + $extensionduedate = $grade->extensionduedate; + } + $showedit = has_capability('mod/assign:submit', $this->context) && + $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled()); + + $showsubmit = $showedit && $submission && ($submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT); + $o .= $this->output->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate, $this->get_instance()->alwaysshowdescription, $submission, @@ -1659,13 +1724,15 @@ class assign { $gradelocked, $this->is_graded($userid), $this->get_instance()->duedate, + $this->get_instance()->cutoffdate, $this->get_submission_plugins(), $this->get_return_action(), $this->get_return_params(), $this->get_course_module()->id, assign_submission_status::GRADER_VIEW, - false, - false)); + $showedit, + $showsubmit, + $extensionduedate)); } if ($grade) { $data = new stdClass(); @@ -1760,7 +1827,8 @@ class assign { $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null, array('cm'=>$this->get_course_module()->id, - 'submissiondrafts'=>$this->get_instance()->submissiondrafts), + 'submissiondrafts'=>$this->get_instance()->submissiondrafts, + 'duedate'=>$this->get_instance()->duedate), 'post', '', array('class'=>'gradingbatchoperationsform')); @@ -1946,7 +2014,7 @@ class assign { /** * Ask the user to confirm they want to perform this batch operation - * @return string + * @return string - the page to view after processing these actions */ private function process_batch_grading_operation() { global $CFG; @@ -1955,7 +2023,8 @@ class assign { $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null, array('cm'=>$this->get_course_module()->id, - 'submissiondrafts'=>$this->get_instance()->submissiondrafts), + 'submissiondrafts'=>$this->get_instance()->submissiondrafts, + 'duedate'=>$this->get_instance()->duedate), 'post', '', array('class'=>'gradingbatchoperationsform')); @@ -1971,11 +2040,13 @@ class assign { $this->process_unlock($userid); } else if ($data->operation == 'reverttodraft') { $this->process_revert_to_draft($userid); + } else if ($data->operation == 'grantextension') { + return 'grantextension'; } } } - return true; + return 'grading'; } /** @@ -2044,10 +2115,13 @@ class assign { if ($this->can_view_submission($user->id)) { $showedit = has_capability('mod/assign:submit', $this->context) && - $this->submissions_open() && ($this->is_any_submission_plugin_enabled()) && $showlinks; + $this->submissions_open($user->id) && ($this->is_any_submission_plugin_enabled()) && $showlinks; $showsubmit = $submission && ($submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT) && $showlinks; $gradelocked = ($grade && $grade->locked) || $this->grading_disabled($user->id); - + $extensionduedate = null; + if ($grade) { + $extensionduedate = $grade->extensionduedate; + } $o .= $this->output->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate, $this->get_instance()->alwaysshowdescription, $submission, @@ -2055,13 +2129,15 @@ class assign { $gradelocked, $this->is_graded($user->id), $this->get_instance()->duedate, + $this->get_instance()->cutoffdate, $this->get_submission_plugins(), $this->get_return_action(), $this->get_return_params(), $this->get_course_module()->id, assign_submission_status::STUDENT_VIEW, $showedit, - $showsubmit)); + $showsubmit, + $extensionduedate)); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); @@ -2142,6 +2218,7 @@ class assign { $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT), $this->is_any_submission_plugin_enabled(), $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED), + $this->get_instance()->cutoffdate, $this->get_instance()->duedate, $this->get_course_module()->id, $this->count_submissions_need_grading() @@ -2257,15 +2334,35 @@ class assign { * has this person already submitted, * is the assignment locked? * + * @param int $userid - Optional userid so we can see if a different user can submit * @return bool */ - private function submissions_open() { + private function submissions_open($userid = 0) { global $USER; + if (!$userid) { + $userid = $USER->id; + } + $time = time(); $dateopen = true; - if ($this->get_instance()->preventlatesubmissions && $this->get_instance()->duedate) { - $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $this->get_instance()->duedate); + $finaldate = false; + if ($this->get_instance()->cutoffdate) { + $finaldate = $this->get_instance()->cutoffdate; + } + // User extensions. + if ($finaldate) { + $grade = $this->get_user_grade($userid, false); + if ($grade && $grade->extensionduedate) { + // Extension can be before cut off date. + if ($grade->extensionduedate > $finaldate) { + $finaldate = $grade->extensionduedate; + } + } + } + + if ($finaldate) { + $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate); } else { $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time); } @@ -2274,23 +2371,23 @@ class assign { return false; } - // now check if this user has already submitted etc. - if (!is_enrolled($this->get_course_context(), $USER)) { + // Now check if this user has already submitted etc. + if (!is_enrolled($this->get_course_context(), $userid)) { return false; } - if ($submission = $this->get_user_submission($USER->id, false)) { + if ($submission = $this->get_user_submission($userid, false)) { if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // drafts are tracked and the student has submitted the assignment return false; } } - if ($grade = $this->get_user_grade($USER->id, false)) { + if ($grade = $this->get_user_grade($userid, false)) { if ($grade->locked) { return false; } } - if ($this->grading_disabled($USER->id)) { + if ($this->grading_disabled($userid)) { return false; } @@ -2477,7 +2574,6 @@ class assign { /** * Notify student upon successful submission * - * @global moodle_database $DB * @param stdClass $submission * @return void */ @@ -2496,7 +2592,6 @@ class assign { /** * Send notifications to graders upon student submissions * - * @global moodle_database $DB * @param stdClass $submission * @return void */ @@ -2579,10 +2674,78 @@ class assign { return true; } + /** + * save the extension date for a single user + * + * @param int $userid The user id + * @param mixed $extensionduedate Either an integer date or null + * @return boolean + */ + private function save_user_extension($userid, $extensionduedate) { + global $DB; + + $grade = $this->get_user_grade($userid, true); + $grade->extensionduedate = $extensionduedate; + $grade->timemodified = time(); + + $result = $DB->update_record('assign_grades', $grade); + + if ($result) { + $this->add_to_log('grant extension', $this->format_grade_for_log($grade)); + } + return $result; + } + + /** + * save extension date + * + * @param moodleform $mform The submitted form + * @return boolean + */ + private function process_save_extension(& $mform) { + global $DB, $CFG; + + // Include extension form. + require_once($CFG->dirroot . '/mod/assign/extensionform.php'); + + // Need submit permission to submit an assignment. + require_capability('mod/assign:grantextension', $this->context); + + $batchusers = optional_param('selectedusers', '', PARAM_TEXT); + $userid = 0; + if (!$batchusers) { + $userid = required_param('userid', PARAM_INT); + $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); + } + $mform = new mod_assign_extension_form(null, array($this->get_course_module()->id, + $userid, + $batchusers, + $this->get_instance(), + null)); + + if ($mform->is_cancelled()) { + return true; + } + + if ($formdata = $mform->get_data()) { + if ($batchusers) { + $users = explode(',', $batchusers); + $result = true; + foreach ($users as $userid) { + $result = $this->save_user_extension($userid, $formdata->extensionduedate) && $result; + } + return $result; + } else { + return $this->save_user_extension($userid, $formdata->extensionduedate); + } + } + return false; + } + + /** * save quick grades * - * @global moodle_database $DB * @return string The result of the save operation */ private function process_save_quick_grades() { @@ -2771,6 +2934,9 @@ class assign { if ($grade->locked) { $info .= get_string('submissionslocked', 'assign') . '. '; } + if ($grade->extensionduedate) { + $info .= get_string('userextensiondate', 'assign', userdate($grade->extensionduedate)); + } return $info; } diff --git a/mod/assign/mod_form.php b/mod/assign/mod_form.php index c2cde375520..000b92e1f1a 100644 --- a/mod/assign/mod_form.php +++ b/mod/assign/mod_form.php @@ -82,12 +82,12 @@ class mod_assign_mod_form extends moodleform_mod { $mform->addElement('date_time_selector', 'duedate', get_string('duedate', 'assign'), array('optional'=>true)); $mform->addHelpButton('duedate', 'duedate', 'assign'); $mform->setDefault('duedate', time()+7*24*3600); + $mform->addElement('date_time_selector', 'cutoffdate', get_string('cutoffdate', 'assign'), array('optional'=>true)); + $mform->addHelpButton('cutoffdate', 'cutoffdate', 'assign'); + $mform->setDefault('cutoffdate', time()+7*24*3600); $mform->addElement('selectyesno', 'alwaysshowdescription', get_string('alwaysshowdescription', 'assign')); $mform->addHelpButton('alwaysshowdescription', 'alwaysshowdescription', 'assign'); $mform->setDefault('alwaysshowdescription', 1); - $mform->addElement('selectyesno', 'preventlatesubmissions', get_string('preventlatesubmissions', 'assign')); - $mform->addHelpButton('preventlatesubmissions', 'preventlatesubmissions', 'assign'); - $mform->setDefault('preventlatesubmissions', 0); $mform->addElement('selectyesno', 'submissiondrafts', get_string('submissiondrafts', 'assign')); $mform->addHelpButton('submissiondrafts', 'submissiondrafts', 'assign'); $mform->setDefault('submissiondrafts', 0); @@ -150,6 +150,17 @@ class mod_assign_mod_form extends moodleform_mod { $errors['duedate'] = get_string('duedatevalidation', 'assign'); } } + if ($data['duedate'] && $data['cutoffdate']) { + if ($data['duedate'] > $data['cutoffdate']) { + $errors['cutoffdate'] = get_string('cutoffdatevalidation', 'assign'); + } + } + if ($data['allowsubmissionsfromdate'] && $data['cutoffdate']) { + if ($data['allowsubmissionsfromdate'] > $data['cutoffdate']) { + $errors['cutoffdate'] = get_string('cutoffdatefromdatevalidation', 'assign'); + } + } + return $errors; } diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php index 7624e6e6bb5..cb41131bac2 100644 --- a/mod/assign/renderable.php +++ b/mod/assign/renderable.php @@ -297,6 +297,8 @@ class assign_submission_status implements renderable { var $graded = false; /** @var int duedate */ var $duedate = 0; + /** @var int cutoffdate */ + public $cutoffdate = 0; /** @var array submissionplugins - the list of submission plugins */ var $submissionplugins = array(); /** @var string returnaction */ @@ -311,6 +313,8 @@ class assign_submission_status implements renderable { var $canedit = false; /** @var bool cansubmit */ var $cansubmit = false; + /** @var int extensionduedate */ + public $extensionduedate = 0; /** * constructor @@ -322,6 +326,7 @@ class assign_submission_status implements renderable { * @param bool $locked * @param bool $graded * @param int $duedate + * @param int $cutoffdate * @param array $submissionplugins * @param string $returnaction * @param array $returnparams @@ -329,10 +334,11 @@ class assign_submission_status implements renderable { * @param string $view * @param bool $canedit * @param bool $cansubmit + * @param int $extensionduedate - Any extension to the due date granted for this user */ public function __construct($allowsubmissionsfromdate, $alwaysshowdescription, $submission, $submissionsenabled, - $locked, $graded, $duedate, $submissionplugins, $returnaction, $returnparams, - $coursemoduleid, $view, $canedit, $cansubmit) { + $locked, $graded, $duedate, $cutoffdate, $submissionplugins, $returnaction, $returnparams, + $coursemoduleid, $view, $canedit, $cansubmit, $extensionduedate) { $this->allowsubmissionsfromdate = $allowsubmissionsfromdate; $this->alwaysshowdescription = $alwaysshowdescription; $this->submission = $submission; @@ -340,6 +346,7 @@ class assign_submission_status implements renderable { $this->locked = $locked; $this->graded = $graded; $this->duedate = $duedate; + $this->cutoffdate = $cutoffdate; $this->submissionplugins = $submissionplugins; $this->returnaction = $returnaction; $this->returnparams = $returnparams; @@ -347,6 +354,7 @@ class assign_submission_status implements renderable { $this->view = $view; $this->canedit = $canedit; $this->cansubmit = $cansubmit; + $this->extensionduedate = $extensionduedate; } } @@ -412,6 +420,8 @@ class assign_grading_summary implements renderable { var $submissionsneedgradingcount = 0; /** @var int duedate - The assignment due date (if one is set) */ var $duedate = 0; + /** @var int cutoffdate - The assignment cut off date (if one is set) */ + var $cutoffdate = 0; /** @var int coursemoduleid - The assignment course module id */ var $coursemoduleid = 0; @@ -423,23 +433,25 @@ class assign_grading_summary implements renderable { * @param int $submissiondraftscount * @param bool $submissionsenabled * @param int $submissionssubmittedcount + * @param int $cutoffdate * @param int $duedate * @param int $coursemoduleid + * @param int $submissionsneedgradingcount */ public function __construct($participantcount, $submissiondraftsenabled, $submissiondraftscount, $submissionsenabled, $submissionssubmittedcount, - $duedate, $coursemoduleid, $submissionsneedgradingcount) { + $cutoffdate, $duedate, $coursemoduleid, $submissionsneedgradingcount) { $this->participantcount = $participantcount; $this->submissiondraftsenabled = $submissiondraftsenabled; $this->submissiondraftscount = $submissiondraftscount; $this->submissionsenabled = $submissionsenabled; $this->submissionssubmittedcount = $submissionssubmittedcount; $this->duedate = $duedate; + $this->cutoffdate = $cutoffdate; $this->coursemoduleid = $coursemoduleid; $this->submissionsneedgradingcount = $submissionsneedgradingcount; } - } /** diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php index c44f451e036..49ec46d23b8 100644 --- a/mod/assign/renderer.php +++ b/mod/assign/renderer.php @@ -265,6 +265,19 @@ class mod_assign_renderer extends plugin_renderer_base { $due = format_time($duedate - $time); } $this->add_table_row_tuple($t, get_string('timeremaining', 'assign'), $due); + + if ($duedate < $time) { + $cutoffdate = $summary->cutoffdate; + if ($cutoffdate) { + if ($cutoffdate > $time) { + $late = get_string('latesubmissionsaccepted', 'assign'); + } else { + $late = get_string('nomoresubmissionsaccepted', 'assign'); + } + $this->add_table_row_tuple($t, get_string('latesubmissions', 'assign'), $late); + } + } + } // all done - write the table @@ -406,14 +419,33 @@ class mod_assign_renderer extends plugin_renderer_base { $duedate = $status->duedate; - if ($duedate >= 1) { + if ($duedate > 0) { $row = new html_table_row(); $cell1 = new html_table_cell(get_string('duedate', 'assign')); $cell2 = new html_table_cell(userdate($duedate)); $row->cells = array($cell1, $cell2); $t->data[] = $row; - // time remaining + if ($status->view == assign_submission_status::GRADER_VIEW) { + if ($status->cutoffdate) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('cutoffdate', 'assign')); + $cell2 = new html_table_cell(userdate($status->cutoffdate)); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + } + + if ($status->extensionduedate) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('extensionduedate', 'assign')); + $cell2 = new html_table_cell(userdate($status->extensionduedate)); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + $duedate = $status->extensionduedate; + } + + // Time remaining. $row = new html_table_row(); $cell1 = new html_table_cell(get_string('timeremaining', 'assign')); if ($duedate - $time <= 0) { @@ -440,7 +472,22 @@ class mod_assign_renderer extends plugin_renderer_base { $t->data[] = $row; } - // last modified + // Show graders whether this submission is editable by students. + if ($status->view == assign_submission_status::GRADER_VIEW) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('open', 'assign')); + if ($status->canedit) { + $cell2 = new html_table_cell(get_string('submissioneditable', 'assign')); + $cell2->attributes = array('class'=>'submissioneditable'); + } else { + $cell2 = new html_table_cell(get_string('submissionnoteditable', 'assign')); + $cell2->attributes = array('class'=>'submissionnoteditable'); + } + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + + // Last modified. if ($status->submission) { $row = new html_table_row(); $cell1 = new html_table_cell(get_string('timemodified', 'assign')); @@ -452,7 +499,12 @@ class mod_assign_renderer extends plugin_renderer_base { if ($plugin->is_enabled() && $plugin->is_visible() && !$plugin->is_empty($status->submission)) { $row = new html_table_row(); $cell1 = new html_table_cell($plugin->get_name()); - $pluginsubmission = new assign_submission_plugin_submission($plugin, $status->submission, assign_submission_plugin_submission::SUMMARY, $status->coursemoduleid, $status->returnaction, $status->returnparams); + $pluginsubmission = new assign_submission_plugin_submission($plugin, + $status->submission, + assign_submission_plugin_submission::SUMMARY, + $status->coursemoduleid, + $status->returnaction, + $status->returnparams); $cell2 = new html_table_cell($this->render($pluginsubmission)); $row->cells = array($cell1, $cell2); $t->data[] = $row; @@ -464,24 +516,28 @@ class mod_assign_renderer extends plugin_renderer_base { $o .= html_writer::table($t); $o .= $this->output->box_end(); - // links - if ($status->canedit) { - if (!$status->submission) { - $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', - array('id' => $status->coursemoduleid, 'action' => 'editsubmission')), get_string('addsubmission', 'assign'), 'get'); - } else { - $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', - array('id' => $status->coursemoduleid, 'action' => 'editsubmission')), get_string('editsubmission', 'assign'), 'get'); + // Links. + if ($status->view == assign_submission_status::STUDENT_VIEW) { + if ($status->canedit) { + if (!$status->submission) { + $urlparams = array('id' => $status->coursemoduleid, 'action' => 'editsubmission'); + $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', $urlparams), + get_string('addsubmission', 'assign'), 'get'); + } else { + $urlparams = array('id' => $status->coursemoduleid, 'action' => 'editsubmission'); + $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', $urlparams), + get_string('editsubmission', 'assign'), 'get'); + } } - } - if ($status->cansubmit) { - // submission.php - $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', - array('id' => $status->coursemoduleid, 'action'=>'submit')), get_string('submitassignment', 'assign'), 'get'); - $o .= $this->output->box_start('boxaligncenter submithelp'); - $o .= get_string('submitassignment_help', 'assign'); - $o .= $this->output->box_end(); + if ($status->cansubmit) { + $urlparams = array('id' => $status->coursemoduleid, 'action'=>'submit'); + $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', $urlparams), + get_string('submitassignment', 'assign'), 'get'); + $o .= $this->output->box_start('boxaligncenter submithelp'); + $o .= get_string('submitassignment_help', 'assign'); + $o .= $this->output->box_end(); + } } $o .= $this->output->container_end(); @@ -537,9 +593,10 @@ class mod_assign_renderer extends plugin_renderer_base { $o .= $this->output->box_start('boxaligncenter gradingtable'); $this->page->requires->js_init_call('M.mod_assign.init_grading_table', array()); $this->page->requires->string_for_js('nousersselected', 'assign'); + $this->page->requires->string_for_js('batchoperationconfirmgrantextension', 'assign'); $this->page->requires->string_for_js('batchoperationconfirmlock', 'assign'); - $this->page->requires->string_for_js('batchoperationconfirmunlock', 'assign'); $this->page->requires->string_for_js('batchoperationconfirmreverttodraft', 'assign'); + $this->page->requires->string_for_js('batchoperationconfirmunlock', 'assign'); $this->page->requires->string_for_js('editaction', 'assign'); // need to get from prefs $o .= $this->flexible_table($table, $table->get_rows_per_page(), true); diff --git a/mod/assign/styles.css b/mod/assign/styles.css index 8b1b67bc688..65963bb718a 100644 --- a/mod/assign/styles.css +++ b/mod/assign/styles.css @@ -130,3 +130,7 @@ div.earlysubmission { #page-mod-assign-view div.gradingtable tr .quickgrademodified { background-color: #FFCC99; } + +td.submissioneditable { + color: red; +} diff --git a/mod/assign/upgradelib.php b/mod/assign/upgradelib.php index ff701c57b0f..0002412334d 100644 --- a/mod/assign/upgradelib.php +++ b/mod/assign/upgradelib.php @@ -89,8 +89,12 @@ class assign_upgrade_manager { $data->allowsubmissionsfromdate = $oldassignment->timeavailable; $data->grade = $oldassignment->grade; $data->submissiondrafts = $oldassignment->resubmit; - $data->preventlatesubmissions = $oldassignment->preventlate; $data->requiresubmissionstatement = 0; + $data->cutoffdate = 0; + // New way to specify no late submissions. + if ($oldassignment->preventlate) { + $data->cutoffdate = $data->duedate; + } $newassignment = new assign(null, null, null); diff --git a/mod/assign/version.php b/mod/assign/version.php index b9a29a3d16c..d9ed52fb11a 100644 --- a/mod/assign/version.php +++ b/mod/assign/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $module->component = 'mod_assign'; // Full name of the plugin (used for diagnostics) -$module->version = 2012081600; // The current module version (Date: YYYYMMDDXX) +$module->version = 2012082100; // The current module version (Date: YYYYMMDDXX) $module->requires = 2012061700; // Requires this Moodle version $module->cron = 60; From 12a1a0da2f582fdf39f1c1be26516b306521eca6 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 11 Jun 2012 11:59:20 +0800 Subject: [PATCH 52/96] MDL-31284: New feature for mod_assign: Team assignments --- .../backup/moodle2/backup_assign_stepslib.php | 11 +- .../moodle2/restore_assign_stepslib.php | 10 +- mod/assign/db/install.xml | 13 +- mod/assign/db/upgrade.php | 55 +- mod/assign/gradingtable.php | 43 ++ mod/assign/lang/en/assign.php | 12 + mod/assign/locallib.php | 555 +++++++++++++++--- mod/assign/mod_form.php | 20 + mod/assign/renderable.php | 46 +- mod/assign/renderer.php | 78 ++- mod/assign/version.php | 2 +- 11 files changed, 724 insertions(+), 121 deletions(-) diff --git a/mod/assign/backup/moodle2/backup_assign_stepslib.php b/mod/assign/backup/moodle2/backup_assign_stepslib.php index 9c2bfc8f3d0..76f914536bd 100644 --- a/mod/assign/backup/moodle2/backup_assign_stepslib.php +++ b/mod/assign/backup/moodle2/backup_assign_stepslib.php @@ -56,7 +56,11 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st 'allowsubmissionsfromdate', 'grade', 'timemodified', - 'completionsubmit')); + 'completionsubmit', + 'requiresubmissionstatement', + 'teamsubmission', + 'requireallteammemberssubmit', + 'teamsubmissiongroupingid')); $submissions = new backup_nested_element('submissions'); @@ -64,7 +68,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st array('userid', 'timecreated', 'timemodified', - 'status')); + 'status', + 'groupid')); $grades = new backup_nested_element('grades'); @@ -116,8 +121,10 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st // Define id annotations $submission->annotate_ids('user', 'userid'); + $submission->annotate_ids('group', 'groupid'); $grade->annotate_ids('user', 'userid'); $grade->annotate_ids('user', 'grader'); + $assign->annotate_ids('grouping', 'teamsubmissiongroupingid'); // Define file annotations $assign->annotate_files('mod_assign', 'intro', null); // This file area hasn't itemid diff --git a/mod/assign/backup/moodle2/restore_assign_stepslib.php b/mod/assign/backup/moodle2/restore_assign_stepslib.php index 50100cb19a7..890c64dc37b 100644 --- a/mod/assign/backup/moodle2/restore_assign_stepslib.php +++ b/mod/assign/backup/moodle2/restore_assign_stepslib.php @@ -73,6 +73,9 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ $data->timemodified = $this->apply_date_offset($data->timemodified); $data->allowsubmissionsfromdate = $this->apply_date_offset($data->allowsubmissionsfromdate); $data->duedate = $this->apply_date_offset($data->duedate); + if ($data->teamsubmissiongroupingid > 0) { + $data->teamsubmissiongroupingid = $this->get_mappingid('grouping', $data->teamsubmissiongroupingid); + } if (!isset($data->cutoffdate)) { $data->cutoffdate = 0; @@ -105,7 +108,12 @@ class restore_assign_activity_structure_step extends restore_activity_structure_ $data->timemodified = $this->apply_date_offset($data->timemodified); $data->timecreated = $this->apply_date_offset($data->timecreated); - $data->userid = $this->get_mappingid('user', $data->userid); + if ($data->userid > 0) { + $data->userid = $this->get_mappingid('user', $data->userid); + } + if ($data->groupid > 0) { + $data->groupid = $this->get_mappingid('group', $data->groupid); + } $newitemid = $DB->insert_record('assign_submission', $data); diff --git a/mod/assign/db/install.xml b/mod/assign/db/install.xml index 2c354d5130d..2ee2938394f 100755 --- a/mod/assign/db/install.xml +++ b/mod/assign/db/install.xml @@ -22,13 +22,17 @@ - + + + + - + + @@ -38,7 +42,8 @@ - + + @@ -90,4 +95,4 @@
    - \ No newline at end of file + diff --git a/mod/assign/db/upgrade.php b/mod/assign/db/upgrade.php index 07a91a8f8f4..0f402d0c708 100644 --- a/mod/assign/db/upgrade.php +++ b/mod/assign/db/upgrade.php @@ -34,11 +34,11 @@ function xmldb_assign_upgrade($oldversion) { if ($oldversion < 2012051700) { - // Define field sendlatenotifications to be added to assign + // Define field to be added to assign. $table = new xmldb_table('assign'); $field = new xmldb_field('sendlatenotifications', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'sendnotifications'); - // Conditionally launch add field sendlatenotifications + // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } @@ -47,16 +47,17 @@ function xmldb_assign_upgrade($oldversion) { upgrade_mod_savepoint(true, 2012051700, 'assign'); } - // Moodle v2.3.0 release upgrade line - // Put any upgrade step following this + // Moodle v2.3.0 release upgrade line. + // Put any upgrade step following this. if ($oldversion < 2012071800) { - // Define field requiresubmissionstatement to be added to assign + // Define field requiresubmissionstatement to be added to assign. $table = new xmldb_table('assign'); $field = new xmldb_field('requiresubmissionstatement', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'timemodified'); - // Conditionally launch add field requiresubmissionstatement + // Conditionally launch add field requiresubmissionstatement. + if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } @@ -67,11 +68,11 @@ function xmldb_assign_upgrade($oldversion) { if ($oldversion < 2012081600) { - // Define field sendlatenotifications to be added to assign. + // Define field to be added to assign. $table = new xmldb_table('assign'); $field = new xmldb_field('completionsubmit', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'timemodified'); - // Conditionally launch add field sendlatenotifications. + // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } @@ -118,6 +119,44 @@ function xmldb_assign_upgrade($oldversion) { upgrade_mod_savepoint(true, 2012082100, 'assign'); } + // Team assignment support. + if ($oldversion < 2012082300) { + + // Define field to be added to assign. + $table = new xmldb_table('assign'); + $field = new xmldb_field('teamsubmission', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'cutoffdate'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('requireallteammemberssubmit', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'teamsubmission'); + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('teamsubmissiongroupingid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'requireallteammemberssubmit'); + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $index = new xmldb_index('teamsubmissiongroupingid', XMLDB_INDEX_NOTUNIQUE, array('teamsubmissiongroupingid')); + // Conditionally launch add index. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + $table = new xmldb_table('assign_submission'); + $field = new xmldb_field('groupid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'status'); + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint(true, 2012082300, 'assign'); + } + return true; } diff --git a/mod/assign/gradingtable.php b/mod/assign/gradingtable.php index 745d5db2182..6032fe14618 100644 --- a/mod/assign/gradingtable.php +++ b/mod/assign/gradingtable.php @@ -153,6 +153,14 @@ class assign_grading_table extends table_sql implements renderable { $headers[] = get_string('status'); } + // Team submission columns + if ($assignment->get_instance()->teamsubmission) { + $columns[] = 'team'; + $headers[] = get_string('submissionteam', 'assign'); + + $columns[] = 'teamstatus'; + $headers[] = get_string('teamsubmissionstatus', 'assign'); + } // Grade $columns[] = 'grade'; @@ -205,6 +213,11 @@ class assign_grading_table extends table_sql implements renderable { $this->no_sorting('select'); $this->no_sorting('outcomes'); + if ($assignment->get_instance()->teamsubmission) { + $this->no_sorting('team'); + $this->no_sorting('teamstatus'); + } + foreach ($this->assignment->get_submission_plugins() as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled()) { $this->no_sorting('assignsubmission_' . $plugin->get_type()); @@ -254,6 +267,36 @@ class assign_grading_table extends table_sql implements renderable { return $o; } + /** + * Get the team info for this user + * + * @param stdClass $row + * @return string The team name + */ + function col_team(stdClass $row) { + $group = $this->assignment->get_submission_group($row->id); + if ($group) { + return $group->name; + } + return get_string('defaultteam', 'assign'); + } + + /** + * Get the team info for this user + * + * @param stdClass $row + * @return string The team name + */ + function col_teamstatus(stdClass $row) { + $submission = $this->assignment->get_group_submission($row->id, 0, false); + $status = ''; + if ($submission) { + $status = $submission->status; + } + return get_string('submissionstatus_' . $status, 'assign'); + } + + /** * Format a list of outcomes * diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php index 4e1746795ba..929f85f474c 100644 --- a/mod/assign/lang/en/assign.php +++ b/mod/assign/lang/en/assign.php @@ -32,6 +32,7 @@ $string['allowsubmissionsfromdatesummary'] = 'This assignment will accept submis $string['allowsubmissionsanddescriptionfromdatesummary'] = 'The assignment details and submission form will be available from {$a}'; $string['alwaysshowdescription'] = 'Always show description'; $string['alwaysshowdescription_help'] = 'If disabled, the Assignment Description above will only become visible to students at the "Allow submissions from" date.'; +$string['applytoteam'] = 'Apply grades and feedback to entire team'; $string['assign:addinstance'] = 'Add a new assignment'; $string['assign:exportownsubmission'] = 'Export own submission'; $string['assign:grade'] = 'Grade assignment'; @@ -86,6 +87,7 @@ $string['cutoffdatevalidation'] = 'Cut-off date must be after the due date.'; $string['cutoffdatefromdatevalidation'] = 'Cut-off date must be after the allow submissions from date.'; $string['defaultplugins'] = 'Default assignment settings'; $string['defaultplugins_help'] = 'These settings define the defaults for all new assignments.'; +$string['defaultteam'] = 'Default team'; $string['deletepluginareyousure'] = 'Delete assignment plugin {$a}: are you sure?'; $string['deletepluginareyousuremessage'] = 'You are about to completely delete the assignment plugin {$a}. This will completely delete everything in the database associated with this assignment plugin. Are you SURE you want to continue?'; $string['deletingplugin'] = 'Deleting plugin {$a}.'; @@ -193,6 +195,7 @@ $string['numberofdraftsubmissions'] = 'Drafts'; $string['numberofparticipants'] = 'Participants'; $string['numberofsubmittedassignments'] = 'Submitted'; $string['numberofsubmissionsneedgrading'] = 'Needs grading'; +$string['numberofteams'] = 'Teams'; $string['offline'] = 'No online submissions required'; $string['open'] = 'Open'; $string['overdue'] = 'Assignment is overdue by: {$a}'; @@ -210,6 +213,8 @@ $string['quickgradingchangessaved'] = 'The grade changes were saved'; $string['quickgrading_help'] = 'Quick grading allows you to assign grades (and outcomes) directly in the submissions table. Quick grading is not compatible with advanced grading and is not recommended when there are multiple markers.'; $string['requiresubmissionstatement'] = 'Require that students accept the submission statement'; $string['requiresubmissionstatement_help'] = 'Require that students accept the submission statement for all assignment submissions for this entire Moodle installation. If this setting is not enabled, then submission statements can be enabled or disabled in the settings for each assignment.'; +$string['requireallteammemberssubmit'] = 'Require all team members submit'; +$string['requireallteammemberssubmit_help'] = 'If enabled, all members of the student team must click the submit button for this assignment before the team submission will be considered as submitted. If disabled, the team submission will be considered as submitted as soon as any member of the student team clicks the submit button.'; $string['reverttodraftforstudent'] = 'Revert submission to draft for student: (id={$a->id}, fullname={$a->fullname}).'; $string['reverttodraft'] = 'Revert the submission to draft status.'; $string['reverttodraftshort'] = 'Revert the submission to draft'; @@ -259,6 +264,7 @@ $string['submissionstatus_new'] = 'New submission'; $string['submissionstatus_'] = 'No submission'; $string['submissionstatus'] = 'Submission status'; $string['submissionstatus_submitted'] = 'Submitted for grading'; +$string['submissionteam'] = 'Team'; $string['submission'] = 'Submission'; $string['submitaction'] = 'Submit'; $string['submitassignment_help'] = 'Once this assignment is submitted you will not be able to make any more changes'; @@ -267,6 +273,11 @@ $string['submittedearly'] = 'Assignment was submitted {$a} early'; $string['submittedlate'] = 'Assignment was submitted {$a} late'; $string['submittedlateshort'] = '{$a} late'; $string['submitted'] = 'Submitted'; +$string['teamsubmission'] = 'Students submit in teams'; +$string['teamsubmission_help'] = 'If enabled students will be divided into teams based on the default set of groups or a custom grouping. A team submission will be shared among team members and all members of the team will see each others changes to the submission.'; +$string['teamsubmissiongroupingid'] = 'Grouping for student teams'; +$string['teamsubmissiongroupingid_help'] = 'This is the grouping that the assignment will use to find groups for student teams. If not set - the default set of groups will be used.'; +$string['teamsubmissionstatus'] = 'Team submission status'; $string['textinstructions'] = 'Assignment instructions'; $string['timemodified'] = 'Last modified'; $string['timeremaining'] = 'Time remaining'; @@ -276,6 +287,7 @@ $string['updategrade'] = 'Update grade'; $string['updatetable'] = 'Save and update table'; $string['upgradenotimplemented'] = 'Upgrade not implemented in plugin ({$a->type} {$a->subtype})'; $string['userextensiondate'] = 'Extension granted until: {$a}'; +$string['userswhoneedtosubmit'] = 'Users who need to submit: {$a}'; $string['viewfeedback'] = 'View feedback'; $string['viewfeedbackforuser'] = 'View feedback for user: {$a}'; $string['viewfullgradingpage'] = 'Open the full grading page to provide feedback'; diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 6297087bb68..3a7deb60684 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -424,6 +424,10 @@ class assign { $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; $update->completionsubmit = !empty($formdata->completionsubmit); + $update->teamsubmission = $formdata->teamsubmission; + $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; + $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; + $returnid = $DB->insert_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST); // cache the course record @@ -644,6 +648,10 @@ class assign { $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; $update->completionsubmit = !empty($formdata->completionsubmit); + $update->teamsubmission = $formdata->teamsubmission; + $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; + $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; + $result = $DB->update_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST); @@ -970,6 +978,24 @@ class assign { } } + /** + * Load a count of valid teams for this assignment + * + * @return int number of valid teams + */ + public function count_teams() { + + $groups = groups_get_all_groups($this->get_course()->id, 0, $this->get_instance()->teamsubmissiongroupingid, 'g.id'); + $count = count($groups); + + // See if there are any users in the default group. + $defaultusers = $this->get_submission_group_members(0, true); + if (count($defaultusers) > 0) { + $count += 1; + } + return $count; + } + /** * Load a count of users enrolled in the current course with the specified permission and group (0 for no group) * @@ -1009,10 +1035,15 @@ class assign { */ public function count_submissions_with_status($status) { global $DB; - return $DB->count_records_sql("SELECT COUNT('x') - FROM {assign_submission} - WHERE assignment = ? AND - status = ?", array($this->get_course_module()->instance, $status)); + $sql = 'SELECT COUNT(id) FROM {assign_submission} WHERE assignment = ? AND status = ?'; + $params = array($this->get_course_module()->instance, $status); + + if ($this->get_instance()->teamsubmission) { + // only look at team submissions + $sql .= ' AND userid = ?'; + $params[] = 0; + } + return $DB->count_records_sql($sql, $params); } /** @@ -1021,7 +1052,7 @@ class assign { * * @return array An array of userids */ - private function get_grading_userid_list(){ + private function get_grading_userid_list() { $filter = get_user_preferences('assign_filter', ''); $table = new assign_grading_table($this, 0, $filter, 0, false); @@ -1039,7 +1070,7 @@ class assign { * @param bool $last This is set to true if this is the last user in the table * @return mixed The user id of the matching user or false if there was an error */ - private function get_userid_for_row($num, $last){ + private function get_userid_for_row($num, $last) { if (!array_key_exists('userid_for_row', $this->cache)) { $this->cache['userid_for_row'] = array(); } @@ -1325,6 +1356,142 @@ class assign { return $o; } + /** + * Get a list of the users in the same group as this user + * + * @param int $groupid The id of the group whose members we want or 0 for the default group + * @param bool $onlyids Whether to retrieve only the user id's + * @return array The users (possibly id's only) + */ + public function get_submission_group_members($groupid, $onlyids) { + $members = array(); + if ($groupid != 0) { + if ($onlyids) { + $allusers = groups_get_members($groupid, 'u.id'); + } else { + $allusers = groups_get_members($groupid); + } + foreach ($allusers as $user) { + if ($this->get_submission_group($user->id)) { + $members[] = $user; + } + } + } else { + $allusers = $this->list_participants(null, $onlyids); + foreach ($allusers as $user) { + if ($this->get_submission_group($user->id) == null) { + $members[] = $user; + } + } + } + return $members; + } + + /** + * Get a list of the users in the same group as this user that have not submitted the assignment + * + * @param int $groupid The id of the group whose members we want or 0 for the default group + * @param bool $onlyids Whether to retrieve only the user id's + * @return array The users (possibly id's only) + */ + public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) { + if (!$this->get_instance()->teamsubmission || !$this->get_instance()->requireallteammemberssubmit) { + return array(); + } + $members = $this->get_submission_group_members($groupid, $onlyids); + + foreach ($members as $id => $member) { + $submission = $this->get_user_submission($member->id, false); + if ($submission && $submission->status != ASSIGN_SUBMISSION_STATUS_DRAFT) { + unset($members[$id]); + } + } + return $members; + } + + /** + * Load the group submission object for a particular user, optionally creating it if required + * + * This will create the user submission and the group submission if required + * + * @param int $userid The id of the user whose submission we want + * @param int $groupid The id of the group for this user - may be 0 in which case it is determined from the userid + * @param bool $create If set to true a new submission object will be created in the database + * @return stdClass The submission + */ + public function get_group_submission($userid, $groupid, $create) { + global $DB; + + if ($groupid == 0) { + $group = $this->get_submission_group($userid); + if ($group) { + $groupid = $group->id; + } + } + + if ($create) { + // Make sure there is a submission for this user. + $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>0, 'userid'=>$userid); + $submission = $DB->get_record('assign_submission', $params); + + if (!$submission) { + $submission = new stdClass(); + $submission->assignment = $this->get_instance()->id; + $submission->userid = $userid; + $submission->groupid = 0; + $submission->timecreated = time(); + $submission->timemodified = $submission->timecreated; + + if ($this->get_instance()->submissiondrafts) { + $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; + } else { + $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; + } + $DB->insert_record('assign_submission', $submission); + } + } + // Now get the group submission. + $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0); + $submission = $DB->get_record('assign_submission', $params); + + if ($submission) { + return $submission; + } + if ($create) { + $submission = new stdClass(); + $submission->assignment = $this->get_instance()->id; + $submission->userid = 0; + $submission->groupid = $groupid; + $submission->timecreated = time(); + $submission->timemodified = $submission->timecreated; + + if ($this->get_instance()->submissiondrafts) { + $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; + } else { + $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; + } + $sid = $DB->insert_record('assign_submission', $submission); + $submission->id = $sid; + return $submission; + } + return false; + } + + /** + * This is used for team assignments to get the group for the specified user. + * If the user is a member of multiple or no groups this will return false + * + * @param int $userid The id of the user whose submission we want + * @return mixed The group or false + */ + public function get_submission_group($userid) { + $groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_instance()->teamsubmissiongroupingid); + if (count($groups) != 1) { + return false; + } + return array_pop($groups); + } + /** * display the submission that is used by a plugin @@ -1564,6 +1731,10 @@ class assign { /** * Load the submission object for a particular user, optionally creating it if required * + * For team assignments there are 2 submissions - the student submission and the team submission + * All files are associated with the team submission but the status of the students contribution is + * recorded separately. + * * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used * @param bool $create optional Defaults to false. If set to true a new submission object will be created in the database * @return stdClass The submission @@ -1574,8 +1745,9 @@ class assign { if (!$userid) { $userid = $USER->id; } - // if the userid is not null then use userid - $submission = $DB->get_record('assign_submission', array('assignment'=>$this->get_instance()->id, 'userid'=>$userid)); + // If the userid is not null then use userid. + $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0); + $submission = $DB->get_record('assign_submission', $params); if ($submission) { return $submission; @@ -1696,7 +1868,7 @@ class assign { if ($offset) { $_POST = array(); } - if(!$userid){ + if (!$userid) { throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum); } $user = $DB->get_record('user', array('id' => $userid)); @@ -1704,6 +1876,21 @@ class assign { $o .= $this->output->render(new assign_user_summary($user, $this->get_course()->id, has_capability('moodle/site:viewfullnames', $this->get_course_context()))); } $submission = $this->get_user_submission($userid, false); + $submissiongroup = null; + $submissiongroupmemberswhohavenotsubmitted = array(); + $teamsubmission = null; + $notsubmitted = array(); + if ($this->get_instance()->teamsubmission) { + $teamsubmission = $this->get_group_submission($userid, 0, false); + $submissiongroup = $this->get_submission_group($userid); + $groupid = 0; + if ($submissiongroup) { + $groupid = $submissiongroup->id; + } + $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false); + + } + // get the current grade $grade = $this->get_user_grade($userid, false); if ($this->can_view_submission($userid)) { @@ -1715,11 +1902,20 @@ class assign { $showedit = has_capability('mod/assign:submit', $this->context) && $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled()); - $showsubmit = $showedit && $submission && ($submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT); + if ($teamsubmission) { + $showsubmit = $showedit && $teamsubmission && ($teamsubmission->status == ASSIGN_SUBMISSION_STATUS_DRAFT); + } else { + $showsubmit = $showedit && $submission && ($submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT); + } + $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context()); $o .= $this->output->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate, $this->get_instance()->alwaysshowdescription, $submission, + $this->get_instance()->teamsubmission, + $teamsubmission, + $submissiongroup, + $notsubmitted, $this->is_any_submission_plugin_enabled(), $gradelocked, $this->is_graded($userid), @@ -1729,10 +1925,13 @@ class assign { $this->get_return_action(), $this->get_return_params(), $this->get_course_module()->id, + $this->get_course()->id, assign_submission_status::GRADER_VIEW, $showedit, $showsubmit, - $extensionduedate)); + $viewfullnames, + $extensionduedate, + $this->get_context())); } if ($grade) { $data = new stdClass(); @@ -1746,11 +1945,18 @@ class assign { // now show the grading form if (!$mform) { - $mform = new mod_assign_grade_form(null, array($this, $data, array('rownum'=>$rownum, 'useridlist'=>$useridlist, 'last'=>$last)), 'post', '', array('class'=>'gradeform')); + $pagination = array( 'rownum'=>$rownum, 'useridlist'=>$useridlist, 'last'=>$last); + $formparams = array($this, $data, $pagination); + $mform = new mod_assign_grade_form(null, + $formparams, + 'post', + '', + array('class'=>'gradeform')); } $o .= $this->output->render(new assign_form('gradingform',$mform)); - $this->add_to_log('view grading form', get_string('viewgradingformforstudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)))); + $msg = get_string('viewgradingformforstudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user))); + $this->add_to_log('view grading form', $msg); $o .= $this->view_footer(); return $o; @@ -2113,18 +2319,43 @@ class assign { $submission = $this->get_user_submission($user->id, false); $o = ''; + $teamsubmission = null; + $submissiongroup = null; + $notsubmitted = array(); + if ($this->get_instance()->teamsubmission) { + $teamsubmission = $this->get_group_submission($user->id, 0, false); + $submissiongroup = $this->get_submission_group($user->id); + $groupid = 0; + if ($submissiongroup) { + $groupid = $submissiongroup->id; + } + $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false); + } + if ($this->can_view_submission($user->id)) { $showedit = has_capability('mod/assign:submit', $this->context) && $this->submissions_open($user->id) && ($this->is_any_submission_plugin_enabled()) && $showlinks; - $showsubmit = $submission && ($submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT) && $showlinks; $gradelocked = ($grade && $grade->locked) || $this->grading_disabled($user->id); + + $showsubmit = ($submission || $teamsubmission) && $showlinks; + if ($teamsubmission && ($teamsubmission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) { + $showsubmit = false; + } + if ($submission && ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) { + $showsubmit = false; + } $extensionduedate = null; if ($grade) { $extensionduedate = $grade->extensionduedate; } + $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context()); $o .= $this->output->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate, $this->get_instance()->alwaysshowdescription, $submission, + $this->get_instance()->teamsubmission, + $teamsubmission, + $submissiongroup, + $notsubmitted, $this->is_any_submission_plugin_enabled(), $gradelocked, $this->is_graded($user->id), @@ -2134,10 +2365,13 @@ class assign { $this->get_return_action(), $this->get_return_params(), $this->get_course_module()->id, + $this->get_course()->id, assign_submission_status::STUDENT_VIEW, $showedit, $showsubmit, - $extensionduedate)); + $viewfullnames, + $extensionduedate, + $this->get_context())); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); @@ -2213,16 +2447,31 @@ class assign { $this->get_course_module()->id)); if ($this->can_grade()) { - $o .= $this->output->render(new assign_grading_summary($this->count_participants(0), - $this->get_instance()->submissiondrafts, - $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT), - $this->is_any_submission_plugin_enabled(), - $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED), - $this->get_instance()->cutoffdate, - $this->get_instance()->duedate, - $this->get_course_module()->id, - $this->count_submissions_need_grading() - )); + if ($this->get_instance()->teamsubmission) { + $summary = new assign_grading_summary($this->count_teams(), + $this->get_instance()->submissiondrafts, + $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT), + $this->is_any_submission_plugin_enabled(), + $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED), + $this->get_instance()->cutoffdate, + $this->get_instance()->duedate, + $this->get_course_module()->id, + $this->count_submissions_need_grading(), + $this->get_instance()->teamsubmission); + $o .= $this->output->render($summary); + } else { + $summary = new assign_grading_summary($this->count_participants(0), + $this->get_instance()->submissiondrafts, + $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT), + $this->is_any_submission_plugin_enabled(), + $this->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED), + $this->get_instance()->cutoffdate, + $this->get_instance()->duedate, + $this->get_course_module()->id, + $this->count_submissions_need_grading(), + $this->get_instance()->teamsubmission); + $o .= $this->output->render($summary); + } } $grade = $this->get_user_grade($USER->id, false); $submission = $this->get_user_submission($USER->id, false); @@ -2291,9 +2540,22 @@ class assign { */ private function gradebook_item_update($submission=NULL, $grade=NULL) { - if($submission != NULL){ + if ($submission != NULL) { + if ($submission->userid == 0) { + // This is a group submission update. + $team = groups_get_members($submission->groupid, 'u.id'); + + foreach ($team as $member) { + $submission->groupid = 0; + $submission->userid = $member->id; + $this->gradebook_item_update($submission, null); + } + return; + } + $gradebookgrade = $this->convert_submission_for_gradebook($submission); - }else{ + + } else { $gradebookgrade = $this->convert_grade_for_gradebook($grade); } // Grading is disabled, return. @@ -2307,15 +2569,80 @@ class assign { } /** - * update grades in the gradebook based on submission time + * update team submission * * @param stdClass $submission + * @param int $userid * @param bool $updatetime * @return bool */ - private function update_submission(stdClass $submission, $updatetime=true) { + private function update_team_submission(stdClass $submission, $userid, $updatetime) { global $DB; + if ($updatetime) { + $submission->timemodified = time(); + } + + // First update the submission for the current user. + $mysubmission = $this->get_user_submission($userid, true); + $mysubmission->status = $submission->status; + + $this->update_submission($mysubmission, 0, $updatetime, false); + + // Now check the team settings to see if this assignment qualifies as submitted or draft. + $team = $this->get_submission_group_members($submission->groupid, true); + + $allsubmitted = true; + $anysubmitted = false; + foreach ($team as $member) { + $membersubmission = $this->get_user_submission($member->id, false); + + if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) { + $allsubmitted = false; + if ($anysubmitted) { + break; + } + } else { + $anysubmitted = true; + } + } + if ($this->get_instance()->requireallteammemberssubmit) { + if ($allsubmitted) { + $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; + } else { + $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; + } + $result= $DB->update_record('assign_submission', $submission); + } else { + if ($anysubmitted) { + $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; + } else { + $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; + } + $result= $DB->update_record('assign_submission', $submission); + } + + $this->gradebook_item_update($submission); + return $result; + } + + + /** + * update grades in the gradebook based on submission time + * + * @param stdClass $submission + * @param int $userid + * @param bool $updatetime + * @param bool $teamsubmission + * @return bool + */ + private function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) { + global $DB; + + if ($teamsubmission) { + return $this->update_team_submission($submission, $userid, $updatetime); + } + if ($updatetime) { $submission->timemodified = time(); } @@ -2404,11 +2731,6 @@ class assign { public function render_area_files($component, $area, $submissionid) { global $USER; - if (!$submissionid) { - $submission = $this->get_user_submission($USER->id,false); - $submissionid = $submission->id; - } - $fs = get_file_storage(); $browser = get_file_browser(); $files = $fs->get_area_files($this->get_context()->id, $component, $area , $submissionid , "timemodified", false); @@ -2578,14 +2900,18 @@ class assign { * @return void */ private function notify_student_submission_receipt(stdClass $submission) { - global $DB; + global $DB, $USER; $adminconfig = $this->get_admin_config(); if (empty($adminconfig->submissionreceipts)) { // No need to do anything return; } - $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST); + if ($submission->userid) { + $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST); + } else { + $user = $USER; + } $this->send_notification($user, $user, 'submissionreceipt', 'assign_notification', $submission->timemodified); } @@ -2596,7 +2922,7 @@ class assign { * @return void */ private function notify_graders(stdClass $submission) { - global $DB; + global $DB, $USER; $late = $this->get_instance()->duedate && ($this->get_instance()->duedate < time()); @@ -2604,7 +2930,11 @@ class assign { return; } - $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST); + if ($submission->userid) { + $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST); + } else { + $user = $USER; + } if ($teachers = $this->get_graders($user->id)) { foreach ($teachers as $teacher) { $this->send_notification($user, $teacher, 'gradersubmissionupdated', 'assign_notification', $submission->timemodified); @@ -2648,7 +2978,12 @@ class assign { if ($mform->get_data() == false) { return false; } - $submission = $this->get_user_submission($USER->id,true); + if ($this->get_instance()->teamsubmission) { + $submission = $this->get_group_submission($USER->id, 0, true); + } else { + $submission = $this->get_user_submission($USER->id, true); + } + if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // Give each submission plugin a chance to process the submission $plugins = $this->get_submission_plugins(); @@ -2657,7 +2992,7 @@ class assign { } $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; - $this->update_submission($submission); + $this->update_submission($submission, $USER->id, true, $this->get_instance()->teamsubmission); $completion = new completion_info($this->get_course()); if ($completion->is_enabled($this->get_course_module()) && $this->get_instance()->completionsubmit) { $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $USER->id); @@ -2987,7 +3322,17 @@ class assign { return true; } if ($data = $mform->get_data()) { - $submission = $this->get_user_submission($USER->id, true); //create the submission if needed & its id + if ($this->get_instance()->teamsubmission) { + $submission = $this->get_group_submission($USER->id, 0, true); + } else { + $submission = $this->get_user_submission($USER->id, true); + } + if ($this->get_instance()->submissiondrafts) { + $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; + } else { + $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; + } + $grade = $this->get_user_grade($USER->id, false); // get the grade to check if it is locked if ($grade && $grade->locked) { print_error('submissionslocked', 'assign'); @@ -3003,7 +3348,7 @@ class assign { } } - $this->update_submission($submission); + $this->update_submission($submission, $USER->id, true, $this->get_instance()->teamsubmission); // Logging if (isset($data->submissionstatement)) { @@ -3189,13 +3534,18 @@ class assign { $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT)); $mform->setType('ajax', PARAM_INT); + if ($this->get_instance()->teamsubmission) { + $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign')); + $mform->setDefault('applytoall', 1); + } + $mform->addElement('hidden', 'action', 'submitgrade'); $mform->setType('action', PARAM_ALPHA); $buttonarray=array(); $buttonarray[] = $mform->createElement('submit', 'savegrade', get_string('savechanges', 'assign')); - if (!$last){ + if (!$last) { $buttonarray[] = $mform->createElement('submit', 'saveandshownext', get_string('savenext','assign')); } $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel')); @@ -3207,7 +3557,7 @@ class assign { $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', get_string('previous','assign')); } - if (!$last){ + if (!$last) { $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', get_string('nosavebutnext', 'assign')); } $mform->addGroup($buttonarray, 'navar', '', array(' '), false); @@ -3282,9 +3632,14 @@ class assign { public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) { global $USER; - $submission = $this->get_user_submission($USER->id, false); + // Team submissions. + if ($this->get_instance()->teamsubmission) { + $submission = $this->get_group_submission($USER->id, 0, false); + } else { + $submission = $this->get_user_submission($USER->id, false); + } - // submission statement + // Submission statement. $adminconfig = $this->get_admin_config(); $requiresubmissionstatement = !empty($adminconfig->requiresubmissionstatement) || @@ -3333,14 +3688,16 @@ class assign { } $submission = $this->get_user_submission($userid, false); + if (!$submission) { return; } $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; - $this->update_submission($submission, false); + $this->update_submission($submission, $USER->id, true, $this->get_instance()->teamsubmission); - // update the modified time on the grade (grader modified) + // Update the modified time on the grade (grader modified). $grade = $this->get_user_grade($userid, true); + $grade->grader = $USER->id; $this->update_grade($grade); $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); @@ -3407,6 +3764,55 @@ class assign { $this->add_to_log('unlock submission', get_string('unlocksubmissionforstudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)))); } + /** + * Apply a grade from a grading form to a user (may be called multiple times for a group submission) + * + * @param stdClass $formdata - the data from the form + * @param int $userid - the user to apply the grade to + * @return void + */ + private function apply_grade_to_user($formdata, $userid) { + global $USER, $CFG, $DB; + + $grade = $this->get_user_grade($userid, true); + $gradingdisabled = $this->grading_disabled($userid); + $gradinginstance = $this->get_grading_instance($userid, $gradingdisabled); + if (!$gradingdisabled) { + if ($gradinginstance) { + $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id); + } else { + // Handle the case when grade is set to No Grade. + if (isset($formdata->grade)) { + $grade->grade= grade_floatval(unformat_float($formdata->grade)); + } + } + } + $grade->grader= $USER->id; + + $adminconfig = $this->get_admin_config(); + $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; + + // Call save in plugins. + foreach ($this->feedbackplugins as $plugin) { + if ($plugin->is_enabled() && $plugin->is_visible()) { + if (!$plugin->save($grade, $formdata)) { + $result = false; + print_error($plugin->get_error()); + } + if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) { + // This is the feedback plugin chose to push comments to the gradebook. + $grade->feedbacktext = $plugin->text_for_gradebook($grade); + $grade->feedbackformat = $plugin->format_for_gradebook($grade); + } + } + } + $this->update_grade($grade); + $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); + + $this->add_to_log('grade submission', $this->format_grade_for_log($grade)); + } + + /** * save outcomes submitted from grading form * @@ -3454,7 +3860,7 @@ class assign { * @return bool - was the grade saved */ private function process_save_grade(&$mform) { - global $USER, $DB, $CFG; + global $CFG; // Include grade form require_once($CFG->dirroot . '/mod/assign/gradeform.php'); @@ -3479,49 +3885,22 @@ class assign { $mform = new mod_assign_grade_form(null, array($this, $data, array('rownum'=>$rownum, 'useridlist'=>$useridlist, 'last'=>false)), 'post', '', array('class'=>'gradeform')); if ($formdata = $mform->get_data()) { - $grade = $this->get_user_grade($userid, true); - $gradingdisabled = $this->grading_disabled($userid); - $gradinginstance = $this->get_grading_instance($userid, $gradingdisabled); - if (!$gradingdisabled) { - if ($gradinginstance) { - $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id); - } else { - // handle the case when grade is set to No Grade - if (isset($formdata->grade)) { - $grade->grade = grade_floatval(unformat_float($formdata->grade)); + if ($this->get_instance()->teamsubmission && $formdata->applytoall) { + $groupid = 0; + if ($this->get_submission_group($userid)) { + $group = $this->get_submission_group($userid); + if ($group) { + $groupid = $group->id; } } - } - $grade->grader= $USER->id; - - $adminconfig = $this->get_admin_config(); - $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; - - // call save in plugins - foreach ($this->feedbackplugins as $plugin) { - if ($plugin->is_enabled() && $plugin->is_visible()) { - if (!$plugin->save($grade, $formdata)) { - $result = false; - print_error($plugin->get_error()); - } - if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) { - // this is the feedback plugin chose to push comments to the gradebook - $grade->feedbacktext = $plugin->text_for_gradebook($grade); - $grade->feedbackformat = $plugin->format_for_gradebook($grade); - } + $members = $this->get_submission_group_members($groupid, true); + foreach ($members as $member) { + // User may exist in multple groups (which should put them in the default group). + $this->apply_grade_to_user($formdata, $member->id); } + } else { + $this->apply_grade_to_user($formdata, $userid); } - $this->process_outcomes($userid, $formdata); - - $grade->mailed = 0; - - $this->update_grade($grade); - - $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); - - $this->add_to_log('grade submission', $this->format_grade_for_log($grade)); - - } else { return false; } diff --git a/mod/assign/mod_form.php b/mod/assign/mod_form.php index 000b92e1f1a..f0afc9963eb 100644 --- a/mod/assign/mod_form.php +++ b/mod/assign/mod_form.php @@ -107,6 +107,26 @@ class mod_assign_mod_form extends moodleform_mod { $mform->addHelpButton('sendlatenotifications', 'sendlatenotifications', 'assign'); $mform->setDefault('sendlatenotifications', 1); $mform->disabledIf('sendlatenotifications', 'sendnotifications', 'eq', 1); + $mform->addElement('selectyesno', 'teamsubmission', get_string('teamsubmission', 'assign')); + $mform->addHelpButton('teamsubmission', 'teamsubmission', 'assign'); + $mform->setDefault('teamsubmission', 0); + $mform->addElement('selectyesno', 'requireallteammemberssubmit', get_string('requireallteammemberssubmit', 'assign')); + $mform->addHelpButton('requireallteammemberssubmit', 'requireallteammemberssubmit', 'assign'); + $mform->setDefault('requireallteammemberssubmit', 0); + $mform->disabledIf('requireallteammemberssubmit', 'teamsubmission', 'eq', 0); + $mform->disabledIf('requireallteammemberssubmit', 'submissiondrafts', 'eq', 0); + + $groupings = groups_get_all_groupings($assignment->get_course()->id); + $options = array(); + $options[0] = get_string('none'); + foreach ($groupings as $grouping) { + $options[$grouping->id] = $grouping->name; + } + $mform->addElement('select', 'teamsubmissiongroupingid', get_string('teamsubmissiongroupingid', 'assign'), $options); + $mform->addHelpButton('teamsubmissiongroupingid', 'teamsubmissiongroupingid', 'assign'); + $mform->setDefault('teamsubmissiongroupingid', 0); + $mform->disabledIf('teamsubmissiongroupingid', 'teamsubmission', 'eq', 0); + // plagiarism enabling form if (!empty($CFG->enableplagiarism)) { diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php index cb41131bac2..c74feafe15d 100644 --- a/mod/assign/renderable.php +++ b/mod/assign/renderable.php @@ -289,6 +289,14 @@ class assign_submission_status implements renderable { var $alwaysshowdescription = false; /** @var stdClass the submission info (may be null) */ var $submission = null; + /** @var boolean teamsubmissionenabled - true or false */ + public $teamsubmissionenabled = false; + /** @var stdClass teamsubmission the team submission info (may be null) */ + public $teamsubmission = null; + /** @var stdClass submissiongroup the submission group info (may be null) */ + public $submissiongroup = null; + /** @var array submissiongroupmemberswhoneedtosubmit list of users who still need to submit */ + public $submissiongroupmemberswhoneedtosubmit = array(); /** @var bool submissionsenabled */ var $submissionsenabled = false; /** @var bool locked */ @@ -305,16 +313,22 @@ class assign_submission_status implements renderable { var $returnaction = ''; /** @var string returnparams */ var $returnparams = array(); + /** @var int courseid */ + public $courseid = 0; /** @var int coursemoduleid */ var $coursemoduleid = 0; /** @var int the view (assign_submission_status::STUDENT_VIEW OR assign_submission_status::GRADER_VIEW) */ var $view = self::STUDENT_VIEW; + /** @var bool canviewfullnames */ + public $canviewfullnames = false; /** @var bool canedit */ var $canedit = false; /** @var bool cansubmit */ var $cansubmit = false; /** @var int extensionduedate */ public $extensionduedate = 0; + /** @var context context */ + public $context = 0; /** * constructor @@ -322,6 +336,10 @@ class assign_submission_status implements renderable { * @param int $allowsubmissionsfromdate * @param bool $alwaysshowdescription * @param stdClass $submission + * @param bool $teamsubmissionenabled + * @param stdClass $teamsubmission + * @param int $submissiongroup + * @param array $submissiongroupmemberswhoneedtosubmit * @param bool $submissionsenabled * @param bool $locked * @param bool $graded @@ -331,17 +349,27 @@ class assign_submission_status implements renderable { * @param string $returnaction * @param array $returnparams * @param int $coursemoduleid + * @param int $courseid * @param string $view * @param bool $canedit * @param bool $cansubmit + * @param bool $canviewfullnames * @param int $extensionduedate - Any extension to the due date granted for this user + * @param context $context - Any extension to the due date granted for this user */ - public function __construct($allowsubmissionsfromdate, $alwaysshowdescription, $submission, $submissionsenabled, + public function __construct($allowsubmissionsfromdate, $alwaysshowdescription, $submission, + $teamsubmissionenabled, $teamsubmission, $submissiongroup, + $submissiongroupmemberswhoneedtosubmit, $submissionsenabled, $locked, $graded, $duedate, $cutoffdate, $submissionplugins, $returnaction, $returnparams, - $coursemoduleid, $view, $canedit, $cansubmit, $extensionduedate) { + $coursemoduleid, $courseid, $view, $canedit, $cansubmit, $canviewfullnames, $extensionduedate, + $context) { $this->allowsubmissionsfromdate = $allowsubmissionsfromdate; $this->alwaysshowdescription = $alwaysshowdescription; $this->submission = $submission; + $this->teamsubmissionenabled = $teamsubmissionenabled; + $this->teamsubmission = $teamsubmission; + $this->submissiongroup = $submissiongroup; + $this->submissiongroupmemberswhoneedtosubmit = $submissiongroupmemberswhoneedtosubmit; $this->submissionsenabled = $submissionsenabled; $this->locked = $locked; $this->graded = $graded; @@ -351,10 +379,13 @@ class assign_submission_status implements renderable { $this->returnaction = $returnaction; $this->returnparams = $returnparams; $this->coursemoduleid = $coursemoduleid; + $this->courseid = $courseid; $this->view = $view; $this->canedit = $canedit; $this->cansubmit = $cansubmit; + $this->canviewfullnames = $canviewfullnames; $this->extensionduedate = $extensionduedate; + $this->context = $context; } } @@ -424,6 +455,8 @@ class assign_grading_summary implements renderable { var $cutoffdate = 0; /** @var int coursemoduleid - The assignment course module id */ var $coursemoduleid = 0; + /** @var boolean teamsubmission - Are team submissions enabled for this assignment */ + public $teamsubmission = false; /** * constructor @@ -437,10 +470,12 @@ class assign_grading_summary implements renderable { * @param int $duedate * @param int $coursemoduleid * @param int $submissionsneedgradingcount + * @param bool $teamsubmission */ - public function __construct($participantcount, $submissiondraftsenabled, $submissiondraftscount, - $submissionsenabled, $submissionssubmittedcount, - $cutoffdate, $duedate, $coursemoduleid, $submissionsneedgradingcount) { + public function __construct($participantcount, $submissiondraftsenabled, + $submissiondraftscount, $submissionsenabled, + $submissionssubmittedcount, $cutoffdate, $duedate, + $coursemoduleid, $submissionsneedgradingcount, $teamsubmission) { $this->participantcount = $participantcount; $this->submissiondraftsenabled = $submissiondraftsenabled; $this->submissiondraftscount = $submissiondraftscount; @@ -450,6 +485,7 @@ class assign_grading_summary implements renderable { $this->cutoffdate = $cutoffdate; $this->coursemoduleid = $coursemoduleid; $this->submissionsneedgradingcount = $submissionsneedgradingcount; + $this->teamsubmission = $teamsubmission; } } diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php index 49ec46d23b8..058048cc1ab 100644 --- a/mod/assign/renderer.php +++ b/mod/assign/renderer.php @@ -232,8 +232,13 @@ class mod_assign_renderer extends plugin_renderer_base { $t = new html_table(); // status - $this->add_table_row_tuple($t, get_string('numberofparticipants', 'assign'), - $summary->participantcount); + if ($summary->teamsubmission) { + $this->add_table_row_tuple($t, get_string('numberofteams', 'assign'), + $summary->participantcount); + } else { + $this->add_table_row_tuple($t, get_string('numberofparticipants', 'assign'), + $summary->participantcount); + } // drafts if ($summary->submissiondraftsenabled) { @@ -378,20 +383,68 @@ class mod_assign_renderer extends plugin_renderer_base { $t = new html_table(); + if ($status->teamsubmissionenabled) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('submissionteam', 'assign')); + $group = $status->submissiongroup; + if ($group) { + $cell2 = new html_table_cell(format_string($group->name, false, $status->context)); + } else { + $cell2 = new html_table_cell(get_string('defaultteam', 'assign')); + } + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + $row = new html_table_row(); $cell1 = new html_table_cell(get_string('submissionstatus', 'assign')); - if ($status->submission) { - $cell2 = new html_table_cell(get_string('submissionstatus_' . $status->submission->status, 'assign')); - $cell2->attributes = array('class'=>'submissionstatus' . $status->submission->status); + if (!$status->teamsubmissionenabled) { + if ($status->submission) { + $cell2 = new html_table_cell(get_string('submissionstatus_' . $status->submission->status, 'assign')); + $cell2->attributes = array('class'=>'submissionstatus' . $status->submission->status); + } else { + if (!$status->submissionsenabled) { + $cell2 = new html_table_cell(get_string('noonlinesubmissions', 'assign')); + } else { + $cell2 = new html_table_cell(get_string('nosubmission', 'assign')); + } + } + $row->cells = array($cell1, $cell2); + $t->data[] = $row; } else { - if (!$status->submissionsenabled) { - $cell2 = new html_table_cell(get_string('noonlinesubmissions', 'assign')); + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('submissionstatus', 'assign')); + if ($status->teamsubmission) { + $submissionsummary = get_string('submissionstatus_' . $status->teamsubmission->status, 'assign'); + $groupid = 0; + if ($status->submissiongroup) { + $groupid = $status->submissiongroup->id; + } + + $members = $status->submissiongroupmemberswhoneedtosubmit; + $userslist = array(); + foreach ($members as $member) { + $url = new moodle_url('/user/view.php', array('id' => $member->id, 'course'=>$status->courseid)); + $userslist[] = $this->output->action_link($url, fullname($member, $status->canviewfullnames)); + } + if (count($userslist) > 0) { + $userstr = join(', ', $userslist); + $submissionsummary .= $this->output->container(get_string('userswhoneedtosubmit', 'assign', $userstr)); + } + + $cell2 = new html_table_cell($submissionsummary); + $cell2->attributes = array('class'=>'submissionstatus' . $status->teamsubmission->status); } else { $cell2 = new html_table_cell(get_string('nosubmission', 'assign')); + if (!$status->submissionsenabled) { + $cell2 = new html_table_cell(get_string('noonlinesubmissions', 'assign')); + } else { + $cell2 = new html_table_cell(get_string('nosubmission', 'assign')); + } } + $row->cells = array($cell1, $cell2); + $t->data[] = $row; } - $row->cells = array($cell1, $cell2); - $t->data[] = $row; // status if ($status->locked) { @@ -488,15 +541,16 @@ class mod_assign_renderer extends plugin_renderer_base { } // Last modified. - if ($status->submission) { + $submission = $status->teamsubmission ? $status->teamsubmission : $status->submission; + if ($submission) { $row = new html_table_row(); $cell1 = new html_table_cell(get_string('timemodified', 'assign')); - $cell2 = new html_table_cell(userdate($status->submission->timemodified)); + $cell2 = new html_table_cell(userdate($submission->timemodified)); $row->cells = array($cell1, $cell2); $t->data[] = $row; foreach ($status->submissionplugins as $plugin) { - if ($plugin->is_enabled() && $plugin->is_visible() && !$plugin->is_empty($status->submission)) { + if ($plugin->is_enabled() && $plugin->is_visible() && !$plugin->is_empty($submission)) { $row = new html_table_row(); $cell1 = new html_table_cell($plugin->get_name()); $pluginsubmission = new assign_submission_plugin_submission($plugin, diff --git a/mod/assign/version.php b/mod/assign/version.php index d9ed52fb11a..e15fe4e93c0 100644 --- a/mod/assign/version.php +++ b/mod/assign/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $module->component = 'mod_assign'; // Full name of the plugin (used for diagnostics) -$module->version = 2012082100; // The current module version (Date: YYYYMMDDXX) +$module->version = 2012082300; // The current module version (Date: YYYYMMDDXX) $module->requires = 2012061700; // Requires this Moodle version $module->cron = 60; From cd01491c11ec90c7da1967b568b131e0475768a1 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 3 Sep 2012 10:31:20 +0800 Subject: [PATCH 53/96] MDL-31284: Backported team assignment fixes from stable branch --- mod/assign/gradingtable.php | 56 ++++++++++++++++++++++++++++++++++--- mod/assign/locallib.php | 9 +++++- mod/assign/renderer.php | 2 +- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/mod/assign/gradingtable.php b/mod/assign/gradingtable.php index 6032fe14618..a5dbd350089 100644 --- a/mod/assign/gradingtable.php +++ b/mod/assign/gradingtable.php @@ -52,6 +52,11 @@ class assign_grading_table extends table_sql implements renderable { private $quickgrading = false; /** @var boolean $hasgrantextension - Only do the capability check once for the entire table */ private $hasgrantextension = false; + /** @var array $groupsubmissions - A static cache of group submissions */ + private $groupsubmissions = array(); + /** @var array $submissiongroups - A static cache of submission groups */ + private $submissiongroups = array(); + /** * overridden constructor keeps a reference to the assignment class that is displaying this table @@ -274,7 +279,9 @@ class assign_grading_table extends table_sql implements renderable { * @return string The team name */ function col_team(stdClass $row) { - $group = $this->assignment->get_submission_group($row->id); + $submission = false; + $group = false; + $this->get_group_and_submission($row->id, $group, $submission); if ($group) { return $group->name; } @@ -282,13 +289,46 @@ class assign_grading_table extends table_sql implements renderable { } /** - * Get the team info for this user + * Use a static cache to try and reduce DB calls. + * + * @param int $userid The user id for this submission + * @param int $groupid The groupid (returned) + * @param mixed $submission The stdClass submission or false (returned) + */ + function get_group_and_submission($userid, &$group, &$submission) { + $group = false; + if (isset($this->submissiongroups[$userid])) { + $group = $this->submissiongroups[$userid]; + } else { + $group = $this->assignment->get_submission_group($userid, false); + $this->submissiongroups[$userid] = $group; + } + + $groupid = 0; + if ($group) { + $groupid = $group->id; + } + + if (isset($this->groupsubmissions[$groupid])) { + $submission = $this->groupsubmissions[$groupid]; + } else { + $submission = $this->assignment->get_group_submission($userid, $groupid, false); + $this->groupsubmissions[$groupid] = $submission; + } + } + + + /** + * Get the team status for this user * * @param stdClass $row * @return string The team name */ function col_teamstatus(stdClass $row) { - $submission = $this->assignment->get_group_submission($row->id, 0, false); + $submission = false; + $group = false; + $this->get_group_and_submission($row->id, $group, $submission); + $status = ''; if ($submission) { $status = $submission->status; @@ -648,7 +688,15 @@ class assign_grading_table extends table_sql implements renderable { $plugin = $this->assignment->get_submission_plugin_by_type(substr($colname, strlen('assignsubmission_'))); if ($plugin->is_visible() && $plugin->is_enabled()) { - if ($row->submissionid) { + if ($this->assignment->get_instance()->teamsubmission) { + $group = false; + $submission = false; + $this->get_group_and_submission($row->id, $group, $submission); + if ($submission) { + return $this->format_plugin_summary_with_link($plugin, $submission, 'grading', array()); + } + } else if ($row->submissionid) { + $submission = new stdClass(); $submission->id = $row->submissionid; $submission->timecreated = $row->firstsubmission; diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 3a7deb60684..02529ae0be4 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -2702,7 +2702,14 @@ class assign { if (!is_enrolled($this->get_course_context(), $userid)) { return false; } - if ($submission = $this->get_user_submission($userid, false)) { + $submission = false; + if ($this->get_instance()->teamsubmission) { + $submission = $this->get_group_submission($USER->id, 0, false); + } else { + $submission = $this->get_user_submission($USER->id, false); + } + if ($submission) { + if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // drafts are tracked and the student has submitted the assignment return false; diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php index 058048cc1ab..30308a3ca21 100644 --- a/mod/assign/renderer.php +++ b/mod/assign/renderer.php @@ -573,7 +573,7 @@ class mod_assign_renderer extends plugin_renderer_base { // Links. if ($status->view == assign_submission_status::STUDENT_VIEW) { if ($status->canedit) { - if (!$status->submission) { + if (!$submission) { $urlparams = array('id' => $status->coursemoduleid, 'action' => 'editsubmission'); $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', $urlparams), get_string('addsubmission', 'assign'), 'get'); From dd1fe178eb76463066c18c66e90d329cdbc94467 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 3 Sep 2012 11:10:03 +0800 Subject: [PATCH 54/96] MDL-31284: Fix for plugin summary display in grading table with team submissions --- mod/assign/renderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php index 30308a3ca21..d64e06c00f9 100644 --- a/mod/assign/renderer.php +++ b/mod/assign/renderer.php @@ -554,7 +554,7 @@ class mod_assign_renderer extends plugin_renderer_base { $row = new html_table_row(); $cell1 = new html_table_cell($plugin->get_name()); $pluginsubmission = new assign_submission_plugin_submission($plugin, - $status->submission, + $submission, assign_submission_plugin_submission::SUMMARY, $status->coursemoduleid, $status->returnaction, From b473171a6f14006ef92e16f0bb8d8b6c85617a4b Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 13 Jun 2012 14:22:09 +0800 Subject: [PATCH 55/96] MDL-31291: Blind marking feature for mod_assign --- mod/assign/assignmentplugin.php | 2 +- .../backup/moodle2/backup_assign_stepslib.php | 4 +- mod/assign/db/access.php | 10 + mod/assign/db/install.xml | 18 +- mod/assign/db/log.php | 1 + mod/assign/db/upgrade.php | 30 ++ mod/assign/gradingtable.php | 28 +- mod/assign/lang/en/assign.php | 10 + mod/assign/lib.php | 10 +- mod/assign/locallib.php | 263 +++++++++++++++++- mod/assign/mod_form.php | 7 + mod/assign/renderable.php | 8 +- mod/assign/renderer.php | 16 +- mod/assign/submission/comments/locallib.php | 13 + mod/assign/submission/onlinetext/locallib.php | 13 +- mod/assign/version.php | 2 +- 16 files changed, 407 insertions(+), 28 deletions(-) diff --git a/mod/assign/assignmentplugin.php b/mod/assign/assignmentplugin.php index dcb31e96b72..d71281b4120 100644 --- a/mod/assign/assignmentplugin.php +++ b/mod/assign/assignmentplugin.php @@ -216,7 +216,7 @@ abstract class assign_plugin { * * @return bool - if false - this plugin will not accept submissions / feedback */ - public final function is_enabled() { + public function is_enabled() { return $this->get_config('enabled'); } diff --git a/mod/assign/backup/moodle2/backup_assign_stepslib.php b/mod/assign/backup/moodle2/backup_assign_stepslib.php index 76f914536bd..494fd14b474 100644 --- a/mod/assign/backup/moodle2/backup_assign_stepslib.php +++ b/mod/assign/backup/moodle2/backup_assign_stepslib.php @@ -60,7 +60,9 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st 'requiresubmissionstatement', 'teamsubmission', 'requireallteammemberssubmit', - 'teamsubmissiongroupingid')); + 'teamsubmissiongroupingid', + 'blindmarking', + 'revealidentities')); $submissions = new backup_nested_element('submissions'); diff --git a/mod/assign/db/access.php b/mod/assign/db/access.php index f3e7ee16a2b..7028038907e 100644 --- a/mod/assign/db/access.php +++ b/mod/assign/db/access.php @@ -93,6 +93,16 @@ $capabilities = array( 'clonepermissionsfrom' => 'gradereport/grader:view' ), + 'mod/assign:revealidentities' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + ); diff --git a/mod/assign/db/install.xml b/mod/assign/db/install.xml index 2ee2938394f..95002326fea 100755 --- a/mod/assign/db/install.xml +++ b/mod/assign/db/install.xml @@ -25,7 +25,9 @@ - + + + @@ -75,7 +77,7 @@ - +
    @@ -94,5 +96,17 @@
    + + + + + + + + + + + +
    diff --git a/mod/assign/db/log.php b/mod/assign/db/log.php index 5401cb89eab..45f19580079 100644 --- a/mod/assign/db/log.php +++ b/mod/assign/db/log.php @@ -30,6 +30,7 @@ $logs = array( array('module'=>'assign', 'action'=>'download all submissions', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'grade submission', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'lock submission', 'mtable'=>'assign', 'field'=>'name'), + array('module'=>'assign', 'action'=>'reveal identities', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'revert submission to draft', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'submission statement accepted', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'submit', 'mtable'=>'assign', 'field'=>'name'), diff --git a/mod/assign/db/upgrade.php b/mod/assign/db/upgrade.php index 0f402d0c708..f02c1616317 100644 --- a/mod/assign/db/upgrade.php +++ b/mod/assign/db/upgrade.php @@ -156,6 +156,36 @@ function xmldb_assign_upgrade($oldversion) { } upgrade_mod_savepoint(true, 2012082300, 'assign'); } + if ($oldversion < 2012082400) { + + // Define field to be added to assign + $table = new xmldb_table('assign_user_mapping'); + + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); + $table->add_key('assignment', XMLDB_KEY_FOREIGN, array('assignment'), 'assign', array('id')); + + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + $table = new xmldb_table('assign'); + $field = new xmldb_field('blindmarking', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'teamsubmissiongroupingid'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $table = new xmldb_table('assign'); + $field = new xmldb_field('revealidentities', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'blindmarking'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint(true, 2012082400, 'assign'); + } return true; diff --git a/mod/assign/gradingtable.php b/mod/assign/gradingtable.php index a5dbd350089..51bc69a0ee3 100644 --- a/mod/assign/gradingtable.php +++ b/mod/assign/gradingtable.php @@ -144,13 +144,19 @@ class assign_grading_table extends table_sql implements renderable { $headers[] = get_string('edit'); } - // User picture - $columns[] = 'picture'; - $headers[] = get_string('pictureofuser'); + // User picture. + if (!$this->assignment->is_blind_marking()) { + $columns[] = 'picture'; + $headers[] = get_string('pictureofuser'); - // Fullname - $columns[] = 'fullname'; - $headers[] = get_string('fullname'); + // Fullname. + $columns[] = 'fullname'; + $headers[] = get_string('fullname'); + } else { + // Record ID. + $columns[] = 'recordid'; + $headers[] = get_string('recordid', 'assign'); + } // Submission status if ($assignment->is_any_submission_plugin_enabled()) { @@ -236,6 +242,16 @@ class assign_grading_table extends table_sql implements renderable { } + /** + * Add a column with an ID that uniquely identifies this user in this assignment + * + * @return string + */ + function col_recordid(stdClass $row) { + return get_string('hiddenuser', 'assign', $this->assignment->get_uniqueid_for_user($row->userid)); + } + + /** * Add the userid to the row class so it can be updated via ajax * diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php index 929f85f474c..d4b4d70c056 100644 --- a/mod/assign/lang/en/assign.php +++ b/mod/assign/lang/en/assign.php @@ -37,6 +37,7 @@ $string['assign:addinstance'] = 'Add a new assignment'; $string['assign:exportownsubmission'] = 'Export own submission'; $string['assign:grade'] = 'Grade assignment'; $string['assign:grantextension'] = 'Grant extension'; +$string['assign:revealidentities'] = 'Reveal student identities'; $string['assign:submit'] = 'Submit assignment'; $string['assign:view'] = 'View assignment'; $string['assignfeedback'] = 'Feedback plugin'; @@ -68,6 +69,8 @@ $string['batchoperationconfirmreverttodraft'] = 'Revert selected submissions to $string['batchoperationlock'] = 'lock submissions'; $string['batchoperationunlock'] = 'unlock submissions'; $string['batchoperationreverttodraft'] = 'revert submissions to draft'; +$string['blindmarking'] = 'Blind marking'; +$string['blindmarking_help'] = 'Blind marking hides the identity of students to markers. Blind marking settings will be locked once a submission or grade has been made in relation to this assignment.'; $string['changegradewarning'] = 'This assignment has graded submissions and changing the grade will not automatically re-calculate existing submission grades. You must re-grade all existing submissions, if you wish to change the grade.'; $string['comment'] = 'Comment'; $string['completionsubmit'] = 'Student must submit to this activity to complete it'; @@ -156,6 +159,7 @@ $string['gradingstatus'] = 'Grading status'; $string['gradingstudentprogress'] = 'Grading student {$a->index} of {$a->count}'; $string['gradingsummary'] = 'Grading summary'; $string['hideshow'] = 'Hide/Show'; +$string['hiddenuser'] = 'Participant {$a}'; $string['instructionfiles'] = 'Instruction files'; $string['invalidgradeforscale'] = 'The grade supplied was not valid for the current scale'; $string['invalidfloatforgrade'] = 'The grade provided could not be understood: {$a}'; @@ -202,6 +206,7 @@ $string['overdue'] = 'Assignment is overdue by: {$a}'; $string['outlinegrade'] = 'Grade: {$a}'; $string['page-mod-assign-x'] = 'Any assignment module page'; $string['page-mod-assign-view'] = 'Assignment module main and submission page'; +$string['participant'] = 'Participant'; $string['pluginadministration'] = 'Assignment administration'; $string['pluginname'] = 'Assignment'; $string['preventsubmissions'] = 'Prevent the user from making any more submissions to this assignment.'; @@ -215,6 +220,9 @@ $string['requiresubmissionstatement'] = 'Require that students accept the submis $string['requiresubmissionstatement_help'] = 'Require that students accept the submission statement for all assignment submissions for this entire Moodle installation. If this setting is not enabled, then submission statements can be enabled or disabled in the settings for each assignment.'; $string['requireallteammemberssubmit'] = 'Require all team members submit'; $string['requireallteammemberssubmit_help'] = 'If enabled, all members of the student team must click the submit button for this assignment before the team submission will be considered as submitted. If disabled, the team submission will be considered as submitted as soon as any member of the student team clicks the submit button.'; +$string['recordid'] = 'Identifier'; +$string['revealidentities'] = 'Reveal student identities'; +$string['revealidentitiesconfirm'] = 'Are you sure you want to reveal student identities for this assignment. This operation cannot be undone. Once the student identities have been revealed, the marks will be released to the gradebook.'; $string['reverttodraftforstudent'] = 'Revert submission to draft for student: (id={$a->id}, fullname={$a->fullname}).'; $string['reverttodraft'] = 'Revert the submission to draft status.'; $string['reverttodraftshort'] = 'Revert the submission to draft'; @@ -299,3 +307,5 @@ $string['viewownsubmissionstatus'] = 'View own submission status page.'; $string['viewsubmissionforuser'] = 'View submission for user: {$a}'; $string['viewsubmission'] = 'View submission'; $string['viewsubmissiongradingtable'] = 'View submission grading table.'; +$string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.'; + diff --git a/mod/assign/lib.php b/mod/assign/lib.php index c981e228213..1636d2e7911 100644 --- a/mod/assign/lib.php +++ b/mod/assign/lib.php @@ -113,7 +113,7 @@ function assign_grading_areas_list() { * @return void */ function assign_extend_settings_navigation(settings_navigation $settings, navigation_node $navref) { - global $PAGE; + global $PAGE, $DB; $cm = $PAGE->cm; if (!$cm) { @@ -144,6 +144,14 @@ function assign_extend_settings_navigation(settings_navigation $settings, naviga $node = $navref->add(get_string('downloadall', 'assign'), $link, navigation_node::TYPE_SETTING); } + if (has_capability('mod/assign:revealidentities', $context)) { + $assignment = $DB->get_record('assign', array('id'=>$cm->instance), 'blindmarking, revealidentities'); + + if ($assignment && $assignment->blindmarking && !$assignment->revealidentities) { + $link = new moodle_url('/mod/assign/view.php', array('id' => $cm->id,'action'=>'revealidentities')); + $node = $navref->add(get_string('revealidentities', 'assign'), $link, navigation_node::TYPE_SETTING); + } + } } diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 02529ae0be4..2dd73c5fd1d 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -215,6 +215,28 @@ class assign { return $this->submissionplugins; } + /** + * Is blind marking enabled and reveal identities not set yet? + * + * @return bool + */ + public function is_blind_marking() { + return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities; + } + + /** + * Does an assignment have submission(s) or grade(s) already? + * + * @return bool + */ + public function has_submissions_or_grades() { + $allgrades = $this->count_grades(); + $allsubmissions = $this->count_submissions(); + if (($allgrades == 0) && ($allsubmissions == 0)) { + return false; + } + return true; + } /** * get a specific submission plugin by its type @@ -353,6 +375,9 @@ class assign { if ($this->process_save_extension($mform)) { $action = 'grading'; } + } else if ($action == 'revealidentitiesconfirm') { + $this->process_reveal_identities(); + $action = 'grading'; } $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT)); @@ -384,6 +409,8 @@ class assign { $o .= $this->check_submit_for_grading($mform); } else if ($action == 'grantextension') { $o .= $this->view_grant_extension($mform); + } else if ($action == 'revealidentities') { + $o .= $this->view_reveal_identities_confirm($mform); } else { $o .= $this->view_submission_page(); } @@ -427,6 +454,7 @@ class assign { $update->teamsubmission = $formdata->teamsubmission; $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; + $update->blindmarking = $formdata->blindmarking; $returnid = $DB->insert_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST); @@ -651,6 +679,7 @@ class assign { $update->teamsubmission = $formdata->teamsubmission; $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; + $update->blindmarking = $formdata->blindmarking; $result = $DB->update_record('assign', $update); @@ -1028,7 +1057,48 @@ class assign { } /** - * Load a count of users enrolled in the current course with the specified permission and group (optional) + * Load a count of grades + * + * @return int number of grades + */ + public function count_grades() { + global $DB; + + if (!$this->has_instance()) { + return 0; + } + + $sql = 'SELECT COUNT(id) FROM {assign_grades} WHERE assignment = ?'; + $params = array($this->get_course_module()->instance); + + return $DB->count_records_sql($sql, $params); + } + + /** + * Load a count of submissions + * + * @return int number of submissions + */ + public function count_submissions() { + global $DB; + + if (!$this->has_instance()) { + return 0; + } + + $sql = 'SELECT COUNT(id) FROM {assign_submission} WHERE assignment = ?'; + $params = array($this->get_course_module()->instance); + + if ($this->get_instance()->teamsubmission) { + // only look at team submissions + $sql .= ' AND userid = ?'; + $params[] = 0; + } + return $DB->count_records_sql($sql, $params); + } + + /** + * Load a count of submissions with a specified status * * @param string $status The submission status - should match one of the constants * @return int number of matching submissions @@ -1147,7 +1217,8 @@ class assign { $timenow = time(); // Collect all submissions from the past 24 hours that require mailing. - $sql = "SELECT s.*, a.course, a.name, g.*, g.id as gradeid, g.timemodified as lastmodified + $sql = "SELECT s.*, a.course, a.name, a.blindmarking, a.revealidentities, + g.*, g.id as gradeid, g.timemodified as lastmodified FROM {assign} a JOIN {assign_grades} g ON g.assignment = a.id LEFT JOIN {assign_submission} s ON s.assignment = a.id AND s.userid = g.userid @@ -1249,7 +1320,15 @@ class assign { $eventtype = 'assign_notification'; $updatetime = $submission->lastmodified; $modulename = get_string('modulename', 'assign'); - self::send_assignment_notification($grader, $user, $messagetype, $eventtype, $updatetime, $mod, $contextmodule, $course, $modulename, $submission->name); + + $uniqueid = 0; + if ($submission->blindmarking && !$submission->revealidentities) { + $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $user->id); + } + self::send_assignment_notification($grader, $user, $messagetype, $eventtype, $updatetime, + $mod, $contextmodule, $course, $modulename, $submission->name, + $submission->blindmarking && !$submission->revealidentities, + $uniqueid); $grade = new stdClass(); $grade->id = $submission->gradeid; @@ -1688,7 +1767,11 @@ class assign { $user = $DB->get_record("user", array("id"=>$userid),'id,username,firstname,lastname', MUST_EXIST); - $prefix = clean_filename(fullname($user) . "_" .$userid . "_"); + if ($this->is_blind_marking()) { + $prefix = clean_filename(get_string('participant', 'assign') . "_" . $this->get_uniqueid_for_user($userid) . "_"); + } else { + $prefix = clean_filename(fullname($user) . "_" . $this->get_uniqueid_for_user($userid) . "_"); + } foreach ($this->submissionplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { @@ -1873,7 +1956,12 @@ class assign { } $user = $DB->get_record('user', array('id' => $userid)); if ($user) { - $o .= $this->output->render(new assign_user_summary($user, $this->get_course()->id, has_capability('moodle/site:viewfullnames', $this->get_course_context()))); + $o .= $this->output->render(new assign_user_summary($user, + $this->get_course()->id, + has_capability('moodle/site:viewfullnames', + $this->get_course_context()), + $this->is_blind_marking(), + $this->get_uniqueid_for_user($user->id))); } $submission = $this->get_user_submission($userid, false); $submissiongroup = null; @@ -1962,6 +2050,34 @@ class assign { return $o; } + /** + * Show a confirmation page to make sure they want to release student identities + * + * @return string + */ + private function view_reveal_identities_confirm() { + global $CFG, $USER; + + require_capability('mod/assign:revealidentities', $this->get_context()); + + $o = ''; + $o .= $this->output->render(new assign_header($this->get_instance(), + $this->get_context(), false, $this->get_course_module()->id)); + + $confirmurl = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, + 'action'=>'revealidentitiesconfirm', + 'sesskey'=>sesskey())); + + $cancelurl = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, + 'action'=>'grading')); + + $o .= $this->output->confirm(get_string('revealidentitiesconfirm', 'assign'), $confirmurl, $cancelurl); + $o .= $this->view_footer(); + $this->add_to_log('view', get_string('viewrevealidentitiesconfirm', 'assign')); + return $o; + } + + /** @@ -2005,6 +2121,10 @@ class assign { $downloadurl = '/mod/assign/view.php?id=' . $this->get_course_module()->id . '&action=downloadall'; $links[$downloadurl] = get_string('downloadall', 'assign'); } + if ($this->is_blind_marking() && has_capability('mod/assign:revealidentities', $this->get_context())) { + $revealidentitiesurl = '/mod/assign/view.php?id=' . $this->get_course_module()->id . '&action=revealidentities'; + $links[$revealidentitiesurl] = get_string('revealidentities', 'assign'); + } $gradingactions = new url_select($links); @@ -2540,6 +2660,10 @@ class assign { */ private function gradebook_item_update($submission=NULL, $grade=NULL) { + // Do not push grade to gradebook if blind marking is active as the gradebook would reveal the students. + if ($this->is_blind_marking()) { + return false; + } if ($submission != NULL) { if ($submission->userid == 0) { // This is a group submission update. @@ -2854,11 +2978,16 @@ class assign { */ public static function send_assignment_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime, $coursemodule, $context, $course, - $modulename, $assignmentname) { + $modulename, $assignmentname, $blindmarking, + $uniqueidforuser) { global $CFG; $info = new stdClass(); - $info->username = fullname($userfrom, true); + if ($blindmarking) { + $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser; + } else { + $info->username = fullname($userfrom, true); + } $info->assignment = format_string($assignmentname,true, array('context'=>$context)); $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id; $info->timeupdated = strftime('%c',$updatetime); @@ -2897,7 +3026,11 @@ class assign { * @return void */ public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) { - self::send_assignment_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime, $this->get_course_module(), $this->get_context(), $this->get_course(), $this->get_module_name(), $this->get_instance()->name); + self::send_assignment_notification($userfrom, $userto, $messagetype, $eventtype, + $updatetime, $this->get_course_module(), $this->get_context(), + $this->get_course(), $this->get_module_name(), + $this->get_instance()->name, $this->is_blind_marking(), + $this->get_uniqueid_for_user($userfrom->id)); } /** @@ -3229,6 +3362,53 @@ class assign { return get_string('quickgradingchangessaved', 'assign'); } + /** + * Reveal student identities to markers (and the gradebook) + * + * @return void + */ + private function process_reveal_identities() { + global $DB, $CFG; + + require_capability('mod/assign:revealidentities', $this->context); + if (!confirm_sesskey()) { + return false; + } + + // Update the assignment record. + $update = new stdClass(); + $update->id = $this->get_instance()->id; + $update->revealidentities = 1; + $DB->update_record('assign', $update); + + // Refresh the instance data. + $this->instance = null; + + // Release the grades to the gradebook. + // First create the column in the gradebook. + $this->update_gradebook(false, $this->get_course_module()->id); + + // Now release all grades. + + $adminconfig = $this->get_admin_config(); + $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; + $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id)); + + $plugin = $this->get_feedback_plugin_by_type($gradebookplugin); + + foreach ($grades as $grade) { + // Fetch any comments for this student. + if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) { + $grade->feedbacktext = $plugin->text_for_gradebook($grade); + $grade->feedbackformat = $plugin->format_for_gradebook($grade); + } + $this->gradebook_item_update(NULL, $grade); + } + + $this->add_to_log('reveal identities', get_string('revealidentities', 'assign')); + } + + /** * save grading options * @@ -4045,5 +4225,72 @@ class assign { return $grades; } + /** + * Call the static version of this function + * + * @param int $userid The userid to lookup + * @return int The unique id + */ + public function get_uniqueid_for_user($userid) { + return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid); + } + + /** + * Foreach participant in the course - assign them a random id + * + * @param int $assignid The assignid to lookup + */ + public static function allocate_unique_ids($assignid) { + global $DB; + + $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST); + $context = context_module::instance($cm->id); + + $currentgroup = groups_get_activity_group($cm, true); + $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id'); + + // shuffle the users + shuffle($users); + + $record = new stdClass(); + $record->assignment = $assignid; + foreach ($users as $user) { + if (!$DB->get_record('assign_user_mapping', array('assignment'=>$assignid, 'userid'=>$user->id), 'id')) { + $record->userid = $user->id; + $DB->insert_record('assign_user_mapping', $record); + } + } + } + + /** + * Lookup this user id and return the unique id for this assignment + * + * @param int $userid The userid to lookup + * @return int The unique id + */ + public static function get_uniqueid_for_user_static($assignid, $userid) { + global $DB; + + // Search for a record. + if ($record = $DB->get_record('assign_user_mapping', array('assignment'=>$assignid, 'userid'=>$userid), 'id')) { + return $record->id; + } + + // Be a little smart about this - there is no record for the current user. + // We should ensure any unallocated ids for the current participant list are distrubited randomly + self::allocate_unique_ids($assignid); + + // Retry the search for a record. + if ($record = $DB->get_record('assign_user_mapping', array('assignment'=>$assignid, 'userid'=>$userid), 'id')) { + return $record->id; + } + + // The requested user must not be a participant. Add a record anyway. + $record = new stdClass(); + $record->assignment = $assignid; + $record->userid = $userid; + + return $DB->insert_record('assign_user_mapping', $record); + } } diff --git a/mod/assign/mod_form.php b/mod/assign/mod_form.php index f0afc9963eb..a1f177d1fb9 100644 --- a/mod/assign/mod_form.php +++ b/mod/assign/mod_form.php @@ -127,6 +127,13 @@ class mod_assign_mod_form extends moodleform_mod { $mform->setDefault('teamsubmissiongroupingid', 0); $mform->disabledIf('teamsubmissiongroupingid', 'teamsubmission', 'eq', 0); + $mform->addElement('selectyesno', 'blindmarking', get_string('blindmarking', 'assign')); + $mform->addHelpButton('blindmarking', 'blindmarking', 'assign'); + $mform->setDefault('blindmarking', 0); + if ($assignment->has_submissions_or_grades() ) { + $mform->freeze('blindmarking'); + } + // plagiarism enabling form if (!empty($CFG->enableplagiarism)) { diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php index c74feafe15d..c4cb86971b3 100644 --- a/mod/assign/renderable.php +++ b/mod/assign/renderable.php @@ -116,6 +116,10 @@ class assign_user_summary implements renderable { public $courseid; /** @var bool $viewfullnames */ public $viewfullnames = false; + /** @var bool $blindmarking */ + public $blindmarking = false; + /** @var int $uniqueidforuser */ + public $uniqueidforuser; /** * Constructor @@ -123,10 +127,12 @@ class assign_user_summary implements renderable { * @param int $courseid * @param bool $viewfullnames */ - public function __construct(stdClass $user, $courseid, $viewfullnames) { + public function __construct(stdClass $user, $courseid, $viewfullnames, $blindmarking, $uniqueidforuser) { $this->user = $user; $this->courseid = $courseid; $this->viewfullnames = $viewfullnames; + $this->blindmarking = $blindmarking; + $this->uniqueidforuser = $uniqueidforuser; } } diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php index d64e06c00f9..734b3dd890e 100644 --- a/mod/assign/renderer.php +++ b/mod/assign/renderer.php @@ -131,12 +131,16 @@ class mod_assign_renderer extends plugin_renderer_base { } $o .= $this->output->container_start('usersummary'); $o .= $this->output->box_start('boxaligncenter usersummarysection'); - $o .= $this->output->user_picture($summary->user); - $o .= $this->output->spacer(array('width'=>30)); - $o .= $this->output->action_link(new moodle_url('/user/view.php', - array('id' => $summary->user->id, - 'course'=>$summary->courseid)), - fullname($summary->user, $summary->viewfullnames)); + if ($summary->blindmarking) { + $o .= get_string('hiddenuser', 'assign', $summary->uniqueidforuser); + } else { + $o .= $this->output->user_picture($summary->user); + $o .= $this->output->spacer(array('width'=>30)); + $o .= $this->output->action_link(new moodle_url('/user/view.php', + array('id' => $summary->user->id, + 'course'=>$summary->courseid)), + fullname($summary->user, $summary->viewfullnames)); + } $o .= $this->output->box_end(); $o .= $this->output->container_end(); diff --git a/mod/assign/submission/comments/locallib.php b/mod/assign/submission/comments/locallib.php index eb879388757..8cdfebbc83d 100644 --- a/mod/assign/submission/comments/locallib.php +++ b/mod/assign/submission/comments/locallib.php @@ -164,4 +164,17 @@ class assign_submission_comments extends assign_submission_plugin { return false; } + /** + * If blind marking is enabled then disable this plugin (it shows names) + * + * @return bool + */ + public function is_enabled() { + if ($this->assignment->has_instance() && $this->assignment->is_blind_marking()) { + return false; + } + return parent::is_enabled(); + } + + } diff --git a/mod/assign/submission/onlinetext/locallib.php b/mod/assign/submission/onlinetext/locallib.php index 729fa2d9ba3..1c5b757c52e 100644 --- a/mod/assign/submission/onlinetext/locallib.php +++ b/mod/assign/submission/onlinetext/locallib.php @@ -250,7 +250,18 @@ class assign_submission_onlinetext extends assign_submission_plugin { if ($onlinetextsubmission) { $user = $DB->get_record("user", array("id"=>$submission->userid),'id,username,firstname,lastname', MUST_EXIST); - $prefix = clean_filename(fullname($user) . "_" .$submission->userid . "_"); + if (!$this->assignment->is_blind_marking()) { + $filename = str_replace('_', '', fullname($user)) . '_' . + $this->assignment->get_uniqueid_for_user($userid) . '_' . + $this->get_name() . '_'; + $prefix = clean_filename($filename); + } else { + $filename = get_string('participant', 'assign') . '_' . + $this->assignment->get_uniqueid_for_user($userid) . '_' . + $this->get_name() . '_'; + $prefix = clean_filename($filename); + } + $finaltext = str_replace('@@PLUGINFILE@@/', $prefix, $onlinetextsubmission->onlinetext); $submissioncontent = "". format_text($finaltext, $onlinetextsubmission->onlineformat, array('context'=>$this->assignment->get_context())). ""; //fetched from database diff --git a/mod/assign/version.php b/mod/assign/version.php index e15fe4e93c0..b8821825e3c 100644 --- a/mod/assign/version.php +++ b/mod/assign/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $module->component = 'mod_assign'; // Full name of the plugin (used for diagnostics) -$module->version = 2012082300; // The current module version (Date: YYYYMMDDXX) +$module->version = 2012082400; // The current module version (Date: YYYYMMDDXX) $module->requires = 2012061700; // Requires this Moodle version $module->cron = 60; From 88cfe469984f6426bfd3d7edeb9ad0dbabc28088 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 3 Sep 2012 11:08:16 +0800 Subject: [PATCH 56/96] MDL-31291: Do not show students names who have not submitted when blind marking is enabled. --- mod/assign/locallib.php | 10 +++++++--- mod/assign/renderable.php | 6 +++++- mod/assign/renderer.php | 6 +++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 2dd73c5fd1d..a8298a2e396 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -1483,6 +1483,10 @@ class assign { $submission = $this->get_user_submission($member->id, false); if ($submission && $submission->status != ASSIGN_SUBMISSION_STATUS_DRAFT) { unset($members[$id]); + } else { + if ($this->is_blind_marking()) { + $members[$id]->alias = get_string('hiddenuser', 'assign') . $this->get_uniqueid_for_user($id); + } } } return $members; @@ -1987,8 +1991,7 @@ class assign { if ($grade) { $extensionduedate = $grade->extensionduedate; } - $showedit = has_capability('mod/assign:submit', $this->context) && - $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled()); + $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled()); if ($teamsubmission) { $showsubmit = $showedit && $teamsubmission && ($teamsubmission->status == ASSIGN_SUBMISSION_STATUS_DRAFT); @@ -2019,7 +2022,8 @@ class assign { $showsubmit, $viewfullnames, $extensionduedate, - $this->get_context())); + $this->get_context(), + $this->is_blind_marking())); } if ($grade) { $data = new stdClass(); diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php index c4cb86971b3..82dd79cd2a2 100644 --- a/mod/assign/renderable.php +++ b/mod/assign/renderable.php @@ -335,6 +335,8 @@ class assign_submission_status implements renderable { public $extensionduedate = 0; /** @var context context */ public $context = 0; + /** @var bool blindmarking - Should we hide student identities from graders? */ + public $blindmarking = false; /** * constructor @@ -362,13 +364,14 @@ class assign_submission_status implements renderable { * @param bool $canviewfullnames * @param int $extensionduedate - Any extension to the due date granted for this user * @param context $context - Any extension to the due date granted for this user + * @param blindmarking $blindmarking - Should we hide student identities from graders? */ public function __construct($allowsubmissionsfromdate, $alwaysshowdescription, $submission, $teamsubmissionenabled, $teamsubmission, $submissiongroup, $submissiongroupmemberswhoneedtosubmit, $submissionsenabled, $locked, $graded, $duedate, $cutoffdate, $submissionplugins, $returnaction, $returnparams, $coursemoduleid, $courseid, $view, $canedit, $cansubmit, $canviewfullnames, $extensionduedate, - $context) { + $context, $blindmarking) { $this->allowsubmissionsfromdate = $allowsubmissionsfromdate; $this->alwaysshowdescription = $alwaysshowdescription; $this->submission = $submission; @@ -392,6 +395,7 @@ class assign_submission_status implements renderable { $this->canviewfullnames = $canviewfullnames; $this->extensionduedate = $extensionduedate; $this->context = $context; + $this->blindmarking = $blindmarking; } } diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php index 734b3dd890e..0a0d4fe7ca3 100644 --- a/mod/assign/renderer.php +++ b/mod/assign/renderer.php @@ -429,7 +429,11 @@ class mod_assign_renderer extends plugin_renderer_base { $userslist = array(); foreach ($members as $member) { $url = new moodle_url('/user/view.php', array('id' => $member->id, 'course'=>$status->courseid)); - $userslist[] = $this->output->action_link($url, fullname($member, $status->canviewfullnames)); + if ($status->view == assign_submission_status::GRADER_VIEW && $status->blindmarking) { + $userslist[] = $member->alias; + } else { + $userslist[] = $this->output->action_link($url, fullname($member, $status->canviewfullnames)); + } } if (count($userslist) > 0) { $userstr = join(', ', $userslist); From b98824c2195b1aa5ba1beaa8c92f1979eafe98df Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Mon, 3 Sep 2012 12:20:39 +0800 Subject: [PATCH 57/96] MDL-31291: Fix a warning on the assignment summary page. --- mod/assign/locallib.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index a8298a2e396..567252f7751 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -2495,7 +2495,8 @@ class assign { $showsubmit, $viewfullnames, $extensionduedate, - $this->get_context())); + $this->get_context(), + $this->is_blind_marking())); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); From 2d0e682d8bd71ec954c4b765a2b2a76c3b59f6fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20V=C3=B6geli?= Date: Mon, 3 Sep 2012 07:25:30 +0200 Subject: [PATCH 58/96] MDL-31365 Fix for safebrowser and securewindow --- lib/outputrenderers.php | 45 +++++++++++++++++------ mod/quiz/accessrule/safebrowser/rule.php | 3 ++ mod/quiz/accessrule/securewindow/rule.php | 2 +- theme/base/config.php | 8 +++- theme/upgrade.txt | 7 +++- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 17cd590268a..28b8490a571 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -508,23 +508,32 @@ class core_renderer extends renderer_base { /** * Return the standard string that says whether you are logged in (and switched * roles/logged in as another user). - * + * @param bool $withlinks if false, then don't include any links in the HTML produced. + * If not set, the default is the nologinlinks option from the theme config.php file, + * and if that is not set, then links are included. * @return string HTML fragment. */ - public function login_info() { + public function login_info($withlinks = null) { global $USER, $CFG, $DB, $SESSION; if (during_initial_install()) { return ''; } + if (is_null($withlinks)) { + $withlinks = empty($this->page->layout_options['nologinlinks']); + } + $loginpage = ((string)$this->page->url === get_login_url()); $course = $this->page->course; - if (session_is_loggedinas()) { $realuser = session_get_realuser(); $fullname = fullname($realuser, true); - $realuserinfo = " [wwwroot/course/loginas.php?id=$course->id&sesskey=".sesskey()."\">$fullname] "; + if ($withlinks) { + $realuserinfo = " [wwwroot/course/loginas.php?id=$course->id&sesskey=".sesskey()."\">$fullname] "; + } else { + $realuserinfo = " [$fullname] "; + } } else { $realuserinfo = ''; } @@ -539,13 +548,21 @@ class core_renderer extends renderer_base { $fullname = fullname($USER, true); // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page) - $username = "wwwroot/user/profile.php?id=$USER->id\">$fullname"; + if ($withlinks) { + $username = "wwwroot/user/profile.php?id=$USER->id\">$fullname"; + } else { + $username = $fullname; + } if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) { - $username .= " from wwwroot}\">{$idprovider->name}"; + if ($withlinks) { + $username .= " from wwwroot}\">{$idprovider->name}"; + } else { + $username .= " from {$idprovider->name}"; + } } if (isguestuser()) { $loggedinas = $realuserinfo.get_string('loggedinasguest'); - if (!$loginpage) { + if (!$loginpage && $withlinks) { $loggedinas .= " (".get_string('login').')'; } } else if (is_role_switched($course->id)) { // Has switched roles @@ -553,15 +570,19 @@ class core_renderer extends renderer_base { if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) { $rolename = ': '.format_string($role->name); } - $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename. - " (wwwroot/course/view.php?id=$course->id&switchrole=0&sesskey=".sesskey()."\">".get_string('switchrolereturn').')'; + $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename; + if ($withlinks) { + $loggedinas .= " (wwwroot/course/view.php?id=$course->id&switchrole=0&sesskey=".sesskey()."\">".get_string('switchrolereturn').')'; + } } else { - $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username).' '. - " (wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').')'; + $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username); + if ($withlinks) { + $loggedinas .= " (wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').')'; + } } } else { $loggedinas = get_string('loggedinnot', 'moodle'); - if (!$loginpage) { + if (!$loginpage && $withlinks) { $loggedinas .= " (".get_string('login').')'; } } diff --git a/mod/quiz/accessrule/safebrowser/rule.php b/mod/quiz/accessrule/safebrowser/rule.php index e73c5b83412..763acdc0286 100644 --- a/mod/quiz/accessrule/safebrowser/rule.php +++ b/mod/quiz/accessrule/safebrowser/rule.php @@ -61,6 +61,9 @@ class quizaccess_safebrowser extends quiz_access_rule_base { public function setup_attempt_page($page) { $page->set_title($this->quizobj->get_course()->shortname . ': ' . $page->title); $page->set_cacheable(false); + $page->set_popup_notification_allowed(false); // Prevent message notifications. + $page->set_heading($page->title); + $page->set_pagelayout('secure'); } /** diff --git a/mod/quiz/accessrule/securewindow/rule.php b/mod/quiz/accessrule/securewindow/rule.php index ede3f934de2..bbeda5449c9 100644 --- a/mod/quiz/accessrule/securewindow/rule.php +++ b/mod/quiz/accessrule/securewindow/rule.php @@ -73,7 +73,7 @@ class quizaccess_securewindow extends quiz_access_rule_base { $page->set_popup_notification_allowed(false); // Prevent message notifications. $page->set_title($this->quizobj->get_course()->shortname . ': ' . $page->title); $page->set_cacheable(false); - $page->set_pagelayout('popup'); + $page->set_pagelayout('secure'); if ($this->quizobj->is_preview_user()) { return; diff --git a/theme/base/config.php b/theme/base/config.php index 7c9aa2d3799..1316822f7ca 100644 --- a/theme/base/config.php +++ b/theme/base/config.php @@ -153,12 +153,18 @@ $THEME->layouts = array( 'regions' => array(), 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true), ), - // The pagelayout used for reports + // The pagelayout used for reports. 'report' => array( 'file' => 'report.php', 'regions' => array('side-pre'), 'defaultregion' => 'side-pre', ), + // The pagelayout used for safebrowser and securewindow. + 'secure' => array( + 'file' => 'general.php', + 'regions' => array(), + 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true, 'nologinlinks'=>true), + ), ); // We don't want the base theme to be shown on the theme selection screen, by setting diff --git a/theme/upgrade.txt b/theme/upgrade.txt index 4d18a33411a..4158d23f2e3 100644 --- a/theme/upgrade.txt +++ b/theme/upgrade.txt @@ -1,6 +1,11 @@ This files describes API changes in /theme/* themes, information provided here is intended especially for theme designer. +=== 2.4 === + +optional changes: +* new optional boolean parameter $withlinks for public function login_info() in lib/outputrenderers.php (MDL-31365) +* new layout option "nologinlinks" and new page layout "secure" e.g. for safebrowser and securewindow (MDL-31365) === 2.3 === @@ -12,4 +17,4 @@ optional changes: required changes: * use new page content placeholder "echo $OUTPUT->main_content()" instead of "echo core_renderer::MAIN_CONTENT_TOKEN" see git commit: 3b3f302855d7621405a8b93e49bd399d67a998d7 -* upgrade report selectors: search for "-course-report-" and replace with "-report-" \ No newline at end of file +* upgrade report selectors: search for "-course-report-" and replace with "-report-" From e2aebdc928a6452528cc280babba3b7dde055ce6 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 14 Aug 2012 16:52:39 +0800 Subject: [PATCH 59/96] MDL-33812 Backup: Respect of backup_auto_keep --- backup/util/dbops/backup_plan_dbops.class.php | 14 +++++++++----- lang/en/admin.php | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backup/util/dbops/backup_plan_dbops.class.php b/backup/util/dbops/backup_plan_dbops.class.php index e169a8a8ba1..8be9f645ff3 100644 --- a/backup/util/dbops/backup_plan_dbops.class.php +++ b/backup/util/dbops/backup_plan_dbops.class.php @@ -197,19 +197,19 @@ abstract class backup_plan_dbops extends backup_dbops { * @param int $courseid/$sectionid/$cmid * @param bool $users Should be true is users were included in the backup * @param bool $anonymised Should be true is user information was anonymized. - * @param bool $useidasname true to use id, false to use strings (default) + * @param bool $withoutname if false, include the name in the file name (default) * @return string The filename to use */ - public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidasname = false) { + public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $withoutname = false) { global $DB; // Calculate backup word $backupword = str_replace(' ', '_', textlib::strtolower(get_string('backupfilename'))); $backupword = trim(clean_filename($backupword), '_'); + // Not $withoutname, lets fetch the name $shortname = ''; - // Not $useidasname, lets calculate it, else $id will be used - if (!$useidasname) { + if (!$withoutname) { // Calculate proper name element (based on type) switch ($type) { case backup::TYPE_1COURSE: @@ -231,7 +231,11 @@ abstract class backup_plan_dbops extends backup_dbops { $shortname = textlib::strtolower(trim(clean_filename($shortname), '_')); } - $name = empty($shortname) ? $id : $shortname; + // The name will always contain the ID, but we append the course short name if requested. + $name = $id; + if (!$withoutname && $shortname != '') { + $name .= '-' . $shortname; + } // Calculate date $backupdateformat = str_replace(' ', '_', get_string('backupnameformat', 'langconfig')); diff --git a/lang/en/admin.php b/lang/en/admin.php index 7f8972a8564..5e7b0d98264 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -68,7 +68,7 @@ $string['availablelicenses'] = 'Available licences'; $string['backgroundcolour'] = 'Transparent colour'; $string['backups'] = 'Backups'; $string['backup_shortname'] = 'Use course name in backup filename'; -$string['backup_shortnamehelp'] = 'Use the course name as part of the backup filename instead of the course id number.'; +$string['backup_shortnamehelp'] = 'Use the course name as part of the backup filename.'; $string['badwordsconfig'] = 'Enter your list of bad words separated by commas.'; $string['badwordsdefault'] = 'If the custom list is empty, a default list from the language pack will be used.'; $string['badwordslist'] = 'Custom bad words list'; From 14ecc5a80c20af0a8ab5b28da742ead1ab722205 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 21 Aug 2012 13:50:55 +0800 Subject: [PATCH 60/96] MDL-33812 Backup: Revert MDL-33521 --- backup/util/helper/backup_cron_helper.class.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/backup/util/helper/backup_cron_helper.class.php b/backup/util/helper/backup_cron_helper.class.php index ea8fda08577..6910e7af82c 100644 --- a/backup/util/helper/backup_cron_helper.class.php +++ b/backup/util/helper/backup_cron_helper.class.php @@ -530,18 +530,7 @@ abstract class backup_cron_automated_helper { if (!empty($dir) && ($storage == 1 || $storage == 2)) { // Calculate backup filename regex, ignoring the date/time/info parts that can be // variable, depending of languages, formats and automated backup settings - - - // MDL-33531: use different filenames depending on backup_shortname option - if ( !empty($config->backup_shortname) ) { - $context = get_context_instance(CONTEXT_COURSE, $course->id); - $courseref = format_string($course->shortname, true, array('context' => $context)); - $courseref = str_replace(' ', '_', $courseref); - $courseref = textlib::strtolower(trim(clean_filename($courseref), '_')); - } else { - $courseref = $course->id; - } - $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$courseref . '-'; + $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$course->id . '-'; $regex = '#^'.preg_quote($filename, '#').'.*\.mbz$#'; // Store all the matching files into fullpath => timemodified array From 61d71da334f2647d80efe63397fa678b69698a4d Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Wed, 29 Aug 2012 16:00:19 +0800 Subject: [PATCH 61/96] MDL-33812 Backup: Added some upgrade information about the changes --- backup/upgrade.txt | 17 +++++++++++++++++ backup/util/dbops/backup_plan_dbops.class.php | 10 +++++----- 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 backup/upgrade.txt diff --git a/backup/upgrade.txt b/backup/upgrade.txt new file mode 100644 index 00000000000..cb020956ed0 --- /dev/null +++ b/backup/upgrade.txt @@ -0,0 +1,17 @@ +This files describes API changes in /backup/*, +information provided here is intended especially for developers. + +=== 2.4 === + +* Since 2.3.1+ the backup file name schema has changed. The ID of the course will always be part of + the filename regardless of the setting 'backup_shortname'. See MDL-33812. + +=== 2.3 === + +* Since 2.3.1+ the backup file name schema has changed. The ID of the course will always be part of + the filename regardless of the setting 'backup_shortname'. See MDL-33812. + +=== 2.2 === + +* Since 2.2.4+ the backup file name schema has changed. The ID of the course will always be part of + the filename regardless of the setting 'backup_shortname'. See MDL-33812. \ No newline at end of file diff --git a/backup/util/dbops/backup_plan_dbops.class.php b/backup/util/dbops/backup_plan_dbops.class.php index 8be9f645ff3..2e2faaeb05e 100644 --- a/backup/util/dbops/backup_plan_dbops.class.php +++ b/backup/util/dbops/backup_plan_dbops.class.php @@ -197,19 +197,19 @@ abstract class backup_plan_dbops extends backup_dbops { * @param int $courseid/$sectionid/$cmid * @param bool $users Should be true is users were included in the backup * @param bool $anonymised Should be true is user information was anonymized. - * @param bool $withoutname if false, include the name in the file name (default) + * @param bool $useidonly only use the ID in the file name * @return string The filename to use */ - public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $withoutname = false) { + public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidonly = false) { global $DB; // Calculate backup word $backupword = str_replace(' ', '_', textlib::strtolower(get_string('backupfilename'))); $backupword = trim(clean_filename($backupword), '_'); - // Not $withoutname, lets fetch the name + // Not $useidonly, lets fetch the name $shortname = ''; - if (!$withoutname) { + if (!$useidonly) { // Calculate proper name element (based on type) switch ($type) { case backup::TYPE_1COURSE: @@ -233,7 +233,7 @@ abstract class backup_plan_dbops extends backup_dbops { // The name will always contain the ID, but we append the course short name if requested. $name = $id; - if (!$withoutname && $shortname != '') { + if (!$useidonly && $shortname != '') { $name .= '-' . $shortname; } From ec7e998f4b76681a03746a69fdc72002e42ff348 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 27 Aug 2012 14:47:01 +0800 Subject: [PATCH 62/96] MDL-35034 Repository: Google Docs does not fail when download is restricted --- lib/googleapi.php | 24 +++++++++++------------- repository/googledocs/lib.php | 4 +++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/googleapi.php b/lib/googleapi.php index c2d529fd6b7..3ace6542d4f 100644 --- a/lib/googleapi.php +++ b/lib/googleapi.php @@ -109,23 +109,21 @@ class google_docs { $source = 'https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key='.$docid.'&exportFormat=xls'; break; case 'pdf': - $title = (string)$gdoc->title; - $source = (string)$gdoc->content[0]->attributes()->src; - break; case 'file': - $title = (string)$gdoc->title; - $source = (string)$gdoc->content[0]->attributes()->src; + $title = (string)$gdoc->title; + // Some files don't have a content probably because the download has been restricted. + if (isset($gdoc->content)) { + $source = (string)$gdoc->content[0]->attributes()->src; + } break; } - if (!empty($source)) { - $files[] = array( 'title' => $title, - 'url' => "{$gdoc->link[0]->attributes()->href}", - 'source' => $source, - 'date' => usertime(strtotime($gdoc->updated)), - 'thumbnail' => (string) $OUTPUT->pix_url(file_extension_icon($title, 32)) - ); - } + $files[] = array( 'title' => $title, + 'url' => "{$gdoc->link[0]->attributes()->href}", + 'source' => $source, + 'date' => usertime(strtotime($gdoc->updated)), + 'thumbnail' => (string) $OUTPUT->pix_url(file_extension_icon($title, 32)) + ); } return $files; diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index 38299decb73..72ca7bbab68 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -92,8 +92,10 @@ class repository_googledocs extends repository { } public function get_file($url, $file = '') { + if (empty($url)) { + throw new repository_exception('cannotdownload', 'repository'); + } $gdocs = new google_docs($this->googleoauth); - $path = $this->prepare_file($file); return $gdocs->download_file($url, $path, self::GETFILE_TIMEOUT); } From 05e521c9bcb8f560b70a416f79cf4d233563eaaa Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 30 Jul 2012 14:10:46 +0800 Subject: [PATCH 63/96] MDL-28346 Backup: Added new status 'warning' for backup result --- .../util/helper/backup_cron_helper.class.php | 55 +++++++++++++++---- lang/en/moodle.php | 1 + report/backups/index.php | 13 ++++- theme/base/style/admin.css | 1 + 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/backup/util/helper/backup_cron_helper.class.php b/backup/util/helper/backup_cron_helper.class.php index ea8fda08577..a974883a527 100644 --- a/backup/util/helper/backup_cron_helper.class.php +++ b/backup/util/helper/backup_cron_helper.class.php @@ -46,6 +46,8 @@ abstract class backup_cron_automated_helper { const BACKUP_STATUS_UNFINISHED = 2; /** Course automated backup was skipped */ const BACKUP_STATUS_SKIPPED = 3; + /** Course automated backup had warnings */ + const BACKUP_STATUS_WARNING = 4; /** Run if required by the schedule set in config. Default. **/ const RUN_ON_SCHEDULE = 0; @@ -139,7 +141,7 @@ abstract class backup_cron_automated_helper { $params = array('courseid' => $course->id, 'time' => $now-31*24*60*60, 'action' => '%view%'); $logexists = $DB->record_exists_select('log', $sqlwhere, $params); if (!$logexists) { - $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED; + $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED; $backupcourse->nextstarttime = $nextstarttime; $DB->update_record('backup_courses', $backupcourse); mtrace('Skipping unchanged course '.$course->fullname); @@ -160,7 +162,7 @@ abstract class backup_cron_automated_helper { $starttime = time(); $backupcourse->laststarttime = time(); - $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED; + $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED; $DB->update_record('backup_courses', $backupcourse); $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id); @@ -169,7 +171,7 @@ abstract class backup_cron_automated_helper { $DB->update_record('backup_courses', $backupcourse); - if ($backupcourse->laststatus) { + if ($backupcourse->laststatus === self::BACKUP_STATUS_OK) { // Clean up any excess course backups now that we have // taken a successful backup. $removedcount = backup_cron_automated_helper::remove_excess_backups($course); @@ -188,17 +190,18 @@ abstract class backup_cron_automated_helper { $message = ""; $count = backup_cron_automated_helper::get_backup_status_array(); - $haserrors = ($count[backup_cron_automated_helper::BACKUP_STATUS_ERROR] != 0 || $count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED] != 0); + $haserrors = ($count[self::BACKUP_STATUS_ERROR] != 0 || $count[self::BACKUP_STATUS_UNFINISHED] != 0); //Build the message text //Summary $message .= get_string('summary')."\n"; $message .= "==================================================\n"; $message .= " ".get_string('courses').": ".array_sum($count)."\n"; - $message .= " ".get_string('ok').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_OK]."\n"; - $message .= " ".get_string('skipped').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_SKIPPED]."\n"; - $message .= " ".get_string('error').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_ERROR]."\n"; - $message .= " ".get_string('unfinished').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED]."\n\n"; + $message .= " ".get_string('ok').": ".$count[self::BACKUP_STATUS_OK]."\n"; + $message .= " ".get_string('skipped').": ".$count[self::BACKUP_STATUS_SKIPPED]."\n"; + $message .= " ".get_string('error').": ".$count[self::BACKUP_STATUS_ERROR]."\n"; + $message .= " ".get_string('unfinished').": ".$count[self::BACKUP_STATUS_UNFINISHED]."\n"; + $message .= " ".get_string('warning').": ".$count[self::BACKUP_STATUS_WARNING]."\n\n"; //Reference if ($haserrors) { @@ -261,6 +264,7 @@ abstract class backup_cron_automated_helper { self::BACKUP_STATUS_OK => 0, self::BACKUP_STATUS_UNFINISHED => 0, self::BACKUP_STATUS_SKIPPED => 0, + self::BACKUP_STATUS_WARNING => 0 ); $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) AS statuscount FROM {backup_courses} bc GROUP BY bc.laststatus'); @@ -334,7 +338,7 @@ abstract class backup_cron_automated_helper { */ public static function launch_automated_backup($course, $starttime, $userid) { - $outcome = true; + $outcome = self::BACKUP_STATUS_OK; $config = get_config('backup'); $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_AUTOMATED, $userid); @@ -369,6 +373,7 @@ abstract class backup_cron_automated_helper { $bc->execute_plan(); $results = $bc->get_results(); + $outcome = self::outcome_from_results($results); $file = $results['backup_destination']; // may be empty if file already moved to target location $dir = $config->backup_auto_destination; $storage = (int)$config->backup_auto_storage; @@ -377,8 +382,10 @@ abstract class backup_cron_automated_helper { } if ($file && !empty($dir) && $storage !== 0) { $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, !$config->backup_shortname); - $outcome = $file->copy_content_to($dir.'/'.$filename); - if ($outcome && $storage === 1) { + if (!$file->copy_content_to($dir.'/'.$filename)) { + $outcome = self::BACKUP_STATUS_ERROR; + } + if ($outcome != self::BACKUP_STATUS_ERROR && $storage === 1) { $file->delete(); } } @@ -387,7 +394,7 @@ abstract class backup_cron_automated_helper { $bc->log('backup_auto_failed_on_course', backup::LOG_ERROR, $course->shortname); // Log error header. $bc->log('Exception: ' . $e->errorcode, backup::LOG_ERROR, $e->a, 1); // Log original exception problem. $bc->log('Debug: ' . $e->debuginfo, backup::LOG_DEBUG, null, 1); // Log original debug information. - $outcome = false; + $outcome = self::BACKUP_STATUS_ERROR; } $bc->destroy(); @@ -396,6 +403,30 @@ abstract class backup_cron_automated_helper { return $outcome; } + /** + * Returns the backup outcome by analysing its results. + * + * @param array $results returned by a backup + * @return int {@link self::BACKUP_STATUS_OK} and other constants + */ + public static function outcome_from_results($results) { + $outcome = self::BACKUP_STATUS_OK; + foreach ($results as $code => $value) { + // Each possible error and warning code has to be specified in this switch + // which basically analyses the results to return the correct backup status. + switch ($code) { + case 'missing_files_in_pool': + $outcome = self::BACKUP_STATUS_WARNING; + break; + } + // If we found the highest error level, we exit the loop. + if ($outcome == self::BACKUP_STATUS_ERROR) { + break; + } + } + return $outcome; + } + /** * Removes deleted courses fromn the backup_courses table so that we don't * waste time backing them up. diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 2fc4fac2df5..f7f308401d6 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1802,6 +1802,7 @@ $string['virusfounduser'] = 'The file you have uploaded, {$a->filename}, has bee $string['virusplaceholder'] = 'This file that has been uploaded was found to contain a virus and has been moved or deleted and the user notified.'; $string['visible'] = 'Visible'; $string['visibletostudents'] = 'Visible to {$a}'; +$string['warning'] = 'Warning'; $string['warningdeleteresource'] = 'Warning: {$a} is referred in a resource. Would you like to update the resource?'; $string['webpage'] = 'Web page'; $string['week'] = 'Week'; diff --git a/report/backups/index.php b/report/backups/index.php index 9dd18315137..1deaa2dca3d 100644 --- a/report/backups/index.php +++ b/report/backups/index.php @@ -27,6 +27,9 @@ require_once('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/backup/lib.php'); +// Required for constants in backup_cron_automated_helper +require_once($CFG->dirroot.'/backup/util/helper/backup_cron_helper.class.php'); + admin_externalpage_setup('reportbackups', '', null, '', array('pagelayout'=>'report')); $table = new html_table; @@ -45,6 +48,7 @@ $strerror = get_string("error"); $strok = get_string("ok"); $strunfinished = get_string("unfinished"); $strskipped = get_string("skipped"); +$strwarning = get_string("warning"); list($select, $join) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx'); $sql = "SELECT bc.*, c.fullname $select @@ -58,15 +62,18 @@ foreach ($rs as $backuprow) { context_instance_preload($backuprow); // Prepare a cell to display the status of the entry - if ($backuprow->laststatus == 1) { + if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_OK) { $status = $strok; $statusclass = 'backup-ok'; // Green - } else if ($backuprow->laststatus == 2) { + } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED) { $status = $strunfinished; $statusclass = 'backup-unfinished'; // Red - } else if ($backuprow->laststatus == 3) { + } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_SKIPPED) { $status = $strskipped; $statusclass = 'backup-skipped'; // Green + } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_WARNING) { + $status = $strwarning; + $statusclass = 'backup-warning'; // Orange } else { $status = $strerror; $statusclass = 'backup-error'; // Red diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index 8c0ce47fc5c..df0e64ff2f5 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -42,6 +42,7 @@ #page-admin-report-backups-index .backup-unfinished {color: #f00000;} #page-admin-report-backups-index .backup-skipped, #page-admin-report-backups-index .backup-ok {color: #006400;} +#page-admin-report-backups-index .backup-warning {color: #ff9900;} #page-admin-qbehaviours .disabled {color: gray;} #page-admin-qbehaviours th {white-space: normal;} From 3b232aeffb6bc59ba45b47bbbbfeff9479881323 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 9 Jul 2012 10:49:25 +0800 Subject: [PATCH 64/96] MDL-28346 Backup: Restore does not fail when a file is missing --- backup/util/dbops/restore_dbops.class.php | 12 +++++++++++- backup/util/plan/restore_structure_step.class.php | 10 ++++++++-- backup/util/ui/restore_ui_stage.class.php | 3 +++ lang/en/backup.php | 1 + 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 3bd86e374db..4b6c63c3e20 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -818,10 +818,13 @@ abstract class restore_dbops { * @param int|null $olditemid * @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping) * @param bool $skipparentitemidctxmatch + * @return array of result object */ public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) { global $DB; + $results = array(); + if ($forcenewcontextid) { // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings, // with questions originally at system/coursecat context in source being restored to course context in target). So we need @@ -901,8 +904,14 @@ abstract class restore_dbops { // this is a regular file, it must be present in the backup pool $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash); + // The file is not found in the backup. if (!file_exists($backuppath)) { - throw new restore_dbops_exception('file_not_found_in_pool', $file); + $result = new stdClass(); + $result->code = 'file_missing_in_backup'; + $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename); + $result->level = backup::LOG_WARNING; + $results[] = $result; + continue; } // create the file in the filepool if it does not exist yet @@ -959,6 +968,7 @@ abstract class restore_dbops { } } $rs->close(); + return $results; } /** diff --git a/backup/util/plan/restore_structure_step.class.php b/backup/util/plan/restore_structure_step.class.php index 42491cf432b..7a1cfa28fdf 100644 --- a/backup/util/plan/restore_structure_step.class.php +++ b/backup/util/plan/restore_structure_step.class.php @@ -218,8 +218,14 @@ abstract class restore_structure_step extends restore_step { */ public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) { $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid; - restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, - $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid); + $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, + $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid); + $resultstoadd = array(); + foreach ($results as $result) { + $this->log($result->message, $result->level); + $resultstoadd[$result->code] = true; + } + $this->task->add_result($resultstoadd); } /** diff --git a/backup/util/ui/restore_ui_stage.class.php b/backup/util/ui/restore_ui_stage.class.php index 6c282544f05..a7464bf49ae 100644 --- a/backup/util/ui/restore_ui_stage.class.php +++ b/backup/util/ui/restore_ui_stage.class.php @@ -772,6 +772,9 @@ class restore_ui_stage_complete extends restore_ui_stage_process { $html .= $renderer->box_end(); } $html .= $renderer->box_start(); + if (array_key_exists('file_missing_in_backup', $this->results)) { + $html .= $renderer->notification(get_string('restorefileweremissing', 'backup'), 'notifyproblem'); + } $html .= $renderer->notification(get_string('restoreexecutionsuccess', 'backup'), 'notifysuccess'); $html .= $renderer->continue_button(new moodle_url('/course/view.php', array( 'id' => $this->get_ui()->get_controller()->get_courseid())), 'get'); diff --git a/lang/en/backup.php b/lang/en/backup.php index 5fde19e8722..e9744d535b4 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -177,6 +177,7 @@ $string['restoreactivity'] = 'Restore activity'; $string['restorecourse'] = 'Restore course'; $string['restorecoursesettings'] = 'Course settings'; $string['restoreexecutionsuccess'] = 'The course was restored successfully, clicking the continue button below will take you to view the course you restored.'; +$string['restorefileweremissing'] = 'Some files could not be restored because they were missing in the backup.'; $string['restorenewcoursefullname'] = 'New course name'; $string['restorenewcourseshortname'] = 'New course short name'; $string['restorenewcoursestartdate'] = 'New start date'; From a173b52da0765ecc39119b74d7579baf650470ec Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 9 Jul 2012 10:49:25 +0800 Subject: [PATCH 65/96] MDL-28346 Backup: Backup does not fail when a file is missing --- backup/moodle2/backup_custom_fields.php | 9 ++- .../util/plan/backup_structure_step.class.php | 11 +++ .../structure/backup_nested_element.class.php | 73 +++++++++++++++++++ backup/util/ui/backup_ui_stage.class.php | 3 + lang/en/backup.php | 1 + 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/backup/moodle2/backup_custom_fields.php b/backup/moodle2/backup_custom_fields.php index e0ceb334d95..12929de6578 100644 --- a/backup/moodle2/backup_custom_fields.php +++ b/backup/moodle2/backup_custom_fields.php @@ -96,14 +96,19 @@ class file_nested_element extends backup_nested_element { if (is_null($this->backupid)) { $this->backupid = $processor->get_var(backup::VAR_BACKUPID); } - parent::process($processor); + return parent::process($processor); } public function fill_values($values) { // Fill values parent::fill_values($values); // Do our own tasks (copy file from moodle to backup) - backup_file_manager::copy_file_moodle2backup($this->backupid, $values); + try { + backup_file_manager::copy_file_moodle2backup($this->backupid, $values); + } catch (file_exception $e) { + $this->add_result(array('missing_files_in_pool' => true)); + $this->add_log('missing file in pool: ' . $e->debuginfo, backup::LOG_WARNING); + } } } diff --git a/backup/util/plan/backup_structure_step.class.php b/backup/util/plan/backup_structure_step.class.php index f62fee936a1..964dd3f9885 100644 --- a/backup/util/plan/backup_structure_step.class.php +++ b/backup/util/plan/backup_structure_step.class.php @@ -94,11 +94,22 @@ abstract class backup_structure_step extends backup_step { // Process structure definition $structure->process($pr); + // Get the results from the nested elements + $results = $structure->get_results(); + + // Get the log messages to append to the log + $logs = $structure->get_logs(); + foreach ($logs as $log) { + $this->log($log->message, $log->level, $log->a, $log->depth, $log->display); + } + // Close everything $xw->stop(); // Destroy the structure. It helps PHP 5.2 memory a lot! $structure->destroy(); + + return $results; } /** diff --git a/backup/util/structure/backup_nested_element.class.php b/backup/util/structure/backup_nested_element.class.php index 9a479013d91..8557ec8ae53 100644 --- a/backup/util/structure/backup_nested_element.class.php +++ b/backup/util/structure/backup_nested_element.class.php @@ -37,6 +37,8 @@ class backup_nested_element extends base_nested_element implements processable { protected $aliases; // Define DB->final element aliases protected $fileannotations; // array of file areas to be searched by file annotations protected $counter; // Number of instances of this element that have been processed + protected $results; // Logs the results we encounter during the process. + protected $logs; // Some log messages that could be retrieved later. /** * Constructor - instantiates one backup_nested_element, specifying its basic info. @@ -55,8 +57,16 @@ class backup_nested_element extends base_nested_element implements processable { $this->aliases = array(); $this->fileannotations = array(); $this->counter = 0; + $this->results = array(); + $this->logs = array(); } + /** + * Process the nested element + * + * @param object $processor the processor + * @return void + */ public function process($processor) { if (!$processor instanceof base_processor) { // No correct processor, throw exception throw new base_element_struct_exception('incorrect_processor'); @@ -113,6 +123,69 @@ class backup_nested_element extends base_nested_element implements processable { $iterator->close(); } + /** + * Saves a log message to an array + * + * @see backup_helper::log() + * @param string $message to add to the logs + * @param int $level level of importance {@link backup::LOG_DEBUG} and other constants + * @param mixed $a to be included in $message + * @param int $depth of the message + * @param display $bool supporting translation via get_string() if true + * @return void + */ + protected function add_log($message, $level, $a = null, $depth = null, $display = false) { + // Adding the result to the oldest parent. + if ($this->get_parent()) { + $parent = $this->get_grandparent(); + $parent->add_log($message, $level, $a, $depth, $display); + } else { + $log = new stdClass(); + $log->message = $message; + $log->level = $level; + $log->a = $a; + $log->depth = $depth; + $log->display = $display; + $this->logs[] = $log; + } + } + + /** + * Saves the results to an array + * + * @param array $result associative array + * @return void + */ + protected function add_result($result) { + if (is_array($result)) { + // Adding the result to the oldest parent. + if ($this->get_parent()) { + $parent = $this->get_grandparent(); + $parent->add_result($result); + } else { + $this->results = array_merge($this->results, $result); + } + } + } + + /** + * Returns the logs + * + * @return array of log objects + */ + public function get_logs() { + return $this->logs; + } + + /** + * Returns the results + * + * @return associative array of results + */ + public function get_results() { + return $this->results; + } + public function set_source_array($arr) { // TODO: Only elements having final elements can set source $this->var_array = $arr; diff --git a/backup/util/ui/backup_ui_stage.class.php b/backup/util/ui/backup_ui_stage.class.php index 472294af5a7..4065212648c 100644 --- a/backup/util/ui/backup_ui_stage.class.php +++ b/backup/util/ui/backup_ui_stage.class.php @@ -487,6 +487,9 @@ class backup_ui_stage_complete extends backup_ui_stage_final { if (!empty($this->results['include_file_references_to_external_content'])) { $output .= $renderer->notification(get_string('filereferencesincluded', 'backup'), 'notifyproblem'); } + if (!empty($this->results['missing_files_in_pool'])) { + $output .= $renderer->notification(get_string('missingfilesinpool', 'backup'), 'notifyproblem'); + } $output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess'); $output .= $renderer->continue_button($restorerul); $output .= $renderer->box_end(); diff --git a/lang/en/backup.php b/lang/en/backup.php index e9744d535b4..e053c96bdee 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -163,6 +163,7 @@ $string['lockedbypermission'] = 'You don\'t have sufficient permissions to chang $string['lockedbyconfig'] = 'This setting has been locked by the default backup settings'; $string['lockedbyhierarchy'] = 'Locked by dependencies'; $string['managefiles'] = 'Manage backup files'; +$string['missingfilesinpool'] = 'Some files could not be saved during the backup, it won\'t be possible to restore them.'; $string['moodleversion'] = 'Moodle version'; $string['moreresults'] = 'There are too many results, enter a more specific search.'; $string['nomatchingcourses'] = 'There are no courses to display'; From 1ba43a9c6fb1e3790762773f314bdb6a5b035600 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Gaudreau Date: Wed, 22 Aug 2012 09:05:11 -0400 Subject: [PATCH 66/96] MDL-34997 - Allow shortened url youtu.be and y2u.be for Youtube filter --- filter/mediaplugin/tests/filter_test.php | 3 +++ lib/medialib.php | 18 +++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/filter/mediaplugin/tests/filter_test.php b/filter/mediaplugin/tests/filter_test.php index 86876102204..c0044fb6ab5 100644 --- a/filter/mediaplugin/tests/filter_test.php +++ b/filter/mediaplugin/tests/filter_test.php @@ -57,6 +57,9 @@ class filter_mediaplugin_testcase extends advanced_testcase { 'test mpg', 'test', 'test file', + 'test file', + 'test file', + 'test file', 'test file', 'test flv', 'test file', diff --git a/lib/medialib.php b/lib/medialib.php index fb8cdf6b7ae..e75e07743be 100644 --- a/lib/medialib.php +++ b/lib/medialib.php @@ -526,8 +526,7 @@ OET; */ class core_media_player_youtube extends core_media_player_external { protected function embed_external(moodle_url $url, $name, $width, $height, $options) { - $site = $this->matches[1]; - $videoid = $this->matches[3]; + $videoid = end($this->matches); $info = trim($name); if (empty($info) or strpos($info, 'http') === 0) { @@ -540,17 +539,22 @@ class core_media_player_youtube extends core_media_player_external { return << + src="https://www.youtube.com/embed/$videoid?rel=0&wmode=transparent" frameborder="0" allowfullscreen="1"> OET; } protected function get_regex() { + // Regex for standard youtube link + $link = '(youtube(-nocookie)?\.com/(?:watch\?v=|v/))'; + // Regex for shortened youtube link + $shortlink = '((youtu|y2u)\.be/)'; + // Initial part of link. - $start = '~^https?://(www\.youtube(-nocookie)?\.com)/'; - // Middle bit: either watch?v= or v/. - $middle = '(?:watch\?v=|v/)([a-z0-9\-_]+)'; + $start = '~^https?://(www\.)?(' . $link . '|' . $shortlink . ')'; + // Middle bit: Video key value + $middle = '([a-z0-9\-_]+)'; return $start . $middle . core_media_player_external::END_LINK_REGEX_PART; } @@ -561,7 +565,7 @@ OET; } public function get_embeddable_markers() { - return array('youtube'); + return array('youtube.com', 'youtube-nocookie.com', 'youtu.be', 'y2u.be'); } } From 87294fa3468b57c40eba1b60aa580b9939d837f4 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 28 Aug 2012 17:14:45 +0800 Subject: [PATCH 67/96] MDL-35101 Repository: Public Flickr fails when the picture has no license --- repository/flickr_public/lib.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/repository/flickr_public/lib.php b/repository/flickr_public/lib.php index b31adaf82d0..42d7e04ed2a 100644 --- a/repository/flickr_public/lib.php +++ b/repository/flickr_public/lib.php @@ -213,13 +213,14 @@ class repository_flickr_public extends repository { public function license4moodle ($license_id) { $license = array( + '0' => 'allrightsreserved', '1' => 'cc-nc-sa', '2' => 'cc-nc', '3' => 'cc-nc-nd', '4' => 'cc', '5' => 'cc-sa', '6' => 'cc-nd', - '7' => 'allrightsreserved' + '7' => 'other' ); return $license[$license_id]; } From b2245b7e8a4ef2d0444a88801f6f940076263a7f Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 28 Aug 2012 17:01:41 +0800 Subject: [PATCH 68/96] MDL-34633 Repository: Flickr supports small images --- repository/flickr/lib.php | 42 +++++++++++++++----- repository/flickr_public/lib.php | 66 ++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/repository/flickr/lib.php b/repository/flickr/lib.php index 478e53b0b0d..84bac4bb284 100644 --- a/repository/flickr/lib.php +++ b/repository/flickr/lib.php @@ -37,6 +37,11 @@ class repository_flickr extends repository { private $flickr; public $photos; + /** + * Stores sizes of images to prevent multiple API call + */ + static private $sizes = array(); + /** * * @param int $repositoryid @@ -228,16 +233,35 @@ class repository_flickr extends repository { * @return string */ private function build_photo_url($photoid) { - $result = $this->flickr->photos_getSizes($photoid); - $url = ''; - if(!empty($result[4])) { - $url = $result[4]['source']; - } elseif(!empty($result[3])) { - $url = $result[3]['source']; - } elseif(!empty($result[2])) { - $url = $result[2]['source']; + $bestsize = $this->get_best_size($photoid); + if (!isset($bestsize['source'])) { + throw new repository_exception('cannotdownload', 'repository'); } - return $url; + return $bestsize['source']; + } + + /** + * Returns the best size for a photo + * + * @param string $photoid the photo identifier + * @return array of information provided by the API + */ + protected function get_best_size($photoid) { + if (!isset(self::$sizes[$photoid])) { + // Sizes are returned from smallest to greatest. + self::$sizes[$photoid] = $this->flickr->photos_getSizes($photoid); + } + $sizes = self::$sizes[$photoid]; + $bestsize = array(); + if (is_array($sizes)) { + while ($bestsize = array_pop($sizes)) { + // Make sure the source is set. Exit the loop if found. + if (isset($bestsize['source'])) { + break; + } + } + } + return $bestsize; } public function get_link($photoid) { diff --git a/repository/flickr_public/lib.php b/repository/flickr_public/lib.php index 42d7e04ed2a..17cdb57a94b 100644 --- a/repository/flickr_public/lib.php +++ b/repository/flickr_public/lib.php @@ -41,6 +41,11 @@ class repository_flickr_public extends repository { private $flickr; public $photos; + /** + * Stores sizes of images to prevent multiple API call + */ + static private $sizes = array(); + /** * constructor method * @@ -403,16 +408,35 @@ class repository_flickr_public extends repository { * @return string */ private function build_photo_url($photoid) { - $result = $this->flickr->photos_getSizes($photoid); - $url = ''; - if(!empty($result[4])) { - $url = $result[4]['source']; - } elseif(!empty($result[3])) { - $url = $result[3]['source']; - } elseif(!empty($result[2])) { - $url = $result[2]['source']; + $bestsize = $this->get_best_size($photoid); + if (!isset($bestsize['source'])) { + throw new repository_exception('cannotdownload', 'repository'); } - return $url; + return $bestsize['source']; + } + + /** + * Returns the best size for a photo + * + * @param string $photoid the photo identifier + * @return array of information provided by the API + */ + protected function get_best_size($photoid) { + if (!isset(self::$sizes[$photoid])) { + // Sizes are returned from smallest to greatest. + self::$sizes[$photoid] = $this->flickr->photos_getSizes($photoid); + } + $sizes = self::$sizes[$photoid]; + $bestsize = array(); + if (is_array($sizes)) { + while ($bestsize = array_pop($sizes)) { + // Make sure the source is set. Exit the loop if found. + if (isset($bestsize['source'])) { + break; + } + } + } + return $bestsize; } public function get_link($photoid) { @@ -435,29 +459,23 @@ class repository_flickr_public extends repository { $author = $info['owner']['username']; } $copyright = get_string('author', 'repository') . ': ' . $author; - $result = $this->flickr->photos_getSizes($photoid); - // download link - $source = ''; - // flickr photo page - $url = ''; - if (!empty($result[4])) { - $source = $result[4]['source']; - $url = $result[4]['url']; - } elseif(!empty($result[3])) { - $source = $result[3]['source']; - $url = $result[3]['url']; - } elseif(!empty($result[2])) { - $source = $result[2]['source']; - $url = $result[2]['url']; + + // If we can read the original secret, it means that we have access to the original picture. + if (isset($info['originalsecret'])) { + $source = $this->flickr->buildPhotoURL($info, 'original'); + } else { + $source = $this->build_photo_url($photoid); } + $result = parent::get_file($source, $file); $path = $result['path']; + if (!empty($this->usewatermarks)) { $img = new moodle_image($path); $img->watermark($copyright, array(10,10), array('ttf'=>true, 'fontsize'=>12))->saveas($path); } - return array('path'=>$path, 'url'=>$url, 'author'=>$info['owner']['realname'], 'license'=>$this->license4moodle($info['license'])); + return array('path'=>$path, 'author'=>$info['owner']['realname'], 'license'=>$this->license4moodle($info['license'])); } /** From ffdd502dbd4932e45cf3558ac305821997c2c4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Mon, 3 Sep 2012 18:49:53 +0200 Subject: [PATCH 69/96] MDL-35184 import adodb 5.17 --- lib/adodb/adodb-active-record.inc.php | 2 +- lib/adodb/adodb-csvlib.inc.php | 2 +- lib/adodb/adodb-datadict.inc.php | 2 +- lib/adodb/adodb-error.inc.php | 2 +- lib/adodb/adodb-errorhandler.inc.php | 2 +- lib/adodb/adodb-errorpear.inc.php | 2 +- lib/adodb/adodb-exceptions.inc.php | 2 +- lib/adodb/adodb-iterator.inc.php | 2 +- lib/adodb/adodb-lib.inc.php | 2 +- lib/adodb/adodb-memcache.lib.inc.php | 2 +- lib/adodb/adodb-pager.inc.php | 2 +- lib/adodb/adodb-pear.inc.php | 2 +- lib/adodb/adodb-perf.inc.php | 2 +- lib/adodb/adodb-php4.inc.php | 2 +- lib/adodb/adodb.inc.php | 19 +- lib/adodb/drivers/adodb-access.inc.php | 2 +- lib/adodb/drivers/adodb-ado.inc.php | 2 +- lib/adodb/drivers/adodb-ado5.inc.php | 2 +- lib/adodb/drivers/adodb-ado_access.inc.php | 2 +- lib/adodb/drivers/adodb-ado_mssql.inc.php | 2 +- lib/adodb/drivers/adodb-borland_ibase.inc.php | 2 +- lib/adodb/drivers/adodb-csv.inc.php | 2 +- lib/adodb/drivers/adodb-db2.inc.php | 2 +- lib/adodb/drivers/adodb-db2oci.inc.php | 2 +- lib/adodb/drivers/adodb-db2ora.inc.php | 2 +- lib/adodb/drivers/adodb-fbsql.inc.php | 2 +- lib/adodb/drivers/adodb-firebird.inc.php | 2 +- lib/adodb/drivers/adodb-ibase.inc.php | 2 +- lib/adodb/drivers/adodb-informix.inc.php | 2 +- lib/adodb/drivers/adodb-informix72.inc.php | 2 +- lib/adodb/drivers/adodb-ldap.inc.php | 2 +- lib/adodb/drivers/adodb-mssql.inc.php | 2 +- lib/adodb/drivers/adodb-mssqlnative.inc.php | 8 +- lib/adodb/drivers/adodb-mssqlpo.inc.php | 2 +- lib/adodb/drivers/adodb-mysql.inc.php | 23 ++- lib/adodb/drivers/adodb-mysqli.inc.php | 2 +- lib/adodb/drivers/adodb-mysqlpo.inc.php | 2 +- lib/adodb/drivers/adodb-mysqlt.inc.php | 2 +- lib/adodb/drivers/adodb-netezza.inc.php | 2 +- lib/adodb/drivers/adodb-oci8.inc.php | 175 +++++++++--------- lib/adodb/drivers/adodb-oci805.inc.php | 2 +- lib/adodb/drivers/adodb-oci8po.inc.php | 2 +- lib/adodb/drivers/adodb-odbc.inc.php | 10 +- lib/adodb/drivers/adodb-odbc_db2.inc.php | 2 +- lib/adodb/drivers/adodb-odbc_mssql.inc.php | 2 +- lib/adodb/drivers/adodb-odbc_oracle.inc.php | 2 +- lib/adodb/drivers/adodb-odbtp.inc.php | 2 +- lib/adodb/drivers/adodb-odbtp_unicode.inc.php | 2 +- lib/adodb/drivers/adodb-oracle.inc.php | 2 +- lib/adodb/drivers/adodb-pdo.inc.php | 2 +- lib/adodb/drivers/adodb-pdo_mssql.inc.php | 2 +- lib/adodb/drivers/adodb-pdo_mysql.inc.php | 2 +- lib/adodb/drivers/adodb-pdo_oci.inc.php | 2 +- lib/adodb/drivers/adodb-pdo_pgsql.inc.php | 2 +- lib/adodb/drivers/adodb-pdo_sqlite.inc.php | 2 +- lib/adodb/drivers/adodb-postgres.inc.php | 2 +- lib/adodb/drivers/adodb-postgres64.inc.php | 2 +- lib/adodb/drivers/adodb-postgres7.inc.php | 2 +- lib/adodb/drivers/adodb-postgres8.inc.php | 2 +- lib/adodb/drivers/adodb-proxy.inc.php | 2 +- lib/adodb/drivers/adodb-sapdb.inc.php | 2 +- lib/adodb/drivers/adodb-sqlanywhere.inc.php | 2 +- lib/adodb/drivers/adodb-sqlite.inc.php | 2 +- lib/adodb/drivers/adodb-sqlite3.inc.php | 2 +- lib/adodb/drivers/adodb-sqlitepo.inc.php | 2 +- lib/adodb/drivers/adodb-sybase.inc.php | 2 +- lib/adodb/drivers/adodb-sybase_ase.inc.php | 2 +- lib/adodb/drivers/adodb-vfp.inc.php | 2 +- lib/adodb/lang/adodb-ar.inc.php | 33 ---- lib/adodb/lang/adodb-bg.inc.php | 37 ---- lib/adodb/lang/adodb-bgutf8.inc.php | 37 ---- lib/adodb/lang/adodb-ca.inc.php | 34 ---- lib/adodb/lang/adodb-cn.inc.php | 35 ---- lib/adodb/lang/adodb-cz.inc.php | 40 ---- lib/adodb/lang/adodb-da.inc.php | 33 ---- lib/adodb/lang/adodb-de.inc.php | 33 ---- lib/adodb/lang/adodb-es.inc.php | 33 ---- lib/adodb/lang/adodb-esperanto.inc.php | 35 ---- lib/adodb/lang/adodb-fa.inc.php | 35 ---- lib/adodb/lang/adodb-fr.inc.php | 33 ---- lib/adodb/lang/adodb-hu.inc.php | 34 ---- lib/adodb/lang/adodb-it.inc.php | 34 ---- lib/adodb/lang/adodb-nl.inc.php | 33 ---- lib/adodb/lang/adodb-pl.inc.php | 35 ---- lib/adodb/lang/adodb-pt-br.inc.php | 35 ---- lib/adodb/lang/adodb-ro.inc.php | 35 ---- lib/adodb/lang/adodb-ru1251.inc.php | 35 ---- lib/adodb/lang/adodb-sv.inc.php | 33 ---- lib/adodb/lang/adodb-uk1251.inc.php | 35 ---- lib/adodb/lang/adodb_th.inc.php | 33 ---- lib/adodb/perf/perf-db2.inc.php | 2 +- lib/adodb/perf/perf-informix.inc.php | 2 +- lib/adodb/perf/perf-mssql.inc.php | 2 +- lib/adodb/perf/perf-mssqlnative.inc.php | 2 +- lib/adodb/perf/perf-mysql.inc.php | 2 +- lib/adodb/perf/perf-oci8.inc.php | 2 +- lib/adodb/perf/perf-postgres.inc.php | 2 +- lib/adodb/readme_moodle.txt | 4 +- lib/thirdpartylibs.xml | 2 +- 99 files changed, 207 insertions(+), 934 deletions(-) delete mode 100644 lib/adodb/lang/adodb-ar.inc.php delete mode 100644 lib/adodb/lang/adodb-bg.inc.php delete mode 100644 lib/adodb/lang/adodb-bgutf8.inc.php delete mode 100644 lib/adodb/lang/adodb-ca.inc.php delete mode 100644 lib/adodb/lang/adodb-cn.inc.php delete mode 100644 lib/adodb/lang/adodb-cz.inc.php delete mode 100644 lib/adodb/lang/adodb-da.inc.php delete mode 100644 lib/adodb/lang/adodb-de.inc.php delete mode 100644 lib/adodb/lang/adodb-es.inc.php delete mode 100644 lib/adodb/lang/adodb-esperanto.inc.php delete mode 100644 lib/adodb/lang/adodb-fa.inc.php delete mode 100644 lib/adodb/lang/adodb-fr.inc.php delete mode 100644 lib/adodb/lang/adodb-hu.inc.php delete mode 100644 lib/adodb/lang/adodb-it.inc.php delete mode 100644 lib/adodb/lang/adodb-nl.inc.php delete mode 100644 lib/adodb/lang/adodb-pl.inc.php delete mode 100644 lib/adodb/lang/adodb-pt-br.inc.php delete mode 100644 lib/adodb/lang/adodb-ro.inc.php delete mode 100644 lib/adodb/lang/adodb-ru1251.inc.php delete mode 100644 lib/adodb/lang/adodb-sv.inc.php delete mode 100644 lib/adodb/lang/adodb-uk1251.inc.php delete mode 100644 lib/adodb/lang/adodb_th.inc.php diff --git a/lib/adodb/adodb-active-record.inc.php b/lib/adodb/adodb-active-record.inc.php index bf20639d3f4..9ea8175a4c4 100644 --- a/lib/adodb/adodb-active-record.inc.php +++ b/lib/adodb/adodb-active-record.inc.php @@ -1,7 +1,7 @@ RecordCount() is used. @@ -3499,23 +3499,22 @@ http://www.stanford.edu/dept/itss/docs/oracle/10g/server.101/b10759/statements_1 * * $upper 0 = lowercase, 1 = uppercase, 2 = whatever is returned by FetchField */ - function GetRowAssoc($upper=1) + function GetRowAssoc($upper=1) { $record = array(); - // if (!$this->fields) return $record; - - if (!$this->bind) { + if (!$this->bind) { $this->GetAssocKeys($upper); } - foreach($this->bind as $k => $v) { - $record[$k] = $this->fields[$v]; + if( isset( $this->fields[$v] ) ) { + $record[$k] = $this->fields[$v]; + } else if (isset($this->fields[$k])) { + $record[$k] = $this->fields[$k]; + } } - return $record; } - /** * Clean up recordset * diff --git a/lib/adodb/drivers/adodb-access.inc.php b/lib/adodb/drivers/adodb-access.inc.php index f5685f347e5..c9efb1fd8f8 100644 --- a/lib/adodb/drivers/adodb-access.inc.php +++ b/lib/adodb/drivers/adodb-access.inc.php @@ -1,6 +1,6 @@ $argDatabasename,'UID'=>$argUsername,'PWD'=>$argPassword); + $connectionInfo = $this->connectionInfo; + $connectionInfo["Database"]=$argDatabasename; + $connectionInfo["UID"]=$argUsername; + $connectionInfo["PWD"]=$argPassword; if ($this->debug) error_log("
    connecting... hostname: $argHostname params: ".var_export($connectionInfo,true)); //if ($this->debug) error_log("
    _connectionID before: ".serialize($this->_connectionID)); if(!($this->_connectionID = sqlsrv_connect($argHostname,$connectionInfo))) { diff --git a/lib/adodb/drivers/adodb-mssqlpo.inc.php b/lib/adodb/drivers/adodb-mssqlpo.inc.php index 76dfe245a21..60bc9c7c3aa 100644 --- a/lib/adodb/drivers/adodb-mssqlpo.inc.php +++ b/lib/adodb/drivers/adodb-mssqlpo.inc.php @@ -1,6 +1,6 @@ rsPrefix .= 'ext_'; } - + + + // SetCharSet - switch the client encoding + function SetCharSet($charset_name) + { + if (!function_exists('mysql_set_charset')) + return false; + + if ($this->charSet !== $charset_name) { + $ok = @mysql_set_charset($charset_name,$this->_connectionID); + if ($ok) { + $this->charSet = $charset_name; + return true; + } + return false; + } + return true; + } + function ServerInfo() { $arr['description'] = ADOConnection::GetOne("select version()"); diff --git a/lib/adodb/drivers/adodb-mysqli.inc.php b/lib/adodb/drivers/adodb-mysqli.inc.php index b1d5d4d35d8..bb556a131a9 100644 --- a/lib/adodb/drivers/adodb-mysqli.inc.php +++ b/lib/adodb/drivers/adodb-mysqli.inc.php @@ -1,6 +1,6 @@ _hasOCIFetchStatement = ADODB_PHPVER >= 0x4200; + $this->_hasOciFetchStatement = ADODB_PHPVER >= 0x4200; if (defined('ADODB_EXTENSION')) $this->rsPrefix .= 'ext_'; } @@ -201,7 +201,7 @@ NATSOFT.DOMAIN = */ function _connect($argHostname, $argUsername, $argPassword, $argDatabasename,$mode=0) { - if (!function_exists('OCIPLogon')) return null; + if (!function_exists('oci_pconnect')) return null; #adodb_backtrace(); $this->_errorMsg = false; @@ -235,22 +235,22 @@ NATSOFT.DOMAIN = //if ($argHostname) print "

    Connect: 1st argument should be left blank for $this->databaseType

    "; if ($mode==1) { $this->_connectionID = ($this->charSet) ? - OCIPLogon($argUsername,$argPassword, $argDatabasename,$this->charSet) + oci_pconnect($argUsername,$argPassword, $argDatabasename,$this->charSet) : - OCIPLogon($argUsername,$argPassword, $argDatabasename) + oci_pconnect($argUsername,$argPassword, $argDatabasename) ; - if ($this->_connectionID && $this->autoRollback) OCIrollback($this->_connectionID); + if ($this->_connectionID && $this->autoRollback) oci_rollback($this->_connectionID); } else if ($mode==2) { $this->_connectionID = ($this->charSet) ? - OCINLogon($argUsername,$argPassword, $argDatabasename,$this->charSet) + oci_new_connect($argUsername,$argPassword, $argDatabasename,$this->charSet) : - OCINLogon($argUsername,$argPassword, $argDatabasename); + oci_new_connect($argUsername,$argPassword, $argDatabasename); } else { $this->_connectionID = ($this->charSet) ? - OCILogon($argUsername,$argPassword, $argDatabasename,$this->charSet) + oci_connect($argUsername,$argPassword, $argDatabasename,$this->charSet) : - OCILogon($argUsername,$argPassword, $argDatabasename); + oci_connect($argUsername,$argPassword, $argDatabasename); } if (!$this->_connectionID) return false; if ($this->_initdate) { @@ -259,7 +259,7 @@ NATSOFT.DOMAIN = // looks like: // Oracle8i Enterprise Edition Release 8.1.7.0.0 - Production With the Partitioning option JServer Release 8.1.7.0.0 - Production - // $vers = OCIServerVersion($this->_connectionID); + // $vers = oci_server_version($this->_connectionID); // if (strpos($vers,'8i') !== false) $this->ansiOuter = true; return true; } @@ -267,7 +267,7 @@ NATSOFT.DOMAIN = function ServerInfo() { $arr['compat'] = $this->GetOne('select value from sys.database_compatible_level'); - $arr['description'] = @OCIServerVersion($this->_connectionID); + $arr['description'] = @oci_server_version($this->_connectionID); $arr['version'] = ADOConnection::_findvers($arr['description']); return $arr; } @@ -285,7 +285,7 @@ NATSOFT.DOMAIN = function _affectedrows() { - if (is_resource($this->_stmt)) return @OCIRowCount($this->_stmt); + if (is_resource($this->_stmt)) return @oci_num_rows($this->_stmt); return 0; } @@ -386,6 +386,13 @@ NATSOFT.DOMAIN = $false = false; $rs = $this->Execute(sprintf("SELECT * FROM ALL_CONSTRAINTS WHERE UPPER(TABLE_NAME)='%s' AND CONSTRAINT_TYPE='P'",$table)); + if (!is_object($rs)) { + if (isset($savem)) + $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + return $false; + } + if ($row = $rs->FetchRow()) $primary_key = $row[1]; //constraint_name @@ -451,7 +458,7 @@ NATSOFT.DOMAIN = if (!$ok) return $this->RollbackTrans(); if ($this->transCnt) $this->transCnt -= 1; - $ret = OCIcommit($this->_connectionID); + $ret = oci_commit($this->_connectionID); $this->_commit = OCI_COMMIT_ON_SUCCESS; $this->autoCommit = true; return $ret; @@ -461,7 +468,7 @@ NATSOFT.DOMAIN = { if ($this->transOff) return true; if ($this->transCnt) $this->transCnt -= 1; - $ret = OCIrollback($this->_connectionID); + $ret = oci_rollback($this->_connectionID); $this->_commit = OCI_COMMIT_ON_SUCCESS; $this->autoCommit = true; return $ret; @@ -477,10 +484,10 @@ NATSOFT.DOMAIN = { if ($this->_errorMsg !== false) return $this->_errorMsg; - if (is_resource($this->_stmt)) $arr = @OCIError($this->_stmt); + if (is_resource($this->_stmt)) $arr = @oci_error($this->_stmt); if (empty($arr)) { - if (is_resource($this->_connectionID)) $arr = @OCIError($this->_connectionID); - else $arr = @OCIError(); + if (is_resource($this->_connectionID)) $arr = @oci_error($this->_connectionID); + else $arr = @oci_error(); if ($arr === false) return ''; } $this->_errorMsg = $arr['message']; @@ -492,10 +499,10 @@ NATSOFT.DOMAIN = { if ($this->_errorCode !== false) return $this->_errorCode; - if (is_resource($this->_stmt)) $arr = @OCIError($this->_stmt); + if (is_resource($this->_stmt)) $arr = @oci_error($this->_stmt); if (empty($arr)) { - $arr = @OCIError($this->_connectionID); - if ($arr == false) $arr = @OCIError(); + $arr = @oci_error($this->_connectionID); + if ($arr == false) $arr = @oci_error(); if ($arr == false) return ''; } @@ -651,34 +658,34 @@ NATSOFT.DOMAIN = foreach($inputarr as $k => $v) { if (is_array($v)) { if (sizeof($v) == 2) // suggested by g.giunta@libero. - OCIBindByName($stmt,":$k",$inputarr[$k][0],$v[1]); + oci_bind_by_name($stmt,":$k",$inputarr[$k][0],$v[1]); else - OCIBindByName($stmt,":$k",$inputarr[$k][0],$v[1],$v[2]); + oci_bind_by_name($stmt,":$k",$inputarr[$k][0],$v[1],$v[2]); } else { $len = -1; if ($v === ' ') $len = 1; - if (isset($bindarr)) { // is prepared sql, so no need to ocibindbyname again + if (isset($bindarr)) { // is prepared sql, so no need to oci_bind_by_name again $bindarr[$k] = $v; } else { // dynamic sql, so rebind every time - OCIBindByName($stmt,":$k",$inputarr[$k],$len); + oci_bind_by_name($stmt,":$k",$inputarr[$k],$len); } } } } - if (!OCIExecute($stmt, OCI_DEFAULT)) { - OCIFreeStatement($stmt); + if (!oci_execute($stmt, OCI_DEFAULT)) { + oci_free_statement($stmt); return $false; } - $ncols = OCINumCols($stmt); + $ncols = oci_num_fields($stmt); for ( $i = 1; $i <= $ncols; $i++ ) { - $cols[] = '"'.OCIColumnName($stmt, $i).'"'; + $cols[] = '"'.oci_field_name($stmt, $i).'"'; } $result = false; - OCIFreeStatement($stmt); + oci_free_statement($stmt); $fields = implode(',', $cols); if ($nrows <= 0) $nrows = 999999999999; else $nrows += $offset; @@ -741,7 +748,7 @@ NATSOFT.DOMAIN = else $sql = "UPDATE $table set $column=EMPTY_{$blobtype}() WHERE $where RETURNING $column INTO :blob"; - $desc = OCINewDescriptor($this->_connectionID, OCI_D_LOB); + $desc = oci_new_descriptor($this->_connectionID, OCI_D_LOB); $arr['blob'] = array($desc,-1,$type); if ($this->session_sharing_force_blob) $this->Execute('ALTER SESSION SET CURSOR_SHARING=EXACT'); $commit = $this->autoCommit; @@ -772,7 +779,7 @@ NATSOFT.DOMAIN = else $sql = "UPDATE $table set $column=EMPTY_{$blobtype}() WHERE $where RETURNING $column INTO :blob"; - $desc = OCINewDescriptor($this->_connectionID, OCI_D_LOB); + $desc = oci_new_descriptor($this->_connectionID, OCI_D_LOB); $arr['blob'] = array($desc,-1,$type); $this->BeginTrans(); @@ -873,12 +880,12 @@ NATSOFT.DOMAIN = { static $BINDNUM = 0; - $stmt = OCIParse($this->_connectionID,$sql); + $stmt = oci_parse($this->_connectionID,$sql); if (!$stmt) { $this->_errorMsg = false; $this->_errorCode = false; - $arr = @OCIError($this->_connectionID); + $arr = @oci_error($this->_connectionID); if ($arr === false) return false; $this->_errorMsg = $arr['message']; @@ -888,9 +895,9 @@ NATSOFT.DOMAIN = $BINDNUM += 1; - $sttype = @OCIStatementType($stmt); + $sttype = @oci_statement_type($stmt); if ($sttype == 'BEGIN' || $sttype == 'DECLARE') { - return array($sql,$stmt,0,$BINDNUM, ($cursor) ? OCINewCursor($this->_connectionID) : false); + return array($sql,$stmt,0,$BINDNUM, ($cursor) ? oci_new_cursor($this->_connectionID) : false); } return array($sql,$stmt,0,$BINDNUM); } @@ -912,7 +919,7 @@ NATSOFT.DOMAIN = function ExecuteCursor($sql,$cursorName='rs',$params=false) { if (is_array($sql)) $stmt = $sql; - else $stmt = ADODB_oci8::Prepare($sql,true); # true to allocate OCINewCursor + else $stmt = ADODB_oci8::Prepare($sql,true); # true to allocate oci_new_cursor if (is_array($stmt) && sizeof($stmt) >= 5) { $hasref = true; @@ -928,7 +935,7 @@ NATSOFT.DOMAIN = $rs = $this->Execute($stmt); if ($rs) { - if ($rs->databaseType == 'array') OCIFreeCursor($stmt[4]); + if ($rs->databaseType == 'array') oci_free_cursor($stmt[4]); else if ($hasref) $rs->_refcursor = $stmt[4]; } return $rs; @@ -955,13 +962,13 @@ NATSOFT.DOMAIN = Some timings: ** Test table has 3 cols, and 1 index. Test to insert 1000 records - Time 0.6081s (1644.60 inserts/sec) with direct OCIParse/OCIExecute + Time 0.6081s (1644.60 inserts/sec) with direct oci_parse/oci_execute Time 0.6341s (1577.16 inserts/sec) with ADOdb Prepare/Bind/Execute Time 1.5533s ( 643.77 inserts/sec) with pure SQL using Execute Now if PHP only had batch/bulk updating like Java or PL/SQL... - Note that the order of parameters differs from OCIBindByName, + Note that the order of parameters differs from oci_bind_by_name, because we default the names to :0, :1, :2 */ function Bind(&$stmt,&$var,$size=4000,$type=false,$name=false,$isOutput=false) @@ -970,12 +977,12 @@ NATSOFT.DOMAIN = if (!is_array($stmt)) return false; if (($type == OCI_B_CURSOR) && sizeof($stmt) >= 5) { - return OCIBindByName($stmt[1],":".$name,$stmt[4],$size,$type); + return oci_bind_by_name($stmt[1],":".$name,$stmt[4],$size,$type); } if ($name == false) { - if ($type !== false) $rez = OCIBindByName($stmt[1],":".$stmt[2],$var,$size,$type); - else $rez = OCIBindByName($stmt[1],":".$stmt[2],$var,$size); // +1 byte for null terminator + if ($type !== false) $rez = oci_bind_by_name($stmt[1],":".$stmt[2],$var,$size,$type); + else $rez = oci_bind_by_name($stmt[1],":".$stmt[2],$var,$size); // +1 byte for null terminator $stmt[2] += 1; } else if (oci_lob_desc($type)) { if ($this->debug) { @@ -983,11 +990,11 @@ NATSOFT.DOMAIN = } //we have to create a new Descriptor here $numlob = count($this->_refLOBs); - $this->_refLOBs[$numlob]['LOB'] = OCINewDescriptor($this->_connectionID, oci_lob_desc($type)); + $this->_refLOBs[$numlob]['LOB'] = oci_new_descriptor($this->_connectionID, oci_lob_desc($type)); $this->_refLOBs[$numlob]['TYPE'] = $isOutput; $tmp = $this->_refLOBs[$numlob]['LOB']; - $rez = OCIBindByName($stmt[1], ":".$name, $tmp, -1, $type); + $rez = oci_bind_by_name($stmt[1], ":".$name, $tmp, -1, $type); if ($this->debug) { ADOConnection::outp("Bind: descriptor has been allocated, var (".$name.") binded"); } @@ -1008,8 +1015,8 @@ NATSOFT.DOMAIN = if ($this->debug) ADOConnection::outp("Bind: name = $name"); - if ($type !== false) $rez = OCIBindByName($stmt[1],":".$name,$var,$size,$type); - else $rez = OCIBindByName($stmt[1],":".$name,$var,$size); // +1 byte for null terminator + if ($type !== false) $rez = oci_bind_by_name($stmt[1],":".$name,$var,$size,$type); + else $rez = oci_bind_by_name($stmt[1],":".$name,$var,$size); // +1 byte for null terminator } return $rez; @@ -1034,7 +1041,7 @@ NATSOFT.DOMAIN = @param [$maxLen] Holds an maximum length of the variable. @param [$type] The data type of $var. Legal values depend on driver. - See OCIBindByName documentation at php.net. + See oci_bind_by_name documentation at php.net. */ function Parameter(&$stmt,&$var,$name,$isOutput=false,$maxLen=4000,$type=false) { @@ -1066,7 +1073,7 @@ NATSOFT.DOMAIN = if (is_array($sql)) { // is prepared sql $stmt = $sql[1]; - // we try to bind to permanent array, so that OCIBindByName is persistent + // we try to bind to permanent array, so that oci_bind_by_name is persistent // and carried out once only - note that max array element size is 4000 chars if (is_array($inputarr)) { $bindpos = $sql[3]; @@ -1078,27 +1085,27 @@ NATSOFT.DOMAIN = $bindarr = array(); foreach($inputarr as $k => $v) { $bindarr[$k] = $v; - OCIBindByName($stmt,":$k",$bindarr[$k],is_string($v) && strlen($v)>4000 ? -1 : 4000); + oci_bind_by_name($stmt,":$k",$bindarr[$k],is_string($v) && strlen($v)>4000 ? -1 : 4000); } $this->_bind[$bindpos] = $bindarr; } } } else { - $stmt=OCIParse($this->_connectionID,$sql); + $stmt=oci_parse($this->_connectionID,$sql); } $this->_stmt = $stmt; if (!$stmt) return false; - if (defined('ADODB_PREFETCH_ROWS')) @OCISetPrefetch($stmt,ADODB_PREFETCH_ROWS); + if (defined('ADODB_PREFETCH_ROWS')) @oci_set_prefetch($stmt,ADODB_PREFETCH_ROWS); if (is_array($inputarr)) { foreach($inputarr as $k => $v) { if (is_array($v)) { if (sizeof($v) == 2) // suggested by g.giunta@libero. - OCIBindByName($stmt,":$k",$inputarr[$k][0],$v[1]); + oci_bind_by_name($stmt,":$k",$inputarr[$k][0],$v[1]); else - OCIBindByName($stmt,":$k",$inputarr[$k][0],$v[1],$v[2]); + oci_bind_by_name($stmt,":$k",$inputarr[$k][0],$v[1],$v[2]); if ($this->debug==99) { if (is_object($v[0])) @@ -1110,10 +1117,10 @@ NATSOFT.DOMAIN = } else { $len = -1; if ($v === ' ') $len = 1; - if (isset($bindarr)) { // is prepared sql, so no need to ocibindbyname again + if (isset($bindarr)) { // is prepared sql, so no need to oci_bind_by_name again $bindarr[$k] = $v; } else { // dynamic sql, so rebind every time - OCIBindByName($stmt,":$k",$inputarr[$k],$len); + oci_bind_by_name($stmt,":$k",$inputarr[$k],$len); } } } @@ -1121,8 +1128,8 @@ NATSOFT.DOMAIN = $this->_errorMsg = false; $this->_errorCode = false; - if (OCIExecute($stmt,$this->_commit)) { -//OCIInternalDebug(1); + if (oci_execute($stmt,$this->_commit)) { + if (count($this -> _refLOBs) > 0) { foreach ($this -> _refLOBs as $key => $value) { @@ -1144,7 +1151,7 @@ NATSOFT.DOMAIN = } } - switch (@OCIStatementType($stmt)) { + switch (@oci_statement_type($stmt)) { case "SELECT": return $stmt; @@ -1153,20 +1160,20 @@ NATSOFT.DOMAIN = if (is_array($sql) && !empty($sql[4])) { $cursor = $sql[4]; if (is_resource($cursor)) { - $ok = OCIExecute($cursor); + $ok = oci_execute($cursor); return $cursor; } return $stmt; } else { if (is_resource($stmt)) { - OCIFreeStatement($stmt); + oci_free_statement($stmt); return true; } return $stmt; } break; default : - // ociclose -- no because it could be used in a LOB? + return true; } } @@ -1204,14 +1211,14 @@ NATSOFT.DOMAIN = { if (!$this->_connectionID) return; - if (!$this->autoCommit) OCIRollback($this->_connectionID); + if (!$this->autoCommit) oci_rollback($this->_connectionID); if (count($this->_refLOBs) > 0) { foreach ($this ->_refLOBs as $key => $value) { $this->_refLOBs[$key]['LOB']->free(); unset($this->_refLOBs[$key]); } } - OCILogoff($this->_connectionID); + oci_close($this->_connectionID); $this->_stmt = false; $this->_connectionID = false; @@ -1383,7 +1390,7 @@ class ADORecordset_oci8 extends ADORecordSet { /* // based on idea by Gaetano Giunta to detect unusual oracle errors // see http://phplens.com/lens/lensforum/msgs.php?id=6771 - $err = OCIError($this->_queryID); + $err = oci_error($this->_queryID); if ($err && $this->connection->debug) ADOConnection::outp($err); */ @@ -1402,7 +1409,7 @@ class ADORecordset_oci8 extends ADORecordSet { function _initrs() { $this->_numOfRows = -1; - $this->_numOfFields = OCInumcols($this->_queryID); + $this->_numOfFields = oci_num_fields($this->_queryID); if ($this->_numOfFields>0) { $this->_fieldobjs = array(); $max = $this->_numOfFields; @@ -1419,13 +1426,13 @@ class ADORecordset_oci8 extends ADORecordSet { { $fld = new ADOFieldObject; $fieldOffset += 1; - $fld->name =OCIcolumnname($this->_queryID, $fieldOffset); - $fld->type = OCIcolumntype($this->_queryID, $fieldOffset); - $fld->max_length = OCIcolumnsize($this->_queryID, $fieldOffset); + $fld->name =oci_field_name($this->_queryID, $fieldOffset); + $fld->type = oci_field_type($this->_queryID, $fieldOffset); + $fld->max_length = oci_field_size($this->_queryID, $fieldOffset); switch($fld->type) { case 'NUMBER': - $p = OCIColumnPrecision($this->_queryID, $fieldOffset); - $sc = OCIColumnScale($this->_queryID, $fieldOffset); + $p = oci_field_precision($this->_queryID, $fieldOffset); + $sc = oci_field_scale($this->_queryID, $fieldOffset); if ($p != 0 && $sc == 0) $fld->type = 'INT'; $fld->scale = $p; break; @@ -1439,7 +1446,7 @@ class ADORecordset_oci8 extends ADORecordSet { return $fld; } - /* For some reason, OCIcolumnname fails when called after _initrs() so we cache it */ + /* For some reason, oci_field_name fails when called after _initrs() so we cache it */ function FetchField($fieldOffset = -1) { return $this->_fieldobjs[$fieldOffset]; @@ -1455,7 +1462,7 @@ class ADORecordset_oci8 extends ADORecordSet { if ($this->EOF) return false; $this->_currentRow++; - if(@OCIfetchinto($this->_queryID,$this->fields,$this->fetchMode)) + if($this->fields = @oci_fetch_array($this->_queryID,$this->fetchMode)) return true; $this->EOF = true; @@ -1465,7 +1472,7 @@ class ADORecordset_oci8 extends ADORecordSet { function MoveNext() { - if (@OCIfetchinto($this->_queryID,$this->fields,$this->fetchMode)) { + if ($this->fields = @oci_fetch_array($this->_queryID,$this->fetchMode)) { $this->_currentRow += 1; return true; } @@ -1485,18 +1492,18 @@ class ADORecordset_oci8 extends ADORecordSet { if (true || !empty($ADODB_OCI8_GETARRAY)) { # does not support $ADODB_ANSI_PADDING_OFF - //OCI_RETURN_NULLS and OCI_RETURN_LOBS is set by OCIfetchstatement + //OCI_RETURN_NULLS and OCI_RETURN_LOBS is set by oci_fetch_all switch($this->adodbFetchMode) { case ADODB_FETCH_NUM: - $ncols = @OCIfetchstatement($this->_queryID, $results, 0, $nRows, OCI_FETCHSTATEMENT_BY_ROW+OCI_NUM); + $ncols = @oci_fetch_all($this->_queryID, $results, 0, $nRows, oci_fetch_all_BY_ROW+OCI_NUM); $results = array_merge(array($this->fields),$results); return $results; case ADODB_FETCH_ASSOC: if (ADODB_ASSOC_CASE != 2 || $this->databaseType != 'oci8') break; - $ncols = @OCIfetchstatement($this->_queryID, $assoc, 0, $nRows, OCI_FETCHSTATEMENT_BY_ROW); + $ncols = @oci_fetch_all($this->_queryID, $assoc, 0, $nRows, oci_fetch_all_BY_ROW); $results = array_merge(array($this->fields),$assoc); return $results; @@ -1510,7 +1517,7 @@ class ADORecordset_oci8 extends ADORecordSet { } */ - /* Optimize SelectLimit() by using OCIFetch() instead of OCIFetchInto() */ + /* Optimize SelectLimit() by using oci_fetch() */ function GetArrayLimit($nrows,$offset=-1) { if ($offset <= 0) { @@ -1519,9 +1526,9 @@ class ADORecordset_oci8 extends ADORecordSet { } $arr = array(); for ($i=1; $i < $offset; $i++) - if (!@OCIFetch($this->_queryID)) return $arr; + if (!@oci_fetch($this->_queryID)) return $arr; - if (!@OCIfetchinto($this->_queryID,$this->fields,$this->fetchMode)) return $arr;; + if (!$this->fields = @oci_fetch_array($this->_queryID,$this->fetchMode)) return $arr;; $results = array(); $cnt = 0; while (!$this->EOF && $nrows != $cnt) { @@ -1556,7 +1563,7 @@ class ADORecordset_oci8 extends ADORecordSet { function _fetch() { - return @OCIfetchinto($this->_queryID,$this->fields,$this->fetchMode); + return $this->fields = @oci_fetch_array($this->_queryID,$this->fetchMode); } /* close() only needs to be called if you are worried about using too much memory while your script @@ -1566,10 +1573,10 @@ class ADORecordset_oci8 extends ADORecordSet { { if ($this->connection->_stmt === $this->_queryID) $this->connection->_stmt = false; if (!empty($this->_refcursor)) { - OCIFreeCursor($this->_refcursor); + oci_free_cursor($this->_refcursor); $this->_refcursor = false; } - @OCIFreeStatement($this->_queryID); + @oci_free_statement($this->_queryID); $this->_queryID = false; } diff --git a/lib/adodb/drivers/adodb-oci805.inc.php b/lib/adodb/drivers/adodb-oci805.inc.php index d8a5589db66..1f5c8c70818 100644 --- a/lib/adodb/drivers/adodb-oci805.inc.php +++ b/lib/adodb/drivers/adodb-oci805.inc.php @@ -1,6 +1,6 @@ debug && $argDatabasename && $this->databaseType != 'vfp') { - ADOConnection::outp("For odbc Connect(), $argDatabasename is not used. Place dsn in 1st parameter."); + if (!empty($argDatabasename) && stristr($argDSN, 'Database=') === false) { + $argDSN = trim($argDSN); + $endDSN = substr($argDSN, strlen($argDSN) - 1); + if ($endDSN != ';') $argDSN .= ';'; + $argDSN .= 'Database='.$argDatabasename; } + if (isset($php_errormsg)) $php_errormsg = ''; if ($this->curmode === false) $this->_connectionID = odbc_connect($argDSN,$argUsername,$argPassword); else $this->_connectionID = odbc_connect($argDSN,$argUsername,$argPassword,$this->curmode); diff --git a/lib/adodb/drivers/adodb-odbc_db2.inc.php b/lib/adodb/drivers/adodb-odbc_db2.inc.php index a05a3333ab2..fd039ba460c 100644 --- a/lib/adodb/drivers/adodb-odbc_db2.inc.php +++ b/lib/adodb/drivers/adodb-odbc_db2.inc.php @@ -1,6 +1,6 @@ -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'ar', - DB_ERROR => ' ', - DB_ERROR_ALREADY_EXISTS => ' ', - DB_ERROR_CANNOT_CREATE => ' ', - DB_ERROR_CANNOT_DELETE => ' ', - DB_ERROR_CANNOT_DROP => ' ', - DB_ERROR_CONSTRAINT => ' ', - DB_ERROR_DIVZERO => ' ', - DB_ERROR_INVALID => ' ', - DB_ERROR_INVALID_DATE => ' ', - DB_ERROR_INVALID_NUMBER => ' ', - DB_ERROR_MISMATCH => ' ', - DB_ERROR_NODBSELECTED => ' ', - DB_ERROR_NOSUCHFIELD => ' ', - DB_ERROR_NOSUCHTABLE => ' ', - DB_ERROR_NOT_CAPABLE => ' ', - DB_ERROR_NOT_FOUND => ' ', - DB_ERROR_NOT_LOCKED => ' ', - DB_ERROR_SYNTAX => ' ', - DB_ERROR_UNSUPPORTED => ' ', - DB_ERROR_VALUE_COUNT_ON_ROW => ' ', - DB_ERROR_INVALID_DSN => 'DSN ', - DB_ERROR_CONNECT_FAILED => ' ', - 0 => ' ', // DB_OK - DB_ERROR_NEED_MORE_DATA => ' ', - DB_ERROR_EXTENSION_NOT_FOUND=> ' ', - DB_ERROR_NOSUCHDB => ' ', - DB_ERROR_ACCESS_VIOLATION => ' ' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-bg.inc.php b/lib/adodb/lang/adodb-bg.inc.php deleted file mode 100644 index ee307c13fed..00000000000 --- a/lib/adodb/lang/adodb-bg.inc.php +++ /dev/null @@ -1,37 +0,0 @@ - -*/ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'bg', - DB_ERROR => ' ', - DB_ERROR_ALREADY_EXISTS => ' ', - DB_ERROR_CANNOT_CREATE => ' ', - DB_ERROR_CANNOT_DELETE => ' ', - DB_ERROR_CANNOT_DROP => ' ', - DB_ERROR_CONSTRAINT => ' ', - DB_ERROR_DIVZERO => ' ', - DB_ERROR_INVALID => '', - DB_ERROR_INVALID_DATE => ' ', - DB_ERROR_INVALID_NUMBER => ' ', - DB_ERROR_MISMATCH => ' ', - DB_ERROR_NODBSELECTED => ' ', - DB_ERROR_NOSUCHFIELD => ' ', - DB_ERROR_NOSUCHTABLE => ' ', - DB_ERROR_NOT_CAPABLE => 'DB backend not capable', - DB_ERROR_NOT_FOUND => ' ', - DB_ERROR_NOT_LOCKED => ' ', - DB_ERROR_SYNTAX => ' ', - DB_ERROR_UNSUPPORTED => ' ', - DB_ERROR_VALUE_COUNT_ON_ROW => ' ', - DB_ERROR_INVALID_DSN => ' DSN', - DB_ERROR_CONNECT_FAILED => ' ', - 0 => ' ', // DB_OK - DB_ERROR_NEED_MORE_DATA => ' ', - DB_ERROR_EXTENSION_NOT_FOUND=> ' ', - DB_ERROR_NOSUCHDB => ' ', - DB_ERROR_ACCESS_VIOLATION => ' ' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-bgutf8.inc.php b/lib/adodb/lang/adodb-bgutf8.inc.php deleted file mode 100644 index 5281ed53b65..00000000000 --- a/lib/adodb/lang/adodb-bgutf8.inc.php +++ /dev/null @@ -1,37 +0,0 @@ - -*/ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'bgutf8', - DB_ERROR => 'неизвестна грешка', - DB_ERROR_ALREADY_EXISTS => 'вече съществува', - DB_ERROR_CANNOT_CREATE => 'не може да бъде създадена', - DB_ERROR_CANNOT_DELETE => 'не може да бъде изтрита', - DB_ERROR_CANNOT_DROP => 'не може да бъде унищожена', - DB_ERROR_CONSTRAINT => 'нарушено условие', - DB_ERROR_DIVZERO => 'деление на нула', - DB_ERROR_INVALID => 'неправилно', - DB_ERROR_INVALID_DATE => 'некоректна дата или час', - DB_ERROR_INVALID_NUMBER => 'невалиден номер', - DB_ERROR_MISMATCH => 'погрешна употреба', - DB_ERROR_NODBSELECTED => 'не е избрана база данни', - DB_ERROR_NOSUCHFIELD => 'несъществуващо поле', - DB_ERROR_NOSUCHTABLE => 'несъществуваща таблица', - DB_ERROR_NOT_CAPABLE => 'DB backend not capable', - DB_ERROR_NOT_FOUND => 'не е намерена', - DB_ERROR_NOT_LOCKED => 'не е заключена', - DB_ERROR_SYNTAX => 'грешен синтаксис', - DB_ERROR_UNSUPPORTED => 'не се поддържа', - DB_ERROR_VALUE_COUNT_ON_ROW => 'некоректен брой колони в реда', - DB_ERROR_INVALID_DSN => 'невалиден DSN', - DB_ERROR_CONNECT_FAILED => 'връзката не може да бъде осъществена', - 0 => 'няма грешки', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'предоставените данни са недостатъчни', - DB_ERROR_EXTENSION_NOT_FOUND=> 'разширението не е намерено', - DB_ERROR_NOSUCHDB => 'несъществуваща база данни', - DB_ERROR_ACCESS_VIOLATION => 'нямате достатъчно права' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-ca.inc.php b/lib/adodb/lang/adodb-ca.inc.php deleted file mode 100644 index 3640ebd0bbb..00000000000 --- a/lib/adodb/lang/adodb-ca.inc.php +++ /dev/null @@ -1,34 +0,0 @@ - 'ca', - DB_ERROR => 'error desconegut', - DB_ERROR_ALREADY_EXISTS => 'ja existeix', - DB_ERROR_CANNOT_CREATE => 'no es pot crear', - DB_ERROR_CANNOT_DELETE => 'no es pot esborrar', - DB_ERROR_CANNOT_DROP => 'no es pot eliminar', - DB_ERROR_CONSTRAINT => 'violaci de constraint', - DB_ERROR_DIVZERO => 'divisi per zero', - DB_ERROR_INVALID => 'no s vlid', - DB_ERROR_INVALID_DATE => 'la data o l\'hora no sn vlides', - DB_ERROR_INVALID_NUMBER => 'el nombre no s vlid', - DB_ERROR_MISMATCH => 'no hi ha coincidncia', - DB_ERROR_NODBSELECTED => 'cap base de dades seleccionada', - DB_ERROR_NOSUCHFIELD => 'camp inexistent', - DB_ERROR_NOSUCHTABLE => 'taula inexistent', - DB_ERROR_NOT_CAPABLE => 'l\'execuci secundria de DB no pot', - DB_ERROR_NOT_FOUND => 'no trobat', - DB_ERROR_NOT_LOCKED => 'no blocat', - DB_ERROR_SYNTAX => 'error de sintaxi', - DB_ERROR_UNSUPPORTED => 'no suportat', - DB_ERROR_VALUE_COUNT_ON_ROW => 'el nombre de columnes no coincideix amb el nombre de valors en la fila', - DB_ERROR_INVALID_DSN => 'el DSN no s vlid', - DB_ERROR_CONNECT_FAILED => 'connexi fallida', - 0 => 'cap error', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'les dades subministrades sn insuficients', - DB_ERROR_EXTENSION_NOT_FOUND=> 'extensi no trobada', - DB_ERROR_NOSUCHDB => 'base de dades inexistent', - DB_ERROR_ACCESS_VIOLATION => 'permisos insuficients' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-cn.inc.php b/lib/adodb/lang/adodb-cn.inc.php deleted file mode 100644 index eb8c7de55c2..00000000000 --- a/lib/adodb/lang/adodb-cn.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - 'cn', - DB_ERROR => 'δ֪', - DB_ERROR_ALREADY_EXISTS => 'Ѿ', - DB_ERROR_CANNOT_CREATE => 'ܴ', - DB_ERROR_CANNOT_DELETE => 'ɾ', - DB_ERROR_CANNOT_DROP => 'ܶ', - DB_ERROR_CONSTRAINT => 'Լ', - DB_ERROR_DIVZERO => '0', - DB_ERROR_INVALID => 'Ч', - DB_ERROR_INVALID_DATE => 'Чڻʱ', - DB_ERROR_INVALID_NUMBER => 'Ч', - DB_ERROR_MISMATCH => 'ƥ', - DB_ERROR_NODBSELECTED => 'ûݿⱻѡ', - DB_ERROR_NOSUCHFIELD => 'ûӦֶ', - DB_ERROR_NOSUCHTABLE => 'ûӦı', - DB_ERROR_NOT_CAPABLE => 'ݿ̨', - DB_ERROR_NOT_FOUND => 'ûз', - DB_ERROR_NOT_LOCKED => 'ûб', - DB_ERROR_SYNTAX => '﷨', - DB_ERROR_UNSUPPORTED => '֧', - DB_ERROR_VALUE_COUNT_ON_ROW => 'ۼֵ', - DB_ERROR_INVALID_DSN => 'ЧԴ (DSN)', - DB_ERROR_CONNECT_FAILED => 'ʧ', - 0 => 'ûд', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'ṩݲܷҪ', - DB_ERROR_EXTENSION_NOT_FOUND=> 'չûб', - DB_ERROR_NOSUCHDB => 'ûӦݿ', - DB_ERROR_ACCESS_VIOLATION => 'ûкʵȨ' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-cz.inc.php b/lib/adodb/lang/adodb-cz.inc.php deleted file mode 100644 index 2424c2446b8..00000000000 --- a/lib/adodb/lang/adodb-cz.inc.php +++ /dev/null @@ -1,40 +0,0 @@ - - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'cz', - DB_ERROR => 'neznm chyba', - DB_ERROR_ALREADY_EXISTS => 'ji? existuje', - DB_ERROR_CANNOT_CREATE => 'nelze vytvo?it', - DB_ERROR_CANNOT_DELETE => 'nelze smazat', - DB_ERROR_CANNOT_DROP => 'nelze odstranit', - DB_ERROR_CONSTRAINT => 'poru?en omezujc podmnky', - DB_ERROR_DIVZERO => 'd?len nulou', - DB_ERROR_INVALID => 'neplatn', - DB_ERROR_INVALID_DATE => 'neplatn datum nebo ?as', - DB_ERROR_INVALID_NUMBER => 'neplatn ?slo', - DB_ERROR_MISMATCH => 'nesouhlas', - DB_ERROR_NODBSELECTED => '?dn databze nen vybrna', - DB_ERROR_NOSUCHFIELD => 'pole nenalezeno', - DB_ERROR_NOSUCHTABLE => 'tabulka nenalezena', - DB_ERROR_NOT_CAPABLE => 'nepodporovno', - DB_ERROR_NOT_FOUND => 'nenalezeno', - DB_ERROR_NOT_LOCKED => 'nezam?eno', - DB_ERROR_SYNTAX => 'syntaktick chyba', - DB_ERROR_UNSUPPORTED => 'nepodporovno', - DB_ERROR_VALUE_COUNT_ON_ROW => '', - DB_ERROR_INVALID_DSN => 'neplatn DSN', - DB_ERROR_CONNECT_FAILED => 'p?ipojen selhalo', - 0 => 'bez chyb', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'mlo zdrojovch dat', - DB_ERROR_EXTENSION_NOT_FOUND=> 'roz??en nenalezeno', - DB_ERROR_NOSUCHDB => 'databze neexistuje', - DB_ERROR_ACCESS_VIOLATION => 'nedostate?n prva' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-da.inc.php b/lib/adodb/lang/adodb-da.inc.php deleted file mode 100644 index ca0e72d6148..00000000000 --- a/lib/adodb/lang/adodb-da.inc.php +++ /dev/null @@ -1,33 +0,0 @@ - 'da', - DB_ERROR => 'ukendt fejl', - DB_ERROR_ALREADY_EXISTS => 'eksisterer allerede', - DB_ERROR_CANNOT_CREATE => 'kan ikke oprette', - DB_ERROR_CANNOT_DELETE => 'kan ikke slette', - DB_ERROR_CANNOT_DROP => 'kan ikke droppe', - DB_ERROR_CONSTRAINT => 'begrænsning krænket', - DB_ERROR_DIVZERO => 'division med nul', - DB_ERROR_INVALID => 'ugyldig', - DB_ERROR_INVALID_DATE => 'ugyldig dato eller klokkeslet', - DB_ERROR_INVALID_NUMBER => 'ugyldigt tal', - DB_ERROR_MISMATCH => 'mismatch', - DB_ERROR_NODBSELECTED => 'ingen database valgt', - DB_ERROR_NOSUCHFIELD => 'felt findes ikke', - DB_ERROR_NOSUCHTABLE => 'tabel findes ikke', - DB_ERROR_NOT_CAPABLE => 'DB backend opgav', - DB_ERROR_NOT_FOUND => 'ikke fundet', - DB_ERROR_NOT_LOCKED => 'ikke låst', - DB_ERROR_SYNTAX => 'syntaksfejl', - DB_ERROR_UNSUPPORTED => 'ikke understøttet', - DB_ERROR_VALUE_COUNT_ON_ROW => 'resulterende antal felter svarer ikke til forespørgslens antal felter', - DB_ERROR_INVALID_DSN => 'ugyldig DSN', - DB_ERROR_CONNECT_FAILED => 'tilslutning mislykkedes', - 0 => 'ingen fejl', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'utilstrækkelige data angivet', - DB_ERROR_EXTENSION_NOT_FOUND=> 'udvidelse ikke fundet', - DB_ERROR_NOSUCHDB => 'database ikke fundet', - DB_ERROR_ACCESS_VIOLATION => 'utilstrækkelige rettigheder' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-de.inc.php b/lib/adodb/lang/adodb-de.inc.php deleted file mode 100644 index 244cb2f66ae..00000000000 --- a/lib/adodb/lang/adodb-de.inc.php +++ /dev/null @@ -1,33 +0,0 @@ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'de', - DB_ERROR => 'Unbekannter Fehler', - DB_ERROR_ALREADY_EXISTS => 'existiert bereits', - DB_ERROR_CANNOT_CREATE => 'kann nicht erstellen', - DB_ERROR_CANNOT_DELETE => 'kann nicht löschen', - DB_ERROR_CANNOT_DROP => 'Tabelle oder Index konnte nicht gelöscht werden', - DB_ERROR_CONSTRAINT => 'Constraint Verletzung', - DB_ERROR_DIVZERO => 'Division durch Null', - DB_ERROR_INVALID => 'ung¨ltig', - DB_ERROR_INVALID_DATE => 'ung¨ltiges Datum oder Zeit', - DB_ERROR_INVALID_NUMBER => 'ung¨ltige Zahl', - DB_ERROR_MISMATCH => 'Unverträglichkeit', - DB_ERROR_NODBSELECTED => 'keine Dantebank ausgewählt', - DB_ERROR_NOSUCHFIELD => 'Feld nicht vorhanden', - DB_ERROR_NOSUCHTABLE => 'Tabelle nicht vorhanden', - DB_ERROR_NOT_CAPABLE => 'Funktion nicht installiert', - DB_ERROR_NOT_FOUND => 'nicht gefunden', - DB_ERROR_NOT_LOCKED => 'nicht gesperrt', - DB_ERROR_SYNTAX => 'Syntaxfehler', - DB_ERROR_UNSUPPORTED => 'nicht Unterst¨tzt', - DB_ERROR_VALUE_COUNT_ON_ROW => 'Anzahl der zur¨ckgelieferten Felder entspricht nicht der Anzahl der Felder in der Abfrage', - DB_ERROR_INVALID_DSN => 'ung¨ltiger DSN', - DB_ERROR_CONNECT_FAILED => 'Verbindung konnte nicht hergestellt werden', - 0 => 'kein Fehler', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'Nicht gen¨gend Daten geliefert', - DB_ERROR_EXTENSION_NOT_FOUND=> 'erweiterung nicht gefunden', - DB_ERROR_NOSUCHDB => 'keine Datenbank', - DB_ERROR_ACCESS_VIOLATION => 'ungen¨gende Rechte' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-es.inc.php b/lib/adodb/lang/adodb-es.inc.php deleted file mode 100644 index 1e0afbb40d9..00000000000 --- a/lib/adodb/lang/adodb-es.inc.php +++ /dev/null @@ -1,33 +0,0 @@ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'es', - DB_ERROR => 'error desconocido', - DB_ERROR_ALREADY_EXISTS => 'ya existe', - DB_ERROR_CANNOT_CREATE => 'imposible crear', - DB_ERROR_CANNOT_DELETE => 'imposible borrar', - DB_ERROR_CANNOT_DROP => 'imposible hacer drop', - DB_ERROR_CONSTRAINT => 'violacion de constraint', - DB_ERROR_DIVZERO => 'division por cero', - DB_ERROR_INVALID => 'invalido', - DB_ERROR_INVALID_DATE => 'fecha u hora invalida', - DB_ERROR_INVALID_NUMBER => 'numero invalido', - DB_ERROR_MISMATCH => 'error', - DB_ERROR_NODBSELECTED => 'no hay base de datos seleccionada', - DB_ERROR_NOSUCHFIELD => 'campo invalido', - DB_ERROR_NOSUCHTABLE => 'tabla no existe', - DB_ERROR_NOT_CAPABLE => 'capacidad invalida para esta DB', - DB_ERROR_NOT_FOUND => 'no encontrado', - DB_ERROR_NOT_LOCKED => 'no bloqueado', - DB_ERROR_SYNTAX => 'error de sintaxis', - DB_ERROR_UNSUPPORTED => 'no soportado', - DB_ERROR_VALUE_COUNT_ON_ROW => 'la cantidad de columnas no corresponden a la cantidad de valores', - DB_ERROR_INVALID_DSN => 'DSN invalido', - DB_ERROR_CONNECT_FAILED => 'fallo la conexion', - 0 => 'sin error', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'insuficientes datos', - DB_ERROR_EXTENSION_NOT_FOUND=> 'extension no encontrada', - DB_ERROR_NOSUCHDB => 'base de datos no encontrada', - DB_ERROR_ACCESS_VIOLATION => 'permisos insuficientes' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-esperanto.inc.php b/lib/adodb/lang/adodb-esperanto.inc.php deleted file mode 100644 index 16ca00e2fac..00000000000 --- a/lib/adodb/lang/adodb-esperanto.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - 'eo', - DB_ERROR => 'nekonata eraro', - DB_ERROR_ALREADY_EXISTS => 'jam ekzistas', - DB_ERROR_CANNOT_CREATE => 'maleblas krei', - DB_ERROR_CANNOT_DELETE => 'maleblas elimini', - DB_ERROR_CANNOT_DROP => 'maleblas elimini (drop)', - DB_ERROR_CONSTRAINT => 'rompo de kondicxoj de provo', - DB_ERROR_DIVZERO => 'divido per 0 (nul)', - DB_ERROR_INVALID => 'malregule', - DB_ERROR_INVALID_DATE => 'malregula dato kaj tempo', - DB_ERROR_INVALID_NUMBER => 'malregula nombro', - DB_ERROR_MISMATCH => 'eraro', - DB_ERROR_NODBSELECTED => 'datumbazo ne elektita', - DB_ERROR_NOSUCHFIELD => 'ne ekzistas kampo', - DB_ERROR_NOSUCHTABLE => 'ne ekzistas tabelo', - DB_ERROR_NOT_CAPABLE => 'DBMS ne povas', - DB_ERROR_NOT_FOUND => 'ne trovita', - DB_ERROR_NOT_LOCKED => 'ne blokita', - DB_ERROR_SYNTAX => 'sintaksa eraro', - DB_ERROR_UNSUPPORTED => 'ne apogata', - DB_ERROR_VALUE_COUNT_ON_ROW => 'nombrilo de valoroj en linio', - DB_ERROR_INVALID_DSN => 'malregula DSN-o', - DB_ERROR_CONNECT_FAILED => 'konekto malsukcesa', - 0 => 'cxio bone', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'ne suficxe da datumo', - DB_ERROR_EXTENSION_NOT_FOUND=> 'etendo ne trovita', - DB_ERROR_NOSUCHDB => 'datumbazo ne ekzistas', - DB_ERROR_ACCESS_VIOLATION => 'ne suficxe da rajto por atingo' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-fa.inc.php b/lib/adodb/lang/adodb-fa.inc.php deleted file mode 100644 index 5594313575e..00000000000 --- a/lib/adodb/lang/adodb-fa.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - */ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'fa', - DB_ERROR => 'خطای ناشناخته', - DB_ERROR_ALREADY_EXISTS => 'وجود دارد', - DB_ERROR_CANNOT_CREATE => 'امکان create وجود ندارد', - DB_ERROR_CANNOT_DELETE => 'امکان حذف وجود ندارد', - DB_ERROR_CANNOT_DROP => 'امکان drop وجود ندارد', - DB_ERROR_CONSTRAINT => 'نقض شرط', - DB_ERROR_DIVZERO => 'تقسیم بر صفر', - DB_ERROR_INVALID => 'نامعتبر', - DB_ERROR_INVALID_DATE => 'زمان یا تاریخ نامعتبر', - DB_ERROR_INVALID_NUMBER => 'عدد نامعتبر', - DB_ERROR_MISMATCH => 'عدم مطابقت', - DB_ERROR_NODBSELECTED => 'بانک اطلاعاتی انتخاب نشده است', - DB_ERROR_NOSUCHFIELD => 'چنین ستونی وجود ندارد', - DB_ERROR_NOSUCHTABLE => 'چنین جدولی وجود ندارد', - DB_ERROR_NOT_CAPABLE => 'backend بانک اطلاعاتی قادر نیست', - DB_ERROR_NOT_FOUND => 'پیدا نشد', - DB_ERROR_NOT_LOCKED => 'قفل نشده', - DB_ERROR_SYNTAX => 'خطای دستوری', - DB_ERROR_UNSUPPORTED => 'پشتیبانی نمی شود', - DB_ERROR_VALUE_COUNT_ON_ROW => 'شمارش مقادیر روی ردیف', - DB_ERROR_INVALID_DSN => 'DSN نامعتبر', - DB_ERROR_CONNECT_FAILED => 'ارتباط برقرار نشد', - 0 => 'بدون خطا', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'داده ناکافی است', - DB_ERROR_EXTENSION_NOT_FOUND=> 'extension پیدا نشد', - DB_ERROR_NOSUCHDB => 'چنین بانک اطلاعاتی وجود ندارد', - DB_ERROR_ACCESS_VIOLATION => 'حق دسترسی ناکافی' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-fr.inc.php b/lib/adodb/lang/adodb-fr.inc.php deleted file mode 100644 index 066a2a5e5b7..00000000000 --- a/lib/adodb/lang/adodb-fr.inc.php +++ /dev/null @@ -1,33 +0,0 @@ - 'fr', - DB_ERROR => 'erreur inconnue', - DB_ERROR_ALREADY_EXISTS => 'existe déjà', - DB_ERROR_CANNOT_CREATE => 'crétion impossible', - DB_ERROR_CANNOT_DELETE => 'effacement impossible', - DB_ERROR_CANNOT_DROP => 'suppression impossible', - DB_ERROR_CONSTRAINT => 'violation de contrainte', - DB_ERROR_DIVZERO => 'division par zéro', - DB_ERROR_INVALID => 'invalide', - DB_ERROR_INVALID_DATE => 'date ou heure invalide', - DB_ERROR_INVALID_NUMBER => 'nombre invalide', - DB_ERROR_MISMATCH => 'erreur de concordance', - DB_ERROR_NODBSELECTED => 'pas de base de donnéessélectionnée', - DB_ERROR_NOSUCHFIELD => 'nom de colonne invalide', - DB_ERROR_NOSUCHTABLE => 'table ou vue inexistante', - DB_ERROR_NOT_CAPABLE => 'fonction optionnelle non installée', - DB_ERROR_NOT_FOUND => 'pas trouvé', - DB_ERROR_NOT_LOCKED => 'non verrouillé', - DB_ERROR_SYNTAX => 'erreur de syntaxe', - DB_ERROR_UNSUPPORTED => 'non supporté', - DB_ERROR_VALUE_COUNT_ON_ROW => 'valeur insérée trop grande pour colonne', - DB_ERROR_INVALID_DSN => 'DSN invalide', - DB_ERROR_CONNECT_FAILED => 'échec à la connexion', - 0 => "pas d'erreur", // DB_OK - DB_ERROR_NEED_MORE_DATA => 'données fournies insuffisantes', - DB_ERROR_EXTENSION_NOT_FOUND=> 'extension non trouvée', - DB_ERROR_NOSUCHDB => 'base de données inconnue', - DB_ERROR_ACCESS_VIOLATION => 'droits insuffisants' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-hu.inc.php b/lib/adodb/lang/adodb-hu.inc.php deleted file mode 100644 index d6f0ef82da6..00000000000 --- a/lib/adodb/lang/adodb-hu.inc.php +++ /dev/null @@ -1,34 +0,0 @@ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'hu', - DB_ERROR => 'ismeretlen hiba', - DB_ERROR_ALREADY_EXISTS => 'mr ltezik', - DB_ERROR_CANNOT_CREATE => 'nem sikerlt ltrehozni', - DB_ERROR_CANNOT_DELETE => 'nem sikerlt trlni', - DB_ERROR_CANNOT_DROP => 'nem sikerlt eldobni', - DB_ERROR_CONSTRAINT => 'szablyok megszegse', - DB_ERROR_DIVZERO => 'oszts nullval', - DB_ERROR_INVALID => 'rvnytelen', - DB_ERROR_INVALID_DATE => 'rvnytelen dtum vagy id', - DB_ERROR_INVALID_NUMBER => 'rvnytelen szm', - DB_ERROR_MISMATCH => 'nem megfelel', - DB_ERROR_NODBSELECTED => 'nincs kivlasztott adatbzis', - DB_ERROR_NOSUCHFIELD => 'nincs ilyen mez', - DB_ERROR_NOSUCHTABLE => 'nincs ilyen tbla', - DB_ERROR_NOT_CAPABLE => 'DB backend nem tmogatja', - DB_ERROR_NOT_FOUND => 'nem tallhat', - DB_ERROR_NOT_LOCKED => 'nincs lezrva', - DB_ERROR_SYNTAX => 'szintaktikai hiba', - DB_ERROR_UNSUPPORTED => 'nem tmogatott', - DB_ERROR_VALUE_COUNT_ON_ROW => 'soron vgzett rtk szmlls', - DB_ERROR_INVALID_DSN => 'hibs DSN', - DB_ERROR_CONNECT_FAILED => 'sikertelen csatlakozs', - 0 => 'nincs hiba', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'tl kevs az adat', - DB_ERROR_EXTENSION_NOT_FOUND=> 'bvtmny nem tallhat', - DB_ERROR_NOSUCHDB => 'nincs ilyen adatbzis', - DB_ERROR_ACCESS_VIOLATION => 'nincs jogosultsg' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-it.inc.php b/lib/adodb/lang/adodb-it.inc.php deleted file mode 100644 index 20c5b93b630..00000000000 --- a/lib/adodb/lang/adodb-it.inc.php +++ /dev/null @@ -1,34 +0,0 @@ - 'it', - DB_ERROR => 'errore sconosciuto', - DB_ERROR_ALREADY_EXISTS => 'esiste già', - DB_ERROR_CANNOT_CREATE => 'non posso creare', - DB_ERROR_CANNOT_DELETE => 'non posso cancellare', - DB_ERROR_CANNOT_DROP => 'non posso eliminare', - DB_ERROR_CONSTRAINT => 'violazione constraint', - DB_ERROR_DIVZERO => 'divisione per zero', - DB_ERROR_INVALID => 'non valido', - DB_ERROR_INVALID_DATE => 'data od ora non valida', - DB_ERROR_INVALID_NUMBER => 'numero non valido', - DB_ERROR_MISMATCH => 'diversi', - DB_ERROR_NODBSELECTED => 'nessun database selezionato', - DB_ERROR_NOSUCHFIELD => 'nessun campo trovato', - DB_ERROR_NOSUCHTABLE => 'nessuna tabella trovata', - DB_ERROR_NOT_CAPABLE => 'DB backend non abilitato', - DB_ERROR_NOT_FOUND => 'non trovato', - DB_ERROR_NOT_LOCKED => 'non bloccato', - DB_ERROR_SYNTAX => 'errore di sintassi', - DB_ERROR_UNSUPPORTED => 'non supportato', - DB_ERROR_VALUE_COUNT_ON_ROW => 'valore inserito troppo grande per una colonna', - DB_ERROR_INVALID_DSN => 'DSN non valido', - DB_ERROR_CONNECT_FAILED => 'connessione fallita', - 0 => 'nessun errore', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'dati inseriti insufficienti', - DB_ERROR_EXTENSION_NOT_FOUND=> 'estensione non trovata', - DB_ERROR_NOSUCHDB => 'database non trovato', - DB_ERROR_ACCESS_VIOLATION => 'permessi insufficienti' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-nl.inc.php b/lib/adodb/lang/adodb-nl.inc.php deleted file mode 100644 index abe77b5282e..00000000000 --- a/lib/adodb/lang/adodb-nl.inc.php +++ /dev/null @@ -1,33 +0,0 @@ - 'nl', - DB_ERROR => 'onbekende fout', - DB_ERROR_ALREADY_EXISTS => 'bestaat al', - DB_ERROR_CANNOT_CREATE => 'kan niet aanmaken', - DB_ERROR_CANNOT_DELETE => 'kan niet wissen', - DB_ERROR_CANNOT_DROP => 'kan niet verwijderen', - DB_ERROR_CONSTRAINT => 'constraint overtreding', - DB_ERROR_DIVZERO => 'poging tot delen door nul', - DB_ERROR_INVALID => 'ongeldig', - DB_ERROR_INVALID_DATE => 'ongeldige datum of tijd', - DB_ERROR_INVALID_NUMBER => 'ongeldig nummer', - DB_ERROR_MISMATCH => 'is incorrect', - DB_ERROR_NODBSELECTED => 'geen database geselecteerd', - DB_ERROR_NOSUCHFIELD => 'onbekend veld', - DB_ERROR_NOSUCHTABLE => 'onbekende tabel', - DB_ERROR_NOT_CAPABLE => 'database systeem is niet tot uitvoer in staat', - DB_ERROR_NOT_FOUND => 'niet gevonden', - DB_ERROR_NOT_LOCKED => 'niet vergrendeld', - DB_ERROR_SYNTAX => 'syntaxis fout', - DB_ERROR_UNSUPPORTED => 'niet ondersteund', - DB_ERROR_VALUE_COUNT_ON_ROW => 'waarde telling op rij', - DB_ERROR_INVALID_DSN => 'ongeldige DSN', - DB_ERROR_CONNECT_FAILED => 'connectie mislukt', - 0 => 'geen fout', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'onvoldoende data gegeven', - DB_ERROR_EXTENSION_NOT_FOUND=> 'extensie niet gevonden', - DB_ERROR_NOSUCHDB => 'onbekende database', - DB_ERROR_ACCESS_VIOLATION => 'onvoldoende rechten' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-pl.inc.php b/lib/adodb/lang/adodb-pl.inc.php deleted file mode 100644 index 9d9e3906762..00000000000 --- a/lib/adodb/lang/adodb-pl.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'pl', - DB_ERROR => 'niezidentyfikowany bd', - DB_ERROR_ALREADY_EXISTS => 'ju istniej', - DB_ERROR_CANNOT_CREATE => 'nie mona stworzy', - DB_ERROR_CANNOT_DELETE => 'nie mona usun', - DB_ERROR_CANNOT_DROP => 'nie mona porzuci', - DB_ERROR_CONSTRAINT => 'pogwacenie uprawnie', - DB_ERROR_DIVZERO => 'dzielenie przez zero', - DB_ERROR_INVALID => 'bdny', - DB_ERROR_INVALID_DATE => 'bdna godzina lub data', - DB_ERROR_INVALID_NUMBER => 'bdny numer', - DB_ERROR_MISMATCH => 'niedopasowanie', - DB_ERROR_NODBSELECTED => 'baza danych nie zostaa wybrana', - DB_ERROR_NOSUCHFIELD => 'nie znaleziono pola', - DB_ERROR_NOSUCHTABLE => 'nie znaleziono tabeli', - DB_ERROR_NOT_CAPABLE => 'nie zdolny', - DB_ERROR_NOT_FOUND => 'nie znaleziono', - DB_ERROR_NOT_LOCKED => 'nie zakmnity', - DB_ERROR_SYNTAX => 'bd skadni', - DB_ERROR_UNSUPPORTED => 'nie obsuguje', - DB_ERROR_VALUE_COUNT_ON_ROW => 'warto liczona w szeregu', - DB_ERROR_INVALID_DSN => 'bdny DSN', - DB_ERROR_CONNECT_FAILED => 'poczenie nie zostao zrealizowane', - 0 => 'brak bdw', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'niedostateczna ilo informacji', - DB_ERROR_EXTENSION_NOT_FOUND=> 'nie znaleziono rozszerzenia', - DB_ERROR_NOSUCHDB => 'nie znaleziono bazy', - DB_ERROR_ACCESS_VIOLATION => 'niedostateczne uprawnienia' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-pt-br.inc.php b/lib/adodb/lang/adodb-pt-br.inc.php deleted file mode 100644 index cd28f7e55ca..00000000000 --- a/lib/adodb/lang/adodb-pt-br.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - 'pt-br', - DB_ERROR => 'erro desconhecido', - DB_ERROR_ALREADY_EXISTS => 'j existe', - DB_ERROR_CANNOT_CREATE => 'impossvel criar', - DB_ERROR_CANNOT_DELETE => 'impossvel exclur', - DB_ERROR_CANNOT_DROP => 'impossvel remover', - DB_ERROR_CONSTRAINT => 'violao do confinamente', - DB_ERROR_DIVZERO => 'diviso por zero', - DB_ERROR_INVALID => 'invlido', - DB_ERROR_INVALID_DATE => 'data ou hora invlida', - DB_ERROR_INVALID_NUMBER => 'nmero invlido', - DB_ERROR_MISMATCH => 'erro', - DB_ERROR_NODBSELECTED => 'nenhum banco de dados selecionado', - DB_ERROR_NOSUCHFIELD => 'campo invlido', - DB_ERROR_NOSUCHTABLE => 'tabela inexistente', - DB_ERROR_NOT_CAPABLE => 'capacidade invlida para este BD', - DB_ERROR_NOT_FOUND => 'no encontrado', - DB_ERROR_NOT_LOCKED => 'no bloqueado', - DB_ERROR_SYNTAX => 'erro de sintaxe', - DB_ERROR_UNSUPPORTED => -'no suportado', - DB_ERROR_VALUE_COUNT_ON_ROW => 'a quantidade de colunas no corresponde ao de valores', - DB_ERROR_INVALID_DSN => 'DSN invlido', - DB_ERROR_CONNECT_FAILED => 'falha na conexo', - 0 => 'sem erro', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'dados insuficientes', - DB_ERROR_EXTENSION_NOT_FOUND=> 'extenso no encontrada', - DB_ERROR_NOSUCHDB => 'banco de dados no encontrado', - DB_ERROR_ACCESS_VIOLATION => 'permisso insuficiente' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-ro.inc.php b/lib/adodb/lang/adodb-ro.inc.php deleted file mode 100644 index bcd7d13228c..00000000000 --- a/lib/adodb/lang/adodb-ro.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - */ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'ro', - DB_ERROR => 'eroare necunoscuta', - DB_ERROR_ALREADY_EXISTS => 'deja exista', - DB_ERROR_CANNOT_CREATE => 'nu se poate creea', - DB_ERROR_CANNOT_DELETE => 'nu se poate sterge', - DB_ERROR_CANNOT_DROP => 'nu se poate executa drop', - DB_ERROR_CONSTRAINT => 'violare de constrain', - DB_ERROR_DIVZERO => 'se divide la zero', - DB_ERROR_INVALID => 'invalid', - DB_ERROR_INVALID_DATE => 'data sau timp invalide', - DB_ERROR_INVALID_NUMBER => 'numar invalid', - DB_ERROR_MISMATCH => 'nepotrivire-mismatch', - DB_ERROR_NODBSELECTED => 'nu exista baza de date selectata', - DB_ERROR_NOSUCHFIELD => 'camp inexistent', - DB_ERROR_NOSUCHTABLE => 'tabela inexistenta', - DB_ERROR_NOT_CAPABLE => 'functie optionala neinstalata', - DB_ERROR_NOT_FOUND => 'negasit', - DB_ERROR_NOT_LOCKED => 'neblocat', - DB_ERROR_SYNTAX => 'eroare de sintaxa', - DB_ERROR_UNSUPPORTED => 'nu e suportat', - DB_ERROR_VALUE_COUNT_ON_ROW => 'valoare prea mare pentru coloana', - DB_ERROR_INVALID_DSN => 'DSN invalid', - DB_ERROR_CONNECT_FAILED => 'conectare esuata', - 0 => 'fara eroare', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'data introduse insuficiente', - DB_ERROR_EXTENSION_NOT_FOUND=> 'extensie negasita', - DB_ERROR_NOSUCHDB => 'nu exista baza de date', - DB_ERROR_ACCESS_VIOLATION => 'permisiuni insuficiente' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-ru1251.inc.php b/lib/adodb/lang/adodb-ru1251.inc.php deleted file mode 100644 index 3a20538a020..00000000000 --- a/lib/adodb/lang/adodb-ru1251.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - 'ru1251', - DB_ERROR => ' ', - DB_ERROR_ALREADY_EXISTS => ' ', - DB_ERROR_CANNOT_CREATE => ' ', - DB_ERROR_CANNOT_DELETE => ' ', - DB_ERROR_CANNOT_DROP => ' (drop)', - DB_ERROR_CONSTRAINT => ' ', - DB_ERROR_DIVZERO => ' 0', - DB_ERROR_INVALID => '', - DB_ERROR_INVALID_DATE => ' ', - DB_ERROR_INVALID_NUMBER => ' ', - DB_ERROR_MISMATCH => '', - DB_ERROR_NODBSELECTED => ' ', - DB_ERROR_NOSUCHFIELD => ' ', - DB_ERROR_NOSUCHTABLE => ' ', - DB_ERROR_NOT_CAPABLE => ' ', - DB_ERROR_NOT_FOUND => ' ', - DB_ERROR_NOT_LOCKED => ' ', - DB_ERROR_SYNTAX => ' ', - DB_ERROR_UNSUPPORTED => ' ', - DB_ERROR_VALUE_COUNT_ON_ROW => ' ', - DB_ERROR_INVALID_DSN => ' DSN', - DB_ERROR_CONNECT_FAILED => ' ', - 0 => ' ', // DB_OK - DB_ERROR_NEED_MORE_DATA => ' ', - DB_ERROR_EXTENSION_NOT_FOUND=> ' ', - DB_ERROR_NOSUCHDB => ' ', - DB_ERROR_ACCESS_VIOLATION => ' ' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-sv.inc.php b/lib/adodb/lang/adodb-sv.inc.php deleted file mode 100644 index a9fd69816c4..00000000000 --- a/lib/adodb/lang/adodb-sv.inc.php +++ /dev/null @@ -1,33 +0,0 @@ - 'en', - DB_ERROR => 'Oknt fel', - DB_ERROR_ALREADY_EXISTS => 'finns redan', - DB_ERROR_CANNOT_CREATE => 'kan inte skapa', - DB_ERROR_CANNOT_DELETE => 'kan inte ta bort', - DB_ERROR_CANNOT_DROP => 'kan inte slppa', - DB_ERROR_CONSTRAINT => 'begrnsning krnkt', - DB_ERROR_DIVZERO => 'division med noll', - DB_ERROR_INVALID => 'ogiltig', - DB_ERROR_INVALID_DATE => 'ogiltigt datum eller tid', - DB_ERROR_INVALID_NUMBER => 'ogiltigt tal', - DB_ERROR_MISMATCH => 'felaktig matchning', - DB_ERROR_NODBSELECTED => 'ingen databas vald', - DB_ERROR_NOSUCHFIELD => 'inget sdant flt', - DB_ERROR_NOSUCHTABLE => 'ingen sdan tabell', - DB_ERROR_NOT_CAPABLE => 'DB backend klarar det inte', - DB_ERROR_NOT_FOUND => 'finns inte', - DB_ERROR_NOT_LOCKED => 'inte lst', - DB_ERROR_SYNTAX => 'syntaxfel', - DB_ERROR_UNSUPPORTED => 'stds ej', - DB_ERROR_VALUE_COUNT_ON_ROW => 'vrde rknat p rad', - DB_ERROR_INVALID_DSN => 'ogiltig DSN', - DB_ERROR_CONNECT_FAILED => 'anslutning misslyckades', - 0 => 'inget fel', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'otillrckligt med data angivet', - DB_ERROR_EXTENSION_NOT_FOUND=> 'utkning hittades ej', - DB_ERROR_NOSUCHDB => 'ingen sdan databas', - DB_ERROR_ACCESS_VIOLATION => 'otillrckliga rttigheter' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb-uk1251.inc.php b/lib/adodb/lang/adodb-uk1251.inc.php deleted file mode 100644 index 675016d125e..00000000000 --- a/lib/adodb/lang/adodb-uk1251.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - 'uk1251', - DB_ERROR => ' ', - DB_ERROR_ALREADY_EXISTS => ' ', - DB_ERROR_CANNOT_CREATE => ' ', - DB_ERROR_CANNOT_DELETE => ' ', - DB_ERROR_CANNOT_DROP => ' (drop)', - DB_ERROR_CONSTRAINT => ' ', - DB_ERROR_DIVZERO => ' 0', - DB_ERROR_INVALID => '', - DB_ERROR_INVALID_DATE => ' ', - DB_ERROR_INVALID_NUMBER => ' ', - DB_ERROR_MISMATCH => '', - DB_ERROR_NODBSELECTED => ' ', - DB_ERROR_NOSUCHFIELD => ' ', - DB_ERROR_NOSUCHTABLE => ' ', - DB_ERROR_NOT_CAPABLE => ' ', - DB_ERROR_NOT_FOUND => ' ', - DB_ERROR_NOT_LOCKED => ' ', - DB_ERROR_SYNTAX => ' ', - DB_ERROR_UNSUPPORTED => ' ', - DB_ERROR_VALUE_COUNT_ON_ROW => ' ', - DB_ERROR_INVALID_DSN => ' DSN', - DB_ERROR_CONNECT_FAILED => '\' ', - 0 => ' ', // DB_OK - DB_ERROR_NEED_MORE_DATA => ' ', - DB_ERROR_EXTENSION_NOT_FOUND=> ' ', - DB_ERROR_NOSUCHDB => ' ', - DB_ERROR_ACCESS_VIOLATION => ' ' -); -?> \ No newline at end of file diff --git a/lib/adodb/lang/adodb_th.inc.php b/lib/adodb/lang/adodb_th.inc.php deleted file mode 100644 index 3fdd99705f7..00000000000 --- a/lib/adodb/lang/adodb_th.inc.php +++ /dev/null @@ -1,33 +0,0 @@ - -$ADODB_LANG_ARRAY = array ( - 'LANG' => 'th', - DB_ERROR => 'error ไม่รู้สาเหตุ', - DB_ERROR_ALREADY_EXISTS => 'มี?ล้ว', - DB_ERROR_CANNOT_CREATE => 'สร้างไม่ได้', - DB_ERROR_CANNOT_DELETE => 'ลบไม่ได้', - DB_ERROR_CANNOT_DROP => 'drop ไม่ได้', - DB_ERROR_CONSTRAINT => 'constraint violation', - DB_ERROR_DIVZERO => 'หา?ด้วยสู?', - DB_ERROR_INVALID => 'ไม่ valid', - DB_ERROR_INVALID_DATE => 'วันที่ เวลา ไม่ valid', - DB_ERROR_INVALID_NUMBER => 'เลขไม่ valid', - DB_ERROR_MISMATCH => 'mismatch', - DB_ERROR_NODBSELECTED => 'ไม่ได้เลือ??านข้อมูล', - DB_ERROR_NOSUCHFIELD => 'ไม่มีฟีลด์นี้', - DB_ERROR_NOSUCHTABLE => 'ไม่มีตารางนี้', - DB_ERROR_NOT_CAPABLE => 'DB backend not capable', - DB_ERROR_NOT_FOUND => 'ไม่พบ', - DB_ERROR_NOT_LOCKED => 'ไม่ได้ล๊อ?', - DB_ERROR_SYNTAX => 'ผิด syntax', - DB_ERROR_UNSUPPORTED => 'ไม่ support', - DB_ERROR_VALUE_COUNT_ON_ROW => 'value count on row', - DB_ERROR_INVALID_DSN => 'invalid DSN', - DB_ERROR_CONNECT_FAILED => 'ไม่สามารถ connect', - 0 => 'no error', // DB_OK - DB_ERROR_NEED_MORE_DATA => 'ข้อมูลไม่เพียงพอ', - DB_ERROR_EXTENSION_NOT_FOUND=> 'ไม่พบ extension', - DB_ERROR_NOSUCHDB => 'ไม่มีข้อมูลนี้', - DB_ERROR_ACCESS_VIOLATION => 'permissions ไม่พอ' -); -?> \ No newline at end of file diff --git a/lib/adodb/perf/perf-db2.inc.php b/lib/adodb/perf/perf-db2.inc.php index 3823420a635..94ac8564a1a 100644 --- a/lib/adodb/perf/perf-db2.inc.php +++ b/lib/adodb/perf/perf-db2.inc.php @@ -1,6 +1,6 @@ adodb AdoDB GPL/BSD - 5.16 + 5.17 2.1+
    From 259208e0abafe5f0aa93ce4c7769a94b4030c17b Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Tue, 4 Sep 2012 01:27:49 +0200 Subject: [PATCH 70/96] MDL-31295 assign: fix comments in upgrade code. --- mod/assign/db/upgrade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod/assign/db/upgrade.php b/mod/assign/db/upgrade.php index 07a91a8f8f4..d0b4d3ee65a 100644 --- a/mod/assign/db/upgrade.php +++ b/mod/assign/db/upgrade.php @@ -83,12 +83,12 @@ function xmldb_assign_upgrade($oldversion) { // Individual extension dates support. if ($oldversion < 2012082100) { - // Define field sendlatenotifications to be added to assign. + // Define field cutoffdate to be added to assign. $table = new xmldb_table('assign'); $field = new xmldb_field('cutoffdate', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'completionsubmit'); - // Conditionally launch add field sendlatenotifications. + // Conditionally launch add field cutoffdate. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } From c3a0938e790eeb670f28ab20f87be400368ea5b4 Mon Sep 17 00:00:00 2001 From: Tim Lock Date: Tue, 28 Aug 2012 13:57:35 +0930 Subject: [PATCH 71/96] MDL-35092: Add proxy support to enrol/paypal IPN --- enrol/paypal/ipn.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/enrol/paypal/ipn.php b/enrol/paypal/ipn.php index 2a28c245e90..67fb06fa997 100644 --- a/enrol/paypal/ipn.php +++ b/enrol/paypal/ipn.php @@ -34,6 +34,7 @@ require("../../config.php"); require_once("lib.php"); require_once($CFG->libdir.'/eventslib.php'); require_once($CFG->libdir.'/enrollib.php'); +require_once($CFG->libdir . '/filelib.php'); /// Keep out casual intruders @@ -89,14 +90,17 @@ if (! $plugin_instance = $DB->get_record("enrol", array("id"=>$data->instanceid, $plugin = enrol_get_plugin('paypal'); /// Open a connection back to PayPal to validate the data -$header = ''; -$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n"; -$header .= "Content-Type: application/x-www-form-urlencoded\r\n"; -$header .= "Content-Length: " . strlen($req) . "\r\n\r\n"; +$c = new curl(); +$options = array( + 'returntransfer' => true, + 'httpheader' => array('application/x-www-form-urlencoded'), + 'timeout' => 30, +); $paypaladdr = empty($CFG->usepaypalsandbox) ? 'www.paypal.com' : 'www.sandbox.paypal.com'; -$fp = fsockopen ($paypaladdr, 80, $errno, $errstr, 30); +$location = "https://$paypaladdr/cgi-bin/webscr"; +$result = $c->post($location, $req, $options); -if (!$fp) { /// Could not open a socket to PayPal - FAIL +if (!$result) { /// Could not connect to PayPal - FAIL echo "

    Error: could not access paypal.com

    "; message_paypal_error_to_admin("Could not access paypal.com to verify payment", $data); die; @@ -104,12 +108,9 @@ if (!$fp) { /// Could not open a socket to PayPal - FAIL /// Connection is OK, so now we post the data to validate it -fputs ($fp, $header.$req); - /// Now read the response and check if everything is OK. -while (!feof($fp)) { - $result = fgets($fp, 1024); +if (strlen($result) > 0) { if (strcmp($result, "VERIFIED") == 0) { // VALID PAYMENT! @@ -296,7 +297,6 @@ while (!feof($fp)) { } } -fclose($fp); exit; From 0e708c34760ee274990e1c5bad96583301a6cc79 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 12 Jul 2012 18:39:01 +0100 Subject: [PATCH 72/96] MDL-34257 quiz 'secure' mode: PAGE initialisation order issues. --- mod/quiz/attempt.php | 4 ++-- mod/quiz/review.php | 3 ++- mod/quiz/summary.php | 4 ++-- theme/base/config.php | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index a24245dd215..d3f350fd893 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -76,8 +76,9 @@ if ($attemptobj->is_finished()) { // Check the access rules. $accessmanager = $attemptobj->get_access_manager(time()); -$messages = $accessmanager->prevent_access(); +$accessmanager->setup_attempt_page($PAGE); $output = $PAGE->get_renderer('mod_quiz'); +$messages = $accessmanager->prevent_access(); if (!$attemptobj->is_preview_user() && $messages) { print_error('attempterror', 'quiz', $attemptobj->view_url(), $output->access_messages($messages)); @@ -120,7 +121,6 @@ $title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number()); $headtags = $attemptobj->get_html_head_contributions($page); $PAGE->set_title(format_string($attemptobj->get_quiz_name())); $PAGE->set_heading($attemptobj->get_course()->fullname); -$accessmanager->setup_attempt_page($PAGE); if ($attemptobj->is_last_page($page)) { $nextpage = -1; diff --git a/mod/quiz/review.php b/mod/quiz/review.php index c19afaa8970..eacc703e27b 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -52,6 +52,8 @@ $attemptobj->check_review_capability(); // Create an object to manage all the other (non-roles) access rules. $accessmanager = $attemptobj->get_access_manager(time()); +$accessmanager->setup_attempt_page($PAGE); + $options = $attemptobj->get_display_options(true); // Check permissions. @@ -103,7 +105,6 @@ if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) { $headtags = $attemptobj->get_html_head_contributions($page, $showall); $PAGE->set_title(format_string($attemptobj->get_quiz_name())); $PAGE->set_heading($attemptobj->get_course()->fullname); -$accessmanager->setup_attempt_page($PAGE); // Summary table start. ============================================================================ diff --git a/mod/quiz/summary.php b/mod/quiz/summary.php index 303638f013c..822e30777d0 100644 --- a/mod/quiz/summary.php +++ b/mod/quiz/summary.php @@ -52,8 +52,9 @@ if ($attemptobj->is_preview_user()) { // Check access. $accessmanager = $attemptobj->get_access_manager(time()); -$messages = $accessmanager->prevent_access(); +$accessmanager->setup_attempt_page($PAGE); $output = $PAGE->get_renderer('mod_quiz'); +$messages = $accessmanager->prevent_access(); if (!$attemptobj->is_preview_user() && $messages) { print_error('attempterror', 'quiz', $attemptobj->view_url(), $output->access_messages($messages)); @@ -89,7 +90,6 @@ $PAGE->blocks->add_fake_block($navbc, reset($regions)); $PAGE->navbar->add(get_string('summaryofattempt', 'quiz')); $PAGE->set_title(format_string($attemptobj->get_quiz_name())); $PAGE->set_heading($attemptobj->get_course()->fullname); -$accessmanager->setup_attempt_page($PAGE); // Display the page. echo $output->summary_page($attemptobj, $displayoptions); diff --git a/theme/base/config.php b/theme/base/config.php index 1316822f7ca..cafa7fb3831 100644 --- a/theme/base/config.php +++ b/theme/base/config.php @@ -162,7 +162,8 @@ $THEME->layouts = array( // The pagelayout used for safebrowser and securewindow. 'secure' => array( 'file' => 'general.php', - 'regions' => array(), + 'regions' => array('side-pre'), + 'defaultregion' => 'side-pre', 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true, 'nologinlinks'=>true), ), ); From 82fb0aa57635e7742f295270988142fec2f7d14a Mon Sep 17 00:00:00 2001 From: Aparup Banerjee Date: Tue, 4 Sep 2012 12:10:30 +0800 Subject: [PATCH 73/96] MDL-31365 theme: mymobile updated to compatible declaration --- theme/mymobile/renderers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/mymobile/renderers.php b/theme/mymobile/renderers.php index b6b49cf273a..98bc7e62718 100644 --- a/theme/mymobile/renderers.php +++ b/theme/mymobile/renderers.php @@ -303,7 +303,7 @@ class theme_mymobile_core_renderer extends core_renderer { * * @return string */ - public function login_info() { + public function login_info($withlinks = null) { global $USER, $CFG, $DB, $SESSION; if (during_initial_install()) { From f0f0fec29572e6670cb46f4325955201f871b519 Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Tue, 4 Sep 2012 10:10:19 -0400 Subject: [PATCH 74/96] MDL-30643 - Updated test cases to handle timezones better. --- lib/tests/statslib_test.php | 81 ++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/lib/tests/statslib_test.php b/lib/tests/statslib_test.php index a93995ea203..81c572e66bc 100644 --- a/lib/tests/statslib_test.php +++ b/lib/tests/statslib_test.php @@ -44,6 +44,7 @@ class statslib_daily_testcase extends advanced_testcase { parent::setUp(); // Settings to force statistic to run during testing + $CFG->timezone = 99; $CFG->statsfirstrun = 'all'; $CFG->statslastdaily = 0; $CFG->statslastexecution = 0; @@ -127,8 +128,9 @@ class statslib_daily_testcase extends advanced_testcase { $guest = $CFG->siteguest; $user1 = $guest + 1; $user2 = $user1 + 1; - $end = 1272772800; - $fpid = (int)$CFG->defaultfrontpageroleid; + $start = stats_get_base_daily(1272758400); + $end = stats_get_next_day_start($start); + $fpid = (int) $CFG->defaultfrontpageroleid; $gr = get_guest_role(); $stid = 5; $nologend = stats_get_next_day_start(stats_get_base_daily(stats_get_start_from('daily'))); @@ -162,7 +164,7 @@ class statslib_daily_testcase extends advanced_testcase { array( // Logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $guest, 'course' => SITEID, 'action' => 'view' @@ -253,7 +255,7 @@ class statslib_daily_testcase extends advanced_testcase { array( // Logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' @@ -345,14 +347,14 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $guest, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => 1272686420, + 'time' => $start + 14420, 'userid' => $guest, 'course' => $course1, 'action' => 'view' @@ -469,21 +471,21 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $guest, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => 1272686420, + 'time' => $start + 14420, 'userid' => $guest, 'course' => $course1, 'action' => 'view' ), array( 'id' => 3, - 'time' => 1272686430, + 'time' => $start + 14430, 'userid' => $guest, 'course' => $course1, 'action' => 'add post' @@ -600,14 +602,14 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => 1272686420, + 'time' => $start + 14420, 'userid' => $user1, 'course' => $course1, 'action' => 'view' @@ -724,21 +726,21 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => 1272686420, + 'time' => $start + 14420, 'userid' => $user1, 'course' => $course1, 'action' => 'view' ), array( 'id' => 3, - 'time' => 1272686430, + 'time' => $start + 14430, 'userid' => $user1, 'course' => $course1, 'action' => 'add post' @@ -855,21 +857,21 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $user2, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => 1272686420, + 'time' => $start + 14420, 'userid' => $user2, 'course' => SITEID, 'action' => 'view' ), array( 'id' => 3, - 'time' => 1272686430, + 'time' => $start + 14430, 'userid' => $user2, 'course' => $course1, 'action' => 'view' @@ -994,14 +996,14 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => 1272686420, + 'time' => $start + 14420, 'userid' => $user1, 'course' => SITEID, 'action' => 'view' @@ -1177,7 +1179,7 @@ class statslib_daily_testcase extends advanced_testcase { $stats = array( // stats_daily 'courseid' => SITEID, - 'timeend' => 1272772800, + 'timeend' => 1272758400, 'roleid' => 0, 'stattype' => 'logins', 'stat1' => 1, @@ -1199,7 +1201,7 @@ class statslib_daily_testcase extends advanced_testcase { $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (14515200))); $DB->insert_record_raw('stats_daily', $stats); - $this->assertEquals(1272772800, stats_get_start_from('daily')); + $this->assertEquals(1272758400, stats_get_start_from('daily')); } /** @@ -1208,8 +1210,8 @@ class statslib_daily_testcase extends advanced_testcase { public function test_statslib_get_base_daily() { global $CFG; - $CFG->timezone = 99; - $this->assertEquals(1272686400, stats_get_base_daily(1272686410)); + $CFG->timezone = 0; + $this->assertEquals(1272672000, stats_get_base_daily(1272686410)); $CFG->timezone = 5; $this->assertEquals(1272654000, stats_get_base_daily(1272686410)); } @@ -1218,7 +1220,10 @@ class statslib_daily_testcase extends advanced_testcase { * Test the function that gets the start of the next day */ public function test_statslib_get_next_day_start() { - $this->assertEquals(1272772800, stats_get_next_day_start(1272686410)); + global $CFG; + + $CFG->timezone = 0; + $this->assertEquals(1272758400, stats_get_next_day_start(1272686410)); } /** @@ -1362,12 +1367,27 @@ class statslib_daily_testcase extends advanced_testcase { $this->prepare_db($logs); stats_temp_table_create(); - stats_temp_table_fill(1272686410, 1272772800); + stats_temp_table_fill(1272686410, 1272758400); $this->assertEquals(1, $DB->count_records('temp_log1')); $this->assertEquals(1, $DB->count_records('temp_log2')); } + /** + * Test the temporary table creation and deletion. + */ + public function test_statslib_temp_table_setup() { + global $DB; + + $logs = array(); + $this->prepare_db($logs); + + stats_temp_table_create(); + stats_temp_table_setup(); + + $this->assertEquals(1, $DB->count_records('temp_enroled')); + } + /** * Test the function that clean out the temporary tables. */ @@ -1401,6 +1421,8 @@ class statslib_daily_testcase extends advanced_testcase { /** * Test the daily stats function * + * @depends test_statslib_get_base_daily + * @depends test_statslib_get_next_day_start * @dataProvider daily_log_provider */ public function test_statslib_cron_daily($logs, $stats) { @@ -1412,7 +1434,7 @@ class statslib_daily_testcase extends advanced_testcase { // Stats cron daily uses mtrace, turn on buffering to silence output. ob_start(); - stats_cron_daily($maxdays=1); + stats_cron_daily(1); ob_end_clean(); $this->verify_stats($stats); @@ -1420,6 +1442,8 @@ class statslib_daily_testcase extends advanced_testcase { /** * Test the daily stats function + * @depends test_statslib_get_base_daily + * @depends test_statslib_get_next_day_start */ public function test_statslib_cron_daily_no_default_profile_id() { global $CFG; @@ -1427,7 +1451,8 @@ class statslib_daily_testcase extends advanced_testcase { $course1 = SITEID + 1; $guest = $CFG->siteguest; - $end = 1272772800; + $start = stats_get_base_daily(1272758400); + $end = stats_get_next_day_start($start); $stid = 5; $fpid = 0; $gr = get_guest_role(); @@ -1435,7 +1460,7 @@ class statslib_daily_testcase extends advanced_testcase { $logs = array( // Logs array( 'id' => 1, - 'time' => 1272686410, + 'time' => $start + 14410, 'userid' => $guest, 'course' => SITEID, 'action' => 'view' From eb0ffd3214ad568ded291936c2cd6e67276350b6 Mon Sep 17 00:00:00 2001 From: Tyler Bannister Date: Tue, 4 Sep 2012 10:11:16 -0400 Subject: [PATCH 75/96] RFC-30643 - Removed get_context_instance calls. - Added fifth table to speed up enrolment look ups. --- lib/db/temp_stats_log_template.xml | 18 ++++- lib/statslib.php | 116 +++++++++++++++++------------ 2 files changed, 86 insertions(+), 48 deletions(-) diff --git a/lib/db/temp_stats_log_template.xml b/lib/db/temp_stats_log_template.xml index 78681cb5629..904180a4804 100644 --- a/lib/db/temp_stats_log_template.xml +++ b/lib/db/temp_stats_log_template.xml @@ -20,7 +20,7 @@ - +
    @@ -37,5 +37,21 @@
    + + + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/lib/statslib.php b/lib/statslib.php index e9ed8c08c3a..00d8a983fb9 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -104,7 +104,7 @@ function stats_cron_daily($maxdays=1) { $now = time(); - $fpcontext = get_context_instance(CONTEXT_COURSE, SITEID, MUST_EXIST); + $fpcontext = context_course::instance(SITEID, MUST_EXIST); // read last execution date from db if (!$timestart = get_config(NULL, 'statslastdaily')) { @@ -165,7 +165,13 @@ function stats_cron_daily($maxdays=1) { $days = 1; $failed = true; } - mtrace("Temporary tables created"); + mtrace('Temporary tables created'); + + if(!stats_temp_table_setup()) { + $days = 1; + $failed = true; + } + mtrace('Enrolments caclulated'); while (!$failed && ($now > $nextmidnight)) { if ($days >= $maxdays) { @@ -190,7 +196,7 @@ function stats_cron_daily($maxdays=1) { break; } - /// find out if any logs available for this day + /// find out if any logs available for this day $sql = "SELECT 'x' FROM {temp_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); @@ -243,15 +249,12 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, - COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 - FROM {role_assignments} ra - JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) - JOIN {enrol} e ON e.courseid = c.instanceid - JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) + SELECT 'enrolments' as stattype, $nextmidnight as timeend, courseid, roleid, + COUNT(DISTINCT userid) as stat1, 0 as stat2 + FROM {temp_enroled} GROUP BY courseid, roleid"; - if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { + if (!$DB->execute($sql)) { $failed = true; break; } @@ -260,17 +263,14 @@ function stats_cron_daily($maxdays=1) { // Set stat2 to the number distinct users with role assignments in the course that were active // using table alias in UPDATE does not work in pg < 8.2 $sql = "UPDATE {temp_stats_daily} - SET stat2 = (SELECT COUNT(DISTINCT ra.userid) - FROM {role_assignments} ra - JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) - JOIN {enrol} e ON e.courseid = c.instanceid - JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - WHERE ra.roleid = {temp_stats_daily}.roleid AND - e.courseid = {temp_stats_daily}.courseid AND - EXISTS (SELECT 'x' - FROM {temp_log1} l - WHERE l.course = {temp_stats_daily}.courseid AND - l.userid = ra.userid)) + SET stat2 = (SELECT COUNT(DISTINCT userid) + FROM {temp_enroled} te + WHERE roleid = {temp_stats_daily}.roleid AND + courseid = {temp_stats_daily}.courseid AND + EXISTS (SELECT 'x' + FROM {temp_log1} l + WHERE l.course = {temp_stats_daily}.courseid AND + l.userid = te.userid)) WHERE {temp_stats_daily}.stattype = 'enrolments' AND {temp_stats_daily}.timeend = $nextmidnight AND {temp_stats_daily}.courseid IN @@ -286,10 +286,9 @@ function stats_cron_daily($maxdays=1) { /// now get course total enrolments (roleid==0) - except frontpage $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, + SELECT 'enrolments', $nextmidnight AS timeend, te.courseid AS courseid, 0 AS roleid, COUNT(DISTINCT userid) AS stat1, 0 AS stat2 - FROM {enrol} e - JOIN {user_enrolments} ue ON ue.enrolid = e.id + FROM {temp_enroled} te GROUP BY courseid HAVING COUNT(DISTINCT userid) > 0"; @@ -301,14 +300,13 @@ function stats_cron_daily($maxdays=1) { // Set stat 2 to the number of enrolled users who were active in the course $sql = "UPDATE {temp_stats_daily} - SET stat2 = (SELECT COUNT(DISTINCT ue.userid) - FROM {enrol} e - JOIN {user_enrolments} ue ON ue.enrolid = e.id - WHERE e.courseid = {temp_stats_daily}.courseid AND + SET stat2 = (SELECT COUNT(DISTINCT te.userid) + FROM {temp_enroled} te + WHERE te.courseid = {temp_stats_daily}.courseid AND EXISTS (SELECT 'x' FROM {temp_log1} l WHERE l.course = {temp_stats_daily}.courseid AND - l.userid = ue.userid)) + l.userid = te.userid)) WHERE {temp_stats_daily}.stattype = 'enrolments' AND {temp_stats_daily}.timeend = $nextmidnight AND {temp_stats_daily}.roleid = 0 AND @@ -422,13 +420,10 @@ function stats_cron_daily($maxdays=1) { FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites FROM {temp_stats_user_daily} sud, - (SELECT DISTINCT ra.userid, ra.roleid, e.courseid - FROM {role_assignments} ra - JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) - JOIN {enrol} e ON e.courseid = c.instanceid - JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - WHERE ra.roleid <> $guestrole AND - ra.userid <> $guest + (SELECT DISTINCT te.userid, te.roleid, te.courseid + FROM {temp_enroled} te + WHERE te.roleid <> $guestrole AND + te.userid <> $guest ) pl WHERE sud.userid = pl.userid AND sud.courseid = pl.courseid AND @@ -457,10 +452,9 @@ function stats_cron_daily($maxdays=1) { WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND (sud.userid = $guest OR sud.userid - NOT IN (SELECT ue.userid - FROM {user_enrolments} ue - JOIN {enrol} e ON ue.enrolid = e.id - WHERE e.courseid = sud.courseid)) + NOT IN (SELECT userid + FROM {temp_enroled} te + WHERE te.courseid = sud.courseid)) ) inline_view GROUP BY timeend, courseid, roleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; @@ -1353,7 +1347,7 @@ function stats_get_report_options($courseid,$mode) { switch ($mode) { case STATS_MODE_GENERAL: $reportoptions[STATS_REPORT_ACTIVITY] = get_string('statsreport'.STATS_REPORT_ACTIVITY); - if ($courseid != SITEID && $context = get_context_instance(CONTEXT_COURSE, $courseid)) { + if ($courseid != SITEID && $context = context_course::instance(CONTEXT_COURSE, $courseid)) { $sql = 'SELECT r.id, r.name FROM {role} r JOIN {stats_daily} s ON s.roleid = r.id WHERE s.courseid = :courseid GROUP BY r.id, r.name'; if ($roles = $DB->get_records_sql($sql, array('courseid' => $courseid))) { foreach ($roles as $role) { @@ -1371,19 +1365,19 @@ function stats_get_report_options($courseid,$mode) { case STATS_MODE_DETAILED: $reportoptions[STATS_REPORT_USER_ACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ACTIVITY); $reportoptions[STATS_REPORT_USER_ALLACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ALLACTIVITY); - if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { + if (has_capability('report/stats:view', context_system::instance())) { $site = get_site(); $reportoptions[STATS_REPORT_USER_LOGINS] = get_string('statsreport'.STATS_REPORT_USER_LOGINS); } break; case STATS_MODE_RANKED: - if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { + if (has_capability('report/stats:view', context_system::instance())) { $reportoptions[STATS_REPORT_ACTIVE_COURSES] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES); $reportoptions[STATS_REPORT_ACTIVE_COURSES_WEIGHTED] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES_WEIGHTED); $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES); $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES_RW] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES_RW); } - break; + break; } return $reportoptions; @@ -1499,7 +1493,7 @@ function stats_temp_table_create() { $tables = array(); $files = array( $xmlfile => array('stats_daily', 'stats_user_daily'), - $tempfile => array('temp_log1', 'temp_log2'), + $tempfile => array('temp_log1', 'temp_log2', 'temp_enroled'), ); foreach ($files as $file => $contents) { @@ -1552,7 +1546,7 @@ function stats_temp_table_drop() { $dbman = $DB->get_manager(); - $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); + $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily', 'temp_enroled'); foreach ($tables as $name) { @@ -1571,6 +1565,35 @@ function stats_temp_table_drop() { /** * Fills the temporary stats tables with new data * + * This function is meant to be called once at the start of stats generation + * + * @param timestart timestamp of the start time of logs view + * @param timeend timestamp of the end time of logs view + * @returns boolen success (true) or failure(false) + */ +function stats_temp_table_setup() { + global $DB; + + $sql = "INSERT INTO {temp_enroled} (userid, courseid, roleid) + + SELECT ue.userid, e.courseid, ra.roleid + FROM {role_assignments} ra + JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) + JOIN {enrol} e ON e.courseid = c.instanceid + JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid)"; + + if (!$DB->execute($sql, array('courselevel' => CONTEXT_COURSE))) { + return false; + } + + return true; +} + +/** + * Fills the temporary stats tables with new data + * + * This function is meant to be called to get a new day of data + * * @param timestart timestamp of the start time of logs view * @param timeend timestamp of the end time of logs view * @returns boolen success (true) or failure(false) @@ -1578,7 +1601,6 @@ function stats_temp_table_drop() { function stats_temp_table_fill($timestart, $timeend) { global $DB; - // Note: We need to create a placeholder here to simulate the effects of old, slow unions $sql = "INSERT INTO {temp_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; From 5c27678d3aa51af7c6e5ce992e52e55a29195a22 Mon Sep 17 00:00:00 2001 From: Aparup Banerjee Date: Wed, 5 Sep 2012 01:51:39 +0800 Subject: [PATCH 76/96] MDL-30643 fixed whitespace --- lib/db/temp_stats_log_template.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/db/temp_stats_log_template.xml b/lib/db/temp_stats_log_template.xml index 904180a4804..da0377d30b4 100644 --- a/lib/db/temp_stats_log_template.xml +++ b/lib/db/temp_stats_log_template.xml @@ -54,4 +54,4 @@ - \ No newline at end of file + From 937b0acba12502c3a922770bbbd45371ba7f0e35 Mon Sep 17 00:00:00 2001 From: Aparup Banerjee Date: Wed, 5 Sep 2012 10:16:35 +0800 Subject: [PATCH 77/96] MDL-26504 blog: fixed SQL to add explicit casting --- lib/db/upgrade.php | 6 +++--- version.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 7447298327a..f3b34fdc6c1 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1184,14 +1184,14 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2012082300.02); } - if ($oldversion < 2012090400.00) { - $subquery = 'SELECT b.id FROM {blog_external} b where ' . $DB->sql_compare_text('b.id') . ' = ' . $DB->sql_compare_text('{post}.content'); + if ($oldversion < 2012090500.00) { + $subquery = 'SELECT b.id FROM {blog_external} b where b.id = ' . $DB->sql_cast_char2int('{post}.content', true); $sql = 'DELETE FROM {post} WHERE {post}.module = \'blog_external\' AND NOT EXISTS (' . $subquery . ') AND ' . $DB->sql_isnotempty('post', 'uniquehash', false, false); $DB->execute($sql); - upgrade_main_savepoint(true, 2012090400.00); + upgrade_main_savepoint(true, 2012090500.00); } return true; diff --git a/version.php b/version.php index 877b8f5d4e6..e567c17ab38 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012090400.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012090500.00; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes From 717a993122b873dd15cd6e9313089909ade88aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Wed, 5 Sep 2012 08:23:36 +0200 Subject: [PATCH 78/96] MDL-35172 fix handling of more than 10 tinymce toolbars --- lib/editor/tinymce/lib.php | 2 +- lib/editor/tinymce/tests/editor_test.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/editor/tinymce/lib.php b/lib/editor/tinymce/lib.php index d1faa1e43b0..ae784d3a1d9 100644 --- a/lib/editor/tinymce/lib.php +++ b/lib/editor/tinymce/lib.php @@ -221,7 +221,7 @@ class tinymce_texteditor extends texteditor { if ($line === '') { continue; } - if ($i == 9) { + if ($i == 10) { // Maximum is ten lines, merge the rest to the last line. $result[9] = $result[9].','.$line; } else { diff --git a/lib/editor/tinymce/tests/editor_test.php b/lib/editor/tinymce/tests/editor_test.php index 27db61bb075..1704bab5813 100644 --- a/lib/editor/tinymce/tests/editor_test.php +++ b/lib/editor/tinymce/tests/editor_test.php @@ -49,5 +49,8 @@ class editor_tinymce_testcase extends advanced_testcase { $result = tinymce_texteditor::parse_toolbar_setting("| \n\n| \n \r"); $this->assertSame(array(), $result); + + $result = tinymce_texteditor::parse_toolbar_setting("one\ntwo\n\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten"); + $this->assertSame(array('one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'), $result); } } From 61712b0fb0b3c152dd8cc6acb37965ec1ff5a773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Wed, 5 Sep 2012 10:01:30 +0200 Subject: [PATCH 79/96] MDL-35077 do not show link to system stats report if stats disabled --- report/stats/settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/report/stats/settings.php b/report/stats/settings.php index c92ec339bba..064453835bb 100644 --- a/report/stats/settings.php +++ b/report/stats/settings.php @@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die; // just a link to course report -$ADMIN->add('reports', new admin_externalpage('reportstats', get_string('pluginname', 'report_stats'), "$CFG->wwwroot/report/stats/index.php", 'report/stats:view')); +$ADMIN->add('reports', new admin_externalpage('reportstats', get_string('pluginname', 'report_stats'), "$CFG->wwwroot/report/stats/index.php", 'report/stats:view', empty($CFG->enablestats))); // no report settings $settings = null; From 393cadf205d8a08e47ff00782b12ea1e037fb431 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 18:01:24 +0200 Subject: [PATCH 80/96] MDL-29662 quiz overrides: use proper course value (was php notice). --- mod/quiz/lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index a14fc8fc659..fc2aa97b47e 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -1153,7 +1153,7 @@ function quiz_update_events($quiz, $override = null) { if (!empty($quiz->coursemodule)) { $cmid = $quiz->coursemodule; } else { - $cmid = get_coursemodule_from_instance('quiz', $quiz->id, $courseid)->id; + $cmid = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course)->id; } $event = new stdClass(); From ac9fe9b3a5912ffb1fe4c6347e0f6f1b110a765b Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:02:07 +0200 Subject: [PATCH 81/96] Revert "MDL-30643 fixed whitespace" This reverts commit 5c27678d3aa51af7c6e5ce992e52e55a29195a22. --- lib/db/temp_stats_log_template.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/db/temp_stats_log_template.xml b/lib/db/temp_stats_log_template.xml index da0377d30b4..904180a4804 100644 --- a/lib/db/temp_stats_log_template.xml +++ b/lib/db/temp_stats_log_template.xml @@ -54,4 +54,4 @@ - + \ No newline at end of file From 447721b44f1acfd927308041c61654ba1057e567 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:02:27 +0200 Subject: [PATCH 82/96] Revert "RFC-30643 - Removed get_context_instance calls." This reverts commit eb0ffd3214ad568ded291936c2cd6e67276350b6. --- lib/db/temp_stats_log_template.xml | 18 +---- lib/statslib.php | 116 ++++++++++++----------------- 2 files changed, 48 insertions(+), 86 deletions(-) diff --git a/lib/db/temp_stats_log_template.xml b/lib/db/temp_stats_log_template.xml index 904180a4804..78681cb5629 100644 --- a/lib/db/temp_stats_log_template.xml +++ b/lib/db/temp_stats_log_template.xml @@ -20,7 +20,7 @@ - +
    @@ -37,21 +37,5 @@
    - - - - - - - - - - - - - - - -
    \ No newline at end of file diff --git a/lib/statslib.php b/lib/statslib.php index 00d8a983fb9..e9ed8c08c3a 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -104,7 +104,7 @@ function stats_cron_daily($maxdays=1) { $now = time(); - $fpcontext = context_course::instance(SITEID, MUST_EXIST); + $fpcontext = get_context_instance(CONTEXT_COURSE, SITEID, MUST_EXIST); // read last execution date from db if (!$timestart = get_config(NULL, 'statslastdaily')) { @@ -165,13 +165,7 @@ function stats_cron_daily($maxdays=1) { $days = 1; $failed = true; } - mtrace('Temporary tables created'); - - if(!stats_temp_table_setup()) { - $days = 1; - $failed = true; - } - mtrace('Enrolments caclulated'); + mtrace("Temporary tables created"); while (!$failed && ($now > $nextmidnight)) { if ($days >= $maxdays) { @@ -196,7 +190,7 @@ function stats_cron_daily($maxdays=1) { break; } - /// find out if any logs available for this day + /// find out if any logs available for this day $sql = "SELECT 'x' FROM {temp_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); @@ -249,12 +243,15 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments' as stattype, $nextmidnight as timeend, courseid, roleid, - COUNT(DISTINCT userid) as stat1, 0 as stat2 - FROM {temp_enroled} + SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, + COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 + FROM {role_assignments} ra + JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) + JOIN {enrol} e ON e.courseid = c.instanceid + JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) GROUP BY courseid, roleid"; - if (!$DB->execute($sql)) { + if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; break; } @@ -263,14 +260,17 @@ function stats_cron_daily($maxdays=1) { // Set stat2 to the number distinct users with role assignments in the course that were active // using table alias in UPDATE does not work in pg < 8.2 $sql = "UPDATE {temp_stats_daily} - SET stat2 = (SELECT COUNT(DISTINCT userid) - FROM {temp_enroled} te - WHERE roleid = {temp_stats_daily}.roleid AND - courseid = {temp_stats_daily}.courseid AND - EXISTS (SELECT 'x' - FROM {temp_log1} l - WHERE l.course = {temp_stats_daily}.courseid AND - l.userid = te.userid)) + SET stat2 = (SELECT COUNT(DISTINCT ra.userid) + FROM {role_assignments} ra + JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) + JOIN {enrol} e ON e.courseid = c.instanceid + JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) + WHERE ra.roleid = {temp_stats_daily}.roleid AND + e.courseid = {temp_stats_daily}.courseid AND + EXISTS (SELECT 'x' + FROM {temp_log1} l + WHERE l.course = {temp_stats_daily}.courseid AND + l.userid = ra.userid)) WHERE {temp_stats_daily}.stattype = 'enrolments' AND {temp_stats_daily}.timeend = $nextmidnight AND {temp_stats_daily}.courseid IN @@ -286,9 +286,10 @@ function stats_cron_daily($maxdays=1) { /// now get course total enrolments (roleid==0) - except frontpage $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments', $nextmidnight AS timeend, te.courseid AS courseid, 0 AS roleid, + SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, COUNT(DISTINCT userid) AS stat1, 0 AS stat2 - FROM {temp_enroled} te + FROM {enrol} e + JOIN {user_enrolments} ue ON ue.enrolid = e.id GROUP BY courseid HAVING COUNT(DISTINCT userid) > 0"; @@ -300,13 +301,14 @@ function stats_cron_daily($maxdays=1) { // Set stat 2 to the number of enrolled users who were active in the course $sql = "UPDATE {temp_stats_daily} - SET stat2 = (SELECT COUNT(DISTINCT te.userid) - FROM {temp_enroled} te - WHERE te.courseid = {temp_stats_daily}.courseid AND + SET stat2 = (SELECT COUNT(DISTINCT ue.userid) + FROM {enrol} e + JOIN {user_enrolments} ue ON ue.enrolid = e.id + WHERE e.courseid = {temp_stats_daily}.courseid AND EXISTS (SELECT 'x' FROM {temp_log1} l WHERE l.course = {temp_stats_daily}.courseid AND - l.userid = te.userid)) + l.userid = ue.userid)) WHERE {temp_stats_daily}.stattype = 'enrolments' AND {temp_stats_daily}.timeend = $nextmidnight AND {temp_stats_daily}.roleid = 0 AND @@ -420,10 +422,13 @@ function stats_cron_daily($maxdays=1) { FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites FROM {temp_stats_user_daily} sud, - (SELECT DISTINCT te.userid, te.roleid, te.courseid - FROM {temp_enroled} te - WHERE te.roleid <> $guestrole AND - te.userid <> $guest + (SELECT DISTINCT ra.userid, ra.roleid, e.courseid + FROM {role_assignments} ra + JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) + JOIN {enrol} e ON e.courseid = c.instanceid + JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) + WHERE ra.roleid <> $guestrole AND + ra.userid <> $guest ) pl WHERE sud.userid = pl.userid AND sud.courseid = pl.courseid AND @@ -452,9 +457,10 @@ function stats_cron_daily($maxdays=1) { WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND (sud.userid = $guest OR sud.userid - NOT IN (SELECT userid - FROM {temp_enroled} te - WHERE te.courseid = sud.courseid)) + NOT IN (SELECT ue.userid + FROM {user_enrolments} ue + JOIN {enrol} e ON ue.enrolid = e.id + WHERE e.courseid = sud.courseid)) ) inline_view GROUP BY timeend, courseid, roleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; @@ -1347,7 +1353,7 @@ function stats_get_report_options($courseid,$mode) { switch ($mode) { case STATS_MODE_GENERAL: $reportoptions[STATS_REPORT_ACTIVITY] = get_string('statsreport'.STATS_REPORT_ACTIVITY); - if ($courseid != SITEID && $context = context_course::instance(CONTEXT_COURSE, $courseid)) { + if ($courseid != SITEID && $context = get_context_instance(CONTEXT_COURSE, $courseid)) { $sql = 'SELECT r.id, r.name FROM {role} r JOIN {stats_daily} s ON s.roleid = r.id WHERE s.courseid = :courseid GROUP BY r.id, r.name'; if ($roles = $DB->get_records_sql($sql, array('courseid' => $courseid))) { foreach ($roles as $role) { @@ -1365,19 +1371,19 @@ function stats_get_report_options($courseid,$mode) { case STATS_MODE_DETAILED: $reportoptions[STATS_REPORT_USER_ACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ACTIVITY); $reportoptions[STATS_REPORT_USER_ALLACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ALLACTIVITY); - if (has_capability('report/stats:view', context_system::instance())) { + if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { $site = get_site(); $reportoptions[STATS_REPORT_USER_LOGINS] = get_string('statsreport'.STATS_REPORT_USER_LOGINS); } break; case STATS_MODE_RANKED: - if (has_capability('report/stats:view', context_system::instance())) { + if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { $reportoptions[STATS_REPORT_ACTIVE_COURSES] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES); $reportoptions[STATS_REPORT_ACTIVE_COURSES_WEIGHTED] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES_WEIGHTED); $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES); $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES_RW] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES_RW); } - break; + break; } return $reportoptions; @@ -1493,7 +1499,7 @@ function stats_temp_table_create() { $tables = array(); $files = array( $xmlfile => array('stats_daily', 'stats_user_daily'), - $tempfile => array('temp_log1', 'temp_log2', 'temp_enroled'), + $tempfile => array('temp_log1', 'temp_log2'), ); foreach ($files as $file => $contents) { @@ -1546,7 +1552,7 @@ function stats_temp_table_drop() { $dbman = $DB->get_manager(); - $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily', 'temp_enroled'); + $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); foreach ($tables as $name) { @@ -1565,35 +1571,6 @@ function stats_temp_table_drop() { /** * Fills the temporary stats tables with new data * - * This function is meant to be called once at the start of stats generation - * - * @param timestart timestamp of the start time of logs view - * @param timeend timestamp of the end time of logs view - * @returns boolen success (true) or failure(false) - */ -function stats_temp_table_setup() { - global $DB; - - $sql = "INSERT INTO {temp_enroled} (userid, courseid, roleid) - - SELECT ue.userid, e.courseid, ra.roleid - FROM {role_assignments} ra - JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) - JOIN {enrol} e ON e.courseid = c.instanceid - JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid)"; - - if (!$DB->execute($sql, array('courselevel' => CONTEXT_COURSE))) { - return false; - } - - return true; -} - -/** - * Fills the temporary stats tables with new data - * - * This function is meant to be called to get a new day of data - * * @param timestart timestamp of the start time of logs view * @param timeend timestamp of the end time of logs view * @returns boolen success (true) or failure(false) @@ -1601,6 +1578,7 @@ function stats_temp_table_setup() { function stats_temp_table_fill($timestart, $timeend) { global $DB; + // Note: We need to create a placeholder here to simulate the effects of old, slow unions $sql = "INSERT INTO {temp_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; From fa4fbd9790f81f9351f8d313f7b6be6ba4832cca Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:02:38 +0200 Subject: [PATCH 83/96] Revert "MDL-30643 - Updated test cases to handle timezones better." This reverts commit f0f0fec29572e6670cb46f4325955201f871b519. --- lib/tests/statslib_test.php | 81 +++++++++++++------------------------ 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/lib/tests/statslib_test.php b/lib/tests/statslib_test.php index 81c572e66bc..a93995ea203 100644 --- a/lib/tests/statslib_test.php +++ b/lib/tests/statslib_test.php @@ -44,7 +44,6 @@ class statslib_daily_testcase extends advanced_testcase { parent::setUp(); // Settings to force statistic to run during testing - $CFG->timezone = 99; $CFG->statsfirstrun = 'all'; $CFG->statslastdaily = 0; $CFG->statslastexecution = 0; @@ -128,9 +127,8 @@ class statslib_daily_testcase extends advanced_testcase { $guest = $CFG->siteguest; $user1 = $guest + 1; $user2 = $user1 + 1; - $start = stats_get_base_daily(1272758400); - $end = stats_get_next_day_start($start); - $fpid = (int) $CFG->defaultfrontpageroleid; + $end = 1272772800; + $fpid = (int)$CFG->defaultfrontpageroleid; $gr = get_guest_role(); $stid = 5; $nologend = stats_get_next_day_start(stats_get_base_daily(stats_get_start_from('daily'))); @@ -164,7 +162,7 @@ class statslib_daily_testcase extends advanced_testcase { array( // Logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $guest, 'course' => SITEID, 'action' => 'view' @@ -255,7 +253,7 @@ class statslib_daily_testcase extends advanced_testcase { array( // Logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' @@ -347,14 +345,14 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $guest, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => $start + 14420, + 'time' => 1272686420, 'userid' => $guest, 'course' => $course1, 'action' => 'view' @@ -471,21 +469,21 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $guest, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => $start + 14420, + 'time' => 1272686420, 'userid' => $guest, 'course' => $course1, 'action' => 'view' ), array( 'id' => 3, - 'time' => $start + 14430, + 'time' => 1272686430, 'userid' => $guest, 'course' => $course1, 'action' => 'add post' @@ -602,14 +600,14 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => $start + 14420, + 'time' => 1272686420, 'userid' => $user1, 'course' => $course1, 'action' => 'view' @@ -726,21 +724,21 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => $start + 14420, + 'time' => 1272686420, 'userid' => $user1, 'course' => $course1, 'action' => 'view' ), array( 'id' => 3, - 'time' => $start + 14430, + 'time' => 1272686430, 'userid' => $user1, 'course' => $course1, 'action' => 'add post' @@ -857,21 +855,21 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $user2, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => $start + 14420, + 'time' => 1272686420, 'userid' => $user2, 'course' => SITEID, 'action' => 'view' ), array( 'id' => 3, - 'time' => $start + 14430, + 'time' => 1272686430, 'userid' => $user2, 'course' => $course1, 'action' => 'view' @@ -996,14 +994,14 @@ class statslib_daily_testcase extends advanced_testcase { array( // logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $user1, 'course' => SITEID, 'action' => 'login' ), array( 'id' => 2, - 'time' => $start + 14420, + 'time' => 1272686420, 'userid' => $user1, 'course' => SITEID, 'action' => 'view' @@ -1179,7 +1177,7 @@ class statslib_daily_testcase extends advanced_testcase { $stats = array( // stats_daily 'courseid' => SITEID, - 'timeend' => 1272758400, + 'timeend' => 1272772800, 'roleid' => 0, 'stattype' => 'logins', 'stat1' => 1, @@ -1201,7 +1199,7 @@ class statslib_daily_testcase extends advanced_testcase { $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (14515200))); $DB->insert_record_raw('stats_daily', $stats); - $this->assertEquals(1272758400, stats_get_start_from('daily')); + $this->assertEquals(1272772800, stats_get_start_from('daily')); } /** @@ -1210,8 +1208,8 @@ class statslib_daily_testcase extends advanced_testcase { public function test_statslib_get_base_daily() { global $CFG; - $CFG->timezone = 0; - $this->assertEquals(1272672000, stats_get_base_daily(1272686410)); + $CFG->timezone = 99; + $this->assertEquals(1272686400, stats_get_base_daily(1272686410)); $CFG->timezone = 5; $this->assertEquals(1272654000, stats_get_base_daily(1272686410)); } @@ -1220,10 +1218,7 @@ class statslib_daily_testcase extends advanced_testcase { * Test the function that gets the start of the next day */ public function test_statslib_get_next_day_start() { - global $CFG; - - $CFG->timezone = 0; - $this->assertEquals(1272758400, stats_get_next_day_start(1272686410)); + $this->assertEquals(1272772800, stats_get_next_day_start(1272686410)); } /** @@ -1367,27 +1362,12 @@ class statslib_daily_testcase extends advanced_testcase { $this->prepare_db($logs); stats_temp_table_create(); - stats_temp_table_fill(1272686410, 1272758400); + stats_temp_table_fill(1272686410, 1272772800); $this->assertEquals(1, $DB->count_records('temp_log1')); $this->assertEquals(1, $DB->count_records('temp_log2')); } - /** - * Test the temporary table creation and deletion. - */ - public function test_statslib_temp_table_setup() { - global $DB; - - $logs = array(); - $this->prepare_db($logs); - - stats_temp_table_create(); - stats_temp_table_setup(); - - $this->assertEquals(1, $DB->count_records('temp_enroled')); - } - /** * Test the function that clean out the temporary tables. */ @@ -1421,8 +1401,6 @@ class statslib_daily_testcase extends advanced_testcase { /** * Test the daily stats function * - * @depends test_statslib_get_base_daily - * @depends test_statslib_get_next_day_start * @dataProvider daily_log_provider */ public function test_statslib_cron_daily($logs, $stats) { @@ -1434,7 +1412,7 @@ class statslib_daily_testcase extends advanced_testcase { // Stats cron daily uses mtrace, turn on buffering to silence output. ob_start(); - stats_cron_daily(1); + stats_cron_daily($maxdays=1); ob_end_clean(); $this->verify_stats($stats); @@ -1442,8 +1420,6 @@ class statslib_daily_testcase extends advanced_testcase { /** * Test the daily stats function - * @depends test_statslib_get_base_daily - * @depends test_statslib_get_next_day_start */ public function test_statslib_cron_daily_no_default_profile_id() { global $CFG; @@ -1451,8 +1427,7 @@ class statslib_daily_testcase extends advanced_testcase { $course1 = SITEID + 1; $guest = $CFG->siteguest; - $start = stats_get_base_daily(1272758400); - $end = stats_get_next_day_start($start); + $end = 1272772800; $stid = 5; $fpid = 0; $gr = get_guest_role(); @@ -1460,7 +1435,7 @@ class statslib_daily_testcase extends advanced_testcase { $logs = array( // Logs array( 'id' => 1, - 'time' => $start + 14410, + 'time' => 1272686410, 'userid' => $guest, 'course' => SITEID, 'action' => 'view' From 06d54f411cbefdbedbbf8c2815af6974edb49db1 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:02:54 +0200 Subject: [PATCH 84/96] Revert "MDL-30643 - Fixing minor log output error and test cases." This reverts commit 2da32bd6a5a0071756c29a4684c73694f0971664. --- lib/statslib.php | 14 ++---- lib/tests/statslib_test.php | 85 +------------------------------------ 2 files changed, 6 insertions(+), 93 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index e9ed8c08c3a..ed720665fca 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -190,19 +190,13 @@ function stats_cron_daily($maxdays=1) { break; } + stats_progress('0'); + /// find out if any logs available for this day $sql = "SELECT 'x' FROM {temp_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); - if ($logspresent) { - // Insert blank record to force Query 10 to generate additional row when no logs for - // the site with userid 0 exist. Added for backwards compatibility. - $DB->insert_record_raw('temp_log1', array('userid' => 0, 'course' => SITEID, 'action' => '')); - } - - stats_progress('0'); - - /// process login info first + /// process login info first $sql = "INSERT INTO {temp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads @@ -388,6 +382,7 @@ function stats_cron_daily($maxdays=1) { SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites FROM {temp_log1} l + WHERE !(course = 0 AND userid = 0) GROUP BY userid, courseid"; if ($logspresent and !$DB->execute($sql, array_merge($params1, $params2))) { @@ -1578,7 +1573,6 @@ function stats_temp_table_drop() { function stats_temp_table_fill($timestart, $timeend) { global $DB; - // Note: We need to create a placeholder here to simulate the effects of old, slow unions $sql = "INSERT INTO {temp_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; diff --git a/lib/tests/statslib_test.php b/lib/tests/statslib_test.php index a93995ea203..3e32143a88b 100644 --- a/lib/tests/statslib_test.php +++ b/lib/tests/statslib_test.php @@ -237,15 +237,6 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), ), ), ), @@ -329,15 +320,6 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), ), ), ), @@ -453,15 +435,6 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), ), ), ), @@ -584,16 +557,7 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), + ), ), ), array( // Test #5 - Login and view course - Tests queries 4, 6, 10, 12, 14 (read) @@ -708,15 +672,6 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), ), ), ), @@ -839,16 +794,7 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), + ), ), ), array( // Test #7 - Login and view course - Tests queries 13 (not enroled), 14 (not default) @@ -978,15 +924,6 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), ), ), ), @@ -1085,15 +1022,6 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), ), ), ), @@ -1503,15 +1431,6 @@ class statslib_daily_testcase extends advanced_testcase { 'statswrites' => 0, 'stattype' => 'activity' ), - array( // Query 10 - default record - 'courseid' => SITEID, - 'userid' => 0, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), ), ); From 8127f098d81a2bb152766d27dc04d9c3e9b420a9 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:03:06 +0200 Subject: [PATCH 85/96] Revert "MDL-30643 - Added statslib test file." This reverts commit 8646f5a4363a4331c8673e2bc98a7372ed789182. --- lib/tests/statslib_test.php | 1448 ----------------------------------- 1 file changed, 1448 deletions(-) delete mode 100644 lib/tests/statslib_test.php diff --git a/lib/tests/statslib_test.php b/lib/tests/statslib_test.php deleted file mode 100644 index 3e32143a88b..00000000000 --- a/lib/tests/statslib_test.php +++ /dev/null @@ -1,1448 +0,0 @@ -. - -/** - * Tests for ../statslib.php - * - * @package core - * @subpackage stats - * @copyright 2012 Tyler Bannister - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->libdir . '/adminlib.php'); -require_once($CFG->libdir . '/statslib.php'); - -/** - * Test functions that affect daily stats - */ -class statslib_daily_testcase extends advanced_testcase { - protected $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); - - /** - * Setup function - * - Allow changes to CFG->debug for testing purposes. - */ - protected function setUp() { - global $CFG; - parent::setUp(); - - // Settings to force statistic to run during testing - $CFG->statsfirstrun = 'all'; - $CFG->statslastdaily = 0; - $CFG->statslastexecution = 0; - $CFG->statsruntimestarthour = date('H'); - $CFG->statsruntimestartminute = 0; - - $this->resetAfterTest(true); - } - - /** - * Function to setup database. - * - * @param array $logs An array of log entries to added to the database. - */ - protected function prepare_db($logs) { - global $CFG, $DB; - - $course1 = SITEID + 1; - $guest = $CFG->siteguest; - $user1 = $guest + 1; - $user2 = $user1 + 1; - - // Create users (user) - $tables['user'] = array( - array('id' => $guest, 'deleted' => 0, 'username' => 'guest'), - array('id' => $user1, 'deleted' => 0, 'username' => 'user1'), - array('id' => $user2, 'deleted' => 0, 'username' => 'user2'), - ); - - // Create courses (course) - $tables['course'] = array( - array('id' => SITEID, 'shortname' => 'site'), - array('id' => $course1, 'shortname' => 'course1'), - ); - - // Create contexts (context) - $tables['context'] = array( - array('id' => 1, 'contextlevel' => CONTEXT_SYSTEM, 'instanceid' => 0), - array('id' => 2, 'contextlevel' => CONTEXT_COURSE, 'instanceid' => SITEID), - array('id' => 3, 'contextlevel' => CONTEXT_COURSE, 'instanceid' => $course1), - array('id' => 4, 'contextlevel' => CONTEXT_USER, 'instanceid' => $guest), - array('id' => 5, 'contextlevel' => CONTEXT_USER, 'instanceid' => $user1), - array('id' => 6, 'contextlevel' => CONTEXT_USER, 'instanceid' => $user2), - ); - - // Create role assignments (role_assignment) - $tables['role_assignments'] = array( - array('id' => 1, 'roleid' => 5, 'contextid' => 3, 'userid' => $user1), - array('id' => 2, 'roleid' => 5, 'contextid' => 2, 'userid' => $user2), - ); - - // Create enrolments (enrol) - $tables['enrol'] = array( - array('id' => 1, 'enrol' => 'manual', 'status' => 0, 'courseid' => $course1, 'roleid' => 5), - array('id' => 2, 'enrol' => 'guest', 'status' => 1, 'courseid' => $course1, 'roleid' => 0), - array('id' => 3, 'enrol' => 'self', 'status' => 1, 'courseid' => $course1, 'roleid' => 5), - ); - - // Create user enrolments (user_enrolments) - $tables['user_enrolments'] = array( - array('id' => 1, 'status' => 0, 'enrolid' => 1, 'userid' => $user1), - ); - - // Create logs (log) - $tables['log'] = $logs; - - // Insert records - foreach ($tables as $table => $rows) { - $DB->delete_records($table, array()); - - foreach ($rows as $row) { - $DB->import_record($table, (object) $row); - } - } - } - - public function daily_log_provider() { - global $CFG; - - $course1 = SITEID + 1; - $guest = $CFG->siteguest; - $user1 = $guest + 1; - $user2 = $user1 + 1; - $end = 1272772800; - $fpid = (int)$CFG->defaultfrontpageroleid; - $gr = get_guest_role(); - $stid = 5; - $nologend = stats_get_next_day_start(stats_get_base_daily(stats_get_start_from('daily'))); - - - // Each test case contains a complete list of expected output because producing - // more or fewer results is an error, as is changing the results in unrelated - // stats entries. - // All queries are identified by the number printed after the query completes in the - // 4 temporary table stats code. - return array( - array( // Test #0 - No logs - Only query 3 should be processed - array( // Logs - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $nologend, - 'roleid' => 5, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - ), - ), - ), - array( // Test #1 - No login - Tests queries 2, 3, 5, 7, 9 (and 8), 10 (read) - array( // Logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $guest, - 'course' => SITEID, - 'action' => 'view' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 0, - 'stat2' => 0 - ), - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 5 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 16 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $gr->id, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 10 - read - 'courseid' => SITEID, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - array( // Test #2 - Single login - Tests queries 1, 2 (with logins), 4 - array( // Logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $user1, - 'course' => SITEID, - 'action' => 'login' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 5 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 0, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 1 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'logins' - ), - array( // Query 10 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - array( // Test #3 - Guest login and view course - Tests queries 11 (read), 13 (guest) - array( // logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $guest, - 'course' => SITEID, - 'action' => 'login' - ), - array( - 'id' => 2, - 'time' => 1272686420, - 'userid' => $guest, - 'course' => $course1, - 'action' => 'view' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 5 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 0, - 'stat2' => 0 - ), - array( // Query 11 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 13 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $gr->id, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 1 - 'courseid' => SITEID, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'logins' - ), - array( // Query 10 - 'courseid' => $course1, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - array( // Query 10 - 'courseid' => SITEID, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - array( // Test #4 - Login, view course, upload assignment - Tests queries 10 + 11 (write), 13 (guest) - array( // logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $guest, - 'course' => SITEID, - 'action' => 'login' - ), - array( - 'id' => 2, - 'time' => 1272686420, - 'userid' => $guest, - 'course' => $course1, - 'action' => 'view' - ), - array( - 'id' => 3, - 'time' => 1272686430, - 'userid' => $guest, - 'course' => $course1, - 'action' => 'add post' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 5 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 0, - 'stat2' => 0 - ), - array( // Query 11 - read + write - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 13 - read + write - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $gr->id, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 1 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 1 - 'courseid' => SITEID, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'logins' - ), - array( // Query 10 - read + write - 'courseid' => $course1, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 1, - 'stattype' => 'activity' - ), - array( // Query 10 - 'courseid' => SITEID, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - array( // Test #5 - Login and view course - Tests queries 4, 6, 10, 12, 14 (read) - array( // logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $user1, - 'course' => SITEID, - 'action' => 'login' - ), - array( - 'id' => 2, - 'time' => 1272686420, - 'userid' => $user1, - 'course' => $course1, - 'action' => 'view' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Queries 3 (stat1) and 4 (stat2) - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Queries 5 (stat1) and 6 (stat2) - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 0, - 'stat2' => 0 - ), - array( // Query 11 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 12 (read) - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 1 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'logins' - ), - array( // Query 10 - 'courseid' => $course1, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - array( // Query 10 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - array( // Test #6 - Login, view course, post - Tests queries 10, 12, 14 (write) - array( // logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $user1, - 'course' => SITEID, - 'action' => 'login' - ), - array( - 'id' => 2, - 'time' => 1272686420, - 'userid' => $user1, - 'course' => $course1, - 'action' => 'view' - ), - array( - 'id' => 3, - 'time' => 1272686430, - 'userid' => $user1, - 'course' => $course1, - 'action' => 'add post' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Queries 3 (stat1) and 4 (stat2) - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Queries 5 (stat1) and 6 (stat2) - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 0, - 'stat2' => 0 - ), - array( // Query 11 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 12 (read + write) - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 1 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 1 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'logins' - ), - array( // Query 10 - 'courseid' => $course1, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 1, - 'stattype' => 'activity' - ), - array( // Query 10 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 0, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - array( // Test #7 - Login and view course - Tests queries 13 (not enroled), 14 (not default) - array( // logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $user2, - 'course' => SITEID, - 'action' => 'login' - ), - array( - 'id' => 2, - 'time' => 1272686420, - 'userid' => $user2, - 'course' => SITEID, - 'action' => 'view' - ), - array( - 'id' => 3, - 'time' => 1272686430, - 'userid' => $user2, - 'course' => $course1, - 'action' => 'view' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 5 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 11 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 13 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $gr->id, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 14 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 1 - 'courseid' => SITEID, - 'userid' => $user2, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'logins' - ), - array( // Query 10 - 'courseid' => $course1, - 'userid' => $user2, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - array( // Query 10 - 'courseid' => SITEID, - 'userid' => $user2, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - array( // Test #8 - Login and view site - Tests queries 15 (front page views) - array( // logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $user1, - 'course' => SITEID, - 'action' => 'login' - ), - array( - 'id' => 2, - 'time' => 1272686420, - 'userid' => $user1, - 'course' => SITEID, - 'action' => 'view' - ), - ), - array( - 'stats_daily' => array( // stats_daily - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ), - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 5 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 9 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 15 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $fpid, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 1 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'logins' - ), - array( // Query 10 - 'courseid' => SITEID, - 'userid' => $user1, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ), - ), - ); - } - - /** - * Compare the expected stats to those in the database. - * - * @param array $stats An array of arrays of arrays of both types of stats - */ - protected function verify_stats($stats) { - global $DB; - - foreach ($stats as $type => $results) { - $records = $DB->get_records($type); - - $this->assertEquals(sizeof($results), sizeof($records), - 'Incorrect number of results returned for '. $type); - - foreach ($results as $result) { - $found = 0; - - foreach ($records as $key => $record) { - $record = (array) $record; - unset($record['id']); - $diff = array_merge(array_diff_assoc($result, $record), - array_diff_assoc($record, $result)); - - if (empty($diff)) { - $found = $key; - break; - } - } - $this->assertGreaterThan(0, $found, 'Expected log '. var_export($result, true) - ." was not found in $type ". var_export($records, true)); - unset($records[$found]); - } - } - } - - /** - * Test progress output when debug is on - */ - public function test_statslib_progress_debug() { - global $CFG; - - $CFG->debug = DEBUG_ALL; - $this->expectOutputString('1:0 '); - stats_progress('init'); - stats_progress('1'); - } - - /** - * Test progress output when debug is off - */ - public function test_statslib_progress_no_debug() { - global $CFG; - - $CFG->debug = DEBUG_NONE; - $this->expectOutputString('.'); - stats_progress('init'); - stats_progress('1'); - } - - /** - * Test the function that gets the start date from the config - */ - public function test_statslib_get_start_from() { - global $CFG, $DB; - - $logs = array( // logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => 1, - 'course' => 1, - 'action' => 'login' - ), - ); - - $stats = array( // stats_daily - 'courseid' => SITEID, - 'timeend' => 1272772800, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 1, - 'stat2' => 1 - ); - - $time = time(); - - // Allow 1 second difference in case we cross a second boundary. - $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (3 * 24 * 3600))); - - $this->prepare_db($logs); - $this->assertEquals(1272686410, stats_get_start_from('daily')); - - $CFG->statsfirstrun = 'none'; - $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (3 * 24 * 3600))); - - $CFG->statsfirstrun = 14515200; - $this->assertLessThanOrEqual(1, stats_get_start_from('daily') - ($time - (14515200))); - - $DB->insert_record_raw('stats_daily', $stats); - $this->assertEquals(1272772800, stats_get_start_from('daily')); - } - - /** - * Test the function that calculates the start of the day - */ - public function test_statslib_get_base_daily() { - global $CFG; - - $CFG->timezone = 99; - $this->assertEquals(1272686400, stats_get_base_daily(1272686410)); - $CFG->timezone = 5; - $this->assertEquals(1272654000, stats_get_base_daily(1272686410)); - } - - /** - * Test the function that gets the start of the next day - */ - public function test_statslib_get_next_day_start() { - $this->assertEquals(1272772800, stats_get_next_day_start(1272686410)); - } - - /** - * Test the function that gets the action names - * - * Note: The function results depend on installed modules. The hard coded lists are the - * defaults for a new Moodle 2.3 install. - */ - public function test_statslib_get_action_names() { - $basepostactions = array ( - 0 => 'add', - 1 => 'delete', - 2 => 'edit', - 3 => 'add mod', - 4 => 'delete mod', - 5 => 'edit sectionenrol', - 6 => 'loginas', - 7 => 'new', - 8 => 'unenrol', - 9 => 'update', - 10 => 'update mod', - 11 => 'upload', - 12 => 'submit', - 13 => 'submit for grading', - 14 => 'talk', - 15 => 'choose', - 16 => 'choose again', - 17 => 'record delete', - 18 => 'add discussion', - 19 => 'add post', - 20 => 'delete discussion', - 21 => 'delete post', - 22 => 'move discussion', - 23 => 'prune post', - 24 => 'update post', - 25 => 'add category', - 26 => 'add entry', - 27 => 'approve entry', - 28 => 'delete category', - 29 => 'delete entry', - 30 => 'edit category', - 31 => 'update entry', - 32 => 'end', - 33 => 'start', - 34 => 'attempt', - 35 => 'close attempt', - 36 => 'preview', - 37 => 'editquestions', - 38 => 'delete attempt', - 39 => 'manualgrade', - ); - - $baseviewactions = array ( - 0 => 'view', - 1 => 'view all', - 2 => 'history', - 3 => 'view submission', - 4 => 'view feedback', - 5 => 'print', - 6 => 'report', - 7 => 'view discussion', - 8 => 'search', - 9 => 'forum', - 10 => 'forums', - 11 => 'subscribers', - 12 => 'view forum', - 13 => 'view entry', - 14 => 'review', - 15 => 'pre-view', - 16 => 'download', - 17 => 'view form', - 18 => 'view graph', - 19 => 'view report', - ); - - $postactions = stats_get_action_names('post'); - - foreach ($basepostactions as $action) { - $this->assertContains($action, $postactions); - } - - $viewactions = stats_get_action_names('view'); - - foreach ($baseviewactions as $action) { - $this->assertContains($action, $viewactions); - } - } - - /** - * Test the temporary table creation and deletion. - */ - public function test_statslib_temp_table_create_and_drop() { - global $DB; - - foreach ($this->tables as $table) { - $this->assertFalse($DB->get_manager()->table_exists($table)); - } - - stats_temp_table_create(); - - foreach ($this->tables as $table) { - $this->assertTrue($DB->get_manager()->table_exists($table)); - } - - stats_temp_table_drop(); - - foreach ($this->tables as $table) { - $this->assertFalse($DB->get_manager()->table_exists($table)); - } - } - - /** - * Test the temporary table creation and deletion. - */ - public function test_statslib_temp_table_fill() { - global $DB; - - $logs = array( // logs - array( - 'id' => 1, - 'time' => 1272686400, - 'userid' => 1, - 'course' => 1, - 'action' => 'login' - ), - array( - 'id' => 2, - 'time' => 1272686410, - 'userid' => 1, - 'course' => 1, - 'action' => 'login' - ), - array( - 'id' => 3, - 'time' => 1272772801, - 'userid' => 1, - 'course' => 1, - 'action' => 'login' - ), - ); - $this->prepare_db($logs); - - stats_temp_table_create(); - stats_temp_table_fill(1272686410, 1272772800); - - $this->assertEquals(1, $DB->count_records('temp_log1')); - $this->assertEquals(1, $DB->count_records('temp_log2')); - } - - /** - * Test the function that clean out the temporary tables. - */ - public function test_statslib_temp_table_clean() { - global $DB; - - $rows = array( - 'temp_log1' => array('id' => 1, 'course' => 1), - 'temp_log2' => array('id' => 1, 'course' => 1), - 'temp_stats_daily' => array('id' => 1, 'courseid' => 1), - 'temp_stats_user_daily' => array('id' => 1, 'courseid' => 1), - ); - - stats_temp_table_create(); - - foreach ($rows as $table => $row) { - $DB->insert_record_raw($table, $row); - $this->assertEquals(1, $DB->count_records($table)); - } - - stats_temp_table_clean(); - - foreach ($rows as $table => $row) { - $this->assertEquals(0, $DB->count_records($table)); - } - - $this->assertEquals(1, $DB->count_records('stats_daily')); - $this->assertEquals(1, $DB->count_records('stats_user_daily')); - } - - /** - * Test the daily stats function - * - * @dataProvider daily_log_provider - */ - public function test_statslib_cron_daily($logs, $stats) { - global $CFG; - - $CFG->debug = DEBUG_NONE; - - $this->prepare_db($logs); - - // Stats cron daily uses mtrace, turn on buffering to silence output. - ob_start(); - stats_cron_daily($maxdays=1); - ob_end_clean(); - - $this->verify_stats($stats); - } - - /** - * Test the daily stats function - */ - public function test_statslib_cron_daily_no_default_profile_id() { - global $CFG; - $CFG->defaultfrontpageroleid = 0; - - $course1 = SITEID + 1; - $guest = $CFG->siteguest; - $end = 1272772800; - $stid = 5; - $fpid = 0; - $gr = get_guest_role(); - - $logs = array( // Logs - array( - 'id' => 1, - 'time' => 1272686410, - 'userid' => $guest, - 'course' => SITEID, - 'action' => 'view' - ), - ); - - $stats = array( - 'stats_daily' => array( - array( // Query 2 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'logins', - 'stat1' => 0, - 'stat2' => 0 - ), - array( // Query 3 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => $stid, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 5 - 'courseid' => $course1, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 7 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'enrolments', - 'stat1' => 3, - 'stat2' => 1 - ), - array( // Query 11 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => 0, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - array( // Query 16 - 'courseid' => SITEID, - 'timeend' => $end, - 'roleid' => $gr->id, - 'stattype' => 'activity', - 'stat1' => 1, - 'stat2' => 0 - ), - ), - 'stats_user_daily' => array( // stats_user_daily - array( // Query 10 - read - 'courseid' => SITEID, - 'userid' => $guest, - 'roleid' => 0, - 'timeend' => $end, - 'statsreads' => 1, - 'statswrites' => 0, - 'stattype' => 'activity' - ), - ), - ); - - $CFG->debug = DEBUG_NONE; - - $this->prepare_db($logs); - - // Stats cron daily uses mtrace, turn on buffering to silence output. - ob_start(); - stats_cron_daily($maxdays=1); - ob_end_clean(); - - $this->verify_stats($stats); - } -} From 6a7fb9f778e129aa898a016b1e75b23045ec6c32 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:03:17 +0200 Subject: [PATCH 86/96] Revert "MDL-30643 - Now loading table definitions from XML files." This reverts commit d352e733b3729ee820de8859d67c859cda81ee34. --- lib/db/temp_stats_log_template.xml | 41 ------ lib/statslib.php | 224 +++++++++++++++-------------- 2 files changed, 116 insertions(+), 149 deletions(-) delete mode 100644 lib/db/temp_stats_log_template.xml diff --git a/lib/db/temp_stats_log_template.xml b/lib/db/temp_stats_log_template.xml deleted file mode 100644 index 78681cb5629..00000000000 --- a/lib/db/temp_stats_log_template.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - -
    -
    -
    \ No newline at end of file diff --git a/lib/statslib.php b/lib/statslib.php index ed720665fca..f44ae1ff571 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -104,7 +104,7 @@ function stats_cron_daily($maxdays=1) { $now = time(); - $fpcontext = get_context_instance(CONTEXT_COURSE, SITEID, MUST_EXIST); + $fpcontext = context_course::instance(SITEID, MUST_EXIST); // read last execution date from db if (!$timestart = get_config(NULL, 'statslastdaily')) { @@ -190,17 +190,17 @@ function stats_cron_daily($maxdays=1) { break; } - stats_progress('0'); + stats_progress('in'); /// find out if any logs available for this day - $sql = "SELECT 'x' FROM {temp_log1} l"; + $sql = "SELECT 'x' FROM {tmp_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); /// process login info first - $sql = "INSERT INTO {temp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) + $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads - FROM {temp_log1} l + FROM {tmp_log1} l WHERE action = 'login' GROUP BY timeend, courseid, userid HAVING count(statsreads) > 0"; @@ -211,11 +211,11 @@ function stats_cron_daily($maxdays=1) { } stats_progress('1'); - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0, COALESCE(SUM(statsreads), 0) as stat1, COUNT('x') as stat2 - FROM {temp_stats_user_daily} + FROM {tmp_stats_user_daily} WHERE stattype = 'logins' AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -235,7 +235,7 @@ function stats_cron_daily($maxdays=1) { // in that case, we'll count non-deleted users. // - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 @@ -251,34 +251,33 @@ function stats_cron_daily($maxdays=1) { } stats_progress('3'); - // Set stat2 to the number distinct users with role assignments in the course that were active // using table alias in UPDATE does not work in pg < 8.2 - $sql = "UPDATE {temp_stats_daily} + $sql = "UPDATE {tmp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ra.userid) FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) JOIN {enrol} e ON e.courseid = c.instanceid JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - WHERE ra.roleid = {temp_stats_daily}.roleid AND - e.courseid = {temp_stats_daily}.courseid AND + WHERE ra.roleid = {tmp_stats_daily}.roleid AND + e.courseid = {tmp_stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {temp_log1} l - WHERE l.course = {temp_stats_daily}.courseid AND + FROM {tmp_log1} l + WHERE l.course = {tmp_stats_daily}.courseid AND l.userid = ra.userid)) - WHERE {temp_stats_daily}.stattype = 'enrolments' AND - {temp_stats_daily}.timeend = $nextmidnight AND - {temp_stats_daily}.courseid IN + WHERE {tmp_stats_daily}.stattype = 'enrolments' AND + {tmp_stats_daily}.timeend = $nextmidnight AND + {tmp_stats_daily}.courseid IN (SELECT DISTINCT course - FROM {temp_log2})"; + FROM {tmp_log2})"; - if ($logspresent and !$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { + if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; break; } stats_progress('4'); - /// now get course total enrolments (roleid==0) - except frontpage - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// now get course total enrolments (roleid==0) - except frontpage + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, COUNT(DISTINCT userid) AS stat1, 0 AS stat2 @@ -293,22 +292,21 @@ function stats_cron_daily($maxdays=1) { } stats_progress('5'); - // Set stat 2 to the number of enrolled users who were active in the course - $sql = "UPDATE {temp_stats_daily} + $sql = "UPDATE {tmp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ue.userid) FROM {enrol} e JOIN {user_enrolments} ue ON ue.enrolid = e.id - WHERE e.courseid = {temp_stats_daily}.courseid AND + WHERE e.courseid = {tmp_stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {temp_log1} l - WHERE l.course = {temp_stats_daily}.courseid AND + FROM {tmp_log1} l + WHERE l.course = {tmp_stats_daily}.courseid AND l.userid = ue.userid)) - WHERE {temp_stats_daily}.stattype = 'enrolments' AND - {temp_stats_daily}.timeend = $nextmidnight AND - {temp_stats_daily}.roleid = 0 AND - {temp_stats_daily}.courseid IN + WHERE {tmp_stats_daily}.stattype = 'enrolments' AND + {tmp_stats_daily}.timeend = $nextmidnight AND + {tmp_stats_daily}.roleid = 0 AND + {tmp_stats_daily}.courseid IN (SELECT l.course - FROM {temp_log2} l + FROM {tmp_log2} l WHERE l.course <> ".SITEID.")"; if ($logspresent and !$DB->execute($sql, array())) { @@ -317,8 +315,8 @@ function stats_cron_daily($maxdays=1) { } stats_progress('6'); - /// frontpage(==site) enrolments total - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// frontpage(==site) enrolments total + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", 0, (SELECT COUNT('x') @@ -326,7 +324,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {temp_log1} l ON l.userid = u.id + JOIN {tmp_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause(); @@ -336,11 +334,11 @@ function stats_cron_daily($maxdays=1) { } stats_progress('7'); - /// Default frontpage role enrolments are all site users (not deleted) + /// Default frontpage role enrolments are all site users (not deleted) if ($defaultfproleid) { // first remove default frontpage role counts if created by previous query $sql = "DELETE - FROM {temp_stats_daily} + FROM {tmp_stats_daily} WHERE stattype = 'enrolments' AND courseid = ".SITEID." AND roleid = $defaultfproleid AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -349,7 +347,7 @@ function stats_cron_daily($maxdays=1) { } stats_progress('8'); - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", $defaultfproleid, (SELECT COUNT('x') @@ -357,7 +355,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {temp_log1} l ON l.userid = u.id + JOIN {tmp_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause();; @@ -373,15 +371,15 @@ function stats_cron_daily($maxdays=1) { } - /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible + /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible list($viewactionssql, $params1) = $DB->get_in_or_equal($viewactions, SQL_PARAMS_NAMED, 'view'); list($postactionssql, $params2) = $DB->get_in_or_equal($postactions, SQL_PARAMS_NAMED, 'post'); - $sql = "INSERT INTO {temp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) + $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) SELECT 'activity' AS stattype, $nextmidnight AS timeend, course as courseid, userid, SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites - FROM {temp_log1} l + FROM {tmp_log1} l WHERE !(course = 0 AND userid = 0) GROUP BY userid, courseid"; @@ -392,13 +390,13 @@ function stats_cron_daily($maxdays=1) { stats_progress('10'); - /// how many view/post actions in each course total - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view/post actions in each course total + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0, SUM(CASE WHEN l.action $viewactionssql THEN 1 ELSE 0 END) AS stat1, SUM(CASE WHEN l.action $postactionssql THEN 1 ELSE 0 END) AS stat2 - FROM {course} c, {temp_log1} l + FROM {course} c, {tmp_log1} l WHERE l.course = c.id GROUP BY courseid"; @@ -409,14 +407,14 @@ function stats_cron_daily($maxdays=1) { stats_progress('11'); - /// how many view actions for each course+role - excluding guests and frontpage + /// how many view actions for each course+role - excluding guests and frontpage - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {temp_stats_user_daily} sud, + FROM {tmp_stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, e.courseid FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) @@ -439,16 +437,16 @@ function stats_cron_daily($maxdays=1) { } stats_progress('12'); - /// how many view actions from guests only in each course - excluding frontpage - /// normal users may enter course with temporary guest access too + /// how many view actions from guests only in each course - excluding frontpage + /// normal users may enter course with temporary guest access too - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight as timeend, courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.courseid, sud.statsreads, sud.statswrites - FROM {temp_stats_user_daily} sud + FROM {tmp_stats_user_daily} sud WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND (sud.userid = $guest OR sud.userid @@ -467,14 +465,14 @@ function stats_cron_daily($maxdays=1) { stats_progress('13'); - /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {temp_stats_user_daily} sud, + FROM {tmp_stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, c.instanceid AS courseid FROM {role_assignments} ra JOIN {context} c ON c.id = ra.contextid @@ -498,14 +496,14 @@ function stats_cron_daily($maxdays=1) { stats_progress('14'); - /// how many view actions for default frontpage role on frontpage only - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view actions for default frontpage role on frontpage only + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', timeend, courseid, $defaultfproleid AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.timeend AS timeend, sud.courseid, sud.statsreads, sud.statswrites - FROM {temp_stats_user_daily} sud + FROM {tmp_stats_user_daily} sud WHERE sud.timeend = :nextm AND sud.courseid = :siteid AND sud.stattype='activity' AND sud.userid <> $guest AND sud.userid <> 0 AND sud.userid @@ -523,8 +521,8 @@ function stats_cron_daily($maxdays=1) { } stats_progress('15'); - /// how many view actions for guests or not-logged-in on frontpage - $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + /// how many view actions for guests or not-logged-in on frontpage + $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) @@ -532,7 +530,7 @@ function stats_cron_daily($maxdays=1) { SELECT pl.statsreads, pl.statswrites FROM ( SELECT sud.statsreads, sud.statswrites - FROM {temp_stats_user_daily} sud + FROM {tmp_stats_user_daily} sud WHERE (sud.userid = $guest OR sud.userid = 0) AND sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND sud.stattype='activity' @@ -567,11 +565,11 @@ function stats_cron_daily($maxdays=1) { if ($failed) { $days--; - mtrace("...error occurred, completed $days days of statistics in {$total} s."); + mtrace("...error occurred, completed $days days of statistics."); return false; } else if ($timeout) { - mtrace("...stopping early, reached maximum number of $maxdays days ({$total} s) - will continue next time."); + mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time."); return false; } else { @@ -709,7 +707,7 @@ function stats_cron_weekly() { set_config('statslastweekly', $nextstartweek); $elapsed = time()-$weekstart; - mtrace(" finished until $nextstartweek: ".userdate($nextstartweek) ." (in $elapsed s)"); + mtrace(" finished until $nextstartweek: ".userdate($nextstartweek) ." ( in $elapsed s)"); $timestart = $nextstartweek; $nextstartweek = stats_get_next_week_start($nextstartweek); @@ -1348,7 +1346,7 @@ function stats_get_report_options($courseid,$mode) { switch ($mode) { case STATS_MODE_GENERAL: $reportoptions[STATS_REPORT_ACTIVITY] = get_string('statsreport'.STATS_REPORT_ACTIVITY); - if ($courseid != SITEID && $context = get_context_instance(CONTEXT_COURSE, $courseid)) { + if ($courseid != SITEID && $context = context_course::instance($courseid)) { $sql = 'SELECT r.id, r.name FROM {role} r JOIN {stats_daily} s ON s.roleid = r.id WHERE s.courseid = :courseid GROUP BY r.id, r.name'; if ($roles = $DB->get_records_sql($sql, array('courseid' => $courseid))) { foreach ($roles as $role) { @@ -1366,13 +1364,13 @@ function stats_get_report_options($courseid,$mode) { case STATS_MODE_DETAILED: $reportoptions[STATS_REPORT_USER_ACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ACTIVITY); $reportoptions[STATS_REPORT_USER_ALLACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ALLACTIVITY); - if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { + if (has_capability('report/stats:view', context_system::instance())) { $site = get_site(); $reportoptions[STATS_REPORT_USER_LOGINS] = get_string('statsreport'.STATS_REPORT_USER_LOGINS); } break; case STATS_MODE_RANKED: - if (has_capability('report/stats:view', get_context_instance(CONTEXT_SYSTEM))) { + if (has_capability('report/stats:view', context_system::instance())) { $reportoptions[STATS_REPORT_ACTIVE_COURSES] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES); $reportoptions[STATS_REPORT_ACTIVE_COURSES_WEIGHTED] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES_WEIGHTED); $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES); @@ -1489,50 +1487,60 @@ function stats_temp_table_create() { stats_temp_table_drop(); - $xmlfile = $CFG->dirroot . '/lib/db/install.xml'; - $tempfile = $CFG->dirroot . '/lib/db/temp_stats_log_template.xml'; - $tables = array(); - $files = array( - $xmlfile => array('stats_daily', 'stats_user_daily'), - $tempfile => array('temp_log1', 'temp_log2'), - ); + $log = new xmldb_table('tmp_log1'); - foreach ($files as $file => $contents) { + $log->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $log->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $log->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $log->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); - $xmldb_file = new xmldb_file($file); - if (!$xmldb_file->fileExists()) { - throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist'); - } - $loaded = $xmldb_file->loadXMLStructure(); - if (!$loaded || !$xmldb_file->isLoaded()) { - throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??'); - } - $xmldb_structure = $xmldb_file->getStructure(); + $log->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $log->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); + $log->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); + $log->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $log->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); - foreach ($contents as $name) { - $table = $xmldb_structure->getTable($name); + $user = new xmldb_table('tmp_stats_daily'); + $user->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); + $user->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $user->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - if (is_null($table)) { - throw new ddl_exception('ddlunknowntable', null, 'The table '. $name .' is not defined in the file '. $xmlfile); - } - $table->setNext(null); - $table->setPrevious(null); - $tables[$name] = $table; - } - } + $user->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $user->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + $user->add_index('tmp_tsd_statype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); + $user->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); - // Rename the standard table templates so that they don't conflict with the standard tables - $tables['stats_daily']->setName('temp_stats_daily'); - $tables['stats_user_daily']->setName('temp_stats_user_daily'); + $daily = new xmldb_table('tmp_stats_user_daily'); + $daily->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $daily->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); + + $daily->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $daily->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $daily->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + $daily->add_index('tmp_tsud_stattype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); + $daily->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); try { + $dbman->create_temp_table($log); - foreach ($tables as $table) { - $dbman->create_temp_table($table); - } + $log->name = 'tmp_log2'; + + $dbman->create_temp_table($log); + + $dbman->create_temp_table($user); + + $dbman->create_temp_table($daily); } catch (Exception $e) { - mtrace('Temporary table creation failed: '. $e->getMessage()); + mtrace("Temporary table creation failed!"); return false; } @@ -1547,7 +1555,7 @@ function stats_temp_table_drop() { $dbman = $DB->get_manager(); - $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); + $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); foreach ($tables as $name) { @@ -1555,7 +1563,7 @@ function stats_temp_table_drop() { $table = new xmldb_table($name); try { - $dbman->drop_table($table); + $dbman->drop_temp_table($table); } catch (Exception $e) { mtrace("Error occured while dropping temporary tables!"); } @@ -1573,7 +1581,7 @@ function stats_temp_table_drop() { function stats_temp_table_fill($timestart, $timeend) { global $DB; - $sql = "INSERT INTO {temp_log1} + $sql = "INSERT INTO {tmp_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; @@ -1581,8 +1589,8 @@ function stats_temp_table_fill($timestart, $timeend) { return false; } - $sql = "INSERT INTO {temp_log2} - SELECT * FROM {temp_log1}"; + $sql = "INSERT INTO {tmp_log2} + SELECT * FROM {tmp_log1}"; if (!$DB->execute($sql)) { return false; @@ -1603,15 +1611,15 @@ function stats_temp_table_clean() { $sql = array(); $sql['up1'] = 'INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2)' - .' SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {temp_stats_daily}'; + .' SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}'; $sql['up2'] = 'INSERT INTO {stats_user_daily}' .' (courseid, userid, roleid, timeend, statsreads, statswrites, stattype)' .' SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype' - .' FROM {temp_stats_user_daily}'; + .' FROM {tmp_stats_user_daily}'; $x = 1; - $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily'); + $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); foreach ($tables as $name) { $sql['tr'. $x] = "TRUNCATE TABLE {{$name}}"; From 583a19d772b78ca8843358aebe7acedead5c8bfc Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:03:30 +0200 Subject: [PATCH 87/96] Revert "MDL-30643 - Moved table create/drop out of loop to fix performance issue." This reverts commit 678f53c5f781d45424020530435382645325ab73. --- lib/statslib.php | 298 +++++++++++++++++------------------------------ 1 file changed, 106 insertions(+), 192 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index f44ae1ff571..93173992ab0 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -75,7 +75,7 @@ define('STATS_MODE_RANKED',3); // admins only - ranks courses * Print daily cron progress * @param string $ident */ -function stats_progress($ident) { +function stats_daily_progress($ident) { static $start = 0; static $init = 0; @@ -156,21 +156,14 @@ function stats_cron_daily($maxdays=1) { mtrace("Running daily statistics gathering, starting at $timestart:"); - $days = 0; - $total = 0; - $failed = false; // failed stats flag - $timeout = false; + $days = 0; + $failed = false; // failed stats flag - if (!stats_temp_table_create()) { - $days = 1; - $failed = true; - } - mtrace("Temporary tables created"); - - while (!$failed && ($now > $nextmidnight)) { + while ($now > $nextmidnight) { if ($days >= $maxdays) { - $timeout = true; - break; + mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time."); + set_cron_lock('statsrunning', null); + return false; } $days++; @@ -183,14 +176,15 @@ function stats_cron_daily($maxdays=1) { $daystart = time(); - stats_progress('init'); + stats_daily_progress('init'); - if (!stats_temp_table_fill($timestart, $nextmidnight)) { + if ( !stats_create_logs_view($timestart, $nextmidnight) ) { $failed = true; break; } - stats_progress('in'); + stats_daily_progress('gen'); + /// find out if any logs available for this day $sql = "SELECT 'x' FROM {tmp_log1} l"; @@ -209,7 +203,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('1'); + stats_daily_progress('1'); $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -222,7 +216,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('2'); + stats_daily_progress('2'); // Enrolments and active enrolled users @@ -249,7 +243,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('3'); + stats_daily_progress('3'); // using table alias in UPDATE does not work in pg < 8.2 $sql = "UPDATE {tmp_stats_daily} @@ -274,7 +268,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('4'); + stats_daily_progress('4'); /// now get course total enrolments (roleid==0) - except frontpage $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -290,7 +284,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('5'); + stats_daily_progress('5'); $sql = "UPDATE {tmp_stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ue.userid) @@ -313,7 +307,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('6'); + stats_daily_progress('6'); /// frontpage(==site) enrolments total $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -332,7 +326,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('7'); + stats_daily_progress('7'); /// Default frontpage role enrolments are all site users (not deleted) if ($defaultfproleid) { @@ -345,7 +339,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('8'); + stats_daily_progress('8'); $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -363,11 +357,11 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('9'); + stats_daily_progress('9'); } else { - stats_progress('x'); - stats_progress('x'); + stats_daily_progress('x'); + stats_daily_progress('x'); } @@ -387,7 +381,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('10'); + stats_daily_progress('10'); /// how many view/post actions in each course total @@ -404,7 +398,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('11'); + stats_daily_progress('11'); /// how many view actions for each course+role - excluding guests and frontpage @@ -435,7 +429,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('12'); + stats_daily_progress('12'); /// how many view actions from guests only in each course - excluding frontpage /// normal users may enter course with temporary guest access too @@ -462,7 +456,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('13'); + stats_daily_progress('13'); /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role @@ -493,7 +487,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('14'); + stats_daily_progress('14'); /// how many view actions for default frontpage role on frontpage only @@ -519,7 +513,7 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('15'); + stats_daily_progress('15'); /// how many view actions for guests or not-logged-in on frontpage $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -543,23 +537,31 @@ function stats_cron_daily($maxdays=1) { $failed = true; break; } - stats_progress('16'); + stats_daily_progress('16'); - stats_temp_table_clean(); + $sql = "INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2) + SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}"; + if (!$DB->execute($sql)) { + $failed = true; + break; + } - stats_progress('out'); + $sql = "INSERT INTO {stats_user_daily} (courseid, userid, roleid, timeend, statsreads, statswrites, stattype) + SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype FROM {tmp_stats_user_daily}"; + if (!$DB->execute($sql)) { + $failed = true; + break; + } // remember processed days set_config('statslastdaily', $nextmidnight); - $elapsed = time()-$daystart; - mtrace(" finished until $nextmidnight: ".userdate($nextmidnight)." (in $elapsed s)"); - $total += $elapsed; + mtrace(" finished until $nextmidnight: ".userdate($nextmidnight)." (in ".(time()-$daystart)." s)"); $timestart = $nextmidnight; $nextmidnight = stats_get_next_day_start($nextmidnight); } - stats_temp_table_drop(); + stats_drop_logs_view(); set_cron_lock('statsrunning', null); @@ -568,12 +570,8 @@ function stats_cron_daily($maxdays=1) { mtrace("...error occurred, completed $days days of statistics."); return false; - } else if ($timeout) { - mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time."); - return false; - } else { - mtrace("...completed $days days of statistics in {$total} s."); + mtrace("...completed $days days of statistics."); return true; } } @@ -626,9 +624,6 @@ function stats_cron_weekly() { $logtimesql = "l.time >= $timestart AND l.time < $nextstartweek"; $stattimesql = "timeend > $timestart AND timeend <= $nextstartweek"; - $weekstart = time(); - stats_progress('init'); - /// process login info first $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads) @@ -643,8 +638,6 @@ function stats_cron_weekly() { $DB->execute($sql); - stats_progress('1'); - $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextstartweek AS timeend, ".SITEID." as courseid, 0, @@ -658,7 +651,6 @@ function stats_cron_weekly() { $DB->execute($sql); - stats_progress('2'); /// now enrolments averages $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -673,7 +665,6 @@ function stats_cron_weekly() { $DB->execute($sql); - stats_progress('3'); /// activity read/write averages $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -688,7 +679,6 @@ function stats_cron_weekly() { $DB->execute($sql); - stats_progress('4'); /// user read/write averages $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads, statswrites) @@ -703,11 +693,8 @@ function stats_cron_weekly() { $DB->execute($sql); - stats_progress('5'); - set_config('statslastweekly', $nextstartweek); - $elapsed = time()-$weekstart; - mtrace(" finished until $nextstartweek: ".userdate($nextstartweek) ." ( in $elapsed s)"); + mtrace(" finished until $nextstartweek: ".userdate($nextstartweek)); $timestart = $nextstartweek; $nextstartweek = stats_get_next_week_start($nextstartweek); @@ -768,9 +755,6 @@ function stats_cron_monthly() { $logtimesql = "l.time >= $timestart AND l.time < $nextstartmonth"; $stattimesql = "timeend > $timestart AND timeend <= $nextstartmonth"; - $monthstart = time(); - stats_progress('init'); - /// process login info first $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads) @@ -784,8 +768,6 @@ function stats_cron_monthly() { $DB->execute($sql); - stats_progress('1'); - $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextstartmonth AS timeend, ".SITEID." as courseid, 0, @@ -799,7 +781,6 @@ function stats_cron_monthly() { $DB->execute($sql); - stats_progress('2'); /// now enrolments averages $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -814,7 +795,6 @@ function stats_cron_monthly() { $DB->execute($sql); - stats_progress('3'); /// activity read/write averages $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2) @@ -829,7 +809,6 @@ function stats_cron_monthly() { $DB->execute($sql); - stats_progress('4'); /// user read/write averages $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads, statswrites) @@ -844,11 +823,8 @@ function stats_cron_monthly() { $DB->execute($sql); - stats_progress('5'); - set_config('statslastmonthly', $nextstartmonth); - $elapsed = time() - $monthstart; - mtrace(" finished until $nextstartmonth: ".userdate($nextstartmonth) ." (in $elapsed s)"); + mtrace(" finished until $nextstartmonth: ".userdate($nextstartmonth)); $timestart = $nextstartmonth; $nextstartmonth = stats_get_next_month_start($nextstartmonth); @@ -1479,107 +1455,36 @@ function stats_check_uptodate($courseid=0) { /** * Create temporary tables to speed up log generation - */ -function stats_temp_table_create() { - global $CFG, $DB; - - $dbman = $DB->get_manager(); // We are going to use database_manager services - - stats_temp_table_drop(); - - $log = new xmldb_table('tmp_log1'); - - $log->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $log->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $log->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $log->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); - - $log->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $log->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); - $log->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); - $log->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $log->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); - - $user = new xmldb_table('tmp_stats_daily'); - $user->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); - $user->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $user->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - - $user->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $user->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - $user->add_index('tmp_tsd_statype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); - $user->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); - - $daily = new xmldb_table('tmp_stats_user_daily'); - $daily->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $daily->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); - - $daily->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $daily->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $daily->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - $daily->add_index('tmp_tsud_stattype_ix', XMLDB_INDEX_NOTUNIQUE, array('stattype')); - $daily->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); - - try { - $dbman->create_temp_table($log); - - $log->name = 'tmp_log2'; - - $dbman->create_temp_table($log); - - $dbman->create_temp_table($user); - - $dbman->create_temp_table($daily); - - } catch (Exception $e) { - mtrace("Temporary table creation failed!"); - return false; - } - - return true; -} - -/** - * Deletes summary logs table for stats calculation - */ -function stats_temp_table_drop() { - global $DB; - - $dbman = $DB->get_manager(); - - $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); - - foreach ($tables as $name) { - - if ($dbman->table_exists($name)) { - $table = new xmldb_table($name); - - try { - $dbman->drop_temp_table($table); - } catch (Exception $e) { - mtrace("Error occured while dropping temporary tables!"); - } - } - } -} - -/** - * Fills the temporary stats tables with new data * * @param timestart timestamp of the start time of logs view * @param timeend timestamp of the end time of logs view * @returns boolen success (true) or failure(false) */ -function stats_temp_table_fill($timestart, $timeend) { - global $DB; +function stats_create_logs_view ( $timestart, $timeend ) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); // We are going to use database_manager services + + stats_drop_logs_view(); + + $table = new xmldb_table('tmp_log1'); + + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); + + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); + $table->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); + $table->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $table->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); + + $dbman->create_temp_table($table); + + $table->name = 'tmp_log2'; + + $dbman->create_temp_table($table); $sql = "INSERT INTO {tmp_log1} SELECT id, userid, course, action FROM {log} l @@ -1596,44 +1501,53 @@ function stats_temp_table_fill($timestart, $timeend) { return false; } + $table = new xmldb_table('tmp_stats_daily'); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); + $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + + $table->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $table->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + $table->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + + $dbman->create_temp_table($table); + + $table = new xmldb_table('tmp_stats_user_daily'); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); + $table->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); + + $table->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + $table->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $table->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); + $table->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); + + $dbman->create_temp_table($table); + return true; } - /** * Deletes summary logs table for stats calculation - * - * @returns boolen success (true) or failure(false) */ -function stats_temp_table_clean() { +function stats_drop_logs_view() { global $DB; - $sql = array(); + $dbman = $DB->get_manager(); - $sql['up1'] = 'INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2)' - .' SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}'; - - $sql['up2'] = 'INSERT INTO {stats_user_daily}' - .' (courseid, userid, roleid, timeend, statsreads, statswrites, stattype)' - .' SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype' - .' FROM {tmp_stats_user_daily}'; - - $x = 1; $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); foreach ($tables as $name) { - $sql['tr'. $x] = "TRUNCATE TABLE {{$name}}"; - $x += 1; - } - - foreach ($sql as $id => $query) { - try { - $DB->execute($query); - } catch (Exception $e) { - mtrace("Error during table cleanup!"); - return false; + if ($dbman->table_exists($name)) { + $table = new xmldb_table($name); + $dbman->drop_temp_table($table); } } - - return true; } From cbcf50a2f4595eca7269153e3a54fed85c43172a Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:03:46 +0200 Subject: [PATCH 88/96] Revert "MDL-30643 - Added additional temporary tables to deal with log growth in daily tables." This reverts commit e710925a24bbbe0a4f0ddfa0b760380091890083. --- lib/statslib.php | 165 ++++++++++++++++++----------------------------- 1 file changed, 61 insertions(+), 104 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index 93173992ab0..936bb465743 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -187,14 +187,14 @@ function stats_cron_daily($maxdays=1) { /// find out if any logs available for this day - $sql = "SELECT 'x' FROM {tmp_log1} l"; + $sql = "SELECT 'x' FROM {tmp_stats_log1} l"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); /// process login info first - $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads) + $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads) SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads - FROM {tmp_log1} l + FROM {tmp_stats_log1} l WHERE action = 'login' GROUP BY timeend, courseid, userid HAVING count(statsreads) > 0"; @@ -205,11 +205,11 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('1'); - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0, COALESCE(SUM(statsreads), 0) as stat1, COUNT('x') as stat2 - FROM {tmp_stats_user_daily} + FROM {stats_user_daily} WHERE stattype = 'logins' AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -229,7 +229,7 @@ function stats_cron_daily($maxdays=1) { // in that case, we'll count non-deleted users. // - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 @@ -246,23 +246,23 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('3'); // using table alias in UPDATE does not work in pg < 8.2 - $sql = "UPDATE {tmp_stats_daily} + $sql = "UPDATE {stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ra.userid) FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) JOIN {enrol} e ON e.courseid = c.instanceid JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - WHERE ra.roleid = {tmp_stats_daily}.roleid AND - e.courseid = {tmp_stats_daily}.courseid AND + WHERE ra.roleid = {stats_daily}.roleid AND + e.courseid = {stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_log1} l - WHERE l.course = {tmp_stats_daily}.courseid AND + FROM {tmp_stats_log1} l + WHERE l.course = {stats_daily}.courseid AND l.userid = ra.userid)) - WHERE {tmp_stats_daily}.stattype = 'enrolments' AND - {tmp_stats_daily}.timeend = $nextmidnight AND - {tmp_stats_daily}.courseid IN + WHERE {stats_daily}.stattype = 'enrolments' AND + {stats_daily}.timeend = $nextmidnight AND + {stats_daily}.courseid IN (SELECT DISTINCT course - FROM {tmp_log2})"; + FROM {tmp_stats_log2})"; if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; @@ -271,7 +271,7 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('4'); /// now get course total enrolments (roleid==0) - except frontpage - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, COUNT(DISTINCT userid) AS stat1, 0 AS stat2 @@ -286,21 +286,21 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('5'); - $sql = "UPDATE {tmp_stats_daily} + $sql = "UPDATE {stats_daily} SET stat2 = (SELECT COUNT(DISTINCT ue.userid) FROM {enrol} e JOIN {user_enrolments} ue ON ue.enrolid = e.id - WHERE e.courseid = {tmp_stats_daily}.courseid AND + WHERE e.courseid = {stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_log1} l - WHERE l.course = {tmp_stats_daily}.courseid AND + FROM {tmp_stats_log1} l + WHERE l.course = {stats_daily}.courseid AND l.userid = ue.userid)) - WHERE {tmp_stats_daily}.stattype = 'enrolments' AND - {tmp_stats_daily}.timeend = $nextmidnight AND - {tmp_stats_daily}.roleid = 0 AND - {tmp_stats_daily}.courseid IN + WHERE {stats_daily}.stattype = 'enrolments' AND + {stats_daily}.timeend = $nextmidnight AND + {stats_daily}.roleid = 0 AND + {stats_daily}.courseid IN (SELECT l.course - FROM {tmp_log2} l + FROM {tmp_stats_log2} l WHERE l.course <> ".SITEID.")"; if ($logspresent and !$DB->execute($sql, array())) { @@ -310,7 +310,7 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('6'); /// frontpage(==site) enrolments total - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", 0, (SELECT COUNT('x') @@ -318,7 +318,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_log1} l ON l.userid = u.id + JOIN {tmp_stats_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause(); @@ -332,7 +332,7 @@ function stats_cron_daily($maxdays=1) { if ($defaultfproleid) { // first remove default frontpage role counts if created by previous query $sql = "DELETE - FROM {tmp_stats_daily} + FROM {stats_daily} WHERE stattype = 'enrolments' AND courseid = ".SITEID." AND roleid = $defaultfproleid AND timeend = $nextmidnight"; if ($logspresent and !$DB->execute($sql)) { @@ -341,7 +341,7 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('8'); - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", $defaultfproleid, (SELECT COUNT('x') @@ -349,7 +349,7 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_log1} l ON l.userid = u.id + JOIN {tmp_stats_log1} l ON l.userid = u.id WHERE u.deleted = 0) AS stat2" . $DB->sql_null_from_clause();; @@ -368,12 +368,12 @@ function stats_cron_daily($maxdays=1) { /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible list($viewactionssql, $params1) = $DB->get_in_or_equal($viewactions, SQL_PARAMS_NAMED, 'view'); list($postactionssql, $params2) = $DB->get_in_or_equal($postactions, SQL_PARAMS_NAMED, 'post'); - $sql = "INSERT INTO {tmp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) + $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) SELECT 'activity' AS stattype, $nextmidnight AS timeend, course as courseid, userid, SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites - FROM {tmp_log1} l + FROM {tmp_stats_log1} l WHERE !(course = 0 AND userid = 0) GROUP BY userid, courseid"; @@ -385,12 +385,12 @@ function stats_cron_daily($maxdays=1) { /// how many view/post actions in each course total - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0, SUM(CASE WHEN l.action $viewactionssql THEN 1 ELSE 0 END) AS stat1, SUM(CASE WHEN l.action $postactionssql THEN 1 ELSE 0 END) AS stat2 - FROM {course} c, {tmp_log1} l + FROM {course} c, {tmp_stats_log1} l WHERE l.course = c.id GROUP BY courseid"; @@ -403,12 +403,12 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for each course+role - excluding guests and frontpage - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud, + FROM {stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, e.courseid FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) @@ -434,13 +434,13 @@ function stats_cron_daily($maxdays=1) { /// how many view actions from guests only in each course - excluding frontpage /// normal users may enter course with temporary guest access too - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight as timeend, courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.courseid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud + FROM {stats_user_daily} sud WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND (sud.userid = $guest OR sud.userid @@ -460,13 +460,13 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud, + FROM {stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, c.instanceid AS courseid FROM {role_assignments} ra JOIN {context} c ON c.id = ra.contextid @@ -491,13 +491,13 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for default frontpage role on frontpage only - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', timeend, courseid, $defaultfproleid AS roleid, SUM(statsreads), SUM(statswrites) FROM ( SELECT sud.timeend AS timeend, sud.courseid, sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud + FROM {stats_user_daily} sud WHERE sud.timeend = :nextm AND sud.courseid = :siteid AND sud.stattype='activity' AND sud.userid <> $guest AND sud.userid <> 0 AND sud.userid @@ -516,7 +516,7 @@ function stats_cron_daily($maxdays=1) { stats_daily_progress('15'); /// how many view actions for guests or not-logged-in on frontpage - $sql = "INSERT INTO {tmp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) + $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity', $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS roleid, SUM(statsreads), SUM(statswrites) @@ -524,7 +524,7 @@ function stats_cron_daily($maxdays=1) { SELECT pl.statsreads, pl.statswrites FROM ( SELECT sud.statsreads, sud.statswrites - FROM {tmp_stats_user_daily} sud + FROM {stats_user_daily} sud WHERE (sud.userid = $guest OR sud.userid = 0) AND sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND sud.stattype='activity' @@ -539,20 +539,6 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('16'); - $sql = "INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2) - SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {tmp_stats_daily}"; - if (!$DB->execute($sql)) { - $failed = true; - break; - } - - $sql = "INSERT INTO {stats_user_daily} (courseid, userid, roleid, timeend, statsreads, statswrites, stattype) - SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype FROM {tmp_stats_user_daily}"; - if (!$DB->execute($sql)) { - $failed = true; - break; - } - // remember processed days set_config('statslastdaily', $nextmidnight); mtrace(" finished until $nextmidnight: ".userdate($nextmidnight)." (in ".(time()-$daystart)." s)"); @@ -1454,7 +1440,7 @@ function stats_check_uptodate($courseid=0) { } /** - * Create temporary tables to speed up log generation + * Creates tmp log table * * @param timestart timestamp of the start time of logs view * @param timeend timestamp of the end time of logs view @@ -1467,7 +1453,7 @@ function stats_create_logs_view ( $timestart, $timeend ) { stats_drop_logs_view(); - $table = new xmldb_table('tmp_log1'); + $table = new xmldb_table('tmp_stats_log1'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); @@ -1475,18 +1461,18 @@ function stats_create_logs_view ( $timestart, $timeend ) { $table->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->add_index('tmp_tl_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); - $table->add_index('tmp_tl_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); - $table->add_index('tmp_tl_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $table->add_index('tmp_tl_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); + $table->add_index('tmp_stats_log_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); + $table->add_index('tmp_stats_log_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); + $table->add_index('tmp_stats_log_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $table->add_index('tmp_stats_log_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); $dbman->create_temp_table($table); - $table->name = 'tmp_log2'; + $table->name = 'tmp_stats_log2'; $dbman->create_temp_table($table); - $sql = "INSERT INTO {tmp_log1} + $sql = "INSERT INTO {tmp_stats_log1} SELECT id, userid, course, action FROM {log} l WHERE l.time >= $timestart AND l.time < $timeend"; @@ -1494,43 +1480,13 @@ function stats_create_logs_view ( $timestart, $timeend ) { return false; } - $sql = "INSERT INTO {tmp_log2} - SELECT * FROM {tmp_log1}"; + $sql = "INSERT INTO {tmp_stats_log2} + SELECT * FROM {tmp_stats_log1}"; if (!$DB->execute($sql)) { return false; } - $table = new xmldb_table('tmp_stats_daily'); - $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'activity'); - $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - - $table->add_index('tmp_tsd_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $table->add_index('tmp_tsd_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); - $table->add_index('tmp_tsd_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - - $dbman->create_temp_table($table); - - $table = new xmldb_table('tmp_stats_user_daily'); - $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('statsreads', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('statswrites', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, 0); - $table->add_field('stattype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null); - - $table->add_index('tmp_tsud_courseid_ix', XMLDB_INDEX_NOTUNIQUE, array('courseid')); - $table->add_index('tmp_tsud_userid_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $table->add_index('tmp_tsud_roleid_ix', XMLDB_INDEX_NOTUNIQUE, array('roleid')); - $table->add_index('tmp_tsud_timeend_ix', XMLDB_INDEX_NOTUNIQUE, array('timeend')); - - $dbman->create_temp_table($table); - return true; } @@ -1542,12 +1498,13 @@ function stats_drop_logs_view() { $dbman = $DB->get_manager(); - $tables = array('tmp_log1', 'tmp_log2', 'tmp_stats_daily', 'tmp_stats_user_daily'); + if ( $dbman->table_exists('tmp_stats_log1') ) { + $table = new xmldb_table('tmp_stats_log1'); + $dbman->drop_temp_table($table); + } - foreach ($tables as $name) { - if ($dbman->table_exists($name)) { - $table = new xmldb_table($name); - $dbman->drop_temp_table($table); - } + if ( $dbman->table_exists('tmp_stats_log2') ) { + $table = new xmldb_table('tmp_stats_log2'); + $dbman->drop_temp_table($table); } } From 44a2eb911be15d0ab090ab1a2aa15b65a0e7fd16 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 5 Sep 2012 19:04:01 +0200 Subject: [PATCH 89/96] Revert "MDL-30643 - Fixed speed issues with stats generation." This reverts commit 5d911452cd9ef60f8f79ed9bd39fc8302211a96c. --- lib/statslib.php | 240 +++++++++++++++++++---------------------------- 1 file changed, 97 insertions(+), 143 deletions(-) diff --git a/lib/statslib.php b/lib/statslib.php index 936bb465743..6b2220fbe25 100644 --- a/lib/statslib.php +++ b/lib/statslib.php @@ -80,12 +80,12 @@ function stats_daily_progress($ident) { static $init = 0; if ($ident == 'init') { - $init = $start = microtime(true); + $init = $start = time(); return; } - $elapsed = round(microtime(true) - $start); - $start = microtime(true); + $elapsed = time() - $start; + $start = time(); if (debugging('', DEBUG_ALL)) { mtrace("$ident:$elapsed ", ''); @@ -176,27 +176,29 @@ function stats_cron_daily($maxdays=1) { $daystart = time(); + $timesql = "l.time >= $timestart AND l.time < $nextmidnight"; + $timesql1 = "l1.time >= $timestart AND l1.time < $nextmidnight"; + $timesql2 = "l2.time >= $timestart AND l2.time < $nextmidnight"; + stats_daily_progress('init'); - if ( !stats_create_logs_view($timestart, $nextmidnight) ) { - $failed = true; - break; - } - - stats_daily_progress('gen'); - /// find out if any logs available for this day - $sql = "SELECT 'x' FROM {tmp_stats_log1} l"; + $sql = "SELECT 'x' + FROM {log} l + WHERE $timesql"; $logspresent = $DB->get_records_sql($sql, null, 0, 1); /// process login info first $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads) - SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid, userid, count(id) as statsreads - FROM {tmp_stats_log1} l - WHERE action = 'login' - GROUP BY timeend, courseid, userid + SELECT 'logins', timeend, courseid, userid, count(statsreads) + FROM ( + SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, l.userid, l.id AS statsreads + FROM {log} l + WHERE action = 'login' AND $timesql + ) inline_view + GROUP BY timeend, courseid, userid HAVING count(statsreads) > 0"; if ($logspresent and !$DB->execute($sql)) { @@ -208,9 +210,13 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0, - COALESCE(SUM(statsreads), 0) as stat1, COUNT('x') as stat2 - FROM {stats_user_daily} - WHERE stattype = 'logins' AND timeend = $nextmidnight"; + COALESCE((SELECT SUM(statsreads) + FROM {stats_user_daily} s1 + WHERE s1.stattype = 'logins' AND timeend = $nextmidnight), 0) AS stat1, + (SELECT COUNT('x') + FROM {stats_user_daily} s2 + WHERE s2.stattype = 'logins' AND timeend = $nextmidnight) AS stat2" . + $DB->sql_null_from_clause(); if ($logspresent and !$DB->execute($sql)) { $failed = true; @@ -231,13 +237,15 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments' as stattype, $nextmidnight as timeend, e.courseid, ra.roleid, - COUNT(DISTINCT ue.userid) as stat1, 0 as stat2 - FROM {role_assignments} ra - JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) - JOIN {enrol} e ON e.courseid = c.instanceid - JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) - GROUP BY courseid, roleid"; + SELECT 'enrolments', timeend, courseid, roleid, COUNT(DISTINCT userid), 0 + FROM ( + SELECT $nextmidnight AS timeend, e.courseid, ra.roleid, ue.userid + FROM {role_assignments} ra + JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel) + JOIN {enrol} e ON e.courseid = c.instanceid + JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid) + ) inline_view + GROUP BY timeend, courseid, roleid"; if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; @@ -255,14 +263,15 @@ function stats_cron_daily($maxdays=1) { WHERE ra.roleid = {stats_daily}.roleid AND e.courseid = {stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_stats_log1} l + FROM {log} l WHERE l.course = {stats_daily}.courseid AND - l.userid = ra.userid)) + l.userid = ra.userid AND $timesql)) WHERE {stats_daily}.stattype = 'enrolments' AND {stats_daily}.timeend = $nextmidnight AND {stats_daily}.courseid IN - (SELECT DISTINCT course - FROM {tmp_stats_log2})"; + (SELECT DISTINCT l.course + FROM {log} l + WHERE $timesql)"; if (!$DB->execute($sql, array('courselevel'=>CONTEXT_COURSE))) { $failed = true; @@ -273,11 +282,13 @@ function stats_cron_daily($maxdays=1) { /// now get course total enrolments (roleid==0) - except frontpage $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'enrolments', $nextmidnight AS timeend, e.courseid AS courseid, 0 AS roleid, - COUNT(DISTINCT userid) AS stat1, 0 AS stat2 - FROM {enrol} e - JOIN {user_enrolments} ue ON ue.enrolid = e.id - GROUP BY courseid + SELECT 'enrolments', timeend, id, nroleid, COUNT(DISTINCT userid), 0 + FROM ( + SELECT $nextmidnight AS timeend, e.courseid AS id, 0 AS nroleid, ue.userid + FROM {enrol} e + JOIN {user_enrolments} ue ON ue.enrolid = e.id + ) inline_view + GROUP BY timeend, id, nroleid HAVING COUNT(DISTINCT userid) > 0"; if ($logspresent and !$DB->execute($sql)) { @@ -292,16 +303,16 @@ function stats_cron_daily($maxdays=1) { JOIN {user_enrolments} ue ON ue.enrolid = e.id WHERE e.courseid = {stats_daily}.courseid AND EXISTS (SELECT 'x' - FROM {tmp_stats_log1} l + FROM {log} l WHERE l.course = {stats_daily}.courseid AND - l.userid = ue.userid)) + l.userid = ue.userid AND $timesql)) WHERE {stats_daily}.stattype = 'enrolments' AND {stats_daily}.timeend = $nextmidnight AND {stats_daily}.roleid = 0 AND {stats_daily}.courseid IN (SELECT l.course - FROM {tmp_stats_log2} l - WHERE l.course <> ".SITEID.")"; + FROM {log} l + WHERE $timesql AND l.course <> ".SITEID.")"; if ($logspresent and !$DB->execute($sql, array())) { $failed = true; @@ -309,7 +320,7 @@ function stats_cron_daily($maxdays=1) { } stats_daily_progress('6'); - /// frontpage(==site) enrolments total + /// frontapge(==site) enrolments total $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'enrolments', $nextmidnight, ".SITEID.", 0, @@ -318,8 +329,8 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_stats_log1} l ON l.userid = u.id - WHERE u.deleted = 0) AS stat2" . + JOIN {log} l ON l.userid = u.id + WHERE u.deleted = 0 AND $timesql) AS stat2" . $DB->sql_null_from_clause(); if ($logspresent and !$DB->execute($sql)) { @@ -349,8 +360,8 @@ function stats_cron_daily($maxdays=1) { WHERE u.deleted = 0) AS stat1, (SELECT COUNT(DISTINCT u.id) FROM {user} u - JOIN {tmp_stats_log1} l ON l.userid = u.id - WHERE u.deleted = 0) AS stat2" . + JOIN {log} l ON l.userid = u.id + WHERE u.deleted = 0 AND $timesql) AS stat2" . $DB->sql_null_from_clause();; if ($logspresent and !$DB->execute($sql)) { @@ -365,17 +376,29 @@ function stats_cron_daily($maxdays=1) { } + /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible list($viewactionssql, $params1) = $DB->get_in_or_equal($viewactions, SQL_PARAMS_NAMED, 'view'); list($postactionssql, $params2) = $DB->get_in_or_equal($postactions, SQL_PARAMS_NAMED, 'post'); $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites) - SELECT 'activity' AS stattype, $nextmidnight AS timeend, course as courseid, userid, - SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads, - SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites - FROM {tmp_stats_log1} l - WHERE !(course = 0 AND userid = 0) - GROUP BY userid, courseid"; + SELECT 'activity' AS stattype, $nextmidnight AS timeend, d.courseid, d.userid, + (SELECT COUNT('x') + FROM {log} l + WHERE l.userid = d.userid AND + l.course = d.courseid AND $timesql AND + l.action $viewactionssql) AS statsreads, + (SELECT COUNT('x') + FROM {log} l + WHERE l.userid = d.userid AND + l.course = d.courseid AND $timesql AND + l.action $postactionssql) AS statswrites + FROM (SELECT DISTINCT u.id AS userid, l.course AS courseid + FROM {user} u, {log} l + WHERE u.id = l.userid AND $timesql + UNION + SELECT 0 AS userid, ".SITEID." AS courseid" . $DB->sql_null_from_clause() . ") d"; + // can not use group by here because pg can not handle it :-( if ($logspresent and !$DB->execute($sql, array_merge($params1, $params2))) { $failed = true; @@ -388,11 +411,18 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0, - SUM(CASE WHEN l.action $viewactionssql THEN 1 ELSE 0 END) AS stat1, - SUM(CASE WHEN l.action $postactionssql THEN 1 ELSE 0 END) AS stat2 - FROM {course} c, {tmp_stats_log1} l - WHERE l.course = c.id - GROUP BY courseid"; + (SELECT COUNT('x') + FROM {log} l1 + WHERE l1.course = c.id AND l1.action $viewactionssql AND + $timesql1) AS stat1, + (SELECT COUNT('x') + FROM {log} l2 + WHERE l2.course = c.id AND l2.action $postactionssql AND + $timesql2) AS stat2 + FROM {course} c + WHERE EXISTS (SELECT 'x' + FROM {log} l + WHERE l.course = c.id and $timesql)"; if ($logspresent and !$DB->execute($sql, array_merge($params1, $params2))) { $failed = true; @@ -405,9 +435,9 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) + SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( - SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites + SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, e.courseid FROM {role_assignments} ra @@ -436,10 +466,9 @@ function stats_cron_daily($maxdays=1) { $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', $nextmidnight as timeend, courseid, $guestrole AS roleid, - SUM(statsreads), SUM(statswrites) + SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites) FROM ( - SELECT sud.courseid, sud.statsreads, sud.statswrites + SELECT $nextmidnight AS timeend, sud.courseid, $guestrole AS nroleid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND sud.stattype='activity' AND @@ -449,7 +478,7 @@ function stats_cron_daily($maxdays=1) { JOIN {enrol} e ON ue.enrolid = e.id WHERE e.courseid = sud.courseid)) ) inline_view - GROUP BY timeend, courseid, roleid + GROUP BY timeend, courseid, nroleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; if ($logspresent and !$DB->execute($sql, array())) { @@ -462,10 +491,9 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, - SUM(statsreads), SUM(statswrites) + SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites) FROM ( - SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites + SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud, (SELECT DISTINCT ra.userid, ra.roleid, c.instanceid AS courseid FROM {role_assignments} ra @@ -493,10 +521,9 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for default frontpage role on frontpage only $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', timeend, courseid, $defaultfproleid AS roleid, - SUM(statsreads), SUM(statswrites) + SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites) FROM ( - SELECT sud.timeend AS timeend, sud.courseid, sud.statsreads, sud.statswrites + SELECT sud.timeend AS timeend, sud.courseid, $defaultfproleid AS nroleid, sud.statsreads, sud.statswrites FROM {stats_user_daily} sud WHERE sud.timeend = :nextm AND sud.courseid = :siteid AND sud.stattype='activity' AND @@ -506,7 +533,7 @@ function stats_cron_daily($maxdays=1) { WHERE ra.roleid <> $guestrole AND ra.roleid <> $defaultfproleid AND ra.contextid = :fpcontext) ) inline_view - GROUP BY timeend, courseid, roleid + GROUP BY timeend, courseid, nroleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; if ($logspresent and !$DB->execute($sql, array('fpcontext'=>$fpcontext->id, 'siteid'=>SITEID, 'nextm'=>$nextmidnight))) { @@ -518,10 +545,9 @@ function stats_cron_daily($maxdays=1) { /// how many view actions for guests or not-logged-in on frontpage $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2) - SELECT 'activity', $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS roleid, - SUM(statsreads), SUM(statswrites) + SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites) FROM ( - SELECT pl.statsreads, pl.statswrites + SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole AS nroleid, pl.statsreads, pl.statswrites FROM ( SELECT sud.statsreads, sud.statswrites FROM {stats_user_daily} sud @@ -530,7 +556,7 @@ function stats_cron_daily($maxdays=1) { sud.stattype='activity' ) pl ) inline_view - GROUP BY timeend, courseid, roleid + GROUP BY timeend, courseid, nroleid HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0"; if ($logspresent and !$DB->execute($sql)) { @@ -547,8 +573,6 @@ function stats_cron_daily($maxdays=1) { $nextmidnight = stats_get_next_day_start($nextmidnight); } - stats_drop_logs_view(); - set_cron_lock('statsrunning', null); if ($failed) { @@ -1438,73 +1462,3 @@ function stats_check_uptodate($courseid=0) { //return error as string return get_string('statscatchupmode','error',$a); } - -/** - * Creates tmp log table - * - * @param timestart timestamp of the start time of logs view - * @param timeend timestamp of the end time of logs view - * @returns boolen success (true) or failure(false) - */ -function stats_create_logs_view ( $timestart, $timeend ) { - global $CFG, $DB; - - $dbman = $DB->get_manager(); // We are going to use database_manager services - - stats_drop_logs_view(); - - $table = new xmldb_table('tmp_stats_log1'); - - $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - $table->add_field('action', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); - - $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->add_index('tmp_stats_log_course_ix', XMLDB_INDEX_NOTUNIQUE, array('course')); - $table->add_index('tmp_stats_log_act_ix', XMLDB_INDEX_NOTUNIQUE, array('action')); - $table->add_index('tmp_stats_log_user_ix', XMLDB_INDEX_NOTUNIQUE, array('userid')); - $table->add_index('tmp_stats_log_usecouact_ix', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action')); - - $dbman->create_temp_table($table); - - $table->name = 'tmp_stats_log2'; - - $dbman->create_temp_table($table); - - $sql = "INSERT INTO {tmp_stats_log1} - SELECT id, userid, course, action FROM {log} l - WHERE l.time >= $timestart AND l.time < $timeend"; - - if (!$DB->execute($sql)) { - return false; - } - - $sql = "INSERT INTO {tmp_stats_log2} - SELECT * FROM {tmp_stats_log1}"; - - if (!$DB->execute($sql)) { - return false; - } - - return true; -} - -/** - * Deletes summary logs table for stats calculation - */ -function stats_drop_logs_view() { - global $DB; - - $dbman = $DB->get_manager(); - - if ( $dbman->table_exists('tmp_stats_log1') ) { - $table = new xmldb_table('tmp_stats_log1'); - $dbman->drop_temp_table($table); - } - - if ( $dbman->table_exists('tmp_stats_log2') ) { - $table = new xmldb_table('tmp_stats_log2'); - $dbman->drop_temp_table($table); - } -} From 7183a1f4a47a5ec066184fb855ef3689884e1324 Mon Sep 17 00:00:00 2001 From: Aparup Banerjee Date: Thu, 6 Sep 2012 17:35:40 +0800 Subject: [PATCH 90/96] Revert "MDL-34257 quiz 'secure' mode: PAGE initialisation order issues." This reverts commit 0e708c34760ee274990e1c5bad96583301a6cc79. note : there is another commit prefixed with MDL-34257 hash 'ddda79c' which was really for MDL-34187. 'ddda79c' is not being reverted as its not really about MDL-34257. --- mod/quiz/attempt.php | 4 ++-- mod/quiz/review.php | 3 +-- mod/quiz/summary.php | 4 ++-- theme/base/config.php | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index d3f350fd893..a24245dd215 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -76,9 +76,8 @@ if ($attemptobj->is_finished()) { // Check the access rules. $accessmanager = $attemptobj->get_access_manager(time()); -$accessmanager->setup_attempt_page($PAGE); -$output = $PAGE->get_renderer('mod_quiz'); $messages = $accessmanager->prevent_access(); +$output = $PAGE->get_renderer('mod_quiz'); if (!$attemptobj->is_preview_user() && $messages) { print_error('attempterror', 'quiz', $attemptobj->view_url(), $output->access_messages($messages)); @@ -121,6 +120,7 @@ $title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number()); $headtags = $attemptobj->get_html_head_contributions($page); $PAGE->set_title(format_string($attemptobj->get_quiz_name())); $PAGE->set_heading($attemptobj->get_course()->fullname); +$accessmanager->setup_attempt_page($PAGE); if ($attemptobj->is_last_page($page)) { $nextpage = -1; diff --git a/mod/quiz/review.php b/mod/quiz/review.php index eacc703e27b..c19afaa8970 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -52,8 +52,6 @@ $attemptobj->check_review_capability(); // Create an object to manage all the other (non-roles) access rules. $accessmanager = $attemptobj->get_access_manager(time()); -$accessmanager->setup_attempt_page($PAGE); - $options = $attemptobj->get_display_options(true); // Check permissions. @@ -105,6 +103,7 @@ if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) { $headtags = $attemptobj->get_html_head_contributions($page, $showall); $PAGE->set_title(format_string($attemptobj->get_quiz_name())); $PAGE->set_heading($attemptobj->get_course()->fullname); +$accessmanager->setup_attempt_page($PAGE); // Summary table start. ============================================================================ diff --git a/mod/quiz/summary.php b/mod/quiz/summary.php index 822e30777d0..303638f013c 100644 --- a/mod/quiz/summary.php +++ b/mod/quiz/summary.php @@ -52,9 +52,8 @@ if ($attemptobj->is_preview_user()) { // Check access. $accessmanager = $attemptobj->get_access_manager(time()); -$accessmanager->setup_attempt_page($PAGE); -$output = $PAGE->get_renderer('mod_quiz'); $messages = $accessmanager->prevent_access(); +$output = $PAGE->get_renderer('mod_quiz'); if (!$attemptobj->is_preview_user() && $messages) { print_error('attempterror', 'quiz', $attemptobj->view_url(), $output->access_messages($messages)); @@ -90,6 +89,7 @@ $PAGE->blocks->add_fake_block($navbc, reset($regions)); $PAGE->navbar->add(get_string('summaryofattempt', 'quiz')); $PAGE->set_title(format_string($attemptobj->get_quiz_name())); $PAGE->set_heading($attemptobj->get_course()->fullname); +$accessmanager->setup_attempt_page($PAGE); // Display the page. echo $output->summary_page($attemptobj, $displayoptions); diff --git a/theme/base/config.php b/theme/base/config.php index cafa7fb3831..1316822f7ca 100644 --- a/theme/base/config.php +++ b/theme/base/config.php @@ -162,8 +162,7 @@ $THEME->layouts = array( // The pagelayout used for safebrowser and securewindow. 'secure' => array( 'file' => 'general.php', - 'regions' => array('side-pre'), - 'defaultregion' => 'side-pre', + 'regions' => array(), 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true, 'nologinlinks'=>true), ), ); From 934f04e7c341fd886a2964e5f41c7c819c7417ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20V=C3=B6geli?= Date: Wed, 5 Sep 2012 22:31:26 +0200 Subject: [PATCH 91/96] MDL-31365 Follow up after discussion --- theme/afterburner/config.php | 10 ++++++++-- theme/overlay/layout/general.php | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/theme/afterburner/config.php b/theme/afterburner/config.php index da3742da0fb..9b041499a3a 100644 --- a/theme/afterburner/config.php +++ b/theme/afterburner/config.php @@ -123,16 +123,22 @@ $THEME->layouts = array( 'file' => 'embedded.php', 'regions' => array() ), - // The pagelayout used for reports + // The pagelayout used for reports. 'report' => array( 'file' => 'default.php', 'regions' => array('side-pre'), 'defaultregion' => 'side-pre', ), + // The pagelayout used for safebrowser and securewindow. + 'secure' => array( + 'file' => 'default.php', + 'regions' => array(), + 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true, 'nologinlinks'=>true), + ), ); $THEME->enable_dock = true; $THEME->rendererfactory = 'theme_overridden_renderer_factory'; -$THEME->csspostprocess = 'afterburner_process_css'; \ No newline at end of file +$THEME->csspostprocess = 'afterburner_process_css'; diff --git a/theme/overlay/layout/general.php b/theme/overlay/layout/general.php index 9e3297770f3..8fc6d8b8e5d 100644 --- a/theme/overlay/layout/general.php +++ b/theme/overlay/layout/general.php @@ -125,7 +125,7 @@ echo $OUTPUT->doctype() ?> - + - + standard_end_of_body_html() ?> - \ No newline at end of file + From b5cd5f83e167b64427dbda8c176c845a023f0ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20V=C3=B6geli?= Date: Thu, 6 Sep 2012 09:48:52 +0200 Subject: [PATCH 92/96] MDL-31365 Regions for pagelayout 'secure' --- theme/afterburner/config.php | 3 ++- theme/base/config.php | 3 ++- theme/canvas/config.php | 9 ++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/theme/afterburner/config.php b/theme/afterburner/config.php index 9b041499a3a..e720e8d578b 100644 --- a/theme/afterburner/config.php +++ b/theme/afterburner/config.php @@ -132,7 +132,8 @@ $THEME->layouts = array( // The pagelayout used for safebrowser and securewindow. 'secure' => array( 'file' => 'default.php', - 'regions' => array(), + 'regions' => array('side-pre', 'side-post'), + 'defaultregion' => 'side-pre', 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true, 'nologinlinks'=>true), ), ); diff --git a/theme/base/config.php b/theme/base/config.php index 1316822f7ca..cd85c660b47 100644 --- a/theme/base/config.php +++ b/theme/base/config.php @@ -162,7 +162,8 @@ $THEME->layouts = array( // The pagelayout used for safebrowser and securewindow. 'secure' => array( 'file' => 'general.php', - 'regions' => array(), + 'regions' => array('side-pre', 'side-post'), + 'defaultregion' => 'side-pre', 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true, 'nologinlinks'=>true), ), ); diff --git a/theme/canvas/config.php b/theme/canvas/config.php index 70d1790a21d..4f4b7e85203 100644 --- a/theme/canvas/config.php +++ b/theme/canvas/config.php @@ -166,7 +166,14 @@ $THEME->layouts = array( 'file' => 'report.php', 'regions' => array('side-pre'), 'defaultregion' => 'side-pre', - ) + ), + // The pagelayout used for safebrowser and securewindow. + 'secure' => array( + 'file' => 'general.php', + 'regions' => array('side-pre', 'side-post'), + 'defaultregion' => 'side-pre', + 'options' => array('nofooter'=>true, 'nonavbar'=>true, 'nocustommenu'=>true, 'nologinlinks'=>true), + ), ); ///////////////////////////////////////////////////////// From 6f050c7deaf6a017c1e2d9eba7dbfe8a0913fb0d Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Thu, 6 Sep 2012 18:31:26 +0200 Subject: [PATCH 93/96] MDL-31291 assign: Fix diffs between installed & upgraded DB schema --- mod/assign/db/upgrade.php | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/mod/assign/db/upgrade.php b/mod/assign/db/upgrade.php index f24b0a772f6..20b7b915366 100644 --- a/mod/assign/db/upgrade.php +++ b/mod/assign/db/upgrade.php @@ -124,20 +124,20 @@ function xmldb_assign_upgrade($oldversion) { // Define field to be added to assign. $table = new xmldb_table('assign'); - $field = new xmldb_field('teamsubmission', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, + $field = new xmldb_field('teamsubmission', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'cutoffdate'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - $field = new xmldb_field('requireallteammemberssubmit', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, + $field = new xmldb_field('requireallteammemberssubmit', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'teamsubmission'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - $field = new xmldb_field('teamsubmissiongroupingid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + $field = new xmldb_field('teamsubmissiongroupingid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'requireallteammemberssubmit'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { @@ -149,7 +149,7 @@ function xmldb_assign_upgrade($oldversion) { $dbman->add_index($table, $index); } $table = new xmldb_table('assign_submission'); - $field = new xmldb_field('groupid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'status'); + $field = new xmldb_field('groupid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'status'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); @@ -158,32 +158,41 @@ function xmldb_assign_upgrade($oldversion) { } if ($oldversion < 2012082400) { - // Define field to be added to assign + // Define table assign_user_mapping to be created $table = new xmldb_table('assign_user_mapping'); + // Adding fields to table assign_user_mapping $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); - $table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + // Adding keys to table assign_user_mapping $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); $table->add_key('assignment', XMLDB_KEY_FOREIGN, array('assignment'), 'assign', array('id')); + $table->add_key('user', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); + // Conditionally launch create table for assign_user_mapping if (!$dbman->table_exists($table)) { $dbman->create_table($table); } + // Define field blindmarking to be added to assign $table = new xmldb_table('assign'); - $field = new xmldb_field('blindmarking', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'teamsubmissiongroupingid'); + $field = new xmldb_field('blindmarking', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'teamsubmissiongroupingid'); + if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } + // Define field revealidentities to be added to assign $table = new xmldb_table('assign'); - $field = new xmldb_field('revealidentities', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'blindmarking'); + $field = new xmldb_field('revealidentities', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'blindmarking'); + if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } + + // assign savepoint reached upgrade_mod_savepoint(true, 2012082400, 'assign'); } From df5ac8d0ecdad838193b9271af3cae79b5faa564 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Thu, 6 Sep 2012 19:39:49 +0200 Subject: [PATCH 94/96] MDL-6424 - blocks - Add missing global vars. --- lib/blocklib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/blocklib.php b/lib/blocklib.php index 8e4e55e898e..246ddee5ba4 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -1114,7 +1114,7 @@ class block_manager { * @return boolean true if anything was done. False if not. */ public function process_url_delete() { - global $CFG; + global $CFG, $PAGE, $OUTPUT; $blockid = optional_param('bui_deleteid', null, PARAM_INT); $confirmdelete = optional_param('bui_confirm', null, PARAM_INT); From 3b70685c26e4ca089460280ccc60cf5d7e3216c0 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Fri, 7 Sep 2012 02:36:04 +0200 Subject: [PATCH 95/96] fix permissions --- lib/tcpdf/config/tcpdf_config.php | 0 mod/assign/db/install.xml | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 lib/tcpdf/config/tcpdf_config.php mode change 100755 => 100644 mod/assign/db/install.xml diff --git a/lib/tcpdf/config/tcpdf_config.php b/lib/tcpdf/config/tcpdf_config.php old mode 100755 new mode 100644 diff --git a/mod/assign/db/install.xml b/mod/assign/db/install.xml old mode 100755 new mode 100644 From 935c3d5ed2d03ac6e1b00f14c0b083ea1b9aada8 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Fri, 7 Sep 2012 02:39:21 +0200 Subject: [PATCH 96/96] weekly release 2.4dev --- version.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.php b/version.php index e567c17ab38..0043e10a19a 100644 --- a/version.php +++ b/version.php @@ -30,11 +30,11 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012090500.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012090700.00; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes -$release = '2.4dev (Build: 20120831)'; // Human-friendly version name +$release = '2.4dev (Build: 20120907)'; // Human-friendly version name $branch = '24'; // this version's branch $maturity = MATURITY_ALPHA; // this version's maturity level