From 8108909a60e8f66ca7415c1034285e8874c8fe00 Mon Sep 17 00:00:00 2001 From: skodak Date: Sun, 7 Oct 2007 13:04:49 +0000 Subject: [PATCH] MDL-9636 grade import fixes - allow null grades, no importcode collisions and stealing, more validation and other bugfixes --- grade/export/lib.php | 2 + grade/import/csv/index.php | 86 ++++++++++++++++++---------- grade/import/grade_import_form.php | 32 ++--------- grade/import/lib.php | 51 +++++++++-------- grade/import/xml/lib.php | 12 ++-- lang/en_utf8/grades.php | 3 +- lib/db/install.xml | 20 ++++--- lib/db/upgrade.php | 90 ++++++++++++++++++------------ version.php | 2 +- 9 files changed, 166 insertions(+), 132 deletions(-) diff --git a/grade/export/lib.php b/grade/export/lib.php index e8d9995f49..28504de299 100755 --- a/grade/export/lib.php +++ b/grade/export/lib.php @@ -149,6 +149,8 @@ class grade_export { $displaytype = null; if ($this->export_letters) { $displaytype = GRADE_DISPLAY_TYPE_LETTER; + } else { + $displaytype = GRADE_DISPLAY_TYPE_REAL; } return grade_format_gradevalue($grade->finalgrade, $this->grade_items[$grade->itemid], false, $displaytype, null); diff --git a/grade/import/csv/index.php b/grade/import/csv/index.php index 80733423a5..90aae6b7c7 100755 --- a/grade/import/csv/index.php +++ b/grade/import/csv/index.php @@ -44,8 +44,12 @@ $mform = new grade_import_form(); // they are somehow not returned with get_data() if (($formdata = data_submitted()) && !empty($formdata->map)) { - // temporary file name supplied by form - $filename = $CFG->dataroot.'/temp/'.clean_param($formdata->filename, PARAM_FILE); + $importcode = clean_param($formdata->importcode, PARAM_FILE); + $filename = $CFG->dataroot.'/temp/gradeimport/cvs/'.$USER->id.'/'.$importcode; + + if (!file_exists($filename)) { + error('error processing upload file'); + } if ($fp = fopen($filename, "r")) { // --- get header (field names) --- @@ -55,7 +59,7 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { $h = trim($h); $header[$i] = $h; // remove whitespace } } else { - error ('could not open file '.$filename); + error ('could not open file'); } $map = array(); @@ -79,7 +83,7 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { } else { // collision unlink($filename); // needs to be uploaded again, sorry - error('mapping collision detected, 2 fields maps to the same grdae item '.$j); + error('mapping collision detected, 2 fields maps to the same grade item '.$j); } } } @@ -97,13 +101,7 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { if ($fp = fopen($filename, "r")) { // read the first line makes sure this doesn't get read again - $header = split($csv_delimiter, clean_param(fgets($fp,1024), PARAM_RAW)); - - // use current (non-conflicting) time stamp - $importcode = time(); - while (get_record('grade_import_values', 'import_code', $importcode)) { - $importcode = time(); - } + $header = split($csv_delimiter, fgets($fp,1024)); $newgradeitems = array(); // temporary array to keep track of what new headers are processed $status = true; @@ -133,7 +131,7 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { $t = explode("_", $map[$key]); $t0 = $t[0]; if (isset($t[1])) { - $t1 = $t[1]; + $t1 = (int)$t[1]; } else { $t1 = ''; } @@ -184,7 +182,8 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { $newgradeitem = new object(); $newgradeitem->itemname = $header[$key]; - $newgradeitem->import_code = $importcode; + $newgradeitem->importcode = $importcode; + $newgradeitem->importer = $USER->id; // failed to insert into new grade item buffer if (!$newgradeitems[$key] = insert_record('grade_import_newitem', addslashes_recursive($newgradeitem))) { @@ -206,6 +205,17 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { break; case 'feedback': if ($t1) { + // case of an id, only maps id of a grade_item + // this was idnumber + if (!$gradeitem = new grade_item(array('id'=>$t1, 'courseid'=>$course->id))) { + // supplied bad mapping, should not be possible since user + // had to pick mapping + $status = false; + import_cleanup($importcode); + notify(get_string('importfailed', 'grades')); + break 3; + } + // t1 is the id of the grade item $feedback = new object(); $feedback->itemid = $t1; @@ -215,10 +225,12 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { break; default: // existing grade items - if (!empty($map[$key]) && $value!=="") { + if (!empty($map[$key])) { + if ($value === '' or $value == '-') { + $value = null; // no grade + } else if (!is_numeric($value)) { // non numeric grade value supplied, possibly mapped wrong column - if (!is_numeric($value)) { echo "
t0 is $t0"; echo "
grade is $value"; $status = false; @@ -239,7 +251,7 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { } // check if grade item is locked if so, abort - if ($gradeitem->locked) { + if ($gradeitem->is_locked()) { $status = false; import_cleanup($importcode); notify(get_string('gradeitemlocked', 'grades')); @@ -266,13 +278,13 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { } // insert results of this students into buffer - if (!empty($newgrades)) { + if ($status and !empty($newgrades)) { foreach ($newgrades as $newgrade) { // check if grade_grade is locked and if so, abort - if ($grade_grade = new grade_grade(array('itemid'=>$newgrade->itemid, 'userid'=>$studentid))) { - if ($grade_grade->locked) { + if (!empty($newgrade->itemid) and $grade_grade = new grade_grade(array('itemid'=>$newgrade->itemid, 'userid'=>$studentid))) { + if ($grade_grade->is_locked()) { // individual grade locked $status = false; import_cleanup($importcode); @@ -281,8 +293,9 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { } } - $newgrade->import_code = $importcode; - $newgrade->userid = $studentid; + $newgrade->importcode = $importcode; + $newgrade->userid = $studentid; + $newgrade->importer = $USER->id; if (!insert_record('grade_import_values', addslashes_recursive($newgrade))) { // could not insert into temporary table $status = false; @@ -294,15 +307,20 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { } // updating/inserting all comments here - if (!empty($newfeedbacks)) { + if ($status and !empty($newfeedbacks)) { foreach ($newfeedbacks as $newfeedback) { - if ($feedback = get_record('grade_import_values', 'import_code', $importcode, 'userid', $studentid, 'itemid', $newfeedback->itemid)) { - $newfeedback ->id = $feedback ->id; + $sql = "SELECT * + FROM {$CFG->prefix}grade_import_values + WHERE importcode=$importcode AND userid=$studentid AND itemid=$newfeedback->itemid AND importer={$USER->id}"; + if ($feedback = get_record_sql($sql)) { + $newfeedback->id = $feedback->id; update_record('grade_import_values', addslashes_recursive($newfeedback)); + } else { // the grade item for this is not updated - $newfeedback->import_code = $importcode; - $newfeedback->userid = $studentid; + $newfeedback->importcode = $importcode; + $newfeedback->userid = $studentid; + $newfeedback->importer = $USER->id; insert_record('grade_import_values', addslashes_recursive($newfeedback)); } } @@ -320,8 +338,6 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { } } else if ($formdata = $mform->get_data()) { - // else if file is just uploaded - $filename = $mform->get_userfile_name(); // Large files are likely to take their time and memory. Let PHP know // that we'll take longer, and that the process should be recycled soon @@ -332,7 +348,14 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { @apache_child_terminate(); } - $text = my_file_get_contents($filename); + // use current (non-conflicting) time stamp + $importcode = get_new_importcode(); + if (!$filename = make_upload_directory('temp/gradeimport/cvs/'.$USER->id, true)) { + die; + } + $filename = $filename.'/'.$importcode; + + $text = $mform->get_file_content('userfile'); // trim utf-8 bom $textlib = textlib_get_instance(); /// normalize line endings and do the encoding conversion @@ -347,7 +370,7 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { $fp = fopen($filename, "r"); // --- get header (field names) --- - $header = split($csv_delimiter, clean_param(fgets($fp,1024), PARAM_RAW)); + $header = split($csv_delimiter, fgets($fp,1024), PARAM_RAW); // print some preview $numlines = 0; // 0 preview lines displayed @@ -387,7 +410,8 @@ if (($formdata = data_submitted()) && !empty($formdata->map)) { } } // display the mapping form with header info processed - $mform2 = new grade_import_mapping_form(qualified_me(), array('gradeitems'=>$gradeitems, 'header'=>$header, 'filename'=>$filename)); + $mform2 = new grade_import_mapping_form(null, array('gradeitems'=>$gradeitems, 'header'=>$header)); + $mform2->set_data(array('importcode'=>$importcode, 'id'=>$id)); $mform2->display(); } else { // display the standard upload file form diff --git a/grade/import/grade_import_form.php b/grade/import/grade_import_form.php index df29572e63..3ca28a95a8 100755 --- a/grade/import/grade_import_form.php +++ b/grade/import/grade_import_form.php @@ -1,5 +1,6 @@ libdir.'/formslib.php'; +require_once($CFG->libdir.'/gradelib.php'); class grade_import_form extends moodleform { function definition (){ @@ -18,19 +19,10 @@ class grade_import_form extends moodleform { $mform->addElement('select', 'encoding', get_string('encoding', 'grades'), $encodings); $options = array('10'=>10, '20'=>20, '100'=>100, '1000'=>1000, '100000'=>100000); - $mform->addElement('select', 'previewrows', 'Preview rows', $options); // TODO: localize + $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'grades'), $options); // TODO: localize $mform->setType('previewrows', PARAM_INT); $this->add_action_buttons(false, get_string('uploadgrades', 'grades')); } - - function get_userfile_name(){ - if ($this->is_submitted() and $this->is_validated()) { - // return the temporary filename to process - return $this->_upload_manager->files['userfile']['tmp_name']; - } else{ - return NULL; - } - } } class grade_import_mapping_form extends moodleform { @@ -41,8 +33,6 @@ class grade_import_mapping_form extends moodleform { // this is an array of headers $header = $this->_customdata['header']; - // temporary filename - $filename = $this->_customdata['filename']; // course id $mform->addElement('header', 'general', get_string('identifier', 'grades')); @@ -71,8 +61,6 @@ class grade_import_mapping_form extends moodleform { } } - include_once($CFG->libdir.'/gradelib.php'); - if ($header) { $i = 0; // index foreach ($header as $h) { @@ -88,23 +76,13 @@ class grade_import_mapping_form extends moodleform { } } - // find a non-conflicting file name based on time stamp - $newfilename = 'cvstemp_'.time(); - while (file_exists($CFG->dataroot.'/temp/'.$newfilename)) { - $newfilename = 'cvstemp_'.time(); - } - - // move the uploaded file - move_uploaded_file($filename, $CFG->dataroot.'/temp/'.$newfilename); - // course id needs to be passed for auth purposes $mform->addElement('hidden', 'map', 1); $mform->setType('map', PARAM_INT); - $mform->addElement('hidden', 'id', optional_param('id')); + $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); - //echo ''; - $mform->addElement('hidden', 'filename', $newfilename); - $mform->setType('filename', PARAM_FILE); + $mform->addElement('hidden', 'importcode'); + $mform->setType('importcode', PARAM_FILE); $this->add_action_buttons(false, get_string('uploadgrades', 'grades')); } diff --git a/grade/import/lib.php b/grade/import/lib.php index d1779a9c95..3169c1a7f4 100755 --- a/grade/import/lib.php +++ b/grade/import/lib.php @@ -1,5 +1,22 @@ libdir.'/gradelib.php'); + +/** + * Returns new improtcode for current user + * @return int importcode + */ +function get_new_importcode() { + global $USER; + + $importcode = time(); + while (get_record('grade_import_values', 'importcode', $importcode, 'importer', $USER->id)) { + $importcode--; + } + + return $importcode; +} + /** * given an import code, commits all entries in buffer tables * (grade_import_value and grade_import_newitem) @@ -11,18 +28,16 @@ * @return bool success */ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verbose=true) { - global $CFG; + global $CFG, $USER; - include_once($CFG->libdir.'/gradelib.php'); - include_once($CFG->libdir.'/grade/grade_item.php'); $commitstart = time(); // start time in case we need to roll back $newitemids = array(); // array to hold new grade_item ids from grade_import_newitem table, mapping array /// first select distinct new grade_items with this batch if ($newitems = get_records_sql("SELECT * - FROM {$CFG->prefix}grade_import_newitem - WHERE import_code = $importcode")) { + FROM {$CFG->prefix}grade_import_newitem + WHERE importcode = $importcode AND importer={$USER->id}")) { // instances of the new grade_items created, cached // in case grade_update fails, so that we can remove them @@ -60,13 +75,12 @@ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verb /// then find all existing items if ($gradeitems = get_records_sql("SELECT DISTINCT (itemid) - FROM {$CFG->prefix}grade_import_values - WHERE import_code = $importcode - AND itemid > 0")) { + FROM {$CFG->prefix}grade_import_values + WHERE importcode = $importcode AND importer={$USER->id} AND itemid > 0")) { $modifieditems = array(); - foreach ($gradeitems as $itemid=>$iteminfo) { + foreach ($gradeitems as $itemid=>$notused) { if (!$gradeitem = new grade_item(array('id'=>$itemid))) { // not supposed to happen, but just in case @@ -114,22 +128,11 @@ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verb * @param string importcode - import batch identifier */ function import_cleanup($importcode) { + global $USER; + // remove entries from buffer table - delete_records('grade_import_values', 'import_code', $importcode); - delete_records('grade_import_newitem', 'import_code', $importcode); + delete_records('grade_import_values', 'importcode', $importcode, 'importer', $USER->id); + delete_records('grade_import_newitem', 'importcode', $importcode, 'importer', $USER->id); } -/// Returns the file as one big long string -function my_file_get_contents($filename, $use_include_path = 0) { - - $data = ""; - $file = @fopen($filename, "rb", $use_include_path); - if ($file) { - while (!feof($file)) { - $data .= fread($file, 1024); - } - fclose($file); - } - return $data; -} ?> diff --git a/grade/import/xml/lib.php b/grade/import/xml/lib.php index ed8e0c9355..7d1907afa9 100644 --- a/grade/import/xml/lib.php +++ b/grade/import/xml/lib.php @@ -6,7 +6,9 @@ require_once $CFG->dirroot.'/grade/lib.php'; require_once $CFG->dirroot.'/grade/import/lib.php'; function import_xml_grades($text, $course, &$error) { - $importcode = time(); //TODO: fix predictable+colliding import code! + global $USER; + + $importcode = get_new_importcode(); $status = true; @@ -46,6 +48,7 @@ function import_xml_grades($text, $course, &$error) { // check if grade_grade is locked and if so, abort if ($grade_grade = new grade_grade(array('itemid'=>$grade_item->id, 'userid'=>$user->id))) { + $grade_grade->grade_item =& $grade_item; if ($grade_grade->is_locked()) { // individual grade locked, abort $status = false; @@ -55,9 +58,10 @@ function import_xml_grades($text, $course, &$error) { } $newgrade = new object(); - $newgrade->itemid = $grade_item->id; - $newgrade->userid = $user->id; - $newgrade->import_code = $importcode; + $newgrade->itemid = $grade_item->id; + $newgrade->userid = $user->id; + $newgrade->importcode = $importcode; + $newgrade->importer = $USER->id; // check grade value exists and is a numeric grade if (isset($result['#']['score'][0]['#'])) { diff --git a/lang/en_utf8/grades.php b/lang/en_utf8/grades.php index 1ed71f6dc1..62f9d7c081 100644 --- a/lang/en_utf8/grades.php +++ b/lang/en_utf8/grades.php @@ -348,8 +348,9 @@ $string['report'] = 'Report'; $string['reportplugins'] = 'Report plugins'; $string['reportsettings'] = 'Report settings'; $string['reprintheaders'] = 'Reprint Headers'; -$string['respectingcurrentdata'] = "leaving current configuration unmodified"; +$string['respectingcurrentdata'] = 'leaving current configuration unmodified'; $string['right'] = 'Right'; +$string['rowpreviewnum'] = 'Preview rows'; $string['savechanges'] = 'Save Changes'; $string['savepreferences'] = 'Save Preferences'; $string['scaledpct'] = 'Scaled %%'; diff --git a/lib/db/install.xml b/lib/db/install.xml index 813cb996ce..0608cc2ed2 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -1542,11 +1542,13 @@ - - + + + - + +
@@ -1555,14 +1557,16 @@ - - - + + + + - + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 18232947fb..1918135e6e 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1309,42 +1309,6 @@ function xmldb_main_upgrade($oldversion=0) { /// Launch create table for grade_grades_history $result = $result && create_table($table); - - /// Define table grade_import_newitem to be created - $table = new XMLDBTable('grade_import_newitem'); - - /// Adding fields to table grade_import_newitem - $table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); - $table->addFieldInfo('itemname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, null, null); - $table->addFieldInfo('import_code', XMLDB_TYPE_INTEGER, '12', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); - - /// Adding keys to table grade_import_newitem - $table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id')); - - /// Launch create table for grade_import_newitem - $result = $result && create_table($table); - - - /// Define table grade_import_values to be created - $table = new XMLDBTable('grade_import_values'); - - /// Adding fields to table grade_import_values - $table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); - $table->addFieldInfo('itemid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null); - $table->addFieldInfo('newgradeitem', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null); - $table->addFieldInfo('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); - $table->addFieldInfo('finalgrade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, null, null, '0'); - $table->addFieldInfo('feedback', XMLDB_TYPE_TEXT, 'medium', null, null, null, null, null, null); - $table->addFieldInfo('import_code', XMLDB_TYPE_INTEGER, '12', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); - - /// Adding keys to table grade_import_values - $table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id')); - $table->addKeyInfo('itemid', XMLDB_KEY_FOREIGN, array('itemid'), 'grade_items', array('id')); - $table->addKeyInfo('newgradeitem', XMLDB_KEY_FOREIGN, array('newgradeitem'), 'grade_import_newitem', array('id')); - - /// Launch create table for grade_import_values - $result = $result && create_table($table); - /// upgrade the old 1.8 gradebook - migrade data into new grade tables if ($result) { require_once($CFG->libdir.'/db/upgradelib.php'); @@ -2354,6 +2318,60 @@ function xmldb_main_upgrade($oldversion=0) { $result = $result && change_field_notnull($table, $field); } + if ($result && $oldversion < 2007100700) { + + /// first drop existing tables - we do not need any data from there + $table = new XMLDBTable('grade_import_values'); + if (table_exists($table)) { + drop_table($table); + } + + $table = new XMLDBTable('grade_import_newitem'); + if (table_exists($table)) { + drop_table($table); + } + + /// Define table grade_import_newitem to be created + $table = new XMLDBTable('grade_import_newitem'); + + /// Adding fields to table grade_import_newitem + $table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); + $table->addFieldInfo('itemname', XMLDB_TYPE_CHAR, '255', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->addFieldInfo('importcode', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->addFieldInfo('importer', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + + /// Adding keys to table grade_import_newitem + $table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->addKeyInfo('importer', XMLDB_KEY_FOREIGN, array('importer'), 'user', array('id')); + + /// Launch create table for grade_import_newitem + $result = $result && create_table($table); + + + /// Define table grade_import_values to be created + $table = new XMLDBTable('grade_import_values'); + + /// Adding fields to table grade_import_values + $table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); + $table->addFieldInfo('itemid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null); + $table->addFieldInfo('newgradeitem', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null); + $table->addFieldInfo('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->addFieldInfo('finalgrade', XMLDB_TYPE_NUMBER, '10, 5', null, null, null, null, null, null); + $table->addFieldInfo('feedback', XMLDB_TYPE_TEXT, 'medium', XMLDB_UNSIGNED, null, null, null, null, null); + $table->addFieldInfo('importcode', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->addFieldInfo('importer', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null); + + /// Adding keys to table grade_import_values + $table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->addKeyInfo('itemid', XMLDB_KEY_FOREIGN, array('itemid'), 'grade_items', array('id')); + $table->addKeyInfo('newgradeitem', XMLDB_KEY_FOREIGN, array('newgradeitem'), 'grade_import_newitem', array('id')); + $table->addKeyInfo('importer', XMLDB_KEY_FOREIGN, array('importer'), 'user', array('id')); + + /// Launch create table for grade_import_values + $result = $result && create_table($table); + + } + /* NOTE: please keep this at the end of upgrade file for now ;-) /// drop old gradebook tables diff --git a/version.php b/version.php index fb5e3a5273..e8f1def9d4 100644 --- a/version.php +++ b/version.php @@ -6,7 +6,7 @@ // This is compared against the values stored in the database to determine // whether upgrades should be performed (see lib/db/*.php) - $version = 2007100500; // YYYYMMDD = date + $version = 2007100700; // YYYYMMDD = date // XY = increments within a single day $release = '1.9 Beta +'; // Human-friendly version name -- 2.39.5