MDL-68163 gradebook: add grade item duplication
authorMatt Davidson <davidso1@rose-hulman.edu>
Tue, 2 Jun 2020 13:28:29 +0000 (09:28 -0400)
committerMatt Davidson <davidso1@rose-hulman.edu>
Tue, 2 Jun 2020 13:28:29 +0000 (09:28 -0400)
grade/edit/tree/index.php
grade/edit/tree/lib.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_item_duplication.feature [new file with mode: 0644]
lang/en/grades.php
lib/grade/grade_item.php
lib/grade/tests/grade_item_test.php

index aa58497..152cd65 100644 (file)
@@ -86,6 +86,17 @@ if ($action == 'moveselect') {
 $grade_edit_tree = new grade_edit_tree($gtree, $movingeid, $gpr);
 
 switch ($action) {
+    case 'duplicate':
+        if ($eid and confirm_sesskey()) {
+            if (!$el = $gtree->locate_element($eid)) {
+                print_error('invalidelementid', '', $returnurl);
+            }
+
+            $object->duplicate();
+            redirect($returnurl);
+        }
+        break;
+
     case 'delete':
         if ($eid && confirm_sesskey()) {
             if (!$grade_edit_tree->element_deletable($element)) {
index 8eac5fe..91744d8 100644 (file)
@@ -149,6 +149,18 @@ class grade_edit_tree {
                 $actionsmenu->add($icon);
             }
 
+            if ($this->element_duplicatable($element)) {
+                $duplicateparams = array();
+                $duplicateparams['id'] = $COURSE->id;
+                $duplicateparams['action'] = 'duplicate';
+                $duplicateparams['eid'] = $eid;
+                $duplicateparams['sesskey'] = sesskey();
+                $aurl = new moodle_url('index.php', $duplicateparams);
+                $duplicateicon = new pix_icon('t/copy', get_string('duplicate'));
+                $icon = new action_menu_link_secondary($aurl, $duplicateicon, get_string('duplicate'));
+                $actionsmenu->add($icon);
+            }
+
             $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'moveselect', 'eid' => $eid, 'sesskey' => sesskey()));
             $moveaction .= $OUTPUT->action_icon($aurl, new pix_icon('t/move', get_string('move')));
         }
@@ -460,6 +472,24 @@ class grade_edit_tree {
         return false;
     }
 
+    /**
+     * Given an element of the grade tree, returns whether it is duplicatable or not (only manual grade items are duplicatable)
+     *
+     * @param array $element
+     * @return bool
+     */
+    public function element_duplicatable($element) {
+        if ($element['type'] != 'item') {
+            return false;
+        }
+
+        $gradeitem = $element['object'];
+        if ($gradeitem->itemtype != 'mod') {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Given the grade tree and an array of element ids (e.g. c15, i42), and expecting the 'moveafter' URL param,
      * moves the selected items to the requested location. Then redirects the user to the given $returnurl
index b17c4cd..b5e6dde 100644 (file)
@@ -117,6 +117,29 @@ class behat_grade extends behat_base {
             "//tr[descendant::*[text() = " . $this->escape($gradeitem) . "]]", 'xpath_element'));
     }
 
+    /**
+     * Duplicates a grade item or category.
+     *
+     * Teacher must be on the grade setup page.
+     *
+     * @Given /^I duplicate the grade item "(?P<grade_item_string>(?:[^"]|\\")*)"$/
+     * @param string $gradeitem
+     */
+    public function i_duplicate_the_grade_item($gradeitem) {
+
+        $gradeitem = behat_context_helper::escape($gradeitem);
+
+        if ($this->running_javascript()) {
+            $xpath = "//tr[contains(.,$gradeitem)]//*[contains(@class,'moodle-actionmenu')]//a[contains(@class,'toggle-display')]";
+            if ($this->getSession()->getPage()->findAll('xpath', $xpath)) {
+                $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
+            }
+        }
+
+        $this->execute("behat_general::i_click_on_in_the", array(get_string('duplicate'), 'link',
+            "//tr[descendant::*[text() = " . $this->escape($gradeitem) . "]]", 'xpath_element'));
+    }
+
     /**
      * Sets a calculated manual grade item. Needs a table with item name - idnumber relation.
      * The step requires you to be in the 'Gradebook setup' page.
diff --git a/grade/tests/behat/grade_item_duplication.feature b/grade/tests/behat/grade_item_duplication.feature
new file mode 100644 (file)
index 0000000..8bbcdad
--- /dev/null
@@ -0,0 +1,45 @@
+@core @core_grades
+Feature: We can duplicate grade items that already exist.
+  In order to quickly create grade items that have similar settings.
+  As a teacher
+  I need to duplicate an existing grade item and check that its values are properly duplicated.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "grade categories" exist:
+      | fullname  | course |
+      | Category1 | C1     |
+    And the following "activities" exist:
+      | activity | course | idnumber | name        | gradecategory |
+      | assign   | C1     | a1       | Assignment1 | Category1     |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And the following "grade items" exist:
+      | itemname | course | category  | idnumber | gradetype | grademax | grademin | gradepass | display | decimals | hidden | weightoverride |
+      | Item1    | C1     | Category1 | 001      | Value     | 80.00    | 5.00     | 40.00     | 1       | 1        | 0      | 1              |
+
+  Scenario: Ensure the duplicated grade item settings match the original grade item
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Setup > Gradebook setup" in the course gradebook
+    And I should not see "Duplicate   Category1"
+    And I should not see "Duplicate   Assignment1"
+    When I duplicate the grade item "Item1"
+    Then I should see "Item1 (copy)"
+    And I follow "Edit   Item1 (copy)"
+    And the field "Item name" matches value "Item1 (copy)"
+    And the field "ID number" matches value ""
+    And the field "Grade type" matches value "Value"
+    And the field "Maximum grade" matches value "80.00"
+    And the field "Minimum grade" matches value "5.00"
+    And the field "Grade to pass" matches value "40.00"
+    And the field "Grade display type" matches value "Real"
+    And the field "Overall decimal places" matches value "1"
+    And the field "Hidden" matches value "0"
+    And the field "Weight adjusted" matches value "1"
index f717e4f..11d5b11 100644 (file)
@@ -163,6 +163,7 @@ $string['dropped'] = 'Dropped';
 $string['droplowestvalues'] = 'Drop {$a} lowest values';
 $string['dropxlowest'] = 'Drop X lowest';
 $string['dropxlowestwarning'] = 'Note: If you use drop x lowest the grading assumes that all items in the category have the same point value. If point values differ results will be unpredictable';
+$string['duplicatedgradeitem'] = '{$a} (copy)';
 $string['duplicatescale'] = 'Duplicate scale';
 $string['edit'] = 'Edit';
 $string['editcalculation'] = 'Edit calculation';
index dd4a16a..1f21f76 100644 (file)
@@ -449,6 +449,41 @@ class grade_item extends grade_object {
         return true;
     }
 
+    /**
+     * Duplicate grade item.
+     *
+     * @return grade_item The duplicate grade item
+     */
+    public function duplicate() {
+        // Convert current object to array.
+        $copy = (array) $this;
+
+        if (empty($copy["id"])) {
+            throw new moodle_exception('invalidgradeitemid');
+        }
+
+        // Remove fields that will be either unique or automatically filled.
+        $removekeys = array();
+        $removekeys[] = 'id';
+        $removekeys[] = 'idnumber';
+        $removekeys[] = 'timecreated';
+        $removekeys[] = 'sortorder';
+        foreach ($removekeys as $key) {
+            unset($copy[$key]);
+        }
+
+        // Addendum to name.
+        $copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]);
+
+        // Create new grade item.
+        $gradeitem = new grade_item($copy);
+
+        // Insert grade item into database.
+        $gradeitem->insert();
+
+        return $gradeitem;
+    }
+
     /**
      * In addition to perform parent::insert(), calls force_regrading() method too.
      *
index a420ceb..5c0b46b 100644 (file)
@@ -1050,4 +1050,78 @@ class core_grade_item_testcase extends grade_base_testcase {
         $this->assertEquals($gradeitem->itemmodule, $event->other['itemmodule']);
         $this->assertEquals('updatedname', $event->other['itemname']);
     }
+
+
+    /**
+     * Test grade item duplication expecting success.
+     */
+    public function test_grade_duplicate_grade_item_success() {
+        $cat = new grade_category();
+        $cat->courseid = $this->courseid;
+        $cat->fullname = 'Grade category';
+        $cat->insert();
+
+        // Method exists.
+        $gi = new grade_item();
+        $this->assertTrue(method_exists($gi, 'duplicate'));
+
+        // Grade item is inserted and valid for duplication.
+        $gi->courseid = $this->courseid;
+        $gi->categoryid = $cat->id;
+        $gi->itemtype = 'manual';
+        $gi->itemname = 'Grade Item 1';
+        $gi->idnumber = '1000';
+        $gi->insert();
+        $gi2 = $gi->duplicate();
+
+        $this->assertEquals($gi->courseid, $gi2->courseid);
+        $this->assertEquals($gi->categoryid, $gi2->categoryid);
+        $this->assertEquals($gi->itemtype, $gi2->itemtype);
+        $this->assertEquals($gi->gradetype, $gi2->gradetype);
+        $this->assertEquals($gi->grademax, $gi2->grademax);
+        $this->assertEquals($gi->grademin, $gi2->grademin);
+        $this->assertEquals($gi->gradepass, $gi2->gradepass);
+        $this->assertEquals($gi->display, $gi2->display);
+        $this->assertEquals($gi->decimals, $gi2->decimals);
+        $this->assertEquals($gi->hidden, $gi2->hidden);
+        $this->assertEquals($gi->weightoverride, $gi2->weightoverride);
+
+        $this->assertNotEquals($gi->id, $gi2->id);
+        $this->assertNotEquals($gi->idnumber, $gi2->idnumber);
+        $this->assertNotEquals($gi->sortorder, $gi2->sortorder);
+        $this->assertNotEquals($gi->itemname, $gi2->itemname);
+    }
+
+    /**
+     * Test grade item duplication exception expected with incomplete grade item.
+     */
+    public function test_grade_duplicate_grade_item_incomplete() {
+        // Grade item is not valid because it is empty.
+        $gi = new grade_item();
+        $gi->courseid = $this->courseid;
+        $this->expectException("moodle_exception");
+        $gi2 = $gi->duplicate();
+    }
+
+    /**
+     * Test grade item duplication exception expected because item must be in db.
+     */
+    public function test_grade_duplicate_grade_item_not_in_db() {
+        $cat = new grade_category();
+        $cat->courseid = $this->courseid;
+        $cat->fullname = 'Grade category';
+        $cat->insert();
+
+        // Grade item is valid for insertion but is not inserted into db.
+        // Duplicate method throws an exception.
+        $gi = new grade_item();
+        $gi->courseid = $this->courseid;
+        $gi->categoryid = $cat->id;
+        $gi->itemtype = 'manual';
+        $gi->itemname = 'Grade Item 1';
+        $gi->idnumber = '1000';
+
+        $this->expectException("moodle_exception");
+        $gi2 = $gi->duplicate();
+    }
 }