* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
* @since 3.5
*/
require_once 'lib/messaging.inc.php'; //Funktionen des Nachrichtensystems
class Course_StatusgroupsController extends AuthenticatedController
{
public function before_filter(&$action, &$args)
{
parent::before_filter($action, $args);
checkObject();
checkObjectModule("participants");
$course = Course::findCurrent();
$this->course_id = $course->id;
$this->course_title = $course->getFullName();
$this->config = CourseConfig::get($this->course_id);
// Check perms
$this->is_dozent = $GLOBALS['perm']->have_studip_perm('dozent', $this->course_id);
$this->is_tutor = $GLOBALS['perm']->have_studip_perm('tutor', $this->course_id);
$this->is_autor = $GLOBALS['perm']->have_studip_perm('autor', $this->course_id);
// Check lock rules
$this->is_locked = LockRules::Check($this->course_id, 'groups');
$this->is_participants_locked = LockRules::Check($this->course_id, 'participants');
PageLayout::setTitle(sprintf('%s - %s', Course::findCurrent()->getFullName(), _('Gruppen')));
PageLayout::addStyleSheet('studip-statusgroups.css');
PageLayout::addScript('studip-statusgroups.js');
}
/**
* Lists all available statusgroups.
*/
public function index_action()
{
Navigation::activateItem('/course/members/statusgroups');
if ($this->is_locked && $this->is_tutor) {
$lockdata = LockRules::getObjectRule($this->course_id);
if ($lockdata['description']) {
PageLayout::postInfo(formatLinks($lockdata['description']));
}
}
// Sorting as given by Request parameters
$this->sort_by = Request::option('sortby', 'nachname');
$this->order = Request::option('order', 'desc');
$this->sort_group = Request::get('sort_group', '');
$this->open_groups = Request::get('open_groups');
// Get all course members (needed for mkdate).
$this->allmembers = SimpleCollection::createFromArray(
CourseMember::findByCourse($this->course_id));
// Find all statusgroups for this course.
$groups = Statusgruppen::findBySeminar_id($this->course_id);
/*
* Check if the current user may join any group at all. This is needed
* for deciding if a Sidebar action for joining a group will be
* displayed.
*/
$joinable = false;
// Fetch membercounts (all at once for performance)
$membercounts = array_column(DBManager::get()->fetchAll(
"SELECT u.`statusgruppe_id`, COUNT(u.`user_id`) as membercount
FROM `statusgruppen` s
JOIN `statusgruppe_user` u USING (`statusgruppe_id`)
WHERE s.`range_id` = ?
GROUP BY `statusgruppe_id`
ORDER BY s.`position` ASC, s.`name` ASC",
[$this->course_id]),
'membercount',
'statusgruppe_id'
);
// Now build actual groups.
$this->groups = [];
foreach ($groups as $g) {
$groupdata = [
'group' => $g,
'members' => [],
'membercount' => $membercounts[$g->id] ?? 0,
'invisible_users' => 0
];
/*
* We only need to load members for a group that shall be sorted
* explicitly, as this group will be loaded at once and not via AJAX.
*/
if ($g->id == $this->sort_group || $this->open_groups) {
if ($this->sort_group == $g->id) {
$sorted = $this->sortMembers($g->members, $this->sort_by, $this->order);
} else {
$sorted = $this->sortMembers($g->members);
}
foreach ($sorted as $member) {
//Note: $member is a StatusgruppeUser object.
//We must get the CourseMember object to correctly
//determine the visibility of the user.
$course_member = CourseMember::findOneBySql(
'user_id = :user_id AND seminar_id = :course_id',
['user_id' => $member->user_id, 'course_id' => $member->group->range_id]
);
if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id) ||
($course_member->visible != 'no')) {
$groupdata['members'][] = $member;
} else {
$groupdata['invisible_users']++;
}
}
$groupdata['load'] = true;
} else {
$groupdata['load'] = false;
}
if (!$this->is_tutor && $g->userMayJoin($GLOBALS['user']->id)) {
$joinable = true;
} else {
$joinable = false;
}
$groupdata['joinable'] = $joinable;
$this->groups[] = $groupdata;
}
/*
* Get number of users that are in no group, this is needed
* for displaying in group header.
*/
$ungrouped_count = DBManager::get()->fetchFirst(
"SELECT COUNT(s.`user_id`) FROM `seminar_user` s WHERE s.`Seminar_id` = :course AND NOT EXISTS (
SELECT u.`user_id` FROM `statusgruppe_user` u
WHERE u.`statusgruppe_id` IN (:groups) AND u.`user_id` = s.`user_id`)",
[
'course' => $this->course_id,
'groups' => DBManager::get()->fetchFirst(
"SELECT `statusgruppe_id` FROM `statusgruppen` WHERE `range_id` = ?",
[$this->course_id])
]);
$this->ungrouped_count = $ungrouped_count[0];
if ($this->ungrouped_count > 0) {
// Create dummy entry for "no group" users.
$no_group = new StdClass();
$no_group->id = 'nogroup';
$no_group->name = _('keiner Gruppe zugeordnet');
$no_group->size = 0;
$no_group->selfassign = 0;
$groupdata = [
'group' => $no_group,
'membercount' => $this->ungrouped_count,
'joinable' => false,
'invisible_users' => 0,
'members' => []
];
$nogroupmembers = DBManager::get()->fetchFirst("SELECT user_id
FROM seminar_user
WHERE `Seminar_id` = :course AND NOT EXISTS (
SELECT `user_id` FROM `statusgruppe_user`
WHERE `statusgruppe_id` IN (:groups) AND `user_id` = seminar_user.`user_id`)",
[
'course' => $this->course_id,
'groups' => array_map(function ($g) { return $g->id; }, $groups)
]);
$this->nogroupmembers = $nogroupmembers;
if ($this->sort_group == 'nogroup') {
$members = $this->allmembers->findby('user_id', $nogroupmembers);
$members = $this->sortMembers($members, $this->sort_by, $this->order);
$groupdata['load'] = true;
} else {
$members = $this->allmembers->findby(
'user_id',
$this->nogroupmembers
);
}
if(!empty($members)) {
foreach ($members as $member) {
//Note: $member is a CourseMember object here.
if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id) ||
($member->visible != 'no')) {
$groupdata['members'][] = $member;
} else {
$groupdata['invisible_users']++;
}
}
}
$this->groups[] = $groupdata;
} else {
$this->nogroupmembers = [];
}
// Prepare search object for MultiPersonSearch.
$this->memberSearch = new PermissionSearch(
'user',
_('Personen suchen'),
'user_id',
[
'permission' => ['user', 'autor', 'tutor', 'dozent'],
'exclude_user' => []
]
);
/*
* Setup sidebar.
*/
$sidebar = Sidebar::get();
$actions = new ActionsWidget();
if ($this->is_tutor) {
if (!$this->is_locked) {
$actions->addLink(
_('Gruppe anlegen'),
$this->url_for('course/statusgroups/edit'),
Icon::create('add')
)->asDialog('size=auto');
$actions->addLink(
_('Mehrere Gruppen anlegen'),
$this->url_for('course/statusgroups/create_groups'),
Icon::create('add')
)->asDialog('size=auto');
}
if (Config::get()->EXPORT_ENABLE) {
$export = new ExportWidget();
$export->addLink(
_('Als Excel-Datei exportieren'),
$this->exportURL([
'course_id' => $this->course_id,
'format' => 'xlsx',
]),
Icon::create('export')
);
$export->addLink(
_('Als CSV-Datei exportieren'),
$this->exportURL([
'course_id' => $this->course_id,
'format' => 'csv',
]),
Icon::create('export')
);
$export->addLink(
_('Als CSV-Datei exportieren'),
$this->exportURL([
'course_id' => $this->course_id,
'format' => 'csv',
]),
Icon::create('export')
);
$export->addLink(
_('Als Word-Datei exportieren'),
$this->export_wordURL([
'course_id' => $this->course_id,
'format' => 'csv',
]),
Icon::create('export')
);
$sidebar->addWidget($export);
}
// Current user may join at least one group => show sidebar action.
} else if ($joinable) {
$actions->addLink(
_('In eine Gruppe eintragen'),
$this->url_for('course/statusgroups/joinables'),
Icon::create('door-enter')
)->asDialog('size=auto');
}
if ($this->open_groups) {
$actions->addLink(
_('Alle Gruppen zuklappen'),
$this->url_for('course/statusgroups'),
Icon::create('arr_2up')
);
} else {
$actions->addLink(
_('Alle Gruppen aufklappen'),
$this->url_for('course/statusgroups', ['open_groups' => '1']),
Icon::create('arr_1down')
);
}
$sidebar->addWidget($actions);
}
/**
* Handles the export of the course member list as a Word document.
*
* @return void
* @throws \PhpOffice\PhpWord\Exception\Exception
*/
public function export_word_action(): void
{
$course = Course::findCurrent();
$file = new \Services\Export\StatusGroupsService($course);
$file->save();
$this->response->add_header('Cache-Control', 'cache, must-revalidate');
$this->render_temporary_file(
$file->getFilePath(),
$file->getFilename(),
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
);
}
/**
*
*/
public function export_action()
{
$export_format = Request::get('format');
if ($export_format !== 'csv' && $export_format !== 'xlsx') {
throw new Exception('Wrong format');
}
$course = Course::findCurrent();
$header = [
_('Gruppe'),
_('Geschlecht'),
_('Titel'),
_('Vorname'),
_('Nachname'),
_('Titel nachgestellt'),
_('Nutzername'),
_('Privatadresse'),
_('Privatnr.'),
_('E-Mail'),
_('Anmeldedatum'),
_('Matrikelnummer'),
_('Studiengänge')
];
$groups = Statusgruppen::findBySeminar_id($this->course_id);
$result = [];
$datafields = DataField::getDataFields('user');
$datafields = array_filter(
$datafields,
fn(DataField $datafield) => $datafield->accessAllowed()
);
if (count($datafields) > 0) {
foreach ($datafields as $datafield) {
$header[] = (string)$datafield->name;
}
}
if ($groups) {
$assigned_with_group = [];
foreach ($groups as $group) {
foreach ($group->members->orderBy('nachname,vorname') as $member) {
$assigned_with_group[$member->user_id] = true;
$result[] = $member->getExportData();
}
}
$members = $course->members->filter(function($group_member) use ($assigned_with_group) {
return !array_key_exists($group_member->user_id, $assigned_with_group);
})->orderBy('nachname,vorname');
foreach ($members as $member) {
$data = ['gruppe' => _('keiner Funktion oder Gruppe zugeordnet')] + $member->getExportData();
unset($data['status']);
unset($data['position']);
$result[$member->user_id] = $data;
}
// data fields
foreach ($members as $member) {
$user_datafields = DataFieldEntry::getDataFieldEntries(
$member->user_id,
'user'
);
foreach ($datafields as $datafield) {
$user_datafield = $user_datafields[$datafield->id] ?? null;
if ($user_datafield) {
$result[$member->user_id][] = $user_datafield->getDisplayValue(false);
} else {
$result[$member->user_id][] = '';
}
}
}
}
$filename = FileManager::cleanFileName(_('Gruppenliste') . ' ' . $this->course_title . '.' . $export_format);
$this->render_spreadsheet($header, $result, $export_format, $filename);
}
/**
* Fetches the members of the given group.
*
* @param String $group_id the statusgroup to get members for.
*/
public function getgroup_action($group_id)
{
$this->sort_by = '';
if ($group_id != 'nogroup') {
$this->group = Statusgruppen::find($group_id);
$this->members = [];
$this->invisible = 0;
if (!empty($this->group->members)) {
//Note: $members consists of StatusgruppeUser objects here.
$members = $this->sortMembers($this->group->members);
foreach ($members as $member) {
//Get the course member object to check the visibility
//in the course.
$course_member = CourseMember::findOneBySql(
'user_id = :user_id AND seminar_id = :course_id',
['user_id' => $member->user_id, 'course_id' => $member->group->range_id]
);
if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id)
|| ($course_member->visible != 'no')) {
$this->members[] = $member;
} else {
$this->invisible++;
}
}
}
} else {
// Create dummy entry for "no group" users.
$no_group = new StdClass();
$no_group->id = 'nogroup';
$no_group->name = _('keiner Gruppe zugeordnet');
$no_group->size = 0;
$no_group->selfassign = 0;
$members = DBManager::get()->fetchAll("SELECT seminar_user.*,
aum.vorname,aum.nachname,aum.email,
aum.username,ui.title_front,ui.title_rear
FROM seminar_user
LEFT JOIN auth_user_md5 aum USING (user_id)
LEFT JOIN user_info ui USING (user_id)
WHERE `Seminar_id` = :course AND NOT EXISTS (
SELECT `user_id` FROM `statusgruppe_user`
WHERE `statusgruppe_id` IN (:groups) AND `user_id` = seminar_user.`user_id`)",
[
'course' => $this->course_id,
'groups' => DBManager::get()->fetchFirst(
"SELECT `statusgruppe_id` FROM `statusgruppen` WHERE `range_id` = ?",
[$this->course_id])
], 'CourseMember::buildExisting');
$members = new SimpleCollection($members);
//Note: $members consists of CourseMember objects here.
$members = $this->sortMembers($members);
$this->invisible = 0;
$this->members = [];
foreach ($members as $member) {
if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id)
|| ($member->visible != 'no')) {
$this->members[] = $member;
} else {
$this->invisible++;
}
}
$this->group = $no_group;
}
}
/**
* Provides extended info about a status group, like maximum number of
* participants, selfassign, exclusive entry, selfassign start and end
* times.
*
* @param String $group_id The group to show info for.
*/
public function groupinfo_action($group_id)
{
$this->group = Statusgruppen::find($group_id);
// Topics can be implicitly assigned via course dates.
$this->topics = $this->group->findTopics();
// Lecturers can be implicitly assigned via course dates.
$this->lecturers = $this->group->findLecturers();
}
/**
* Shows a list of all groups that can be joined by current user
* and allows the user to select one.
*/
public function joinables_action()
{
$this->joinables = SimpleCollection::createFromArray(
Statusgruppen::findJoinableGroups($this->course_id, $GLOBALS['user']->id))
->orderBy('position asc, name asc');
}
/**
* Adds selected persons to given group. user_ids to add come from a
* MultiPersonSearch object which was triggered in group actions.
*
* @param String $group_id
*/
public function add_member_action($group_id)
{
$g = Statusgruppen::find($group_id);
// Get selected persons.
$mp = MultiPersonSearch::load('add_statusgroup_member' . $group_id);
$success = 0;
$fail = 0;
foreach ($mp->getAddedUsers() as $a) {
if (!CourseMember::exists([$this->course_id, $a])) {
CourseMember::insertCourseMember($this->course_id, $a, User::find($a)->perms == 'user' ? 'user' : 'autor');
}
$s = new StatusgruppeUser();
$s->statusgruppe_id = $group_id;
$s->user_id = $a;
if ($s->store() !== false) {
$success++;
} else {
$fail++;
}
}
if ($success > 0 && $fail == 0) {
PageLayout::postSuccess(sprintf(ngettext(
'%u Person wurde zu %s hinzugefügt.',
'%u Personen wurden zu %s hinzugefügt.',
$success), $success, htmlReady($g->name)
));
} else if ($success > 0 && $fail > 0) {
$successMsg = sprintf(ngettext(
'%u Person wurde zu %s hinzugefügt.',
'%u Personen wurden zu %s hinzugefügt.',
$success), $success, htmlReady($g->name)
);
$failMsg = sprintf(ngettext(
'%u Person konnte nicht zu %s hinzugefügt werden.',
'%u Personen konnten nicht zu %s hinzugefügt werden.',
$fail), $fail, htmlReady($g->name)
);
PageLayout::postWarning($successMsg . ' ' . $failMsg);
} else if ($success == 0 && $fail > 0) {
PageLayout::postError(sprintf(ngettext(
'%u Person konnte nicht zu %s hinzugefügt werden.',
'%u Personen konnten nicht zu %s hinzugefügt werden.',
$success), $success, htmlReady($g->name)
));
}
$this->relocate('course/statusgroups');
}
/**
* Allows editing of a given statusgroup or creating a new one.
* @param String $group_id ID of the group to edit
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function edit_action($group_id = '')
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
// Fetch group with given ID or create a new one.
$this->group = new Statusgruppen($group_id);
// Check if course has regular times.
$this->cycles = SeminarCycleDate::findBySeminar_id($this->course_id);
// Check if course has single dates, not belonging to a regular cycle.
$dates = CourseDate::findBySeminar_id($this->course_id);
$this->singledates = array_filter($dates, function ($d) {
return !((bool) $d->metadate_id);
});
}
/**
* Saves changes to given statusgroup or creates a new entry.
*
* @param String $group_id ID of the group to edit
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function save_action($group_id = '')
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
/*
* Check if a valid end time was given.
*/
$selfassign = Request::int('selfassign', 0);
$start_time = 0;
$end_time = 0;
if ($selfassign) {
$end_time = strtotime(Request::get('selfassign_end', 'now'));
$start_time = strtotime(Request::get('selfassign_start', 'now'));
if ($end_time <= $start_time) {
$end_time = 0;
}
}
$position = null;
if (Statusgruppen::exists($group_id)) {
$position = Statusgruppen::find($group_id)->position;
}
// Exclusive entry makes sense only when selfassign is set in general.
if ($selfassign !== 0) {
$selfassign += Request::int('exclusive', 0);
// Selfassign is not set but exclusive selfassign or some timeframe -> show warning message
} else if (
Request::bool('exclusive')
|| $start_time
|| $end_time
) {
PageLayout::postWarning(_('Einstellungen zum Eintrag in eine Gruppe oder zum Eintragszeitraum können ' .
'nur gespeichert werden, wenn der Selbsteintrag aktiviert ist.'));
}
$group = Statusgruppen::createOrUpdate(
$group_id,
Request::get('name'),
$position,
$this->course_id,
Request::int('size', 0),
$selfassign,
$start_time,
$end_time,
Request::bool('makefolder', false),
Request::getArray('dates'),
Request::bool('blubber', false)
);
$group->description = trim(Request::get('description')) ?: null;
$group->store();
if (!$group_id) {
PageLayout::postSuccess(sprintf(
_('Die Gruppe "%s" wurde angelegt.'),
htmlReady($group->name)
));
} else {
PageLayout::postSuccess(sprintf(
_('Die Daten der Gruppe "%s" wurden gespeichert.'),
htmlReady($group->name)
));
}
$this->relocate('course/statusgroups');
}
/**
* Deletes the given statusgroup.
*
* @param String $group_id ID of the group to delete
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function delete_action($group_id)
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
$group = Statusgruppen::find($group_id);
$groupname = $group->name;
$group->delete();
PageLayout::postSuccess(sprintf(
_('Die Gruppe "%s" wurde gelöscht.'),
htmlReady($groupname)
));
$this->relocate('course/statusgroups');
}
/**
* Removes the given user from the given statusgroup.
*
* @param String $user_id user to remove
* @param String $group_id affected group
*/
public function delete_member_action($user_id, $group_id)
{
$g = Statusgruppen::find($group_id);
if (!$this->is_tutor && ($user_id !== $GLOBALS['user']->id || !$g->userMayLeave($user_id))) {
throw new AccessDeniedException();
}
$s = StatusgruppeUser::find([$group_id, $user_id]);
$name = $s->user->getFullName();
if ($s->delete()) {
if ($user_id == $GLOBALS['user']->id) {
PageLayout::postSuccess(sprintf(
_('Sie wurden aus der Gruppe %s ausgetragen.'),
htmlReady($g->name)
));
} else {
PageLayout::postSuccess(sprintf(
_('%s wurde aus der Gruppe %s ausgetragen.'),
htmlReady($name),
htmlReady($g->name)
));
}
} else {
if ($user_id == $GLOBALS['user']->id) {
PageLayout::postError(sprintf(
_('Sie konnten nicht aus der Gruppe %s ausgetragen werden.'),
htmlReady($g->name)
));
} else {
PageLayout::postError(sprintf(
_('%s konnte nicht aus der Gruppe %s ausgetragen werden.'),
htmlReady($name),
htmlReady($g->name)
));
}
}
$this->relocate('course/statusgroups');
}
public function move_member_action($user_id, $group_id)
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
$this->source_group = $group_id;
$this->members = [$user_id];
// Find possible target groups.
$this->target_groups = SimpleCollection::createFromArray(
Statusgruppen::findByRange_id($this->course_id))
->orderBy('position, name')
->filter(function ($g) use ($group_id) { return $g->id != $group_id; });
}
/**
* Provides the possibility to batch create several groups at once.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function create_groups_action()
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
// Check if course has regular times.
$this->has_cycles = count(SeminarCycleDate::findBySeminar_id($this->course_id)) > 0;
// Check if course has single dates, not belonging to a regular cycle.
$dates = CourseDate::findBySeminar_id($this->course_id);
$this->has_singledates = count(array_filter($dates, function ($d) {
return !((bool) $d->metadate_id);
})) > 0;
// Check if course has topics.
$topics = CourseTopic::findBySeminar_id($this->course_id);
$paper_topics = array_filter($topics, function ($topic) {
return $topic->paper_related;
});
$this->has_topics = count($topics) > 0;
$this->has_paper_related_topics = count($paper_topics) > 0;
}
/**
* Adds the current user to the given group.
*
* @throws AccessDeniedException if current user may not join the given group.
*/
public function join_action($group_id = '')
{
// group_id can also be given per request.
if (!$group_id) {
CSRFProtection::verifyUnsafeRequest();
$group_id = Request::option('target_group');
// Safety check if no group_id at all.
if (!$group_id) {
throw new Trails\Exception(400);
}
}
$g = Statusgruppen::find($group_id);
if (!$g->userMayJoin($GLOBALS['user']->id)) {
throw new AccessDeniedException();
}
$s = new StatusgruppeUser();
$s->user_id = $GLOBALS['user']->id;
$s->statusgruppe_id = $group_id;
if ($s->store()) {
PageLayout::postSuccess(sprintf(
_('Sie wurden als Mitglied der Gruppe %s eingetragen.'),
htmlReady($g->name)
));
} else {
PageLayout::postError(sprintf(
_('Sie konnten nicht als Mitglied der Gruppe %s eingetragen werden.'),
htmlReady($g->name)
));
}
$this->relocate('course/statusgroups');
}
/**
* Removes the current user from the given group.
*
* @throws AccessDeniedException if current user may not join the given group.
*/
public function leave_action($group_id)
{
$g = Statusgruppen::find($group_id);
if (!$g->userMayLeave($GLOBALS['user']->id)) {
throw new AccessDeniedException();
}
$s = StatusgruppeUser::find([$group_id, $GLOBALS['user']->id]);
if ($s->delete()) {
PageLayout::postSuccess(sprintf(
_('Sie wurden aus der Gruppe %s ausgetragen.'),
htmlReady($g->name)
));
} else {
PageLayout::postError(sprintf(
_('Sie konnten nicht aus der Gruppe %s ausgetragen werden.'),
htmlReady($g->name)
));
}
$this->relocate('course/statusgroups');
}
/**
* Batch creation of statusgroups according to given settings.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_create_action()
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
$counter = 0;
// Create a number of groups, sequentially named.
if (Request::option('mode') == 'numbering') {
if (Request::get('numbering_type') == 2) {
$numbering = 'A';
} else {
$numbering = Request::int('startnumber', 1);
}
for ($i = 0 ; $i < Request::int('number') ; $i++) {
Statusgruppen::createOrUpdate(
null,
Request::get('prefix') . ' ' . $numbering++,
null,
$this->course_id,
Request::int('size', 0),
Request::int('selfassign', 0) + Request::int('exclusive', 0),
strtotime(Request::get('selfassign_start', 'now')),
strtotime(Request::get('selfassign_end', 0)),
Request::bool('makefolder', false)
);
$counter++;
}
// Create groups by course metadata, like topics, dates or lecturers.
} else if (Request::option('mode') == 'coursedata') {
$mode = Request::option('createmode');
switch ($mode) {
// Create groups per topic.
case 'topics':
case 'paper_related':
$topics = SimpleCollection::createFromArray(
CourseTopic::findBySeminar_id($this->course_id)
)->filter(function ($topic) use ($mode) {
return $mode !== 'paper_related'
|| $topic->paper_related;
})->orderBy('priority');
foreach ($topics as $t) {
$group = Statusgruppen::createOrUpdate(
null,
_('Thema:') . ' ' . $t->title,
null,
$this->course_id,
Request::int('size', 0),
Request::int('selfassign', 0) + Request::int('exclusive', 0),
strtotime(Request::get('selfassign_start', 'now')),
strtotime(Request::get('selfassign_end', 0)),
Request::bool('makefolder', false)
);
// Connect group to dates that are assigned to the given topic.
$dates = CourseDate::findByIssue_id($t->id);
foreach ($dates as $d) {
$d->statusgruppen->append($group);
$d->store();
}
$counter++;
}
break;
// Create groups per (regular and irregular) dates.
case 'dates':
// Find regular cycles first and create corresponding groups.
$cycles = SimpleCollection::createFromArray(
SeminarCycleDate::findBySeminar_id($this->course_id));
foreach ($cycles as $c) {
$name = $c->toString();
// Append description to group title if applicable.
if ($c->description) {
$name .= ' ' . mila($c->description, 30);
}
// Get name of most used room and append to group title.
if ($room = $c->getMostBookedRoom()) {
$name .= ' (' . $room->name . ')';
} else {
$room = $c->getMostUsedFreetextRoomName();
if ($room) {
$name .= ' (' . $room . ')';
}
}
$group = Statusgruppen::createOrUpdate(
null,
$name,
null,
$this->course_id,
Request::int('size', 0),
Request::int('selfassign', 0) + Request::int('exclusive', 0),
strtotime(Request::get('selfassign_start', 'now')),
strtotime(Request::get('selfassign_end', 0)),
Request::bool('makefolder', false)
);
// Connect group to dates that are assigned to the given cycle.
foreach ($c->dates as $d) {
$d->statusgruppen->append($group);
$d->store();
}
$counter++;
}
// Now find irregular dates and create groups.
$dates = CourseDate::findBySeminar_id($this->course_id);
$singledates = array_filter($dates, function ($d) { return !((bool) $d->metadate_id); });
foreach ($singledates as $d) {
$name = $d->getFullName();
// Get room name and append to group title.
if ($room = $d->getRoomName()) {
$name .= ' (' . $room . ')';
}
$group = Statusgruppen::createOrUpdate(
null,
$name,
$counter + 1,
$this->course_id,
Request::int('size', 0),
Request::int('selfassign', 0) + Request::int('exclusive', 0),
strtotime(Request::get('selfassign_start', 'now')),
strtotime(Request::get('selfassign_end', 0)),
Request::bool('makefolder', false)
);
$d->statusgruppen->append($group);
$d->store();
$counter++;
}
break;
// Create groups per lecturer.
case 'lecturers':
$lecturers = SimpleCollection::createFromArray(
CourseMember::findByCourseAndStatus($this->course_id, 'dozent'))->orderBy('position');
foreach ($lecturers as $l) {
Statusgruppen::createOrUpdate(
null,
$l->getUserFullname('full'),
null,
$this->course_id,
Request::int('size', 0),
Request::int('selfassign', 0) + Request::int('exclusive', 0),
strtotime(Request::get('selfassign_start', 'now')),
strtotime(Request::get('selfassign_end', 0)),
Request::bool('makefolder', false)
);
$counter++;
}
break;
}
}
if ($counter > 0) {
PageLayout::postSuccess(sprintf(
ngettext('Eine Gruppe wurde angelegt.', '%u Gruppen wurden angelegt.', $counter),
$counter)
);
}
$this->relocate('course/statusgroups');
}
/**
* Batch action for several groups or group members at once.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_action_action()
{
// Non-tutors may not access this.
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
// Actions for selected groups.
if (Request::submitted('batch_groups')) {
$groups = Request::getArray('groups');
if ($groups) {
$this->groups = SimpleCollection::createFromArray(
Statusgruppen::findMany($groups)
)->orderBy('position, name');
switch (Request::option('groups_action')) {
case 'edit_size':
PageLayout::setTitle(_('Gruppengröße bearbeiten'));
$this->edit_size = true;
$sizes = [];
// Check for diverging values on all groups.
foreach ($this->groups as $group) {
$sizes[$group->size] = true;
}
// Get default group size
$this->size = max(array_keys($sizes));
break;
case 'edit_selfassign':
PageLayout::setTitle(_('Selbsteintrag bearbeiten'));
$this->edit_selfassign = true;
$selfassign = 0;
$exclusive = 0;
$selfassign_start = [];
$selfassign_end = [];
// Check for diverging values on all groups.
foreach ($this->groups as $group) {
if ((int)$group->selfassign == 1) {
$selfassign++;
}
if ((int)$group->selfassign === 2) {
$selfassign++;
$exclusive++;
}
$selfassign_start[$group->selfassign_start] = true;
$selfassign_end[$group->selfassign_end] = true;
}
if ($selfassign > 0) {
$this->selfassign = true;
}
if ($exclusive > 0) {
$this->exclusive = true;
}
// Selfassign start time set for all selected groups?
if (count($selfassign_start) <= 1) {
// Just one entry, take it as value for all.
$selfassign_starts = array_keys($selfassign_start);
$start = array_pop($selfassign_starts);
$this->selfassign_start = $start ? date('d.m.Y H:i', $start) : '';
} else {
// Different entries, mark this.
$this->selfassign_start = -1;
}
// Selfassign end time set for all selected groups?
if (count($selfassign_end) <= 1) {
// Just one entry, take it as value for all.
$selfassign_ends = array_keys($selfassign_end);
$end = array_pop($selfassign_ends);
$this->selfassign_end = $end ? date('d.m.Y H:i', $end) : '';
} else {
// Different entries, mark this.
$this->selfassign_end = -1;
}
break;
case 'write_message':
//Send the group-IDs of the selected groups
//to messages/write:
$this->redirect(
URLHelper::getURL(
'dispatch.php/messages/write',
[
'default_subject' => $this->course_title,
'group_ids' => $groups
]
)
);
break;
case 'copy':
$this->keepRequest();
$this->redirect($this->copyURL());
return;
case 'delete':
PageLayout::setTitle(_('Gruppe(n) löschen?'));
$this->askdelete = true;
break;
default:
$this->relocate('course/statusgroups');
}
} else {
PageLayout::postError(_('Sie haben keine Gruppe ausgewählt.'));
}
// Actions for selected group members.
} else if (Request::submitted('batch_members')) {
// Which group is selected?
$group_id = key(Request::getArray('batch_members'));
// Get selected group members.
$group = Request::getArray('group');
$this->members = array_keys($group[$group_id]);
// Get selected action for group members.
$actions = Request::getArray('members_action');
$action = $actions[$group_id] ?? '';
switch ($action) {
case 'move':
case 'copy':
if ($action === 'move') {
PageLayout::setTitle(_('Gruppenmitglieder verschieben'));
$this->movemembers = true;
} else {
PageLayout::setTitle(_('Gruppenmitglieder kopieren'));
$this->copymembers = true;
}
$this->source_group = $group_id;
// Find possible target groups.
$this->target_groups = SimpleCollection::createFromArray(
Statusgruppen::findByRange_id($this->course_id))
->orderBy('position, name')
->filter(function ($g) use ($group_id) { return $g->id != $group_id; });
break;
case 'delete':
PageLayout::setTitle(_('Gruppenmitglieder entfernen'));
$this->deletemembers = true;
$this->source_group = Statusgruppen::find($group_id);
break;
case 'cancel':
PageLayout::setTitle(_('Personen aus Veranstaltung austragen'));
$this->cancelmembers = true;
$this->source_group = Statusgruppen::find($group_id);
break;
}
}
}
/**
* Deletes several groups at once.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_delete_groups_action()
{
CSRFProtection::verifyUnsafeRequest();
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
Statusgruppen::findEachMany(
function ($group) {
$group->delete();
},
Request::optionArray('groups')
);
Statusgruppen::reorderPositionsForRange($this->course_id);
PageLayout::postSuccess(_('Die ausgewählten Gruppen wurden gelöscht.'));
$this->relocate('course/statusgroups');
}
/**
* Sets data for several groups at once.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_save_groups_size_action()
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
$groups = Statusgruppen::findMany(Request::getArray('groups'));
foreach ($groups as $g) {
Statusgruppen::createOrUpdate(
$g->id,
$g->name,
$g->position,
$this->course_id,
Request::int('size', 0),
$g->selfassign,
$g->selfassign_start,
$g->selfassign_end,
$g->hasFolder(),
null,
$g->hasBlubber()
);
}
PageLayout::postSuccess(_('Die Einstellungen der ausgewählten Gruppen wurden gespeichert.'));
$this->relocate('course/statusgroups');
}
/**
* Sets data for several groups at once.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_save_groups_selfassign_action()
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
$groups = Statusgruppen::findMany(Request::getArray('groups'));
$selfassign = Request::int('selfassign', 0);
$selfassign_start = 0;
$selfassign_end = 0;
if ($selfassign) {
$selfassign += Request::int('exclusive', 0);
$selfassign_start = strtotime(Request::get('selfassign_start', 'now'));
$selfassign_end = strtotime(Request::get('selfassign_end', 0));
}
foreach ($groups as $g) {
Statusgruppen::createOrUpdate(
$g->id,
$g->name,
$g->position,
$this->course_id,
$g->size,
$selfassign,
$selfassign_start,
$selfassign_end,
$g->hasFolder(),
null,
$g->hasBlubber()
);
}
PageLayout::postSuccess(_('Die Einstellungen der ausgewählten Gruppen wurden gespeichert.'));
$this->relocate('course/statusgroups');
}
/**
* Moves selected group members to another group.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_move_members_action()
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
$success = 0;
$error = 0;
$members = Request::getArray('members');
foreach ($members as $m) {
$stored = false;
// Add user to target statusgroup (if not already in there).
if (!StatusgruppeUser::exists([Request::option('target_group'), $m])) {
$s = new StatusgruppeUser();
$s->user_id = $m;
$s->statusgruppe_id = Request::option('target_group');
if ($s->store()) {
$stored = true;
}
}
// Delete old group membership.
$source = Request::option('source');
if ($stored) {
if ($source != 'nogroup') {
$old = StatusgruppeUser::find([$source, $m]);
if ($old->delete()) {
$success++;
} else {
$error++;
}
} else {
$success++;
}
}
}
$groupname = Statusgruppen::find(Request::option('target_group'))->name;
// Everything completed successfully => success message.
if ($success && !$error) {
PageLayout::postSuccess(sprintf(ngettext('%u Person wurde in die Gruppe %s verschoben.',
'%u Personen wurden in die Gruppe %s verschoben.',
$success), $success, htmlReady($groupname)));
// Some entries worked, some didn't => warning message.
} else if ($success && $error) {
PageLayout::postWarning(
sprintf(ngettext('%u Person wurde in die Gruppe %s verschoben.',
'%u Personen wurden in die Gruppe %s verschoben.',
$success), $success, htmlReady($groupname)) . '
' .
sprintf(ngettext('%u Person konnte nicht in die Gruppe %s verschoben werden.',
'%u Personen konnten nicht in die Gruppe %s verschoben werden.',
$error), $error, htmlReady($groupname))
);
// All is lost => error message.
} else if ($error) {
PageLayout::postError(sprintf(ngettext('%u Person konnte nicht in die Gruppe %s verschoben werden.',
'%u Personen konnten nicht in die Gruppe %s verschoben werden.',
$error), $error, htmlReady($groupname)));
}
$this->relocate('course/statusgroups');
}
/**
* Copies selected group members to another group.
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_copy_members_action()
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
$success = 0;
$members = Request::optionArray('members');
foreach ($members as $m) {
// Add user to target statusgroup (if not already in there).
if (!StatusgruppeUser::exists(array(Request::option('target_group'), $m))) {
$s = new StatusgruppeUser();
$s->user_id = $m;
$s->statusgruppe_id = Request::option('target_group');
if ($s->store()) {
$success++;
}
}
}
$groupname = Statusgruppen::find(Request::option('target_group'))->name;
// Everything completed successfully => success message.
if ($success) {
PageLayout::postSuccess(sprintf(ngettext('%u Person wurde in die Gruppe %s kopiert.',
'%u Personen wurden in die Gruppe %s kopiert.',
$success), $success, htmlReady($groupname)));
}
$this->relocate('course/statusgroups');
}
/**
* Removes selected group members from given group.
*
* @param String $group_id group to remove members from.
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_delete_members_action($group_id)
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
$success = 0;
$error = 0;
$members = Request::getArray('members');
foreach ($members as $m) {
// Remove user from target statusgroup.
$s = StatusgruppeUser::find([$group_id, $m]);
if ($s->delete()) {
$success++;
} else {
$error++;
}
}
$groupname = Statusgruppen::find($group_id)->name;
// Everything completed successfully => success message.
if ($success && !$error) {
PageLayout::postSuccess(sprintf(ngettext('%u Person wurde aus der Gruppe %s entfernt.',
'%u Personen wurden aus der Gruppe %s entfernt.',
$success), $success, htmlReady($groupname)));
// Some entries worked, some didn't => warning message.
} else if ($success && $error) {
PageLayout::postWarning(
sprintf(ngettext('%u Person wurde aus der Gruppe %s entfernt.',
'%u Personen wurden aus der Gruppe %s entfernt.',
$success), $success, htmlReady($groupname)) . '
' .
sprintf(ngettext('%u Person konnte nicht aus der Gruppe %s entfernt werden.',
'%u Personen konnten nicht aus der Gruppe %s entfernt werden.',
$error), $error, htmlReady($groupname))
);
// All is lost => error message.
} else if ($error) {
PageLayout::postError(sprintf(ngettext('%u Person konnte nicht aus der Gruppe %s entfernt werden.',
'%u Personen konnten nicht aus der Gruppe %s entfernt werden.',
$error), $error, htmlReady($groupname)));
}
$this->relocate('course/statusgroups');
}
/**
* Removes selected group members from the course (cancels their admission).
*
* @throws AccessDeniedException if access not allowed with current permission level.
*/
public function batch_cancel_members_action()
{
if (!$this->is_tutor) {
throw new AccessDeniedException();
}
CSRFProtection::verifyUnsafeRequest();
$members = Request::getArray('members');
$course = Course::find($this->course_id);
$removed_users = [];
$errors = [];
User::findEachMany(
function (User $user) use ($course, &$errors, &$removed_users) {
try {
$course->deleteMember($user, true);
$removed_users[] = $user->getFullName();
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
},
array_column($members, 'user_id')
);
if (!empty($errors)) {
PageLayout::postError(
_('Die folgenden Fehler traten beim Austragen von Personen auf:'),
$errors
);
}
if (!empty($removed_users)) {
if (count($removed_users) <= 5) {
PageLayout::postSuccess(
_('Die folgenden Personen wurden ausgetragen:'),
$removed_users
);
} else {
PageLayout::postSuccess(
_('%u Personen wurden ausgetragen.'),
count($removed_users)
);
}
}
$this->relocate('course/statusgroups');
}
public function order_action()
{
if (!Request::isPost()) {
throw new MethodNotAllowedException();
}
if (!$this->is_tutor || $this->is_locked) {
throw new AccessDeniedException();
}
$id = Request::option('id');
$index = Request::int('index');
$group = Statusgruppen::find($id);
if (!$group || $group->range_id !== $this->course_id) {
throw new Exception('Invalid group or group does not belong to course');
}
$groups = $group->course->statusgruppen
->filter(fn(Statusgruppen $grp) => $grp->id !== $group->id)
->getArrayCopy();
array_splice($groups, $index, 0, [$group]);
foreach ($groups as $i => $g) {
$g->position = $i;
$g->store();
}
$this->render_nothing();
}
private function sortMembers(SimpleCollection $members, $sort_by = null, $sort_dir = null)
{
$order = "nachname asc, vorname asc";
if ($sort_by && $sort_dir) {
$order = "{$sort_by} {$sort_dir}, {$order}";
}
return $members->orderBy($order);
}
public function details_action(Statusgruppen $group): void
{
$course = Course::findCurrent();
if ($course->id !== $group->range_id) {
throw new AccessDeniedException();
}
PageLayout::setTitle(sprintf(
_('Personen der Gruppe %s'),
$group->name
));
$this->group = $group;
}
public function copy_action(): void
{
PageLayout::setTitle(_('Gruppen in andere Veranstaltung kopieren'));
$this->group_ids = Request::optionArray('groups');
$this->search = new MyCoursesSearch('Seminar_id', User::findCurrent()->perms, [
':userid' => User::findCurrent()->id,
':exclude' => [$this->course_id],
]);
$this->response->add_header('X-Dialog-Size', 'medium');
}
public function do_copy_action(): void
{
if (!Request::isPost()) {
throw new MethodNotAllowedException();
}
$target_course_id = Request::option('course_id');
$target_course = Course::find($target_course_id);
BasicDataWizardStep::copyParticipantsAndGroups(
$target_course,
$this->course_id,
false,
true,
Request::bool('copy_members', false),
Request::optionArray('group_ids')
);
PageLayout::postSuccess(sprintf(
_('Die Gruppen wurden in die Veranstaltung %s kopiert.'),
sprintf(
'%s',
URLHelper::getLink('dispatch.php/course/go', ['to' => $target_course_id], true),
htmlReady($target_course->getFullName())
),
));
$this->redirect($this->indexURL());
}
}