From 53b8ad2fe8d3abaccd5d0c33a1067fa475af481b Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Wed, 28 Feb 2024 14:42:43 +0100 Subject: [PATCH 01/15] add teamvote fields to db and to mod_form and alter distribution algorithm for teamratings --- db/install.xml | 4 + db/upgrade.php | 28 +++++++ locallib.php | 30 ++++++++ mod_form.php | 49 +++++++++++- settings.php | 29 +++++++ solver/ford-fulkerson-koegel.php | 91 +++++++++++++++------- solver/solver-template.php | 128 +++++++++++++++++++++++++++++-- version.php | 2 +- 8 files changed, 325 insertions(+), 36 deletions(-) diff --git a/db/install.xml b/db/install.xml index 5433f236..c56bd8fb 100644 --- a/db/install.xml +++ b/db/install.xml @@ -23,12 +23,15 @@ + + + @@ -52,6 +55,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index e111a143..a65d760c 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -218,5 +218,33 @@ function xmldb_ratingallocate_upgrade($oldversion) { upgrade_mod_savepoint(true, 2023050900, 'ratingallocate'); } + if ($oldversion < 2024020500) { + + // 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'); + + // Conditionally launch add fields to ratingallocate table. + if (!$dbman->field_exists($table, $field_teamvote)) { + $dbman->add_field($table, $field_teamvote); + } + if (!$dbman->field_exists($table, $field_teamvotegroupingid)) { + $dbman->add_field($table, $field_teamvotegroupingid); + } + + // Define field groupid to be added to ratingallocate_ratings. + $ratingstable = new xmldb_table('ratingallocate_ratings'); + $field_groupid = new xmldb_field('groupid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Conditionally launch add field. + if (!$dbman->field_exists($ratingstable, $field_groupid)) { + $dbman->add_field($ratingstable, $field_groupid); + } + + // Ratingallocate savepoint reached. + upgrade_mod_savepoint(true, 2024020500, 'ratingallocate'); + } + return true; } diff --git a/locallib.php b/locallib.php index 615d0e70..c58566b2 100644 --- a/locallib.php +++ b/locallib.php @@ -1277,6 +1277,36 @@ public function get_ratings_for_rateable_choices() { return $fromraters; } + /** + * Returns the groups in the teamvote grouping with the amount of groupmembers + * + * @return array|false Array of the form groupid => membercount if teamvote is enabled, false if not + * @throws dml_exception + */ + public function get_teamvote_goups() { + if ($this->db->get_field(this_db\ratingallocate::TABLE, 'teamvote', ['id' => $this->ratingallocateid]) == 1) { + + // 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) { + return $record->members; + }, $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() { + + } + /** * distribution of choices for each user * take care about max_execution_time and memory_limit diff --git a/mod_form.php b/mod_form.php index 61f3099e..832f6d92 100644 --- a/mod_form.php +++ b/mod_form.php @@ -66,7 +66,9 @@ public function definition() { global $CFG, $PAGE; $mform = $this->_form; - $disablestrategy = $this->get_disable_strategy(); + $info = $this->get_disable_strategy(true); + $disablestrategy = $info['disable_strategy']; + $ratingallocate = $info['ratingallocate']; // Adding the "general" fieldset, where all the common settings are showed. $mform->addElement('header', 'general', get_string('general', 'form')); @@ -137,6 +139,39 @@ public function definition() { $mform->addElement('static', self::STRATEGY_OPTIONS_PLACEHOLDER . '[' . $strategy . ']', '', ''); } + // Add settings for voting in groups + $mform->addElement('header', 'groupvotesettings', get_string('groupvotesettings', RATINGALLOCATE_MOD_NAME)); + + $name = get_string('teamvote', RATINGALLOCATE_MOD_NAME); + $mform->addElement('selectyesno', 'teamvote', $name); + $mform->addHelpButton('teamvote', 'teamvote', RATINGALLOCATE_MOD_NAME); + if ($disablestrategy) { + $mform->freeze('teamvote'); + } + + $name = get_string('preventvotenotingroup', RATINGALLOCATE_MOD_NAME); + $mform->addElement('selectyesno', 'preventvotenotingroup', $name); + $mform->addHelpButton('preventvotenotingroup', + 'preventvotenotingroup', + RATINGALLOCATE_MOD_NAME); + $mform->setType('preventvotenotingroup', PARAM_BOOL); + $mform->hideIf('preventvotenotingroup', 'teamvote', 'eq', 0); + + $groupings = groups_get_all_groupings($ratingallocate->ratingallocate->dbrecord->course); + $options = array(); + $options[0] = get_string('none'); + foreach ($groupings as $grouping) { + $options[$grouping->id] = $grouping->name; + } + + $name = get_string('teamvotegroupingid', RATINGALLOCATE_MOD_NAME); + $mform->addElement('select', 'teamvotegroupingid', $name, $options); + $mform->addHelpButton('teamvotegroupingid', 'teamvotegroupingid', RATINGALLOCATE_MOD_NAME); + $mform->hideIf('teamvotegroupingid', 'teamvote', 'eq', 0); + if ($disablestrategy) { + $mform->freeze('teamsubmissiongroupingid'); + } + // Add standard elements, common to all modules. $this->standard_coursemodule_elements(); @@ -144,6 +179,15 @@ public function definition() { $this->add_action_buttons(); } + /** + * Returns wether strategy and teamvote settings should be disabled. + * (True, if there are already ratings for this instance, False if there are not). + * + * @param $includeratingallocate Bool Wether to also return the ratingallocate instance. + * @return array|bool + * @throws coding_exception + * @throws dml_exception + */ public function get_disable_strategy($includeratingallocate = false) { $update = $this->optional_param('update', 0, PARAM_INT); if ($update != 0) { @@ -270,6 +314,9 @@ public function validation($data, $files) { if ($ratingallocate->ratingallocate->dbrecord->strategy !== $data['strategy']) { $errors['strategy'] = get_string('strategy_altered_after_preferences', self::MOD_NAME); } + if($ratingallocate->ratingallocate->dbrecord->teamvote !== $data['teamvote']) { + $errors['teamvote'] = get_string('teamvote_altered_after_preferences', self::MOD_NAME); + } } if (empty($data['strategy'])) { diff --git a/settings.php b/settings.php index 1c4c0ac1..19cb3ac4 100644 --- a/settings.php +++ b/settings.php @@ -50,4 +50,33 @@ $settings->add(new admin_setting_configcheckbox('ratingallocate_algorithm_force_background_execution', new lang_string('algorithmforcebackground', 'ratingallocate'), new lang_string('configalgorithmforcebackground', 'ratingallocate'), 0)); + + $name = new lang_string('teamvote', 'ratingallocate'); + $description = new lang_string('teamvote_help', 'ratingallocate'); + $setting = new admin_setting_configcheckbox('ratingallocate/teamvote', + $name, + $description, + 0); + $setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + $name = new lang_string('preventvotenotingroup', 'ratingallocate'); + $description = new lang_string('preventvotenotingroup_help', 'ratingallocate'); + $setting = new admin_setting_configcheckbox('ratingallocate/preventvotenotingroup', + $name, + $description, + 0); + $setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false); + $setting->set_locked_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + + $name = new lang_string('teamvotegroupingid', 'ratingallocate'); + $description = new lang_string('teamvotegroupingid_help', 'ratingallocate'); + $setting = new admin_setting_configempty('ratingallocate/teamvotegroupingid', + $name, + $description); + $setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false); + $settings->add($setting); + } diff --git a/solver/ford-fulkerson-koegel.php b/solver/ford-fulkerson-koegel.php index d8f2c1e6..f060e16b 100644 --- a/solver/ford-fulkerson-koegel.php +++ b/solver/ford-fulkerson-koegel.php @@ -42,36 +42,75 @@ class solver_ford_fulkerson extends distributor { * according to the computed distriution. * */ - public function compute_distribution($choicerecords, $ratings, $usercount) { - $groupdata = array(); - foreach ($choicerecords as $record) { - $groupdata[$record->id] = $record; - } + 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; + $sink = $groupcount + $usercount + 1; + list($fromuserid, $touserid, $fromgroupid, $togroupid) = $this->setup_id_conversions($usercount, $ratings); + + $this->setup_graph($groupcount, $usercount, $fromuserid, $fromgroupid, $ratings, $groupdata, $source, $sink); + + // Now that the datastructure is complete, we can start the algorithm + // This is an adaptation of the Ford-Fulkerson algorithm + // (http://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm). + for ($i = 1; $i <= $usercount; $i++) { + // Look for an augmenting path (a shortest path from the source to the sink). + $path = $this->find_shortest_path_bellmanf_koegel($source, $sink); + // If there is no such path, it is impossible to fit any more users into groups. + if (is_null($path)) { + // Stop the algorithm. + continue; + } + // Reverse the augmenting path, thereby distributing a user into a group. + $this->augment_flow($path); + } - $groupcount = count($groupdata); - // Index of source and sink in the graph. - $source = 0; - $sink = $groupcount + $usercount + 1; - list($fromuserid, $touserid, $fromgroupid, $togroupid) = $this->setup_id_conversions($usercount, $ratings); - - $this->setup_graph($groupcount, $usercount, $fromuserid, $fromgroupid, $ratings, $groupdata, $source, $sink); - - // Now that the datastructure is complete, we can start the algorithm - // This is an adaptation of the Ford-Fulkerson algorithm - // (http://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm). - for ($i = 1; $i <= $usercount; $i++) { - // Look for an augmenting path (a shortest path from the source to the sink). - $path = $this->find_shortest_path_bellmanf_koegel($source, $sink); - // If there is no such path, it is impossible to fit any more users into groups. - if (is_null($path)) { - // Stop the algorithm. - continue; + return $this->extract_allocation($touserid, $togroupid); + + } else { + + $groupdata = array(); + foreach ($choicerecords as $record) { + $groupdata[$record->id] = $record; } - // Reverse the augmenting path, thereby distributing a user into a group. - $this->augment_flow($path); + + $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); + + $this->setup_graph_for_teamvote($groupcount, $teamcount, $fromteamid, $fromgroupid, $ratings, $groupdata, $source, $sink); + + // Now that the datastructure is complete, we can start the algorithm + // This is an adaptation of the Ford-Fulkerson algorithm + // (http://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm). + for ($i = 1; $i <= $teamcount; $i++) { + // Look for an augmenting path (a shortest path from the source to the sink). + $path = $this->find_shortest_path_bellmanf_koegel($source, $sink); + // If there is no such path, it is impossible to fit any more users into groups. + if (is_null($path)) { + // Stop the algorithm. + continue; + } + // Reverse the augmenting path, thereby distributing a user into a group. + $this->augment_flow($path, $teamvote, $toteamid); + } + + return $this->extract_allocation($toteamid, $togroupid, $teamvote); + } - return $this->extract_allocation($touserid, $togroupid); } /** diff --git a/solver/solver-template.php b/solver/solver-template.php index 3b9e8370..7c5b6cea 100644 --- a/solver/solver-template.php +++ b/solver/solver-template.php @@ -98,14 +98,31 @@ public function distribute_users(\ratingallocate $ratingallocate) { $usercount = count($ratingallocate->get_raters_in_course()); - $distributions = $this->compute_distribution($choicerecords, $ratings, $usercount); + $teamvote = $ratingallocate->get_teamvote_goups(); + + $distributions = $this->compute_distribution($choicerecords, $ratings, $usercount, $teamvote); // Perform all allocation manipulation / inserts in one transaction. $transaction = $ratingallocate->db->start_delegated_transaction(); $ratingallocate->clear_all_allocations(); - foreach ($distributions as $choiceid => $users) { + $userdistributions = array(); + + if (!$teamvote) { + $userdistributions = $distributions; + } else { + // Map choiceids to every user of the team it is mapped to. + $userids = array(); + foreach ($distributions as $choiceid => $teamids) { + foreach ($teamids as $teamid) { + $userids[$teamid] = groups_get_members($teamid, 'id'); + } + $userdistributions[$choiceid] = array_merge($userids); + } + } + + foreach ($userdistributions as $choiceid => $users) { foreach ($users as $userid) { $ratingallocate->add_allocation($choiceid, $userid, $ratingallocate->ratingallocate->id); } @@ -116,7 +133,7 @@ public function distribute_users(\ratingallocate $ratingallocate) { /** * Extracts a distribution/allocation from the graph. * - * @param $touserid a map mapping from indexes in the graph to userids + * @param $touserid a map mapping from indexes in the graph to userids (or teamids) * @param $tochoiceid a map mapping from indexes in the graph to choiceids * @return an array of the form array(groupid => array(userid, ...), ...) */ @@ -173,6 +190,44 @@ public static function setup_id_conversions($usercount, $ratings) { return array($fromuserid, $touserid, $fromchoiceid, $tochoiceid); } + /** + * Setup conversions between ids of teams and choices to their node-ids in the graph + * @param type $teamcount + * @param type $ratings + * @return array($fromteamid, $toteamid, $fromchoiceid, $tochoiceid); + */ + public static function setup_id_conversions_for_teamvote($teamcount, $ratings) { + // These tables convert teamids to their index in the graph + // The range is [1..$teamcount]. + $fromteamid = array(); + $toteamid = array(); + // These tables convert choiceids to their index in the graph + // The range is [$teamcount + 1 .. $teamcount + $choicecount]. + $fromchoiceid = array(); + $tochoiceid = array(); + + // Team counter. + $ti = 1; + // Group counter. + $gi = $teamcount + 1; + + // Fill the conversion tables for group and team ids. + foreach ($ratings as $rating) { + if (!array_key_exists($rating->groupid, $fromteamid)) { + $fromteamid[$rating->groupid] = $ti; + $toteamid[$ti] = $rating->groupid; + $ti++; + } + if (!array_key_exists($rating->choiceid, $fromchoiceid)) { + $fromchoiceid[$rating->choiceid] = $gi; + $tochoiceid[$gi] = $rating->choiceid; + $gi++; + } + } + + return array($fromteamid, $toteamid, $fromchoiceid, $tochoiceid); + } + /** * Sets up $this->graph * @param type $choicecount @@ -220,14 +275,63 @@ protected function setup_graph($choicecount, $usercount, $fromuserid, $fromchoic } } + /** + * Sets up $this->graph + * @param type $choicecount + * @param type $teamcount + * @param type $fromteamid + * @param type $fromchoiceid + * @param type $ratings + * @param type $choicedata + * @param type $source + * @param type $sink + */ + protected function setup_graph_for_teamvote($choicecount, $teamcount, $fromteamid, $fromchoiceid, $ratings, $choicedata, $source, $sink, + $weightmult = 1) { + // Construct the datastructures for the algorithm + // A directed weighted bipartite graph. + // A source is connected to all users with unit cost. + // The teams are connected to their choices with cost equal to their rating. + // The choices are connected to a sink with 0 cost. + $this->graph = array(); + // Add source, sink and number of nodes to the graph. + $this->graph[$source] = array(); + $this->graph[$sink] = array(); + $this->graph['count'] = $choicecount + $teamcount + 2; + + // Add teams and choices to the graph and connect them to the source and sink. + foreach ($fromteamid as $id => $team) { + $this->graph[$team] = array(); + $this->graph[$source][] = new edge($source, $team, 0); + } + foreach ($fromchoiceid as $id => $choice) { + $this->graph[$choice] = array(); + if ($choicedata[$id]->maxsize > 0) { + $this->graph[$choice][] = new edge($choice, $sink, 0, $choicedata[$id]->maxsize); + } + } + + // Add the edges representing the ratings to the graph. + foreach ($ratings as $id => $rating) { + $team = $fromteamid[$rating->groupid]; + $choice = $fromchoiceid[$rating->choiceid]; + $weight = $rating->rating; + if ($weight > 0) { + $this->graph[$team][] = new edge($team, $choice, $weightmult * $weight); + } + } + + // Andere Kanten noch entsprechend groupmembers gewichten? + } + /** * Augments the flow in the network, i.e. augments the overall 'satisfaction' - * by distributing users to choices + * by distributing users (or teams) to choices * Reverses all edges along $path in $graph * @param type $path path from t to s * @throws moodle_exception */ - protected function augment_flow($path) { + protected function augment_flow($path, $teamvote=false, $toteamid=null) { if (is_null($path) || count($path) < 2) { throw new \moodle_exception('invalid_path', 'ratingallocate'); } @@ -246,9 +350,17 @@ protected function augment_flow($path) { } } // The second to last node in a path has to be a choice-node. - // Reduce its space by one, because one user just got distributed into it. - if ($i == 1 && $edge->space > 1) { - $edge->space--; + // The node before that has to be a teamnode (or usernode), distribute them to this choice. + if (!$teamvote) { + // If teamvote=false, reduce its space by one, because one user just got distributed into it. + $space = 1; + } else { + // If teamvote is enabled, reduce its space by amount of groupmembers. + $space = $teamvote[$toteamid[$path[$i+1]]]; + } + + if ($i == 1 && $edge->space > $space) { + $edge->space = $edge->space - $space; } else { // Remove the edge. array_splice($this->graph[$from], $foundedgeid, 1); diff --git a/version.php b/version.php index 57cc2f0d..027f9ea7 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023101900; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2024020500; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061500; // Requires Moodle 3.9+. $plugin->maturity = MATURITY_STABLE; $plugin->release = 'v4.3-r1'; From 3814171d5b5cb1e65be4a48fab61b51b3ee8ce5a Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Mon, 4 Mar 2024 11:56:28 +0100 Subject: [PATCH 02/15] 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'; From 645f156cabac008d70cd33ed0c6b106fabd84fd9 Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Wed, 6 Mar 2024 09:07:57 +0100 Subject: [PATCH 03/15] codestyle fixes --- db/upgrade.php | 20 +++++++++++--------- lang/en/ratingallocate.php | 2 +- locallib.php | 7 +++---- mod_form.php | 6 +++--- solver/ford-fulkerson-koegel.php | 2 -- solver/solver-template.php | 4 ++-- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/db/upgrade.php b/db/upgrade.php index 1ddf77b4..40a3c8ba 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -222,19 +222,21 @@ function xmldb_ratingallocate_upgrade($oldversion) { // 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'); + $fieldteamvote = new xmldb_field('teamvote', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); + $fieldteamvotegroupingid = new xmldb_field('teamvotegroupingid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $fieldpreventvotenotingroup = 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)) { - $dbman->add_field($table, $field_teamvote); + if (!$dbman->field_exists($table, $fieldteamvote)) { + $dbman->add_field($table, $fieldteamvote); } - if (!$dbman->field_exists($table, $field_teamvotegroupingid)) { - $dbman->add_field($table, $field_teamvotegroupingid); + if (!$dbman->field_exists($table, $fieldteamvotegroupingid)) { + $dbman->add_field($table, $fieldteamvotegroupingid); } - if (!$dbman->field_exists($table, $field_preventvotenotingroup)) { - $dbman->add_field($table, $field_preventvotenotingroup); + if (!$dbman->field_exists($table, $fieldpreventvotenotingroup)) { + $dbman->add_field($table, $fieldpreventvotenotingroup); } // Define field groupid to be added to ratingallocate_ratings. diff --git a/lang/en/ratingallocate.php b/lang/en/ratingallocate.php index d5d1cb8b..31616865 100644 --- a/lang/en/ratingallocate.php +++ b/lang/en/ratingallocate.php @@ -304,7 +304,7 @@ $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['teamvotegroupingid_help'] = 'This is the grouping that the activity 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.'; diff --git a/locallib.php b/locallib.php index f0214511..eee10e89 100644 --- a/locallib.php +++ b/locallib.php @@ -1294,7 +1294,7 @@ public function get_teamvote_goups() { $usersnogroup = array_diff($this->get_raters_in_course(), groups_get_grouping_members($groupingid)); $groupdata = new stdClass(); - $groupdata->courseid =$this->course->id; + $groupdata->courseid = $this->course->id; $groupdata->idnumber = $this->ratingallocateid; $groupdata->name = 'delete after algorithm run'; @@ -1319,7 +1319,6 @@ public function get_teamvote_goups() { WHERE g.groupingid = :groupingid GROUP BY groupid'; - // Return array should have the form groupid => membercount. $groups = array_map(function ($record) { return $record->members; @@ -1771,7 +1770,7 @@ public function delete_ratings_of_user($userid) { $choices = $this->get_choices(); $teamvote = ($DB->get_field('ratingallocate', 'teamvote', ['id' => $this->ratingallocateid]) == 1); - if ($teamvote && $votegroup=$this->get_vote_group($userid)) { + if ($teamvote && $votegroup = $this->get_vote_group($userid)) { // If teamvote is enabled, delete ratings for this group. foreach ($choices as $id => $choice) { @@ -1822,7 +1821,7 @@ public function save_ratings_to_db($userid, array $data) { try { $teamvote = ($DB->get_field('ratingallocate', 'teamvote', ['id' => $this->ratingallocateid]) == 1); - if ($teamvote && $votegroup=$this->get_vote_group($userid)) { + if ($teamvote && $votegroup = $this->get_vote_group($userid)) { $votegroupid = $votegroup->id; $ratingexists = array( 'groupid' => $votegroupid diff --git a/mod_form.php b/mod_form.php index 832f6d92..cb3e2baa 100644 --- a/mod_form.php +++ b/mod_form.php @@ -139,7 +139,7 @@ public function definition() { $mform->addElement('static', self::STRATEGY_OPTIONS_PLACEHOLDER . '[' . $strategy . ']', '', ''); } - // Add settings for voting in groups + // Add settings for voting in groups. $mform->addElement('header', 'groupvotesettings', get_string('groupvotesettings', RATINGALLOCATE_MOD_NAME)); $name = get_string('teamvote', RATINGALLOCATE_MOD_NAME); @@ -169,7 +169,7 @@ public function definition() { $mform->addHelpButton('teamvotegroupingid', 'teamvotegroupingid', RATINGALLOCATE_MOD_NAME); $mform->hideIf('teamvotegroupingid', 'teamvote', 'eq', 0); if ($disablestrategy) { - $mform->freeze('teamsubmissiongroupingid'); + $mform->freeze('teamvotegroupingid'); } // Add standard elements, common to all modules. @@ -314,7 +314,7 @@ public function validation($data, $files) { if ($ratingallocate->ratingallocate->dbrecord->strategy !== $data['strategy']) { $errors['strategy'] = get_string('strategy_altered_after_preferences', self::MOD_NAME); } - if($ratingallocate->ratingallocate->dbrecord->teamvote !== $data['teamvote']) { + if ($ratingallocate->ratingallocate->dbrecord->teamvote !== $data['teamvote']) { $errors['teamvote'] = get_string('teamvote_altered_after_preferences', self::MOD_NAME); } } diff --git a/solver/ford-fulkerson-koegel.php b/solver/ford-fulkerson-koegel.php index fcf462f6..e781dfe0 100644 --- a/solver/ford-fulkerson-koegel.php +++ b/solver/ford-fulkerson-koegel.php @@ -44,8 +44,6 @@ class solver_ford_fulkerson extends distributor { */ public function compute_distribution($choicerecords, $ratings, $usercount, $teamvote) { - - $groupdata = array(); foreach ($choicerecords as $record) { $groupdata[$record->id] = $record; diff --git a/solver/solver-template.php b/solver/solver-template.php index 3cdfe8ec..cf5ebfab 100644 --- a/solver/solver-template.php +++ b/solver/solver-template.php @@ -121,7 +121,7 @@ public function distribute_users(\ratingallocate $ratingallocate) { $userdistributions[$choiceid] = array_merge($userids); } - // We have to delete the provisionally groups containing only one user + // We have to delete the provisionally groups containing only one user. $ratingallocate->delete_groups_for_usersnogroup($teamvote); } @@ -360,7 +360,7 @@ protected function augment_flow($path, $teamvote=false, $toteamid=null) { $space = 1; } else { // If teamvote is enabled, reduce its space by amount of groupmembers. - $space = $teamvote[$toteamid[$path[$i+1]]]; + $space = $teamvote[$toteamid[$path[$i + 1]]]; } if ($i == 1 && $edge->space > $space) { From 9027fb85d0845e9a1ffc23d99e6eda8bca0979a5 Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Wed, 27 Mar 2024 10:11:45 +0100 Subject: [PATCH 04/15] started writing a new algorithm for finding constrained shortest path, altered manual allocation form to show teams of users --- classes/ratings_and_allocations_table.php | 37 +++++ form_manual_allocation.php | 4 + locallib.php | 10 +- solver/edmonds-karp.php | 183 +++++++++++++++++++++- solver/solver-template.php | 2 +- 5 files changed, 231 insertions(+), 5 deletions(-) diff --git a/classes/ratings_and_allocations_table.php b/classes/ratings_and_allocations_table.php index 48e386b4..d4de1ee6 100644 --- a/classes/ratings_and_allocations_table.php +++ b/classes/ratings_and_allocations_table.php @@ -56,6 +56,11 @@ class ratings_and_allocations_table extends \table_sql { */ private $showgroups; + /** + * @var bool if true the table should show a column with the teams of the teamvote grouping. + */ + private $showteams; + /** * @var bool if true the cells are rendered as radio buttons */ @@ -95,6 +100,7 @@ public function __construct(\mod_ratingallocate_renderer $renderer, $titles, $ra $this->shownames = true; // We only show the group column if at least one group is being used in at least one active restriction setting of a choice. $this->showgroups = !empty($allgroupsofchoices); + $this->showteams = (bool) $this->ratingallocate->get_teamvote_goups(); } /** @@ -181,6 +187,10 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary $this->ratingallocate->get_choice_groups($choice->id)); } } + if ($this->showteams) { + $columns[] = 'teams'; + $headers[] = get_string('teams'); + } } // Setup filter. @@ -211,12 +221,20 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary $this->define_headers($headers); // Set additional table settings. + if ($this->showteams) { + //$this->sortable(true, 'teams'); + } else { + + } $this->sortable(true, 'lastname'); $tableclasses = 'ratingallocate_ratings_table'; if ($this->showgroups) { $tableclasses .= ' includegroups'; $this->no_sorting('groups'); } + if ($this->showteams) { + $this->no_sorting('teams'); + } $this->set_attribute('class', $tableclasses); $this->initialbars(true); @@ -327,6 +345,18 @@ private function add_user_ratings_row($user, $userratings, $userallocations) { }, $groupsofuser); $row['groups'] = implode(';', $groupnames); } + if ($this->showteams) { + $teamofuser = array_filter(array_keys($this->ratingallocate->get_teamvote_goups()), + function($groupid) use ($user) { + return groups_is_member($groupid,$user->id); + } + ); + $teamname = array_map(function ($team) { + return $team->name; + }, $teamofuser); + // We should only have one team for each user, but we cant ensure that at this point. + $row['teams'] = implode(';', $teamname); + } } foreach ($userratings as $choiceid => $userrating) { @@ -662,6 +692,10 @@ function($c) { } + private function sort_by_teams ($teams) { + + } + /** * Sets up the sql statement for querying the table data. */ @@ -673,6 +707,7 @@ public function init_sql() { $userids = $this->filter_userids($userids); $sortfields = $this->get_sort_columns(); + var_dump($sortfields); $fields = "u.*"; if ($userids) { $where = "u.id in (" . implode(",", $userids) . ")"; @@ -690,6 +725,8 @@ public function init_sql() { $from .= " LEFT JOIN {ratingallocate_ratings} r$i ON u.id = r$i.userid AND r$i.choiceid = :choiceid$i "; $fields .= ", r$i.rating as $key"; $params["choiceid$i"] = $id; + } else if (substr($key, 0, 5) == "teams") { + //$from .= } } diff --git a/form_manual_allocation.php b/form_manual_allocation.php index 62356a13..c5f01237 100644 --- a/form_manual_allocation.php +++ b/form_manual_allocation.php @@ -148,6 +148,10 @@ public function definition_after_data() { $mform->setDefault('filtergroup', $filter['groupselect']); $mform->getElement('filtergroup')->setSelected($filter['groupselect']); + if ($this->ratingallocate->get_teamvote_goups()) { + + } + $PAGE->requires->js_call_amd('mod_ratingallocate/radiobuttondeselect', 'init'); // The rest must be done through output buffering due to the way flextable works. diff --git a/locallib.php b/locallib.php index eee10e89..2c9475b1 100644 --- a/locallib.php +++ b/locallib.php @@ -327,6 +327,8 @@ private function process_action_give_rating() { } else if ($status === self::DISTRIBUTION_STATUS_RATING_IN_PROGRESS) { // Rating is possible... + // Adde votegroup name zu form. + // Suche das richtige Formular nach Strategie. $strategyform = 'ratingallocate\\' . $this->ratingallocate->strategy . '\\mod_ratingallocate_view_form'; @@ -1290,8 +1292,14 @@ public function get_teamvote_goups() { // 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)); + $usersnogroup = array(); + foreach ($this->get_raters_in_course() as $rater) { + if (!in_array($rater, groups_get_grouping_members($groupingid))) { + $usersnogroup[] = $rater; + } + } $groupdata = new stdClass(); $groupdata->courseid = $this->course->id; diff --git a/solver/edmonds-karp.php b/solver/edmonds-karp.php index 74959b2f..57c1b093 100644 --- a/solver/edmonds-karp.php +++ b/solver/edmonds-karp.php @@ -67,6 +67,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team } else { + var_dump("Teamvote = true"); $teamcount = count($teamvote); $sink = $choicecount + $teamcount + 1; @@ -79,7 +80,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team // 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. + while ($path = $this->find_shortest_path_bellf_cspf($source, $sink, $teamvote, $toteamid)) { // 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. @@ -90,14 +91,190 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team } + /** + * Find the shortest path with constraint (enough space for all teammembers in choice). + * This is a modified version of the Yen Algorithm for the consstrained shortest path first problem. + * + * @param $from + * @param $to + * @param $teamvote + * @param $toteamid + * @return array|mixed|null array of the nodes in the path, null if no path found. + */ + private function find_shortest_path_bellf_cspf ($from, $to, $teamvote, $toteamid) { + + // Find the first shortest path. + $pathcandidates = array(); + $pathcandidates[0] = $this->find_shortest_path_bellf($from, $to); + + $nopathfound = is_null($pathcandidates[0]); + + // Check if the path fulfills our constraint: space in choice left >= teammembers. + $constraintflag = true; + $foundedge = null; + foreach ($this->graph[$pathcandidates[0][1]] as $edge) { + if ($edge->to == $pathcandidates[0][0]) { + $foundedge = $edge; + break; + } + } + if ($foundedge->space <= $teamvote[$toteamid[$pathcandidates[0][2]]]) { + $constraintflag = false; + } + + if ($constraintflag) { + // We just found the shortest path fulfilling the constraint. + return $pathcandidates[0]; + } + $constraintflag = true; + + // Array of the potential next shortest paths. + $nextpaths = array(); + $restoreedges = array(); + $restorenodes = array(); + + // Now find the next shortest path. + $k = 1; + // Exit if there are no more shortest paths (nopathfound=true). + while (!$nopathfound && $k < 100) { + for ($i = 0; $i < count($pathcandidates[$k - 1]); $i++) { + + var_dump("Im algo
"); + var_dump($pathcandidates); + var_dump(":Pathcandidates
"); + + // Spurnode ranges from first to next to last node in previous shortest path. + $spurnode = $pathcandidates[$k - 1][$i]; + $rootpath = array_slice($pathcandidates[$k - 1], 0, $i+1, true); + + foreach ($pathcandidates as $path) { + + if ($rootpath == array_slice($path, 0, $i+1, true)) { + foreach ($this->graph[$path[$i + 1]] as $index => $edge) { + if ($edge->to == $path[$i]) { + // Remove the links that are part of the previous shortest paths. + // Which share the same root path. + $restoreedges[$path[$i + 1]][$index] = $edge; + array_splice($this->graph[$path[$i + 1]], $index, 1); + break; + } + } + } else { + continue; + } + + foreach ($rootpath as $rootpathnode) { + if ($rootpathnode != $spurnode) { + // Remove $rootpathnode from graph. + foreach ($this->graph as $index => $graphnode) { + if ($graphnode == $rootpathnode) { + $restorenodes[$index] = $graphnode; + unset($this->graph[$index]); + } + } + } + } + + // Calculate the spur path from the spur node to the sink. + $spurpath = $this->find_shortest_path_bellf($i, $to); + + // Entire path is made up of the root path and spur path. + $totalpath = array_merge($rootpath, $spurpath); + + // Add the potential next shortest path to the heap. + $nextpaths[] = $totalpath; + + // Now add back edges and nodes that were removed from the graph. + foreach ($restoreedges as $index1 => $node) { + foreach ($node as $index2 => $edge) { + $this->graph[$index1][$index2] = $edge; + } + } + foreach ($restorenodes as $index => $node) { + $this->graph[$index] = $node; + } + } + + if (empty($nextpaths)) { + var_dump("No path found
"); + $nopathfound = true; + break; + } + + var_dump($nextpaths); + // Sort the potential next shortest paths by cost. -> nextpaths[0] = best path with lowest cost. + usort($nextpaths, function ($path1, $path2) { + return ($this->get_cost_of_path($path1) - $this->get_cost_of_path($path2)); + }); + var_dump("
Sortieren...
"); + var_dump($nextpaths); + + // Check if the next best path fullfillst our constraint. + foreach ($this->graph[$nextpaths[0][1]] as $edge) { + if ($edge->to == $nextpaths[0][0]) { + $foundedge = $edge; + break; + } + } + if ($foundedge->space <= $teamvote[$toteamid[$nextpaths[0][2]]]) { + $constraintflag = false; + } + + if ($constraintflag) { + var_dump("Path found"); + return $nextpaths[0]; + } + + $pathcandidates[$k] = $nextpaths[0]; + + // Reset flag condition. + $constraintflag = true; + + array_pop($nextpaths); + } + $k++; + } + return null; + } + + /** + * Returns the cost of the path by adding the weight of all edges in the path. + * + * @param $path + * @return int cost + */ + private function get_cost_of_path ($path) { + + $cost = 0; + + for ($i = count($path) - 1; $i > 0; $i--) { + $from = $path[$i]; + $to = $path[$i - 1]; + $edge = null; + // Find the edge. + foreach ($this->graph[$from] as $index => $edge) { + if ($edge->to == $to) { + $cost += $edge->weight; + break; + } + } + } + + return $cost; + } + /** * Bellman-Ford acc. to Cormen * - * @param $from index of starting node - * @param $to index of end node + * @param $from int index of starting node + * @param $to int index of end node * @return array with the of the nodes in the path */ private function find_shortest_path_bellf($from, $to) { + + // We have to alter this method to fit teamvote (find the shortest path with flow >= teammembers). + // This is a constrained shortest path first problem. + // Table of distances known so far. $dists = array(); // Table of predecessors (used to reconstruct the shortest path later). diff --git a/solver/solver-template.php b/solver/solver-template.php index cf5ebfab..82728f24 100644 --- a/solver/solver-template.php +++ b/solver/solver-template.php @@ -116,7 +116,7 @@ public function distribute_users(\ratingallocate $ratingallocate) { $userids = array(); foreach ($distributions as $choiceid => $teamids) { foreach ($teamids as $teamid) { - $userids[$teamid] = groups_get_members($teamid, 'id'); + $userids[$teamid] = groups_get_members($teamid, 'u.id'); } $userdistributions[$choiceid] = array_merge($userids); } From 05316577e8d0b1b892ad39c0bb2011c1e487c671 Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Mon, 15 Apr 2024 09:12:10 +0200 Subject: [PATCH 05/15] modified ratings_and_allocations_table.php to include team of each user --- classes/ratings_and_allocations_table.php | 63 ++++++++++++++++++++--- form_manual_allocation.php | 4 -- locallib.php | 18 +++++++ mod_form.php | 4 +- styles.css | 11 ++-- 5 files changed, 83 insertions(+), 17 deletions(-) diff --git a/classes/ratings_and_allocations_table.php b/classes/ratings_and_allocations_table.php index d4de1ee6..71e4a780 100644 --- a/classes/ratings_and_allocations_table.php +++ b/classes/ratings_and_allocations_table.php @@ -189,7 +189,7 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary } if ($this->showteams) { $columns[] = 'teams'; - $headers[] = get_string('teams'); + $headers[] = get_string('teams', 'mod_ratingallocate'); } } @@ -222,19 +222,20 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary // Set additional table settings. if ($this->showteams) { - //$this->sortable(true, 'teams'); + $this->sortable(true, 'teams'); } else { - + $this->sortable(true, 'lastname'); } - $this->sortable(true, 'lastname'); + $tableclasses = 'ratingallocate_ratings_table'; if ($this->showgroups) { $tableclasses .= ' includegroups'; $this->no_sorting('groups'); } if ($this->showteams) { - $this->no_sorting('teams'); + $tableclasses .= ' includeteams'; } + $this->set_attribute('class', $tableclasses); $this->initialbars(true); @@ -352,7 +353,7 @@ function($groupid) use ($user) { } ); $teamname = array_map(function ($team) { - return $team->name; + return groups_get_group($team, 'name')->name; }, $teamofuser); // We should only have one team for each user, but we cant ensure that at this point. $row['teams'] = implode(';', $teamname); @@ -403,6 +404,10 @@ private function add_summary_row() { // In case we are showing groups, the second column is the group column and needs to be skipped in summary row. $row[] = ''; } + if ($this->showteams) { + // In case we are showing teams, the third (second) column is the teams column and needs to be skipped in summary row. + $row[] = ''; + } } foreach ($this->choicesum as $choiceid => $sum) { @@ -707,7 +712,45 @@ public function init_sql() { $userids = $this->filter_userids($userids); $sortfields = $this->get_sort_columns(); + + + // To do vardumps entfernen. var_dump($sortfields); + var_dump("
sortdata: "); + var_dump($this->sortdata); + var_dump("
sortorder: "); + var_dump($this->get_sort_order()); + + // If we have teamvote enabled, always order by team first, in order to always show users in their teams. + if ($this->showteams) { + + $sortdata = array([ + 'sortby' => 'teams', + 'sortorder' => SORT_ASC + ]); + + foreach (array_keys($sortfields) as $column) { + if (substr($column, 0, 5) != "teams") { + $sortdata[] =[ + 'sortby' => $column, + 'sortorder' => SORT_ASC + ]; + } + } + $this->set_sortdata($sortdata); + $this->set_sorting_preferences(); + + } + $sortfields = $this->get_sort_columns(); + + var_dump("
sortdata nach preferences: "); + var_dump($this->sortdata); + var_dump("
sortcolumns nach preferences: "); + var_dump($this->get_sort_columns()); + var_dump("
sortorder nach preferences: "); + var_dump($this->get_sort_order()); + + $fields = "u.*"; if ($userids) { $where = "u.id in (" . implode(",", $userids) . ")"; @@ -720,13 +763,19 @@ public function init_sql() { $params = array(); for ($i = 0; $i < count($sortfields); $i++) { $key = array_keys($sortfields)[$i]; + + // If sortfields contain 'teams', it is always on first position. if (substr($key, 0, 6) == "choice") { $id = substr($key, 7); $from .= " LEFT JOIN {ratingallocate_ratings} r$i ON u.id = r$i.userid AND r$i.choiceid = :choiceid$i "; $fields .= ", r$i.rating as $key"; $params["choiceid$i"] = $id; } else if (substr($key, 0, 5) == "teams") { - //$from .= + $fields .= ", gm.groupid as teams"; + $from .= " LEFT JOIN {groups_members} gm ON u.id=gm.userid LEFT JOIN {groupings_groups} gg ON gm.groupid=gg.groupid + LEFT JOIN {ratingallocate} r ON gg.groupingid=r.teamvotegroupingid"; + $where .= " AND r.id = :ratingallocateid"; + $params["ratingallocateid"] = $this->ratingallocate->get_ratingallocateid(); } } diff --git a/form_manual_allocation.php b/form_manual_allocation.php index c5f01237..62356a13 100644 --- a/form_manual_allocation.php +++ b/form_manual_allocation.php @@ -148,10 +148,6 @@ public function definition_after_data() { $mform->setDefault('filtergroup', $filter['groupselect']); $mform->getElement('filtergroup')->setSelected($filter['groupselect']); - if ($this->ratingallocate->get_teamvote_goups()) { - - } - $PAGE->requires->js_call_amd('mod_ratingallocate/radiobuttondeselect', 'init'); // The rest must be done through output buffering due to the way flextable works. diff --git a/locallib.php b/locallib.php index 2c9475b1..543d7042 100644 --- a/locallib.php +++ b/locallib.php @@ -1371,6 +1371,24 @@ public function delete_groups_for_usersnogroup($usergroups) { } + /** + * Returns the group in the teamvotegrouping this user is a member of. + * (Should return only one groupid, please only call if teamvote is enabled). + * + * @param $userid + * @return false|mixed The groupid + * @throws dml_exception + */ + public function get_teamvotegroup_for_user($userid) { + + $sql = 'SELECT gm.groupid FROM {groups_members} gm INNER JOIN {groupings_groups} gg ON gm.groupid=gg.groupid + INNER JOIN {ratingallocate} r ON gg.groupingid=r.teamvotegroupingid + WHERE gm.userid = :userid AND r.id = :ratingallocateid'; + $groupid = $this->db->get_record_sql($sql, ['userid' => $userid, 'ratingallocateid' => $this->ratingallocateid]); + return $groupid->groupid; + + } + /** * distribution of choices for each user * take care about max_execution_time and memory_limit diff --git a/mod_form.php b/mod_form.php index cb3e2baa..b8089f3d 100644 --- a/mod_form.php +++ b/mod_form.php @@ -63,7 +63,7 @@ public function __construct($current, $section, $cm, $course) { * Defines forms elements */ public function definition() { - global $CFG, $PAGE; + global $CFG, $PAGE, $COURSE; $mform = $this->_form; $info = $this->get_disable_strategy(true); @@ -157,7 +157,7 @@ public function definition() { $mform->setType('preventvotenotingroup', PARAM_BOOL); $mform->hideIf('preventvotenotingroup', 'teamvote', 'eq', 0); - $groupings = groups_get_all_groupings($ratingallocate->ratingallocate->dbrecord->course); + $groupings = groups_get_all_groupings($COURSE->id); $options = array(); $options[0] = get_string('none'); foreach ($groupings as $grouping) { diff --git a/styles.css b/styles.css index 9ef50c4a..772b70e2 100644 --- a/styles.css +++ b/styles.css @@ -81,19 +81,22 @@ } .ratingallocate_ratings_table.includegroups tbody td:nth-child(2), -.ratingallocate_ratings_table.includegroups thead th:nth-child(2) { +.ratingallocate_ratings_table.includegroups thead th:nth-child(2), +.ratingallocate_ratings_table.includeteams tbody td:nth-child(2), +.ratingallocate_ratings_table.includeteams thead th:nth-child(2) { position: sticky; left: 10rem; border-right: 2px solid #dee2e6; } -.ratingallocate_ratings_table:not(.includegroups) tbody td:first-child, -.ratingallocate_ratings_table:not(.includegroups) thead th:first-child { +.ratingallocate_ratings_table:not(.includegroups, .includeteams) tbody td:first-child, +.ratingallocate_ratings_table:not(.includegroups, .includeteams) thead th:first-child { border-right: 2px solid #dee2e6; } .ratingallocate_ratings_table thead th:first-child, -.ratingallocate_ratings_table.includegroups thead th:nth-child(2) { +.ratingallocate_ratings_table.includegroups thead th:nth-child(2), +.ratingallocate_ratings_table.includeteams thead th:nth-child(2) { z-index: 3; } From 9768092d4d7bcb21b2acbc1caded55487c29102d Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Mon, 6 May 2024 09:18:39 +0200 Subject: [PATCH 06/15] add groupname to the view for each groupmember at give_rating, check that user to group mapping is unique in the teamvotegrouping --- lang/en/ratingallocate.php | 4 ++ locallib.php | 72 ++++++++++++++-------------------- mod_form.php | 15 +++++++ renderable.php | 1 + renderer.php | 15 ++++++- strategy/strategy_template.php | 9 +++++ 6 files changed, 72 insertions(+), 44 deletions(-) diff --git a/lang/en/ratingallocate.php b/lang/en/ratingallocate.php index 31616865..4d17960e 100644 --- a/lang/en/ratingallocate.php +++ b/lang/en/ratingallocate.php @@ -73,6 +73,7 @@ $string['your_allocated_choice'] = 'Your Allocation'; $string['you_are_not_allocated'] = 'You were not allocated to any choice!'; $string['your_rating'] = 'Your Rating'; +$string['rating_for_team'] = 'Rating for Group {$a}'; $string['edit_rating'] = 'Edit Rating'; $string['delete_rating'] = 'Delete Rating'; $string['results_not_yet_published'] = 'Results have not yet been published.'; @@ -80,6 +81,7 @@ $string['too_few_choices_to_rate'] = 'There are too few choices to rate! Students have to rank at least {$a} choices!'; $string['at_least_one_rateable_choices_needed'] = 'You need at least one rateable choice.'; $string['no_rating_possible'] = 'Currently, there is no rating possible!'; +$string['no_member_of_team'] = 'This Ratingallocate requires voting in groups. You are not a member of any group, so you cannot submit a rating. Please contact your teacher to be added to a group.'; // // $string['allocation_manual_explain_only_raters'] = 'Select a choice to be assigned to a user. @@ -305,9 +307,11 @@ $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 activity will use to find groups for student groups. If not set, the default set of groups will be used.'; +$string['user_with_multiple_groups'] = 'Users have to be uniquely mapped to a group in a Group-Voting grouping. There might be users who are members of multiple groups in the selected grouping. Please modify the groups in the course settings or choose another grouping.'; $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['teams'] = 'Team'; $string['err_required'] = 'You need to provide a value for this field.'; $string['err_minimum'] = 'The minimum value for this field is {$a}.'; diff --git a/locallib.php b/locallib.php index 543d7042..dfcb5fbd 100644 --- a/locallib.php +++ b/locallib.php @@ -325,39 +325,43 @@ private function process_action_give_rating() { if (!$DB->record_exists('ratingallocate_choices', array('ratingallocateid' => $this->ratingallocateid))) { $renderer->add_notification(get_string('no_choice_to_rate', RATINGALLOCATE_MOD_NAME)); } else if ($status === self::DISTRIBUTION_STATUS_RATING_IN_PROGRESS) { - // Rating is possible... - // Adde votegroup name zu form. + if ($this->db->get_field(this_db\ratingallocate::TABLE, 'preventvotenotingroup', ['id' => $this->ratingallocateid]) == 1 + && !$this->get_vote_group($USER->id)) { + $renderer->add_notification(get_string('no_member_of_team', RATINGALLOCATE_MOD_NAME)); + } else { + // Rating is possible... - // Suche das richtige Formular nach Strategie. - $strategyform = 'ratingallocate\\' . $this->ratingallocate->strategy . '\\mod_ratingallocate_view_form'; + // Suche das richtige Formular nach Strategie. + $strategyform = 'ratingallocate\\' . $this->ratingallocate->strategy . '\\mod_ratingallocate_view_form'; - $mform = new $strategyform($PAGE->url->out(), $this); - $mform->add_action_buttons(); + $mform = new $strategyform($PAGE->url->out(), $this); + $mform->add_action_buttons(); - if ($mform->is_cancelled()) { - // Return to view. - redirect("$CFG->wwwroot/mod/ratingallocate/view.php?id=" . $this->coursemodule->id); - return ""; - } else if ($mform->is_submitted() && $mform->is_validated() && $data = $mform->get_data()) { - // Save submitted data and call default page. - $this->save_ratings_to_db($USER->id, $data->data); + if ($mform->is_cancelled()) { + // Return to view. + redirect("$CFG->wwwroot/mod/ratingallocate/view.php?id=" . $this->coursemodule->id); + return ""; + } else if ($mform->is_submitted() && $mform->is_validated() && $data = $mform->get_data()) { + // Save submitted data and call default page. + $this->save_ratings_to_db($USER->id, $data->data); - // Return to view. - redirect( + // Return to view. + redirect( "$CFG->wwwroot/mod/ratingallocate/view.php?id=" . $this->coursemodule->id, get_string('ratings_saved', RATINGALLOCATE_MOD_NAME), null, \core\output\notification::NOTIFY_SUCCESS - ); - } + ); + } - $mform->definition_after_data(); + $mform->definition_after_data(); - $output .= $renderer->render_ratingallocate_strategyform($mform); - // Logging. - $event = \mod_ratingallocate\event\rating_viewed::create_simple( + $output .= $renderer->render_ratingallocate_strategyform($mform); + // Logging. + $event = \mod_ratingallocate\event\rating_viewed::create_simple( context_module::instance($this->coursemodule->id), $this->ratingallocateid); - $event->trigger(); + $event->trigger(); + } } } return $output; @@ -1230,6 +1234,7 @@ public function handle_view() { $choicestatus->showuserinfo = has_capability('mod/ratingallocate:give_rating', $this->context, null, false); $choicestatus->algorithmstarttime = $this->ratingallocate->algorithmstarttime; $choicestatus->algorithmstatus = $this->get_algorithm_status(); + $choicestatus->teamid = $this->get_vote_group($USER->id)->id; $choicestatusoutput = $renderer->render($choicestatus); } else { $choicestatusoutput = ""; @@ -1359,11 +1364,12 @@ public function add_groupid_to_ratings($userid, $groupid) { public function delete_groups_for_usersnogroup($usergroups) { - $sql = 'SELECT id FROM {groups} WHERE id IN ( :groups ) AND idnumber = :ratingallocateid AND name = :name'; + $sql = 'SELECT id FROM {groups} WHERE id IN ( :groups ) AND idnumber = :ratingallocateid AND name = :name AND courseid = :courseid'; $delgroups = $this->db->get_records_sql($sql, [ 'groups' => implode(" , ", array_keys($usergroups)), 'ratingallocateid' => $this->ratingallocateid, - 'name' => 'delete after algorithm run' + 'name' => 'delete after algorithm run', + 'courseid' => $this->ratingallocate->course ]); foreach ($delgroups as $group) { groups_delete_group($group); @@ -1371,24 +1377,6 @@ public function delete_groups_for_usersnogroup($usergroups) { } - /** - * Returns the group in the teamvotegrouping this user is a member of. - * (Should return only one groupid, please only call if teamvote is enabled). - * - * @param $userid - * @return false|mixed The groupid - * @throws dml_exception - */ - public function get_teamvotegroup_for_user($userid) { - - $sql = 'SELECT gm.groupid FROM {groups_members} gm INNER JOIN {groupings_groups} gg ON gm.groupid=gg.groupid - INNER JOIN {ratingallocate} r ON gg.groupingid=r.teamvotegroupingid - WHERE gm.userid = :userid AND r.id = :ratingallocateid'; - $groupid = $this->db->get_record_sql($sql, ['userid' => $userid, 'ratingallocateid' => $this->ratingallocateid]); - return $groupid->groupid; - - } - /** * distribution of choices for each user * take care about max_execution_time and memory_limit diff --git a/mod_form.php b/mod_form.php index b8089f3d..f7c1b1ef 100644 --- a/mod_form.php +++ b/mod_form.php @@ -295,6 +295,7 @@ public function definition_after_data() { * Checks that accesstimestart is before accesstimestop */ public function validation($data, $files) { + global $COURSE; $errors = parent::validation($data, $files); if ($data['accesstimestop'] <= $data['accesstimestart']) { @@ -332,6 +333,20 @@ public function validation($data, $files) { } } } + + // Check if group-user allocation in teamvotegrouping is 1:1. + $usersingrouping = groups_get_grouping_members($data['teamvotegroupingid'], 'u.id'); + $userinmultipleteams = false; + foreach ($usersingrouping as $user) { + if (count(groups_get_user_groups($COURSE->id, $user)[$data['teamvotegroupingid']]) > 1) { + $userinmultipleteams = true; + break; + } + } + if ($userinmultipleteams) { + $errors['teamvotegroupingid'] = get_string('user_with_multiple_groups', self::MOD_NAME); + } + return $errors; } diff --git a/renderable.php b/renderable.php index 96874eb4..c809b673 100644 --- a/renderable.php +++ b/renderable.php @@ -65,4 +65,5 @@ class ratingallocate_choice_status implements renderable { public $showuserinfo; public $algorithmstarttime; public $algorithmstatus; + public $teamid; } diff --git a/renderer.php b/renderer.php index a792df32..4839b7ea 100644 --- a/renderer.php +++ b/renderer.php @@ -64,7 +64,13 @@ public function render_ratingallocate_header(ratingallocate_header $header) { */ public function render_ratingallocate_strategyform($mform) { $o = ''; - $o .= $this->heading(get_string('your_rating', RATINGALLOCATE_MOD_NAME), 2); + + if ($teamid = $mform->get_teamid()) { + $o .= $this->heading(get_string('rating_for_team', RATINGALLOCATE_MOD_NAME, groups_get_group_name($teamid)), 2); + } else { + var_dump($teamid); + $o .= $this->heading(get_string('your_rating', RATINGALLOCATE_MOD_NAME), 2); + } $o .= $this->format_text($mform->get_strategy_description_header() . '
' . $mform->describe_strategy()); $o .= $mform->to_html(); @@ -134,7 +140,12 @@ public function render_ratingallocate_choice_status(ratingallocate_choice_status // Print own choices or full list of available choices. if (!empty($status->ownchoices) && $status->showuserinfo && $accesstimestart < $time) { $row = new html_table_row(); - $cell1 = new html_table_cell(get_string('your_rating', RATINGALLOCATE_MOD_NAME)); + + if ($status->teamid) { + $cell1 = new html_table_cell(get_string('rating_for_team', RATINGALLOCATE_MOD_NAME, groups_get_group_name($status->teamid))); + } else { + $cell1 = new html_table_cell(get_string('your_rating', RATINGALLOCATE_MOD_NAME)); + } $choiceshtml = array(); foreach ($status->ownchoices as $choice) { diff --git a/strategy/strategy_template.php b/strategy/strategy_template.php index 24caa823..cfbad391 100644 --- a/strategy/strategy_template.php +++ b/strategy/strategy_template.php @@ -196,12 +196,16 @@ abstract class ratingallocate_strategyform extends \moodleform { private $strategy; + private $teamid; + /** * * @param string $url The page url * @param \ratingallocate $ratingallocate The calling ratingallocate instance */ public function __construct($url, \ratingallocate $ratingallocate) { + global $USER; + $this->ratingallocate = $ratingallocate; // Load strategy options. $allstrategyoptions = json_decode($this->ratingallocate->ratingallocate->setting, true); @@ -212,6 +216,7 @@ public function __construct($url, \ratingallocate $ratingallocate) { $this->strategyoptions = array(); } $this->strategy = $this->construct_strategy($this->strategyoptions); + $this->teamid = $ratingallocate->get_vote_group($USER->id)->id; parent::__construct($url); } @@ -228,6 +233,10 @@ protected function get_strategy() { return $this->strategy; } + public function get_teamid() { + return $this->teamid; + } + /** * inherited from moodleform: a child class must call parent::definition() first to execute * ratingallocate_strategyform::definition From 43dcf7bedcd5e6e57dbf34d534b167c58b6a3487 Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Thu, 23 May 2024 09:07:19 +0200 Subject: [PATCH 07/15] alter distibution algorithm --- classes/task/cron_task.php | 11 ++++ locallib.php | 27 ++++++++++ solver/edmonds-karp.php | 107 +++++++++++++++++++++++++++++-------- solver/solver-template.php | 10 ++-- 4 files changed, 131 insertions(+), 24 deletions(-) diff --git a/classes/task/cron_task.php b/classes/task/cron_task.php index c91ded43..edd08b20 100644 --- a/classes/task/cron_task.php +++ b/classes/task/cron_task.php @@ -73,7 +73,17 @@ public function execute() { return true; } + // For testing purpsoses + + // Clear eventually scheduled distribution of unallocated users. + $ratingallocate->clear_distribute_unallocated_tasks(); + // Run allocation. + $ratingallocate->distrubute_choices(); + + + // To do wieder alte funktion... // Only start the algorithm, if it should be run by the cron and hasn't been started somehow, yet. + /* if ($ratingallocate->ratingallocate->runalgorithmbycron === "1" && $ratingallocate->get_algorithm_status() === \mod_ratingallocate\algorithm_status::NOTSTARTED) { // Clear eventually scheduled distribution of unallocated users. @@ -81,6 +91,7 @@ public function execute() { // Run allocation. $ratingallocate->distrubute_choices(); } + */ } return true; } diff --git a/locallib.php b/locallib.php index dfcb5fbd..464132fc 100644 --- a/locallib.php +++ b/locallib.php @@ -1284,6 +1284,33 @@ public function get_ratings_for_rateable_choices() { return $fromraters; } + /** + * Returns all ratings for active choices but takes teamvote into consideration + */ + public function get_ratings_for_rateable_choices_with_teamvote() { + $sql = 'SELECT ra.* + FROM {ratingallocate_choices} c + JOIN ( SELECT min(r.id) as id, r.choiceid, r.rating, r.groupid + FROM {ratingallocate_ratings} r + GROUP BY r.choiceid, r.rating, r.groupid ) ra + ON c.id = ra.choiceid + WHERE c.ratingallocateid = :ratingallocateid AND c.active = 1'; + + $ratings = $this->db->get_records_sql($sql, array( + 'ratingallocateid' => $this->ratingallocateid + )); + /* + $raters = $this->get_raters_in_course(); + + // Filter out everyone who can't give ratings. + $fromraters = array_filter($ratings, function($rating) use ($raters) { + return array_key_exists($rating->userid, $raters); + }); + */ + + return $ratings; + } + /** * Returns the groups in the teamvote grouping with the amount of groupmembers * diff --git a/solver/edmonds-karp.php b/solver/edmonds-karp.php index 57c1b093..8fba19ae 100644 --- a/solver/edmonds-karp.php +++ b/solver/edmonds-karp.php @@ -80,7 +80,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team // 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_cspf($source, $sink, $teamvote, $toteamid)) { // If the function returns null, the while will stop. + while ($path = $this->find_shortest_path_bellf_cspf2($source, $sink, $teamvote, $toteamid)) { // 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. @@ -93,7 +93,56 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team /** * Find the shortest path with constraint (enough space for all teammembers in choice). - * This is a modified version of the Yen Algorithm for the consstrained shortest path first problem. + * This is a modified version of the Yen Algorithm for the constrained shortest path first problem. + * + * @param $from + * @param $to + * @param $teamvote + * @param $toteamid + * @return array|mixed|null array of the nodes in the path, null if no path found. + */ + private function find_shortest_path_bellf_cspf2 ($from, $to, $teamvote, $toteamid) { + + // Stop if no shortest path is found. + $i=1; + while ($pathcandidate = $this->find_shortest_path_bellf($from, $to)) { + // var_dump($this->graph); + var_dump($i); + var_dump($pathcandidate); + $foundedge = null; + + foreach ($this->graph[$pathcandidate[1]] as $index => $edge) { + if ($edge->to == $pathcandidate[0]) { + $foundedge = $edge; + $foundedgeid = $index; + break; + } + } + if ($foundedge->space > $teamvote[$toteamid[$pathcandidate[2]]]) { + // We just found the shortest path fulfilling the constraint. + return $pathcandidate; + } else { + // Remove the edge since this path is impossible. + array_splice($this->graph[1], $foundedgeid, 1); + // Add a new edge in the opposite direction whose weight has an opposite sign + // array_push($this->graph[$to], new edge($to, $from, -1 * $edge->weight)); + // according to php doc, this is faster. + // $this->graph[2][] = new edge(2, 1, -1 * $edge->weight); + } + unset($pathcandidate); + $i++; + } + var_dump("nix gefunden"); + return null; + + } + + + + + /** + * Find the shortest path with constraint (enough space for all teammembers in choice). + * This is a modified version of the Yen Algorithm for the constrained shortest path first problem. * * @param $from * @param $to @@ -103,30 +152,38 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team */ private function find_shortest_path_bellf_cspf ($from, $to, $teamvote, $toteamid) { + var_dump("find shortest path cspf"); + var_dump($this->graph); // Find the first shortest path. $pathcandidates = array(); $pathcandidates[0] = $this->find_shortest_path_bellf($from, $to); $nopathfound = is_null($pathcandidates[0]); - // Check if the path fulfills our constraint: space in choice left >= teammembers. - $constraintflag = true; - $foundedge = null; - foreach ($this->graph[$pathcandidates[0][1]] as $edge) { - if ($edge->to == $pathcandidates[0][0]) { - $foundedge = $edge; - break; + + // If the path exists, check if the path fulfills our constraint: space in choice left >= teammembers. + if (!$nopathfound) { + $constraintflag = true; + $foundedge = null; + + $pclenth = count($pathcandidates[0]); + foreach ($this->graph[$pathcandidates[0][1]] as $edge) { + if ($edge->to == $pathcandidates[0][0]) { + $foundedge = $edge; + break; + } + } + if ($foundedge->space <= $teamvote[$toteamid[$pathcandidates[0][2]]]) { + $constraintflag = false; + } + + if ($constraintflag) { + // We just found the shortest path fulfilling the constraint. + return $pathcandidates[0]; } - } - if ($foundedge->space <= $teamvote[$toteamid[$pathcandidates[0][2]]]) { - $constraintflag = false; + $constraintflag = true; } - if ($constraintflag) { - // We just found the shortest path fulfilling the constraint. - return $pathcandidates[0]; - } - $constraintflag = true; // Array of the potential next shortest paths. $nextpaths = array(); @@ -177,6 +234,11 @@ private function find_shortest_path_bellf_cspf ($from, $to, $teamvote, $toteamid // Calculate the spur path from the spur node to the sink. $spurpath = $this->find_shortest_path_bellf($i, $to); + if (is_null($spurpath)) { + var_dump("No spurpath"); + $nopathfound = true; + break; + } // Entire path is made up of the root path and spur path. $totalpath = array_merge($rootpath, $spurpath); @@ -210,13 +272,14 @@ private function find_shortest_path_bellf_cspf ($from, $to, $teamvote, $toteamid var_dump($nextpaths); // Check if the next best path fullfillst our constraint. - foreach ($this->graph[$nextpaths[0][1]] as $edge) { - if ($edge->to == $nextpaths[0][0]) { + $pclenth = count($pathcandidates[0]); + foreach ($this->graph[$pathcandidates[0][1]] as $edge) { + if ($edge->to == $pathcandidates[0][0]) { $foundedge = $edge; break; } } - if ($foundedge->space <= $teamvote[$toteamid[$nextpaths[0][2]]]) { + if ($foundedge->space <= $teamvote[$toteamid[$pathcandidates[0][2]]]) { $constraintflag = false; } @@ -225,6 +288,7 @@ private function find_shortest_path_bellf_cspf ($from, $to, $teamvote, $toteamid return $nextpaths[0]; } + // Not sure if saving all the paths is even necessary... $pathcandidates[$k] = $nextpaths[0]; // Reset flag condition. @@ -234,6 +298,7 @@ private function find_shortest_path_bellf_cspf ($from, $to, $teamvote, $toteamid } $k++; } + var_dump("Path not found"); return null; } @@ -247,7 +312,7 @@ private function get_cost_of_path ($path) { $cost = 0; - for ($i = count($path) - 1; $i > 0; $i--) { + for ($i = count($path)-1; $i > 0; $i--) { $from = $path[$i]; $to = $path[$i - 1]; $edge = null; diff --git a/solver/solver-template.php b/solver/solver-template.php index 82728f24..1a4aa4df 100644 --- a/solver/solver-template.php +++ b/solver/solver-template.php @@ -93,7 +93,11 @@ public function distribute_users(\ratingallocate $ratingallocate) { // Load data from database. $choicerecords = $ratingallocate->get_rateable_choices(); - $ratings = $ratingallocate->get_ratings_for_rateable_choices(); + if ($teamvote) { + $ratings = $ratingallocate->get_ratings_for_rateable_choices_with_teamvote(); + } else { + $ratings = $ratingallocate->get_ratings_for_rateable_choices(); + } // Randomize the order of the entries to prevent advantages for early entry. shuffle($ratings); @@ -116,9 +120,9 @@ public function distribute_users(\ratingallocate $ratingallocate) { $userids = array(); foreach ($distributions as $choiceid => $teamids) { foreach ($teamids as $teamid) { - $userids[$teamid] = groups_get_members($teamid, 'u.id'); + array_merge($userids, groups_get_members($teamid, 'u.id')); } - $userdistributions[$choiceid] = array_merge($userids); + $userdistributions[$choiceid] = $userids; } // We have to delete the provisionally groups containing only one user. From 94b287895df2aad4cadc362c4ada6973c3cfa24b Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Fri, 9 Aug 2024 15:31:47 +0200 Subject: [PATCH 08/15] fix interference with choices_usegroups by listing a coarser list of groups in the form selector --- classes/ratings_and_allocations_table.php | 4 +- form_modify_choice.php | 9 +++- lang/en/ratingallocate.php | 2 + locallib.php | 64 ++++++++++++++++++++++- solver/solver-template.php | 2 +- 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/classes/ratings_and_allocations_table.php b/classes/ratings_and_allocations_table.php index 71e4a780..b34936f6 100644 --- a/classes/ratings_and_allocations_table.php +++ b/classes/ratings_and_allocations_table.php @@ -100,7 +100,7 @@ public function __construct(\mod_ratingallocate_renderer $renderer, $titles, $ra $this->shownames = true; // We only show the group column if at least one group is being used in at least one active restriction setting of a choice. $this->showgroups = !empty($allgroupsofchoices); - $this->showteams = (bool) $this->ratingallocate->get_teamvote_goups(); + $this->showteams = (bool) $this->ratingallocate->get_teamvote_groups(); } /** @@ -347,7 +347,7 @@ private function add_user_ratings_row($user, $userratings, $userallocations) { $row['groups'] = implode(';', $groupnames); } if ($this->showteams) { - $teamofuser = array_filter(array_keys($this->ratingallocate->get_teamvote_goups()), + $teamofuser = array_filter(array_keys($this->ratingallocate->get_teamvote_groups()), function($groupid) use ($user) { return groups_is_member($groupid,$user->id); } diff --git a/form_modify_choice.php b/form_modify_choice.php index 7fbdaf40..a8b695c6 100644 --- a/form_modify_choice.php +++ b/form_modify_choice.php @@ -113,10 +113,17 @@ public function definition() { $mform->addHelpButton($elementname, 'choice_usegroups', RATINGALLOCATE_MOD_NAME); $elementname = 'groupselector'; - $options = $this->ratingallocate->get_group_selections(); + if ($teamvotegroupingid = $this->ratingallocate->get_teamvote_groupingid()) { + $options = $this->ratingallocate->get_group_selections( + $this->ratingallocate->get_coarser_groups_for_grouping($teamvotegroupingid) + ); + } else { + $options = $this->ratingallocate->get_group_selections(); + } $selector = $mform->addelement('searchableselector', $elementname, get_string('choice_groupselect', RATINGALLOCATE_MOD_NAME), $options); $selector->setMultiple(true); + $mform->addHelpButton($elementname, 'choice_groupselect'); $mform->hideIf('groupselector', 'usegroups'); if ($this->choice) { diff --git a/lang/en/ratingallocate.php b/lang/en/ratingallocate.php index 4d17960e..40cc56f7 100644 --- a/lang/en/ratingallocate.php +++ b/lang/en/ratingallocate.php @@ -277,6 +277,8 @@ * Disabling the restriction means that this choice will be available to anyone. * Enabling the restriction without specifying a single group means that this choice will be *not* available for anyone.'; $string['choice_groupselect'] = 'Groups'; +$string['choice_groupselect_help'] = 'If voting in groups is enabled, the selection of groups to choose from has to be a coarser grouping than the grouping of vote in groups, in order to avoid interference. +Please change the groupingid of the group-voting-grouping in the activity settings, if your desired group is not listed.'; $string['edit_choice'] = 'Edit choice'; $string['rating_endtime'] = 'Rating ends at'; $string['rating_begintime'] = 'Rating begins at'; diff --git a/locallib.php b/locallib.php index 464132fc..5613688b 100644 --- a/locallib.php +++ b/locallib.php @@ -1317,7 +1317,7 @@ public function get_ratings_for_rateable_choices_with_teamvote() { * @return array|false Array of the form groupid => membercount if teamvote is enabled, false if not * @throws dml_exception */ - public function get_teamvote_goups() { + public function get_teamvote_groups() { 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]); @@ -1369,6 +1369,20 @@ public function get_teamvote_goups() { return false; } + /** + * Return the teamvote groupingid if teamvote is enabled, false if not. + * + * @return false|mixed false or the groupingid + * @throws dml_exception + */ + public function get_teamvote_groupingid() + { + if ($this->db->get_field(this_db\ratingallocate::TABLE, 'teamvote', ['id' => $this->ratingallocateid]) == 1) { + return $this->db->get_field(this_db\ratingallocate::TABLE, 'teamvotegroupingid', ['id' => $this->ratingallocateid]); + } + return false; + } + /** * Adds the groupid to all rating records with this userid. Should only be used for ratings with groupid 0. * @@ -2391,6 +2405,54 @@ public function update_choice_groups($choiceid, $groupids) { } } + /** + * Returns all groups that are coarser than the groups in the grouping. + * So all groups that only contain groups in the grouping completely or not at all. + * + * @param $groupingid + * @return array An array of groups + */ + public function get_coarser_groups_for_grouping($groupingid) { + + $courseid = $this->course->id; + $allgroups = groups_get_all_groups($courseid); + $groupsingrouping = groups_get_all_groups($courseid, 0, $groupingid); + + $coarsergroups = []; + + // Now iterate over all groups and check. + // If all groups in the grouping are either completely or not contained in the group. + foreach ($allgroups as $outergroup) { + $coarser = true; + foreach ($groupsingrouping as $innergroup) { + $innergroupmembers = groups_get_members($innergroup->id); + + $notcontained = true; + $completelycontained = true; + foreach ($innergroupmembers as $groupmember) { + // Check if innergroup is not at all conatained in outergroup. + if (groups_is_member($outergroup, $groupmember->id)) { + $notcontained = false; + } else { + // Now check if innergroup is completely contained in outergroup + $completelycontained = false; + } + } + // If innergroup is partially contained in outergroup, outergroup cannot be coarser. + if (!($notcontained || $completelycontained)) { + $coarser = false; + } + + } + if ($coarser) { + $coarsergroups[] = $outergroup; + } + } + + return $coarsergroups; + + } + /** * @return bool true, if all strategy settings are ok. */ diff --git a/solver/solver-template.php b/solver/solver-template.php index 1a4aa4df..96989870 100644 --- a/solver/solver-template.php +++ b/solver/solver-template.php @@ -89,7 +89,7 @@ public static function compute_target_function($ratings, $distribution) { */ public function distribute_users(\ratingallocate $ratingallocate) { - $teamvote = $ratingallocate->get_teamvote_goups(); + $teamvote = $ratingallocate->get_teamvote_groups(); // Load data from database. $choicerecords = $ratingallocate->get_rateable_choices(); From ba2786a4afdee5d322d6526c5cbf313be05c7c6d Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Wed, 21 Aug 2024 16:11:33 +0200 Subject: [PATCH 09/15] fix edmonds-karp distribution algorithm for teamvote --- locallib.php | 10 ++++++---- mod_form.php | 2 +- solver/edmonds-karp.php | 34 ++++++++++++++++++-------------- solver/ford-fulkerson-koegel.php | 2 +- solver/solver-template.php | 25 ++++++++++++++++------- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/locallib.php b/locallib.php index 5613688b..a4147ea9 100644 --- a/locallib.php +++ b/locallib.php @@ -1925,12 +1925,14 @@ public function save_ratings_to_db($userid, array $data) { $rating->groupid = $votegroupid; $DB->insert_record('ratingallocate_ratings', $rating); - // If teamvote is enabled, create ratings for all groupmembers. + // If teamvote is enabled, create ratings for all other 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); + if ($member->id != $userid) { + $rating->userid = $member->id; + $DB->insert_record('ratingallocate_ratings', $rating); + } } } @@ -2431,7 +2433,7 @@ public function get_coarser_groups_for_grouping($groupingid) { $completelycontained = true; foreach ($innergroupmembers as $groupmember) { // Check if innergroup is not at all conatained in outergroup. - if (groups_is_member($outergroup, $groupmember->id)) { + if (groups_is_member($outergroup->id, $groupmember->id)) { $notcontained = false; } else { // Now check if innergroup is completely contained in outergroup diff --git a/mod_form.php b/mod_form.php index f7c1b1ef..3f2310e9 100644 --- a/mod_form.php +++ b/mod_form.php @@ -338,7 +338,7 @@ public function validation($data, $files) { $usersingrouping = groups_get_grouping_members($data['teamvotegroupingid'], 'u.id'); $userinmultipleteams = false; foreach ($usersingrouping as $user) { - if (count(groups_get_user_groups($COURSE->id, $user)[$data['teamvotegroupingid']]) > 1) { + if (count(groups_get_user_groups($COURSE->id, $user->id)[$data['teamvotegroupingid']]) > 1) { $userinmultipleteams = true; break; } diff --git a/solver/edmonds-karp.php b/solver/edmonds-karp.php index 8fba19ae..9961b9c1 100644 --- a/solver/edmonds-karp.php +++ b/solver/edmonds-karp.php @@ -50,7 +50,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team $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 @@ -59,7 +59,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team // 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. + // Reverse the augmenting path, thereby distributing a user into a group. $this->augment_flow($path); unset($path); // Clear up old path. } @@ -73,7 +73,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team list($fromteamid, $toteamid, $fromchoiceid, $tochoiceid) = $this->setup_id_conversions_for_teamvote($teamcount, $ratings); - $this->setup_graph_for_teamvote($choicecount, $teamcount, $fromteamid, $fromchoiceid, $ratings, $choicedata, $source, $sink, -1); + $this->setup_graph_for_teamvote($choicecount, $teamcount, $fromteamid, $fromchoiceid, $ratings, $choicedata, $source, $sink, $teamvote, -1); // Now that the datastructure is complete, we can start the algorithm // This is an adaptation of the Ford-Fulkerson algorithm @@ -82,7 +82,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team // Look for an augmenting path (a shortest path from the source to the sink). while ($path = $this->find_shortest_path_bellf_cspf2($source, $sink, $teamvote, $toteamid)) { // If the function returns null, the while will stop. // Reverse the augmentin path, thereby distributing a user into a group. - $this->augment_flow($path); + $this->augment_flow($path, $teamvote, $toteamid); unset($path); // Clear up old path. } return $this->extract_allocation($toteamid, $tochoiceid); @@ -105,10 +105,8 @@ private function find_shortest_path_bellf_cspf2 ($from, $to, $teamvote, $toteami // Stop if no shortest path is found. $i=1; - while ($pathcandidate = $this->find_shortest_path_bellf($from, $to)) { - // var_dump($this->graph); - var_dump($i); - var_dump($pathcandidate); + while (($pathcandidate = $this->find_shortest_path_bellf($from, $to)) && $i<10) { + $foundedge = null; foreach ($this->graph[$pathcandidate[1]] as $index => $edge) { @@ -118,21 +116,27 @@ private function find_shortest_path_bellf_cspf2 ($from, $to, $teamvote, $toteami break; } } - if ($foundedge->space > $teamvote[$toteamid[$pathcandidate[2]]]) { + + if ($foundedge->space >= $teamvote[$toteamid[$pathcandidate[2]]]) { // We just found the shortest path fulfilling the constraint. return $pathcandidate; + } else { + + $foundedgetoremove = null; + foreach ($this->graph[$pathcandidate[2]] as $index => $edge) { + if ($edge->to == $pathcandidate[1]) { + $foundedgetoremove = $edge; + $foundedgeidtoremove = $index; + } + } + // Remove the edge since this path is impossible. - array_splice($this->graph[1], $foundedgeid, 1); - // Add a new edge in the opposite direction whose weight has an opposite sign - // array_push($this->graph[$to], new edge($to, $from, -1 * $edge->weight)); - // according to php doc, this is faster. - // $this->graph[2][] = new edge(2, 1, -1 * $edge->weight); + array_splice($this->graph[$pathcandidate[2]], $foundedgeidtoremove, 1); } unset($pathcandidate); $i++; } - var_dump("nix gefunden"); return null; } diff --git a/solver/ford-fulkerson-koegel.php b/solver/ford-fulkerson-koegel.php index e781dfe0..a9ac5343 100644 --- a/solver/ford-fulkerson-koegel.php +++ b/solver/ford-fulkerson-koegel.php @@ -83,7 +83,7 @@ public function compute_distribution($choicerecords, $ratings, $usercount, $team $sink = $groupcount + $teamcount + 1; 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); + $this->setup_graph_for_teamvote($groupcount, $teamcount, $fromteamid, $fromgroupid, $ratings, $groupdata, $source, $sink, $teamvote); // Now that the datastructure is complete, we can start the algorithm // This is an adaptation of the Ford-Fulkerson algorithm diff --git a/solver/solver-template.php b/solver/solver-template.php index 96989870..d48bfaac 100644 --- a/solver/solver-template.php +++ b/solver/solver-template.php @@ -117,10 +117,15 @@ public function distribute_users(\ratingallocate $ratingallocate) { $userdistributions = $distributions; } else { // Map choiceids to every user of the team it is mapped to. - $userids = array(); + foreach ($distributions as $choiceid => $teamids) { + $userids = array(); foreach ($teamids as $teamid) { - array_merge($userids, groups_get_members($teamid, 'u.id')); + $userids = array_merge($userids, + array_map(function($user) { + return $user->id; + }, groups_get_members($teamid, 'u.id')) + ); } $userdistributions[$choiceid] = $userids; } @@ -143,7 +148,7 @@ public function distribute_users(\ratingallocate $ratingallocate) { * * @param $touserid a map mapping from indexes in the graph to userids (or teamids) * @param $tochoiceid a map mapping from indexes in the graph to choiceids - * @return an array of the form array(groupid => array(userid, ...), ...) + * @return array of the form array(groupid => array(userid, ...), ...) */ protected function extract_allocation($touserid, $tochoiceid) { $distribution = array(); @@ -157,6 +162,7 @@ protected function extract_allocation($touserid, $tochoiceid) { } } } + return $distribution; } @@ -293,9 +299,10 @@ protected function setup_graph($choicecount, $usercount, $fromuserid, $fromchoic * @param type $choicedata * @param type $source * @param type $sink + * @param type $teamvote */ protected function setup_graph_for_teamvote($choicecount, $teamcount, $fromteamid, $fromchoiceid, $ratings, $choicedata, $source, $sink, - $weightmult = 1) { + $teamvote, $weightmult = 1) { // Construct the datastructures for the algorithm // A directed weighted bipartite graph. // A source is connected to all users with unit cost. @@ -324,8 +331,9 @@ protected function setup_graph_for_teamvote($choicecount, $teamcount, $fromteami $team = $fromteamid[$rating->groupid]; $choice = $fromchoiceid[$rating->choiceid]; $weight = $rating->rating; + $membercount = $teamvote[$rating->groupid]; if ($weight > 0) { - $this->graph[$team][] = new edge($team, $choice, $weightmult * $weight); + $this->graph[$team][] = new edge($team, $choice, $weightmult * $weight * $membercount); } } @@ -357,18 +365,21 @@ protected function augment_flow($path, $teamvote=false, $toteamid=null) { break; } } - // The second to last node in a path has to be a choice-node. + // The node before that has to be a teamnode (or usernode), distribute them to this choice. if (!$teamvote) { // If teamvote=false, reduce its space by one, because one user just got distributed into it. $space = 1; } else { // If teamvote is enabled, reduce its space by amount of groupmembers. - $space = $teamvote[$toteamid[$path[$i + 1]]]; + $space = $teamvote[$toteamid[$path[2]]]; } + // The second to last node in a path has to be a choice-node. if ($i == 1 && $edge->space > $space) { + $edge->space = $edge->space - $space; + } else { // Remove the edge. array_splice($this->graph[$from], $foundedgeid, 1); From fb56992b4012829a1aef5f552386776ae55de7fb Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Thu, 19 Sep 2024 14:23:02 +0200 Subject: [PATCH 10/15] modify allocations_table for teamvote --- classes/allocations_table.php | 33 +++++++++++++++++++++++++++++++++ locallib.php | 14 ++++++++++++++ styles.css | 7 +++++++ 3 files changed, 54 insertions(+) diff --git a/classes/allocations_table.php b/classes/allocations_table.php index 6418a037..116ef573 100644 --- a/classes/allocations_table.php +++ b/classes/allocations_table.php @@ -51,6 +51,8 @@ public function __construct($ratingallocate) { $download = optional_param('download', '', PARAM_ALPHA); $this->is_downloading($download, $ratingallocate->ratingallocate->name . '-allocations', 'allocations'); } + // If teamvote is enabled, show allocation of teams. + $this->showteams = (bool) $this->ratingallocate->get_teamvote_groups(); } /** @@ -106,6 +108,12 @@ public function setup_table() { $this->no_sorting('users'); } + if ($this->showteams) { + $columns[] = 'teams'; + $headers[] = get_string('teams', 'mod_ratingallocate'); + $this->no_sorting('teams'); + } + $this->define_columns($columns); $this->define_headers($headers); @@ -142,6 +150,7 @@ public function build_table_by_sql() { $allocations = $this->ratingallocate->get_allocations(); $users = $this->ratingallocate->get_raters_in_course(); + $listedteams = []; foreach ($allocations as $allocation) { $userid = $allocation->userid; @@ -157,6 +166,27 @@ public function build_table_by_sql() { } unset($userwithrating[$userid]); } + if ($this->showteams) { + + $teamids = $this->ratingallocate->get_teamids_for_allocation($allocation->id); + if (array_key_exists($allocation->choiceid, $data)) { + foreach ($teamids as $teamid) { + $teamname = groups_get_group_name($teamid); + if (!in_array($teamname, $listedteams)) { + if (object_property_exists($data[$allocation->choiceid], 'teams')) { + $data[$allocation->choiceid]->teams .= ', '; + } else { + $data[$allocation->choiceid]->teams = ''; + } + + $data[$allocation->choiceid]->teams .= $teamname; + $listedteams[] = $teamname; + } + + } + + } + } } // Enrich data with empty string for choices with no allocation. @@ -164,6 +194,9 @@ public function build_table_by_sql() { if (!property_exists($row, 'users')) { $row->users = ''; } + if ($this->showteams && !property_exists($row, 'teams')) { + $row->teams = ''; + } } // If there are users, which rated but were not allocated, add them to a special row. diff --git a/locallib.php b/locallib.php index a4147ea9..2aa2a822 100644 --- a/locallib.php +++ b/locallib.php @@ -1635,6 +1635,20 @@ public function get_allocations() { return $records; } + public function get_teamids_for_allocation($allocationid) { + $sql = 'SELECT r.groupid + FROM {ratingallocate_allocations} al + LEFT JOIN {ratingallocate_choices} c ON al.choiceid = c.id + LEFT JOIN {ratingallocate_ratings} r + ON al.userid = r.userid AND al.choiceid = r.choiceid + WHERE al.ratingallocateid = :ratingallocateid AND c.active = 1 AND al.id = :allocationid'; + $teamids = $this->db->get_fieldset_sql($sql, [ + 'ratingallocateid' => $this->ratingallocateid, + 'allocationid' => $allocationid, + ]); + return $teamids; + } + /** * Removes all allocations for choices in $ratingallocateid */ diff --git a/styles.css b/styles.css index 772b70e2..16438ea1 100644 --- a/styles.css +++ b/styles.css @@ -124,6 +124,13 @@ font-weight: bold; } +.ratingallocate_ratings_table tbody td:first-child, +.ratingallocate_ratings_table.includeteams tbody td:nth-child(2) { + background-color: #fff; + z-index: 1; + font-weight: bold; +} + .ratingallocate_ratings_table_container .no-overflow { overflow: unset; } From fa73140b1a000833635fd21e094b131e6db8bd1e Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Tue, 24 Sep 2024 14:50:48 +0200 Subject: [PATCH 11/15] modify ratings_and_allocations_table.php by only showing rows for each team if teamvote is enabled --- classes/ratings_and_allocations_table.php | 264 ++++++++++++++++++---- locallib.php | 23 +- 2 files changed, 248 insertions(+), 39 deletions(-) diff --git a/classes/ratings_and_allocations_table.php b/classes/ratings_and_allocations_table.php index b34936f6..4e6b950a 100644 --- a/classes/ratings_and_allocations_table.php +++ b/classes/ratings_and_allocations_table.php @@ -100,7 +100,10 @@ public function __construct(\mod_ratingallocate_renderer $renderer, $titles, $ra $this->shownames = true; // We only show the group column if at least one group is being used in at least one active restriction setting of a choice. $this->showgroups = !empty($allgroupsofchoices); - $this->showteams = (bool) $this->ratingallocate->get_teamvote_groups(); + $teamvote = $this->ratingallocate->get_teamvote_groups(); + $this->ratingallocate->delete_groups_for_usersnogroup($teamvote); + $this->showteams = (bool) $teamvote; + } /** @@ -172,9 +175,20 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary $columns[] = 'email'; $headers[] = get_string('email'); } + if ($this->showteams) { + $columns[] = 'teams'; + $headers[] = get_string('teams', 'mod_ratingallocate'); + } } else { - $columns[] = 'fullname'; - $headers[] = get_string('ratings_table_user', RATINGALLOCATE_MOD_NAME); + if ($this->showteams) { + $columns[] = 'teams'; + $headers[] = get_string('teams', 'mod_ratingallocate'); + $columns[] = 'teammembers'; + $headers[] = get_string('allocations_table_users', RATINGALLOCATE_MOD_NAME); + } else { + $columns[] = 'fullname'; + $headers[] = get_string('allocations_table_users', RATINGALLOCATE_MOD_NAME); + } } // We only want to add a group column, if at least one choice has an active group restriction. if ($this->showgroups) { @@ -187,10 +201,7 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary $this->ratingallocate->get_choice_groups($choice->id)); } } - if ($this->showteams) { - $columns[] = 'teams'; - $headers[] = get_string('teams', 'mod_ratingallocate'); - } + } // Setup filter. @@ -257,35 +268,102 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary */ public function build_table_by_sql($ratings, $allocations, $writeable = false) { + global $COURSE; $this->writeable = $writeable; $users = $this->rawdata; - // Group all ratings per user to match table structure. - $ratingsbyuser = array(); - foreach ($ratings as $rating) { - if (empty($ratingsbyuser[$rating->userid])) { - $ratingsbyuser[$rating->userid] = array(); + if ($this->showteams) { + // Group all ratings per team to match table structure. + $ratingsbyteam = []; + foreach ($ratings as $rating) { + if (empty($ratingsbyteam[$rating->groupid])) { + $ratingsbyteam[$rating->groupid] = []; + } + $ratingsbyteam[$rating->groupid][$rating->choiceid] = $rating->rating; } - $ratingsbyuser[$rating->userid][$rating->choiceid] = $rating->rating; - } + // Group all memberships per team per choice. + $allocationsbyteams = []; + foreach ($allocations as $allocation) { + foreach ($this->ratingallocate->get_teamids_for_allocation($allocation->id) as $teamid) { + if (empty($allocationsbyteams[$teamid])) { + $allocationsbyteams[$teamid] = []; + } + $allocationsbyteams[$teamid][$allocation->choiceid] = true; + } + } + // Add rating rows for each team. + $teamvotegrouping = $this->ratingallocate->get_teamvote_groupingid(); + $teams = groups_get_all_groups($COURSE->id, 0, $teamvotegrouping); + + foreach ($teams as $team) { + $teamratings = isset($ratingsbyteam[$team->id]) ? $ratingsbyteam[$team->id] : []; + $teamallocations = isset($allocationsbyteams[$team->id]) ? $allocationsbyteams[$team->id] : []; + $this->add_team_ratings_row($team, $teamratings, $teamallocations); + } + + // We need to add seperate rows for users without team, if preventvotenotingroup is disabled. + if (!$preventvotenotingroup = $this->ratingallocate->get_preventvotenotingroup()) { + // There are users that voted, but are not in a team. + // Get all users not in a group of the teamvote grouping. + $usersnogroup = []; + foreach ($this->ratingallocate->get_raters_in_course() as $rater) { + if (!in_array($rater, groups_get_grouping_members($teamvotegrouping))) { + $usersnogroup[] = $rater; + } + } + $ratingsbyuser = []; + foreach ($ratings as $rating) { + if (empty($ratingsbyuser[$rating->userid])) { + $ratingsbyuser[$rating->userid] = []; + } + $ratingsbyuser[$rating->userid][$rating->choiceid] = $rating->rating; + } + // Group all memberships per user per choice. + $allocationsbyuser = []; + foreach ($allocations as $allocation) { + if (empty($allocationsbyuser[$allocation->userid])) { + $allocationsbyuser[$allocation->userid] = []; + } + $allocationsbyuser[$allocation->userid][$allocation->choiceid] = true; + } + + // Add rating rows for each user. + foreach ($usersnogroup as $user) { + $userratings = isset($ratingsbyuser[$user->id]) ? $ratingsbyuser[$user->id] : []; + $userallocations = isset($allocationsbyuser[$user->id]) ? $allocationsbyuser[$user->id] : array(); + $this->add_user_ratings_row_without_team($user, $userratings, $userallocations); + } - // Group all memberships per user per choice. - $allocationsbyuser = array(); - foreach ($allocations as $allocation) { - if (empty($allocationsbyuser[$allocation->userid])) { - $allocationsbyuser[$allocation->userid] = array(); } - $allocationsbyuser[$allocation->userid][$allocation->choiceid] = true; - } + } else { + // Group all ratings per user to match table structure. + $ratingsbyuser = array(); + foreach ($ratings as $rating) { + if (empty($ratingsbyuser[$rating->userid])) { + $ratingsbyuser[$rating->userid] = array(); + } + $ratingsbyuser[$rating->userid][$rating->choiceid] = $rating->rating; + } + // Group all memberships per user per choice. + $allocationsbyuser = array(); + foreach ($allocations as $allocation) { + if (empty($allocationsbyuser[$allocation->userid])) { + $allocationsbyuser[$allocation->userid] = array(); + } + $allocationsbyuser[$allocation->userid][$allocation->choiceid] = true; + } + + // Add rating rows for each user. + foreach ($users as $user) { + $userratings = isset($ratingsbyuser[$user->id]) ? $ratingsbyuser[$user->id] : array(); + $userallocations = isset($allocationsbyuser[$user->id]) ? $allocationsbyuser[$user->id] : array(); + $this->add_user_ratings_row($user, $userratings, $userallocations); + } - // Add rating rows for each user. - foreach ($users as $user) { - $userratings = isset($ratingsbyuser[$user->id]) ? $ratingsbyuser[$user->id] : array(); - $userallocations = isset($allocationsbyuser[$user->id]) ? $allocationsbyuser[$user->id] : array(); - $this->add_user_ratings_row($user, $userratings, $userallocations); } + if (!$this->is_downloading()) { $this->add_summary_row(); $this->print_hidden_user_fields($users); @@ -346,18 +424,6 @@ private function add_user_ratings_row($user, $userratings, $userallocations) { }, $groupsofuser); $row['groups'] = implode(';', $groupnames); } - if ($this->showteams) { - $teamofuser = array_filter(array_keys($this->ratingallocate->get_teamvote_groups()), - function($groupid) use ($user) { - return groups_is_member($groupid,$user->id); - } - ); - $teamname = array_map(function ($team) { - return groups_get_group($team, 'name')->name; - }, $teamofuser); - // We should only have one team for each user, but we cant ensure that at this point. - $row['teams'] = implode(';', $teamname); - } } foreach ($userratings as $choiceid => $userrating) { @@ -391,6 +457,128 @@ function($groupid) use ($user) { $this->add_data_keyed($this->format_row($row)); } + /** + * Adds one row for each team + * + * @param $team object of the group for which a row should be added. + * @param $teamratings array consisting of pairs of choiceid to rating for the team. + * @param $teamallocations array constisting of pairs of choiceid and allocation of the team. + */ + private function add_team_ratings_row($team, array $teamratings, array $teamallocations) { + + $row = convert_to_array($team); + + if ($this->shownames) { + $row['teams'] = groups_get_group_name($team->id); + + // Add names of the teammembers. + $teammembers = groups_get_members($team->id); + $namesofteammembers = implode(", ", + array_map(function($member) { + return $member->firstname . " " . $member->lastname; + }, $teammembers) + ); + $row['teammembers'] = $namesofteammembers; + + // We only can add groups if at least one choice has an active group restriction. + if ($this->showgroups) { + // List groups, that all teammembers are in. + $groupsofteam = array_filter($this->groupsofallchoices, function($group) use ($teammembers) { + foreach ($teammembers as $member) { + if (!groups_is_member($group->id, $member->id)) { + return false; + } + } + return true; + }); + $groupnames = array_map(function($group) { + return $group->name; + }, $groupsofteam); + $row['groups'] = implode(';', $groupnames); + } + } + + foreach ($teamratings as $choiceid => $teamrating) { + $row[self::CHOICE_COL . $choiceid] = array( + 'rating' => $teamrating, + 'hasallocation' => false // May be overridden later. + ); + } + + // Process allocations separately, since assignment can exist for choices that have not been rated. + // $teamallocations *currently* has 0..1 elements, so this loop is rather fast. + foreach ($teamallocations as $choiceid => $teamallocation) { + if (!$teamallocation) { + // Presumably, $userallocation is always true. But maybe that assumption is wrong someday? + continue; + } + + $rowkey = self::CHOICE_COL . $choiceid; + if (!isset($row[$rowkey])) { + // Team has not rated this choice, but it was assigned to it. + $row[$rowkey] = array( + 'rating' => null, + 'hasallocation' => true + ); + } else { + // Team has rated this choice. + $row[$rowkey]['hasallocation'] = true; + } + } + + $this->add_data_keyed($this->format_row($row)); + } + + private function add_user_ratings_row_without_team($user, $userratings, $userallocations) { + + $row = convert_to_array($user); + + if ($this->shownames) { + $row['teams'] = ''; + $row['teammembers'] = $user->firstname . ' ' . $user->lastname; + // We only can add groups if at least one choice has an active group restriction. + if ($this->showgroups) { + $groupsofuser = array_filter($this->groupsofallchoices, function($group) use ($user) { + return groups_is_member($group->id, $user->id); + }); + $groupnames = array_map(function($group) { + return $group->name; + }, $groupsofuser); + $row['groups'] = implode(';', $groupnames); + } + } + + foreach ($userratings as $choiceid => $userrating) { + $row[self::CHOICE_COL . $choiceid] = array( + 'rating' => $userrating, + 'hasallocation' => false // May be overridden later. + ); + } + + // Process allocations separately, since assignment can exist for choices that have not been rated. + // $userallocations *currently* has 0..1 elements, so this loop is rather fast. + foreach ($userallocations as $choiceid => $userallocation) { + if (!$userallocation) { + // Presumably, $userallocation is always true. But maybe that assumption is wrong someday? + continue; + } + + $rowkey = self::CHOICE_COL . $choiceid; + if (!isset($row[$rowkey])) { + // User has not rated this choice, but it was assigned to him/her. + $row[$rowkey] = array( + 'rating' => null, + 'hasallocation' => true + ); + } else { + // User has rated this choice. + $row[$rowkey]['hasallocation'] = true; + } + } + + $this->add_data_keyed($this->format_row($row)); + } + /** * Will be called by build_table when processing the summary row */ diff --git a/locallib.php b/locallib.php index 2aa2a822..13a19709 100644 --- a/locallib.php +++ b/locallib.php @@ -1234,7 +1234,11 @@ public function handle_view() { $choicestatus->showuserinfo = has_capability('mod/ratingallocate:give_rating', $this->context, null, false); $choicestatus->algorithmstarttime = $this->ratingallocate->algorithmstarttime; $choicestatus->algorithmstatus = $this->get_algorithm_status(); - $choicestatus->teamid = $this->get_vote_group($USER->id)->id; + if ($this->get_vote_group($USER->id)) { + $choicestatus->teamid = $this->get_vote_group($USER->id)->id; + } else { + $choicestatus->teamid = false; + } $choicestatusoutput = $renderer->render($choicestatus); } else { $choicestatusoutput = ""; @@ -1369,6 +1373,16 @@ public function get_teamvote_groups() { return false; } + /** + * Returns wether preventing users not in a group of the teamvote grouping from voting is enabled. + * + * @return bool preventvotenotingroup + * @throws dml_exception + */ + public function get_preventvotenotingroup() { + return $this->db->get_field(this_db\ratingallocate::TABLE, 'preventvotenotingroup', ['id' => $this->ratingallocateid]) == 1; + } + /** * Return the teamvote groupingid if teamvote is enabled, false if not. * @@ -1635,6 +1649,13 @@ public function get_allocations() { return $records; } + /** + * Returns the teamids related to this allocation + * + * @param $allocationid + * @return array of groupids + * @throws dml_exception + */ public function get_teamids_for_allocation($allocationid) { $sql = 'SELECT r.groupid FROM {ratingallocate_allocations} al From 9ce721479898a10cfb35ded07ea6adb55e43852d Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Tue, 1 Oct 2024 11:00:08 +0200 Subject: [PATCH 12/15] fix display of teams and student without teams in tables --- classes/allocations_table.php | 2 +- classes/ratings_and_allocations_table.php | 4 +--- locallib.php | 6 +++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/classes/allocations_table.php b/classes/allocations_table.php index 116ef573..efc3cfad 100644 --- a/classes/allocations_table.php +++ b/classes/allocations_table.php @@ -52,7 +52,7 @@ public function __construct($ratingallocate) { $this->is_downloading($download, $ratingallocate->ratingallocate->name . '-allocations', 'allocations'); } // If teamvote is enabled, show allocation of teams. - $this->showteams = (bool) $this->ratingallocate->get_teamvote_groups(); + $this->showteams = (bool) $this->ratingallocate->get_teamvote_groupingid(); } /** diff --git a/classes/ratings_and_allocations_table.php b/classes/ratings_and_allocations_table.php index 4e6b950a..591119f9 100644 --- a/classes/ratings_and_allocations_table.php +++ b/classes/ratings_and_allocations_table.php @@ -100,9 +100,7 @@ public function __construct(\mod_ratingallocate_renderer $renderer, $titles, $ra $this->shownames = true; // We only show the group column if at least one group is being used in at least one active restriction setting of a choice. $this->showgroups = !empty($allgroupsofchoices); - $teamvote = $this->ratingallocate->get_teamvote_groups(); - $this->ratingallocate->delete_groups_for_usersnogroup($teamvote); - $this->showteams = (bool) $teamvote; + $this->showteams = (bool) $this->ratingallocate->get_teamvote_groupingid(); } diff --git a/locallib.php b/locallib.php index 13a19709..e0173501 100644 --- a/locallib.php +++ b/locallib.php @@ -1316,7 +1316,9 @@ public function get_ratings_for_rateable_choices_with_teamvote() { } /** - * Returns the groups in the teamvote grouping with the amount of groupmembers + * Returns the groups in the teamvote grouping with the amount of groupmembers. + * Since this function creates unnecessary groups, only use in advance of running the endmonds karp algorithm. + * In order to check wether teamvote is enabled, please use get_teamvote_groupingid(). * * @return array|false Array of the form groupid => membercount if teamvote is enabled, false if not * @throws dml_exception @@ -1337,6 +1339,7 @@ public function get_teamvote_groups() { } } + // This is a little ugly, but the only way the algortihm can run including teams. $groupdata = new stdClass(); $groupdata->courseid = $this->course->id; $groupdata->idnumber = $this->ratingallocateid; @@ -1385,6 +1388,7 @@ public function get_preventvotenotingroup() { /** * Return the teamvote groupingid if teamvote is enabled, false if not. + * Use this method to check wether teamvote is enabled. * * @return false|mixed false or the groupingid * @throws dml_exception From 59bf4ead1de16e3fe4b3bf59e17b6bb77e054f90 Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Tue, 19 Nov 2024 14:43:09 +0100 Subject: [PATCH 13/15] annotate new distribution algorithm --- classes/ratings_and_allocations_table.php | 15 --------------- classes/task/cron_task.php | 5 +---- locallib.php | 3 +++ solver/edmonds-karp.php | 2 -- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/classes/ratings_and_allocations_table.php b/classes/ratings_and_allocations_table.php index 591119f9..6817fd9c 100644 --- a/classes/ratings_and_allocations_table.php +++ b/classes/ratings_and_allocations_table.php @@ -899,14 +899,6 @@ public function init_sql() { $sortfields = $this->get_sort_columns(); - - // To do vardumps entfernen. - var_dump($sortfields); - var_dump("
sortdata: "); - var_dump($this->sortdata); - var_dump("
sortorder: "); - var_dump($this->get_sort_order()); - // If we have teamvote enabled, always order by team first, in order to always show users in their teams. if ($this->showteams) { @@ -929,13 +921,6 @@ public function init_sql() { } $sortfields = $this->get_sort_columns(); - var_dump("
sortdata nach preferences: "); - var_dump($this->sortdata); - var_dump("
sortcolumns nach preferences: "); - var_dump($this->get_sort_columns()); - var_dump("
sortorder nach preferences: "); - var_dump($this->get_sort_order()); - $fields = "u.*"; if ($userids) { diff --git a/classes/task/cron_task.php b/classes/task/cron_task.php index edd08b20..266d9b28 100644 --- a/classes/task/cron_task.php +++ b/classes/task/cron_task.php @@ -81,9 +81,6 @@ public function execute() { $ratingallocate->distrubute_choices(); - // To do wieder alte funktion... - // Only start the algorithm, if it should be run by the cron and hasn't been started somehow, yet. - /* if ($ratingallocate->ratingallocate->runalgorithmbycron === "1" && $ratingallocate->get_algorithm_status() === \mod_ratingallocate\algorithm_status::NOTSTARTED) { // Clear eventually scheduled distribution of unallocated users. @@ -91,7 +88,7 @@ public function execute() { // Run allocation. $ratingallocate->distrubute_choices(); } - */ + } return true; } diff --git a/locallib.php b/locallib.php index e0173501..d03083e7 100644 --- a/locallib.php +++ b/locallib.php @@ -1448,6 +1448,9 @@ public function distrubute_choices() { $this->origdbrecord->algorithmstarttime = time(); $this->db->update_record(this_db\ratingallocate::TABLE, $this->origdbrecord); + + // Since edmonds-karp algrothm is always used, we did not implement a teamvote-distribution algorithm. + // For ford-fulkerson-koegel. Keep this in mind if the distributor is changed in the future. $distributor = new solver_edmonds_karp(); $timestart = microtime(true); $distributor->distribute_users($this); diff --git a/solver/edmonds-karp.php b/solver/edmonds-karp.php index 9961b9c1..b3d1747c 100644 --- a/solver/edmonds-karp.php +++ b/solver/edmonds-karp.php @@ -142,8 +142,6 @@ private function find_shortest_path_bellf_cspf2 ($from, $to, $teamvote, $toteami } - - /** * Find the shortest path with constraint (enough space for all teammembers in choice). * This is a modified version of the Yen Algorithm for the constrained shortest path first problem. From 43ca20258a43c7850faf3d5fe81f3716cd5edfc3 Mon Sep 17 00:00:00 2001 From: Irina Hoppe Date: Tue, 26 Nov 2024 15:49:00 +0100 Subject: [PATCH 14/15] behat test for user view of teamrating --- tests/behat/vote_in_teams.feature | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/behat/vote_in_teams.feature diff --git a/tests/behat/vote_in_teams.feature b/tests/behat/vote_in_teams.feature new file mode 100644 index 00000000..e2915039 --- /dev/null +++ b/tests/behat/vote_in_teams.feature @@ -0,0 +1,63 @@ +@mod @mod_ratingallocate @javascript +Feature: When students rate in groups every teammember should see the rating. + + Background: + Given the following "courses" exist: + | fullname | shortname | category | groupmode | + | Course 1 | C1 | 0 | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G1 | + And the following "groupings" exist: + | name | course | idnumber | + | Grouping 1 | C1 | GG1 | + And the following "grouping groups" exist: + | grouping | group | + | GG1 | G1 | + And the following "activities" exist: + | activity | course | idnumber | name | teamvote | preventvotenotingroup | teamvotegroupingid | + | ratingallocate | C1 | ra1 | My Fair Allocation | 1 | 1 | GG1 | + And I log in as "teacher1" + And I am on the "My Fair Allocation" "ratingallocate activity" page + And I press "Edit Choices" + And I add a new choice with the values: + | title | My first choice | + | Description (optional) | Test 1 | + | maxsize | 2 | + And I add a new choice with the values: + | title | My second choice | + | Description (optional) | Test 2 | + | maxsize | 2 | + And I log out + + @javascript + Scenario: Ratings are saved for each teammember. + When I log in as "student1" + And I am on the "My Fair Allocation" "ratingallocate activity" page + And I press "Edit Rating" + And I press "Save changes" + Then the user "student1" should have ratings + And the user "student2" should have ratings + + @javascript + Scenario: Users without group cannot create rating. + When I log in as "student3" + And I am on the "My Fair Allocation" "ratingallocate activity" page + Then I should see "This Ratingallocate requires voting in groups. You are not a member of any group, so you cannot submit a rating. Please contact your teacher to be added to a group." + And I should not see "Edit Rating" \ No newline at end of file From 5577292b3adc48108b9a7bb9d42dd7bb1bec0f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mei=C3=9Fner?= Date: Thu, 13 Feb 2025 12:15:32 +0100 Subject: [PATCH 15/15] Fix failing Gherkin lint check --- tests/behat/vote_in_teams.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/behat/vote_in_teams.feature b/tests/behat/vote_in_teams.feature index e2915039..17935d18 100644 --- a/tests/behat/vote_in_teams.feature +++ b/tests/behat/vote_in_teams.feature @@ -60,4 +60,4 @@ Feature: When students rate in groups every teammember should see the rating. When I log in as "student3" And I am on the "My Fair Allocation" "ratingallocate activity" page Then I should see "This Ratingallocate requires voting in groups. You are not a member of any group, so you cannot submit a rating. Please contact your teacher to be added to a group." - And I should not see "Edit Rating" \ No newline at end of file + And I should not see "Edit Rating"