From 3814171d5b5cb1e65be4a48fab61b51b3ee8ce5a Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Mon, 4 Mar 2024 11:56:28 +0100 Subject: [PATCH] modify distribution algorithm, save groupid to db for each rating and add field preventvotenotingroup to ratingallocate table --- db/install.xml | 1 + db/upgrade.php | 8 +- lang/en/ratingallocate.php | 9 ++ locallib.php | 178 +++++++++++++++++++++++++++---- solver/edmonds-karp.php | 56 +++++++--- solver/ford-fulkerson-koegel.php | 31 +++--- solver/solver-template.php | 8 +- version.php | 2 +- 8 files changed, 233 insertions(+), 60 deletions(-) diff --git a/db/install.xml b/db/install.xml index c56bd8fb..77309db7 100644 --- a/db/install.xml +++ b/db/install.xml @@ -25,6 +25,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index a65d760c..1ddf77b4 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -218,12 +218,13 @@ function xmldb_ratingallocate_upgrade($oldversion) { upgrade_mod_savepoint(true, 2023050900, 'ratingallocate'); } - if ($oldversion < 2024020500) { + if ($oldversion < 2024030100) { // Define fields teamvote and teamvotegroupingid to be added to ratingallocate. $table = new xmldb_table('ratingallocate'); $field_teamvote = new xmldb_field('teamvote', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); $field_teamvotegroupingid = new xmldb_field('teamvotegroupingid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $field_preventvotenotingroup = new xmldb_field('preventvotenotingroup', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); // Conditionally launch add fields to ratingallocate table. if (!$dbman->field_exists($table, $field_teamvote)) { @@ -232,6 +233,9 @@ function xmldb_ratingallocate_upgrade($oldversion) { if (!$dbman->field_exists($table, $field_teamvotegroupingid)) { $dbman->add_field($table, $field_teamvotegroupingid); } + if (!$dbman->field_exists($table, $field_preventvotenotingroup)) { + $dbman->add_field($table, $field_preventvotenotingroup); + } // Define field groupid to be added to ratingallocate_ratings. $ratingstable = new xmldb_table('ratingallocate_ratings'); @@ -243,7 +247,7 @@ function xmldb_ratingallocate_upgrade($oldversion) { } // Ratingallocate savepoint reached. - upgrade_mod_savepoint(true, 2024020500, 'ratingallocate'); + upgrade_mod_savepoint(true, 2024030100, 'ratingallocate'); } return true; diff --git a/lang/en/ratingallocate.php b/lang/en/ratingallocate.php index 325b03e9..d5d1cb8b 100644 --- a/lang/en/ratingallocate.php +++ b/lang/en/ratingallocate.php @@ -300,6 +300,15 @@ $string['strategyspecificoptions'] = 'Strategy specific options'; $string['strategy_altered_after_preferences'] = 'Strategy cannot be changed after preferences where submitted'; +$string['groupvotesettings'] = 'Group Voting Settings'; +$string['teamvote'] = 'Students rate in groups'; +$string['teamvote_help'] = 'If enabled, students will be divided into groups based on the default set of groups or a custom grouping. A group rating will count for all group members.'; +$string['teamvotegroupingid'] = 'Grouping for student groups'; +$string['teamvotegroupingid_help'] = 'This is the grouping that the assignment will use to find groups for student groups. If not set, the default set of groups will be used.'; +$string['teamvote_altered_after_preferences'] = 'Group Voting settings cannot be changed after preferences were submitted'; +$string['preventvotenotingroup'] = 'Require group to vote'; +$string['preventvotenotingroup_help'] = 'If enabled, users who are not members of a group will be unable to give a rating.'; + $string['err_required'] = 'You need to provide a value for this field.'; $string['err_minimum'] = 'The minimum value for this field is {$a}.'; $string['err_maximum'] = 'The maximum value for this field is {$a}.'; diff --git a/locallib.php b/locallib.php index c58566b2..f0214511 100644 --- a/locallib.php +++ b/locallib.php @@ -1286,12 +1286,39 @@ public function get_ratings_for_rateable_choices() { public function get_teamvote_goups() { if ($this->db->get_field(this_db\ratingallocate::TABLE, 'teamvote', ['id' => $this->ratingallocateid]) == 1) { + $groupingid = $this->db->get_field(this_db\ratingallocate::TABLE, 'teamvotegroupingid', ['id' => $this->ratingallocateid]); + + // If voting for users not in groups is not disabled, we have to also consider the users that do not have a group. + if ($this->db->get_field(this_db\ratingallocate::TABLE, 'preventvotenotingroup', ['id' => $this->ratingallocateid]) == 0) { + // Get all users not in a group of the teamvote grouping. + $usersnogroup = array_diff($this->get_raters_in_course(), groups_get_grouping_members($groupingid)); + + $groupdata = new stdClass(); + $groupdata->courseid =$this->course->id; + $groupdata->idnumber = $this->ratingallocateid; + $groupdata->name = 'delete after algorithm run'; + + foreach ($usersnogroup as $user) { + + // Create group and add user. Group will be deleted after distributing the users + $groupid = groups_create_group($groupdata); + groups_add_member($groupid, $user); + + // Add group to grouping. + $this->db->insert_record('groupings_groups', ['groupingid' => $groupingid, 'groupid' => $groupid]); + + // Add groupid to ratings of this user. + $this->add_groupid_to_ratings($user->id, $groupid); + + } + } + // Get the groups that are in the teamvote grouping and their amount of groupmembers. $sql = 'SELECT m.groupid as groupid, COUNT(m.userid) AS members FROM {groupings_groups} g INNER JOIN {groups_members} m ON g.groupid=m.groupid WHERE g.groupingid = :groupingid GROUP BY groupid'; - $groupingid = $this->db->get_field(this_db\ratingallocate::TABLE, 'teamvotegroupingid', ['id' => $this->ratingallocateid]); + // Return array should have the form groupid => membercount. $groups = array_map(function ($record) { @@ -1299,11 +1326,41 @@ public function get_teamvote_goups() { }, $this->db->get_records_sql($sql, ['groupingid' => $groupingid])); return $groups; } - // Anderen Default return überlegen? passend zu Graphenkanten. + return false; } - public function get_users_in_teams() { + /** + * Adds the groupid to all rating records with this userid. Should only be used for ratings with groupid 0. + * + * @param $userid + * @param $groupid + * @return void + * @throws dml_exception + */ + public function add_groupid_to_ratings($userid, $groupid) { + + $sql = 'SELECT ra.* FROM {ratingallocate_ratings} ra INNER JOIN {ratingallocate_choices} c + ON ra.choiceid=c.id WHERE c.ratingallocateid = :ratingallocateid AND ra.userid = :userid'; + $ratings = $this->db->get_records_sql($sql, ['ratingallocateid' => $this->ratingallocateid, 'userid' => $userid]); + foreach ($ratings as $rating) { + $rating->groupid = $groupid; + $this->db->update_record('ratingallocate_ratings', $rating); + } + + } + + public function delete_groups_for_usersnogroup($usergroups) { + + $sql = 'SELECT id FROM {groups} WHERE id IN ( :groups ) AND idnumber = :ratingallocateid AND name = :name'; + $delgroups = $this->db->get_records_sql($sql, [ + 'groups' => implode(" , ", array_keys($usergroups)), + 'ratingallocateid' => $this->ratingallocateid, + 'name' => 'delete after algorithm run' + ]); + foreach ($delgroups as $group) { + groups_delete_group($group); + } } @@ -1635,17 +1692,20 @@ public function create_moodle_groups() { * @return array */ public function get_rating_data_for_user($userid) { + $sql = "SELECT c.id as choiceid, c.title, c.explanation, c.ratingallocateid, - c.maxsize, c.usegroups, r.rating, r.id AS ratingid, r.userid - FROM {ratingallocate_choices} c - LEFT JOIN {ratingallocate_ratings} r - ON c.id = r.choiceid and r.userid = :userid - WHERE c.ratingallocateid = :ratingallocateid AND c.active = 1 - ORDER by c.title"; + c.maxsize, c.usegroups, r.rating, r.id AS ratingid, r.userid + FROM {ratingallocate_choices} c + LEFT JOIN {ratingallocate_ratings} r + ON c.id = r.choiceid and r.userid = :userid + WHERE c.ratingallocateid = :ratingallocateid AND c.active = 1 + ORDER by c.title"; + return $this->db->get_records_sql($sql, array( - 'ratingallocateid' => $this->ratingallocateid, - 'userid' => $userid + 'ratingallocateid' => $this->ratingallocateid, + 'userid' => $userid )); + } /** @@ -1698,7 +1758,7 @@ public function delete_all_ratings() { } /** - * Delete all ratings of a users + * Delete all ratings of a users and if teamvote is enabled also the ratings of all groupmembers * @param int $userid */ public function delete_ratings_of_user($userid) { @@ -1710,14 +1770,33 @@ public function delete_ratings_of_user($userid) { $choices = $this->get_choices(); - foreach ($choices as $id => $choice) { - $data = array( + $teamvote = ($DB->get_field('ratingallocate', 'teamvote', ['id' => $this->ratingallocateid]) == 1); + if ($teamvote && $votegroup=$this->get_vote_group($userid)) { + + // If teamvote is enabled, delete ratings for this group. + foreach ($choices as $id => $choice) { + $data = array( + 'groupid' => $votegroup->id, + 'choiceid' => $id + ); + + // Actually delete the rating. + $DB->delete_records('ratingallocate_ratings', $data); + } + + } else { + + // Delete rating for just this user. + foreach ($choices as $id => $choice) { + $data = array( 'userid' => $userid, 'choiceid' => $id - ); + ); + + // Actually delete the rating. + $DB->delete_records('ratingallocate_ratings', $data); + } - // Actually delete the rating. - $DB->delete_records('ratingallocate_ratings', $data); } $transaction->allow_commit(); @@ -1741,23 +1820,44 @@ public function save_ratings_to_db($userid, array $data) { $transaction = $DB->start_delegated_transaction(); $loggingdata = array(); try { + + $teamvote = ($DB->get_field('ratingallocate', 'teamvote', ['id' => $this->ratingallocateid]) == 1); + if ($teamvote && $votegroup=$this->get_vote_group($userid)) { + $votegroupid = $votegroup->id; + $ratingexists = array( + 'groupid' => $votegroupid + ); + } else { + $votegroupid = 0; + $ratingexists = array( + 'userid' => $userid + ); + } + foreach ($data as $id => $rdata) { $rating = new stdClass (); $rating->rating = $rdata['rating']; - $ratingexists = array( - 'choiceid' => $rdata['choiceid'], - 'userid' => $userid - ); + $ratingexists['choiceid'] = $rdata['choiceid']; if ($DB->record_exists('ratingallocate_ratings', $ratingexists)) { // The rating exists, we need to update its value - // We get the id from the database. + // We get the id from the database. (There are records for each userid so ignore multiple). - $oldrating = $DB->get_record('ratingallocate_ratings', $ratingexists); + $oldrating = $DB->get_record('ratingallocate_ratings', $ratingexists, IGNORE_MULTIPLE); if ($oldrating->{this_db\ratingallocate_ratings::RATING} != $rating->rating) { $rating->id = $oldrating->id; + $rating->groupid = $votegroupid; $DB->update_record('ratingallocate_ratings', $rating); + // If teamvote is enabled, update the ratings for all groupmembers. + if ($teamvote && $votegroup) { + $teammembers = groups_get_members($votegroupid, 'u.id'); + foreach ($teammembers as $member) { + $rating->userid = $member->id; + $DB->update_record('ratingallocate_ratings', $rating); + } + } + // Logging. array_push($loggingdata, array('choiceid' => $oldrating->choiceid, 'rating' => $rating->rating)); @@ -1768,8 +1868,18 @@ public function save_ratings_to_db($userid, array $data) { $rating->userid = $userid; $rating->choiceid = $rdata['choiceid']; $rating->ratingallocateid = $this->ratingallocateid; + $rating->groupid = $votegroupid; $DB->insert_record('ratingallocate_ratings', $rating); + // If teamvote is enabled, create ratings for all groupmembers. + if ($teamvote && $votegroup) { + $teammembers = groups_get_members($votegroupid, 'u.id'); + foreach ($teammembers as $member) { + $rating->userid = $member->id; + $DB->insert_record('ratingallocate_ratings', $rating); + } + } + // Logging. array_push($loggingdata, array('choiceid' => $rating->choiceid, 'rating' => $rating->rating)); @@ -1789,6 +1899,28 @@ public function save_ratings_to_db($userid, array $data) { } } + /** + * This is used for team votings 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 rating we want + * @return mixed The group or false + */ + public function get_vote_group($userid) { + + global $DB; + + $teamgroupingid = $DB->get_field('ratingallocate', 'teamvotegroupingid', ['id' => $this->ratingallocateid]); + $usergroups = groups_get_all_groups($this->course->id, $userid, $teamgroupingid, 'g.*', false, true); + if (count($usergroups) != 1) { + $return = false; + } else { + $return = array_pop($usergroups); + } + + return $return; + } + /** * Returns all active choices in the instance with $ratingallocateid */ diff --git a/solver/edmonds-karp.php b/solver/edmonds-karp.php index 9fdf8846..74959b2f 100644 --- a/solver/edmonds-karp.php +++ b/solver/edmonds-karp.php @@ -34,32 +34,60 @@ public function get_name() { return 'edmonds_karp'; } - public function compute_distribution($choicerecords, $ratings, $usercount) { + public function compute_distribution($choicerecords, $ratings, $usercount, $teamvote) { $choicedata = array(); foreach ($choicerecords as $record) { $choicedata[$record->id] = $record; } $choicecount = count($choicedata); + // Index of source and sink in the graph. $source = 0; - $sink = $choicecount + $usercount + 1; - list($fromuserid, $touserid, $fromchoiceid, $tochoiceid) = $this->setup_id_conversions($usercount, $ratings); + if (!$teamvote) { + + $sink = $choicecount + $usercount + 1; + + list($fromuserid, $touserid, $fromchoiceid, $tochoiceid) = $this->setup_id_conversions($usercount, $ratings); + + $this->setup_graph($choicecount, $usercount, $fromuserid, $fromchoiceid, $ratings, $choicedata, $source, $sink, -1); + + // Now that the datastructure is complete, we can start the algorithm + // This is an adaptation of the Ford-Fulkerson algorithm + // with Bellman-Ford as search function (see: Edmonds-Karp in Introduction to Algorithms) + // http://stackoverflow.com/questions/6681075/while-loop-in-php-with-assignment-operator + // Look for an augmenting path (a shortest path from the source to the sink). + while ($path = $this->find_shortest_path_bellf($source, $sink)) { // If the function returns null, the while will stop. + // Reverse the augmentin path, thereby distributing a user into a group. + $this->augment_flow($path); + unset($path); // Clear up old path. + } + return $this->extract_allocation($touserid, $tochoiceid); + + } else { + + $teamcount = count($teamvote); + $sink = $choicecount + $teamcount + 1; + + list($fromteamid, $toteamid, $fromchoiceid, $tochoiceid) = $this->setup_id_conversions_for_teamvote($teamcount, $ratings); - $this->setup_graph($choicecount, $usercount, $fromuserid, $fromchoiceid, $ratings, $choicedata, $source, $sink, -1); + $this->setup_graph_for_teamvote($choicecount, $teamcount, $fromteamid, $fromchoiceid, $ratings, $choicedata, $source, $sink, -1); + + // Now that the datastructure is complete, we can start the algorithm + // This is an adaptation of the Ford-Fulkerson algorithm + // with Bellman-Ford as search function (see: Edmonds-Karp in Introduction to Algorithms) + // http://stackoverflow.com/questions/6681075/while-loop-in-php-with-assignment-operator + // Look for an augmenting path (a shortest path from the source to the sink). + while ($path = $this->find_shortest_path_bellf($source, $sink)) { // If the function returns null, the while will stop. + // Reverse the augmentin path, thereby distributing a user into a group. + $this->augment_flow($path); + unset($path); // Clear up old path. + } + return $this->extract_allocation($toteamid, $tochoiceid); - // Now that the datastructure is complete, we can start the algorithm - // This is an adaptation of the Ford-Fulkerson algorithm - // with Bellman-Ford as search function (see: Edmonds-Karp in Introduction to Algorithms) - // http://stackoverflow.com/questions/6681075/while-loop-in-php-with-assignment-operator - // Look for an augmenting path (a shortest path from the source to the sink). - while ($path = $this->find_shortest_path_bellf($source, $sink)) { // If the function returns null, the while will stop. - // Reverse the augmentin path, thereby distributing a user into a group. - $this->augment_flow($path); - unset($path); // Clear up old path. } - return $this->extract_allocation($touserid, $tochoiceid); + } /** diff --git a/solver/ford-fulkerson-koegel.php b/solver/ford-fulkerson-koegel.php index f060e16b..fcf462f6 100644 --- a/solver/ford-fulkerson-koegel.php +++ b/solver/ford-fulkerson-koegel.php @@ -44,16 +44,19 @@ class solver_ford_fulkerson extends distributor { */ public function compute_distribution($choicerecords, $ratings, $usercount, $teamvote) { - if (!$teamvote) { - $groupdata = array(); - foreach ($choicerecords as $record) { - $groupdata[$record->id] = $record; - } - $groupcount = count($groupdata); - // Index of source and sink in the graph. - $source = 0; + $groupdata = array(); + foreach ($choicerecords as $record) { + $groupdata[$record->id] = $record; + } + + $groupcount = count($groupdata); + // Index of source and sink in the graph. + $source = 0; + + if (!$teamvote) { + $sink = $groupcount + $usercount + 1; list($fromuserid, $touserid, $fromgroupid, $togroupid) = $this->setup_id_conversions($usercount, $ratings); @@ -78,17 +81,9 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team } else { - $groupdata = array(); - foreach ($choicerecords as $record) { - $groupdata[$record->id] = $record; - } - - $groupcount = count($groupdata); - // Index of source and sink in the graph. - $source = 0; $teamcount = count($teamvote); $sink = $groupcount + $teamcount + 1; - list($fromteamid, $toteamid, $fromgroupid, $togroupid) = $this->setup_id_conversions($usercount, $ratings); + list($fromteamid, $toteamid, $fromgroupid, $togroupid) = $this->setup_id_conversions_for_teamvote($usercount, $ratings); $this->setup_graph_for_teamvote($groupcount, $teamcount, $fromteamid, $fromgroupid, $ratings, $groupdata, $source, $sink); @@ -107,7 +102,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team $this->augment_flow($path, $teamvote, $toteamid); } - return $this->extract_allocation($toteamid, $togroupid, $teamvote); + return $this->extract_allocation($toteamid, $togroupid); } diff --git a/solver/solver-template.php b/solver/solver-template.php index 7c5b6cea..3cdfe8ec 100644 --- a/solver/solver-template.php +++ b/solver/solver-template.php @@ -89,6 +89,8 @@ public static function compute_target_function($ratings, $distribution) { */ public function distribute_users(\ratingallocate $ratingallocate) { + $teamvote = $ratingallocate->get_teamvote_goups(); + // Load data from database. $choicerecords = $ratingallocate->get_rateable_choices(); $ratings = $ratingallocate->get_ratings_for_rateable_choices(); @@ -98,8 +100,6 @@ public function distribute_users(\ratingallocate $ratingallocate) { $usercount = count($ratingallocate->get_raters_in_course()); - $teamvote = $ratingallocate->get_teamvote_goups(); - $distributions = $this->compute_distribution($choicerecords, $ratings, $usercount, $teamvote); // Perform all allocation manipulation / inserts in one transaction. @@ -120,6 +120,10 @@ public function distribute_users(\ratingallocate $ratingallocate) { } $userdistributions[$choiceid] = array_merge($userids); } + + // We have to delete the provisionally groups containing only one user + $ratingallocate->delete_groups_for_usersnogroup($teamvote); + } foreach ($userdistributions as $choiceid => $users) { diff --git a/version.php b/version.php index 027f9ea7..9fa3a76a 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024020500; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2024030100; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061500; // Requires Moodle 3.9+. $plugin->maturity = MATURITY_STABLE; $plugin->release = 'v4.3-r1';