diff --git a/classes/allocations_table.php b/classes/allocations_table.php
index 6418a037..efc3cfad 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_groupingid();
}
/**
@@ -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/classes/ratings_and_allocations_table.php b/classes/ratings_and_allocations_table.php
index 48e386b4..6817fd9c 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,8 @@ 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_groupingid();
+
}
/**
@@ -166,9 +173,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) {
@@ -181,6 +199,7 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary
$this->ratingallocate->get_choice_groups($choice->id));
}
}
+
}
// Setup filter.
@@ -211,12 +230,21 @@ public function setup_table($choices, $hidenorating = null, $showallocnecessary
$this->define_headers($headers);
// Set additional table settings.
- $this->sortable(true, 'lastname');
+ 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) {
+ $tableclasses .= ' includeteams';
+ }
+
$this->set_attribute('class', $tableclasses);
$this->initialbars(true);
@@ -238,35 +266,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;
+ }
+ // 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);
}
- $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();
+ // 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);
+ }
+
+ }
+ } 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);
}
- $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);
}
+
if (!$this->is_downloading()) {
$this->add_summary_row();
$this->print_hidden_user_fields($users);
@@ -360,6 +455,128 @@ private function add_user_ratings_row($user, $userratings, $userallocations) {
$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
*/
@@ -373,6 +590,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) {
@@ -662,6 +883,10 @@ function($c) {
}
+ private function sort_by_teams ($teams) {
+
+ }
+
/**
* Sets up the sql statement for querying the table data.
*/
@@ -673,6 +898,30 @@ public function init_sql() {
$userids = $this->filter_userids($userids);
$sortfields = $this->get_sort_columns();
+
+ // 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();
+
+
$fields = "u.*";
if ($userids) {
$where = "u.id in (" . implode(",", $userids) . ")";
@@ -685,11 +934,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") {
+ $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/classes/task/cron_task.php b/classes/task/cron_task.php
index c91ded43..266d9b28 100644
--- a/classes/task/cron_task.php
+++ b/classes/task/cron_task.php
@@ -73,7 +73,14 @@ public function execute() {
return true;
}
- // Only start the algorithm, if it should be run by the cron and hasn't been started somehow, yet.
+ // For testing purpsoses
+
+ // Clear eventually scheduled distribution of unallocated users.
+ $ratingallocate->clear_distribute_unallocated_tasks();
+ // Run allocation.
+ $ratingallocate->distrubute_choices();
+
+
if ($ratingallocate->ratingallocate->runalgorithmbycron === "1" &&
$ratingallocate->get_algorithm_status() === \mod_ratingallocate\algorithm_status::NOTSTARTED) {
// Clear eventually scheduled distribution of unallocated users.
@@ -81,6 +88,7 @@ public function execute() {
// Run allocation.
$ratingallocate->distrubute_choices();
}
+
}
return true;
}
diff --git a/db/install.xml b/db/install.xml
index 5433f236..77309db7 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -23,12 +23,16 @@
+
+
+
+
@@ -52,6 +56,7 @@
+
diff --git a/db/upgrade.php b/db/upgrade.php
index e111a143..40a3c8ba 100644
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -218,5 +218,39 @@ function xmldb_ratingallocate_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2023050900, 'ratingallocate');
}
+ if ($oldversion < 2024030100) {
+
+ // Define fields teamvote and teamvotegroupingid to be added to ratingallocate.
+ $table = new xmldb_table('ratingallocate');
+ $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, $fieldteamvote)) {
+ $dbman->add_field($table, $fieldteamvote);
+ }
+ if (!$dbman->field_exists($table, $fieldteamvotegroupingid)) {
+ $dbman->add_field($table, $fieldteamvotegroupingid);
+ }
+ if (!$dbman->field_exists($table, $fieldpreventvotenotingroup)) {
+ $dbman->add_field($table, $fieldpreventvotenotingroup);
+ }
+
+ // 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, 2024030100, 'ratingallocate');
+ }
+
return true;
}
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 325b03e9..40cc56f7 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.
@@ -275,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';
@@ -300,6 +304,17 @@
$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 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}.';
$string['err_maximum'] = 'The maximum value for this field is {$a}.';
diff --git a/locallib.php b/locallib.php
index 615d0e70..d03083e7 100644
--- a/locallib.php
+++ b/locallib.php
@@ -325,37 +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...
- // Suche das richtige Formular nach Strategie.
- $strategyform = 'ratingallocate\\' . $this->ratingallocate->strategy . '\\mod_ratingallocate_view_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...
- $mform = new $strategyform($PAGE->url->out(), $this);
- $mform->add_action_buttons();
+ // Suche das richtige Formular nach Strategie.
+ $strategyform = 'ratingallocate\\' . $this->ratingallocate->strategy . '\\mod_ratingallocate_view_form';
- 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);
+ $mform = new $strategyform($PAGE->url->out(), $this);
+ $mform->add_action_buttons();
- // Return to view.
- redirect(
+ 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(
"$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;
@@ -1228,6 +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();
+ 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 = "";
@@ -1277,6 +1288,154 @@ 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.
+ * 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
+ */
+ 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]);
+
+ // 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();
+ foreach ($this->get_raters_in_course() as $rater) {
+ if (!in_array($rater, groups_get_grouping_members($groupingid))) {
+ $usersnogroup[] = $rater;
+ }
+ }
+
+ // 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;
+ $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';
+
+ // 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;
+ }
+
+ 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.
+ * Use this method to check wether teamvote is enabled.
+ *
+ * @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.
+ *
+ * @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 AND courseid = :courseid';
+ $delgroups = $this->db->get_records_sql($sql, [
+ 'groups' => implode(" , ", array_keys($usergroups)),
+ 'ratingallocateid' => $this->ratingallocateid,
+ 'name' => 'delete after algorithm run',
+ 'courseid' => $this->ratingallocate->course
+ ]);
+ foreach ($delgroups as $group) {
+ groups_delete_group($group);
+ }
+
+ }
+
/**
* distribution of choices for each user
* take care about max_execution_time and memory_limit
@@ -1289,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);
@@ -1494,6 +1656,27 @@ 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
+ 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
*/
@@ -1605,17 +1788,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
));
+
}
/**
@@ -1668,7 +1854,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) {
@@ -1680,14 +1866,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();
@@ -1711,23 +1916,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));
@@ -1738,8 +1964,20 @@ 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 other groupmembers.
+ if ($teamvote && $votegroup) {
+ $teammembers = groups_get_members($votegroupid, 'u.id');
+ foreach ($teammembers as $member) {
+ if ($member->id != $userid) {
+ $rating->userid = $member->id;
+ $DB->insert_record('ratingallocate_ratings', $rating);
+ }
+ }
+ }
+
// Logging.
array_push($loggingdata,
array('choiceid' => $rating->choiceid, 'rating' => $rating->rating));
@@ -1759,6 +1997,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
*/
@@ -2189,6 +2449,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->id, $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/mod_form.php b/mod_form.php
index 61f3099e..3f2310e9 100644
--- a/mod_form.php
+++ b/mod_form.php
@@ -63,10 +63,12 @@ public function __construct($current, $section, $cm, $course) {
* Defines forms elements
*/
public function definition() {
- global $CFG, $PAGE;
+ global $CFG, $PAGE, $COURSE;
$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($COURSE->id);
+ $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('teamvotegroupingid');
+ }
+
// 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) {
@@ -251,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']) {
@@ -270,6 +315,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'])) {
@@ -285,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->id)[$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/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/edmonds-karp.php b/solver/edmonds-karp.php
index 9fdf8846..b3d1747c 100644
--- a/solver/edmonds-karp.php
+++ b/solver/edmonds-karp.php
@@ -34,42 +34,314 @@ 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 augmenting 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 {
+
+ var_dump("Teamvote = true");
+ $teamcount = count($teamvote);
+ $sink = $choicecount + $teamcount + 1;
+
+ 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, $teamvote, -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_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, $teamvote, $toteamid);
+ unset($path); // Clear up old path.
+ }
+ return $this->extract_allocation($toteamid, $tochoiceid);
+
+ }
+
+ }
+
+ /**
+ * 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
+ * @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)) && $i<10) {
+
+ $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 {
- $this->setup_graph($choicecount, $usercount, $fromuserid, $fromchoiceid, $ratings, $choicedata, $source, $sink, -1);
+ $foundedgetoremove = null;
+ foreach ($this->graph[$pathcandidate[2]] as $index => $edge) {
+ if ($edge->to == $pathcandidate[1]) {
+ $foundedgetoremove = $edge;
+ $foundedgeidtoremove = $index;
+ }
+ }
- // 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.
+ // Remove the edge since this path is impossible.
+ array_splice($this->graph[$pathcandidate[2]], $foundedgeidtoremove, 1);
+ }
+ unset($pathcandidate);
+ $i++;
}
- return $this->extract_allocation($touserid, $tochoiceid);
+ 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
+ * @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) {
+
+ 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]);
+
+
+ // 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];
+ }
+ $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);
+ 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);
+
+ // 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.
+ $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) {
+ var_dump("Path found");
+ return $nextpaths[0];
+ }
+
+ // Not sure if saving all the paths is even necessary...
+ $pathcandidates[$k] = $nextpaths[0];
+
+ // Reset flag condition.
+ $constraintflag = true;
+
+ array_pop($nextpaths);
+ }
+ $k++;
+ }
+ var_dump("Path not found");
+ 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/ford-fulkerson-koegel.php b/solver/ford-fulkerson-koegel.php
index d8f2c1e6..a9ac5343 100644
--- a/solver/ford-fulkerson-koegel.php
+++ b/solver/ford-fulkerson-koegel.php
@@ -42,7 +42,8 @@ class solver_ford_fulkerson extends distributor {
* according to the computed distriution.
*
*/
- public function compute_distribution($choicerecords, $ratings, $usercount) {
+ public function compute_distribution($choicerecords, $ratings, $usercount, $teamvote) {
+
$groupdata = array();
foreach ($choicerecords as $record) {
$groupdata[$record->id] = $record;
@@ -51,27 +52,58 @@ public function compute_distribution($choicerecords, $ratings, $usercount) {
$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;
+
+ if (!$teamvote) {
+
+ $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);
}
- // Reverse the augmenting path, thereby distributing a user into a group.
- $this->augment_flow($path);
+
+ return $this->extract_allocation($touserid, $togroupid);
+
+ } else {
+
+ $teamcount = count($teamvote);
+ $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, $teamvote);
+
+ // 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);
+
}
- return $this->extract_allocation($touserid, $togroupid);
}
/**
diff --git a/solver/solver-template.php b/solver/solver-template.php
index 3b9e8370..d48bfaac 100644
--- a/solver/solver-template.php
+++ b/solver/solver-template.php
@@ -89,23 +89,53 @@ public static function compute_target_function($ratings, $distribution) {
*/
public function distribute_users(\ratingallocate $ratingallocate) {
+ $teamvote = $ratingallocate->get_teamvote_groups();
+
// 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);
$usercount = count($ratingallocate->get_raters_in_course());
- $distributions = $this->compute_distribution($choicerecords, $ratings, $usercount);
+ $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.
+
+ foreach ($distributions as $choiceid => $teamids) {
+ $userids = array();
+ foreach ($teamids as $teamid) {
+ $userids = array_merge($userids,
+ array_map(function($user) {
+ return $user->id;
+ }, groups_get_members($teamid, 'u.id'))
+ );
+ }
+ $userdistributions[$choiceid] = $userids;
+ }
+
+ // We have to delete the provisionally groups containing only one user.
+ $ratingallocate->delete_groups_for_usersnogroup($teamvote);
+
+ }
+
+ foreach ($userdistributions as $choiceid => $users) {
foreach ($users as $userid) {
$ratingallocate->add_allocation($choiceid, $userid, $ratingallocate->ratingallocate->id);
}
@@ -116,9 +146,9 @@ 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, ...), ...)
+ * @return array of the form array(groupid => array(userid, ...), ...)
*/
protected function extract_allocation($touserid, $tochoiceid) {
$distribution = array();
@@ -132,6 +162,7 @@ protected function extract_allocation($touserid, $tochoiceid) {
}
}
}
+
return $distribution;
}
@@ -173,6 +204,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 +289,65 @@ 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
+ * @param type $teamvote
+ */
+ protected function setup_graph_for_teamvote($choicecount, $teamcount, $fromteamid, $fromchoiceid, $ratings, $choicedata, $source, $sink,
+ $teamvote, $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;
+ $membercount = $teamvote[$rating->groupid];
+ if ($weight > 0) {
+ $this->graph[$team][] = new edge($team, $choice, $weightmult * $weight * $membercount);
+ }
+ }
+
+ // 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');
}
@@ -245,10 +365,21 @@ protected function augment_flow($path) {
break;
}
}
+
+ // 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[2]]];
+ }
+
// 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--;
+ 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/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
diff --git a/styles.css b/styles.css
index 9ef50c4a..16438ea1 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;
}
@@ -121,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;
}
diff --git a/tests/behat/vote_in_teams.feature b/tests/behat/vote_in_teams.feature
new file mode 100644
index 00000000..17935d18
--- /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"
diff --git a/version.php b/version.php
index 57cc2f0d..9fa3a76a 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 = 2024030100; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2020061500; // Requires Moodle 3.9+.
$plugin->maturity = MATURITY_STABLE;
$plugin->release = 'v4.3-r1';