Skip to content
Snippets Groups Projects
Commit ec684bbd authored by Ron Lucke's avatar Ron Lucke Committed by Marcus Eibrink-Lunzenauer
Browse files

StEP00362: Rechte- und Zugriffsverwaltung für Arbeitsplatz > Lernmaterialien

Closes #919

Merge request studip/studip!639
parent 93fb3b44
No related branches found
No related tags found
No related merge requests found
Showing
with 1012 additions and 294 deletions
...@@ -432,4 +432,41 @@ class Contents_CoursewareController extends AuthenticatedController ...@@ -432,4 +432,41 @@ class Contents_CoursewareController extends AuthenticatedController
$this->render_pdf($element->pdfExport($this->user, $with_children), trim($element->title).'.pdf'); $this->render_pdf($element->pdfExport($this->user, $with_children), trim($element->title).'.pdf');
} }
/**
* To display the shared courseware
*
* @param string $entry_element_id the shared struct element id
*/
public function shared_content_courseware_action($entry_element_id)
{
global $perm, $user;
$navigation = new Navigation(_('Geteiltes Lernmaterial'), 'dispatch.php/contents/courseware/shared_content_courseware/' . $entry_element_id);
Navigation::addItem('/contents/courseware/shared_content_courseware', $navigation);
Navigation::activateItem('/contents/courseware/shared_content_courseware');
$this->entry_element_id = $entry_element_id;
$struct = \Courseware\StructuralElement::findOneBySQL(
"id = ? AND range_type = 'user'",
[$this->entry_element_id]
);
if (!$struct) {
throw new Trails_Exception(404, _('Der geteilte Inhalt kann nicht gefunden werden.'));
}
if (!$struct->canRead($user) && !$struct->canEdit($user)) {
throw new AccessDeniedException();
}
$this->user_id = $struct->owner_id;
$this->licenses = $this->getLicences();
$this->oer_enabled = Config::get()->OERCAMPUS_ENABLED && $perm->have_perm(Config::get()->OER_PUBLIC_STATUS);
$this->setCoursewareSidebar();
}
} }
...@@ -110,7 +110,8 @@ class MultipersonsearchController extends AuthenticatedController ...@@ -110,7 +110,8 @@ class MultipersonsearchController extends AuthenticatedController
* This needs to be done in one single action to provider a similar * This needs to be done in one single action to provider a similar
* usability for no-JavaScript users as for JavaScript users. * usability for no-JavaScript users as for JavaScript users.
*/ */
public function no_js_form_action() { public function no_js_form_action()
{
if (!empty($_POST)) { if (!empty($_POST)) {
CSRFProtection::verifyUnsafeRequest(); CSRFProtection::verifyUnsafeRequest();
...@@ -243,4 +244,41 @@ class MultipersonsearchController extends AuthenticatedController ...@@ -243,4 +244,41 @@ class MultipersonsearchController extends AuthenticatedController
} }
public function ajax_search_vue_action($name)
{
$searchterm = Request::get('s');
$searchterm = str_replace(',', ' ', $searchterm);
$searchterm = preg_replace('/\s+/u', ' ', $searchterm);
$result = [];
// execute searchobject if searchterm is at least 3 chars long
if (mb_strlen($searchterm) >= 3) {
$mp = MultiPersonSearch::load($name);
$mp->setSearchObject(new StandardSearch('user_id'));
$searchObject = $mp->getSearchObject();
$result = array_map(function ($r) {
return $r['user_id'];
}, $searchObject->getResults($searchterm, [], 50));
$result = User::findFullMany($result, 'ORDER BY Nachname ASC, Vorname ASC');
$alreadyMember = $mp->getDefaultSelectedUsersIDs();
}
$output = [];
foreach ($result as $user) {
$output[] = [
'id' => $user->id,
'avatar' => Avatar::getAvatar($user->id)->getURL(Avatar::SMALL),
'text' => "{$user->nachname}, {$user->vorname} -- {$user->perms} ({$user->username})",
'selected' => $alreadyMember === null ? false : in_array($user->id, $alreadyMember),
'nachname' => $user->nachname,
'vorname' => $user->vorname,
'username' => $user->username,
'formatted-name' => trim($user->getFullName())
];
}
$this->render_json($output);
}
} }
<div
id="courseware-index-app"
entry-element-id="<?= $entry_element_id ?>"
entry-type="sharedusers"
entry-id="<?= $entry_element_id ?>"
oer-enabled='<?= $oer_enabled ?>'
oer-title="<?= Config::get()->OER_TITLE ?>"
licenses='<?= $licenses ?>'
>
</div>
...@@ -308,7 +308,7 @@ class RouteMap ...@@ -308,7 +308,7 @@ class RouteMap
private function addAuthenticatedCoursewareRoutes(RouteCollectorProxy $group): void private function addAuthenticatedCoursewareRoutes(RouteCollectorProxy $group): void
{ {
$group->get('/{type:courses|users}/{id}/courseware', Routes\Courseware\CoursewareInstancesShow::class); $group->get('/{type:courses|users|sharedusers}/{id}/courseware', Routes\Courseware\CoursewareInstancesShow::class);
$group->patch('/courseware-instances/{id}', Routes\Courseware\CoursewareInstancesUpdate::class); $group->patch('/courseware-instances/{id}', Routes\Courseware\CoursewareInstancesUpdate::class);
$this->addRelationship( $this->addRelationship(
$group, $group,
...@@ -420,6 +420,10 @@ class RouteMap ...@@ -420,6 +420,10 @@ class RouteMap
$group->patch('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackUpdate::class); $group->patch('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackUpdate::class);
$group->delete('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackDelete::class); $group->delete('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackDelete::class);
$group->get('/courseware-structural-elements-shared', Routes\Courseware\StructuralElementsSharedIndex::class);
$group->get('/courseware-structural-elements-released', Routes\Courseware\StructuralElementsReleasedIndex::class);
$group->get('/courseware-blocks/{id}/user-data-field', Routes\Courseware\UserDataFieldOfBlocksShow::class); $group->get('/courseware-blocks/{id}/user-data-field', Routes\Courseware\UserDataFieldOfBlocksShow::class);
$group->get('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsShow::class); $group->get('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsShow::class);
$group->patch('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsUpdate::class); $group->patch('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsUpdate::class);
......
...@@ -59,7 +59,23 @@ class Authority ...@@ -59,7 +59,23 @@ class Authority
public static function canUpdateBlock(User $user, Block $resource) public static function canUpdateBlock(User $user, Block $resource)
{ {
if ($resource->isBlocked()) { if ($resource->isBlocked()) {
return $resource->getBlockerUserId() == $user->id; $structural_element = $resource->container->structural_element;
if ($structural_element->range_type === 'user') {
if ($structural_element->range_id === $user->id) {
return true;
}
return $structural_element->canEdit($user);
}
$perm = $GLOBALS['perm']->have_studip_perm(
$structural_element->course->config->COURSEWARE_EDITING_PERMISSION,
$structural_element->course->id,
$user->id
);
return $resource->getBlockerUserId() === $user->id || $perm;
} }
return self::canUpdateContainer($user, $resource->container); return self::canUpdateContainer($user, $resource->container);
...@@ -72,7 +88,36 @@ class Authority ...@@ -72,7 +88,36 @@ class Authority
public static function canUpdateEditBlocker(User $user, $resource) public static function canUpdateEditBlocker(User $user, $resource)
{ {
return $resource->edit_blocker_id == '' || $resource->edit_blocker_id === $user->id; $structural_element = null;
if ($resource instanceof Block) {
$structural_element = $resource->container->structural_element;
}
if ($resource instanceof Container) {
$structural_element = $resource->structural_element;
}
if ($resource instanceof StructuralElement) {
$structural_element = $resource;
}
if ($structural_element === null) {
return false;
}
if ($structural_element->range_type === 'user') {
if ($structural_element->range_id === $user->id) {
return true;
}
return $structural_element->canEdit($user);
}
$perm = $GLOBALS['perm']->have_studip_perm(
$structural_element->course->config->COURSEWARE_EDITING_PERMISSION,
$structural_element->course->id,
$user->id
);
return $resource->edit_blocker_id == '' || $resource->edit_blocker_id === $user->id || $perm;
} }
public static function canShowContainer(User $user, Container $resource) public static function canShowContainer(User $user, Container $resource)
...@@ -163,6 +208,18 @@ class Authority ...@@ -163,6 +208,18 @@ class Authority
return $GLOBALS['perm']->have_perm('root', $user->id); return $GLOBALS['perm']->have_perm('root', $user->id);
} }
public static function canIndexStructuralElementsShared(User $user)
{
//TODO ?
return true;
}
public static function canIndexStructuralElementsReleased(User $user)
{
//TODO ?
return true;
}
public static function canReorderStructuralElements(User $user, $resource) public static function canReorderStructuralElements(User $user, $resource)
{ {
return self::canUpdateStructuralElement($user, $resource); return self::canUpdateStructuralElement($user, $resource);
......
...@@ -26,6 +26,7 @@ trait CoursewareInstancesHelper ...@@ -26,6 +26,7 @@ trait CoursewareInstancesHelper
'courses' => 'getCoursewareCourse', 'courses' => 'getCoursewareCourse',
'user' => 'getCoursewareUser', 'user' => 'getCoursewareUser',
'users' => 'getCoursewareUser', 'users' => 'getCoursewareUser',
'sharedusers' => 'getSharedCoursewareUser',
]; ];
if (!($method = $methods[$rangeType])) { if (!($method = $methods[$rangeType])) {
throw new BadRequestException('Invalid range type: "' . $rangeType . '".'); throw new BadRequestException('Invalid range type: "' . $rangeType . '".');
......
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\StructuralElement;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\JsonApiController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Class StructuralElementsReleasedIndex.
*/
class StructuralElementsReleasedIndex extends JsonApiController
{
protected $allowedPagingParameters = ['offset', 'limit'];
protected $allowedIncludePaths = [
'ancestors',
'children',
'containers',
'containers.blocks',
'containers.blocks.edit-blocker',
'containers.blocks.editor',
'containers.blocks.owner',
'containers.blocks.user-data-field',
'containers.blocks.user-progress',
'course',
'editor',
'owner',
'parent',
];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$user = $this->getUser($request);
if (!Authority::canIndexStructuralElementsReleased($user)) {
throw new AuthorizationFailedException();
}
list($offset, $limit) = $this->getOffsetAndLimit();
$resources = [];
$contents = StructuralElement::findBySQL(
'range_id = ? AND range_type = ? ORDER BY mkdate DESC',
[$user->id, 'user']
);
foreach ($contents as $content) {
if ((count($content->read_approval) && count($content->read_approval['users']) > 0) || (count($content->write_approval) && count($content->write_approval['users']) > 0)) {
$resources[] = $content;
}
}
return $this->getPaginatedContentResponse($resources, count($resources));
}
}
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\StructuralElement;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\JsonApiController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Class StructuralElementsSharedIndex.
*/
class StructuralElementsSharedIndex extends JsonApiController
{
protected $allowedPagingParameters = ['offset', 'limit'];
protected $allowedIncludePaths = [
'ancestors',
'children',
'containers',
'containers.blocks',
'containers.blocks.edit-blocker',
'containers.blocks.editor',
'containers.blocks.owner',
'containers.blocks.user-data-field',
'containers.blocks.user-progress',
'course',
'editor',
'owner',
'parent',
];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$user = $this->getUser($request);
if (!Authority::canIndexStructuralElementsShared($user)) {
throw new AuthorizationFailedException();
}
list($offset, $limit) = $this->getOffsetAndLimit();
$resources = [];
$contents = StructuralElement::findBySQL(
'range_id != ? AND range_type = ? ORDER BY mkdate DESC',
[$user->id, 'user']
);
foreach ($contents as $content) {
if (!count($content->read_approval) || !count($content->write_approval)) {
continue;
}
$add_content = false;
foreach ($content->read_approval['users'] as $listedUserPerm) {
if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read']) {
$add_content = true;
}
}
foreach ($content->write_approval['users'] as $listedUserPerm) {
if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read']) {
$add_content = true;
}
}
if ($add_content) {
$resources[] = $content;
}
}
return $this->getPaginatedContentResponse($resources, count($resources));
}
}
...@@ -169,14 +169,22 @@ class StructuralElement extends \SimpleORMap ...@@ -169,14 +169,22 @@ class StructuralElement extends \SimpleORMap
return self::getCourseware($courseId, 'course'); return self::getCourseware($courseId, 'course');
} }
private static function getCourseware(string $rangeId, string $rangeType): ?StructuralElement public static function getSharedCoursewareUser(string $root_id): ?StructuralElement
{ {
/** @var ?StructuralElement $result */ return self::getCourseware('', '', $root_id);
$result = self::findOneBySQL( }
'range_id = ?
AND range_type = ? AND parent_id IS NULL', private static function getCourseware(string $rangeId, string $rangeType, string $root_id = null): ?StructuralElement
[$rangeId, $rangeType] {
); if ($root_id) {
$result = self::find($root_id);
} else {
$result = self::findOneBySQL(
'range_id = ?
AND range_type = ? AND parent_id IS NULL',
[$rangeId, $rangeType]
);
}
return $result; return $result;
} }
...@@ -222,7 +230,11 @@ class StructuralElement extends \SimpleORMap ...@@ -222,7 +230,11 @@ class StructuralElement extends \SimpleORMap
switch ($this->range_type) { switch ($this->range_type) {
case 'user': case 'user':
return $this->range_id === $user->id; if ($this->range_id === $user->id) {
return true;
}
return $this->hasWriteApproval($user);
case 'course': case 'course':
$hasEditingPermission = $this->hasEditingPermission($user); $hasEditingPermission = $this->hasEditingPermission($user);
...@@ -273,11 +285,12 @@ class StructuralElement extends \SimpleORMap ...@@ -273,11 +285,12 @@ class StructuralElement extends \SimpleORMap
switch ($this->range_type) { switch ($this->range_type) {
case 'user': case 'user':
// Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen. if ($this->range_id === $user->id) {
if ($this->range_id === $user->id) {
return true; return true;
} }
return $this->hasReadApproval($user);
$link = StructuralElement::findOneBySQL('target_id = ?', [$this->id]); $link = StructuralElement::findOneBySQL('target_id = ?', [$this->id]);
if ($link) { if ($link) {
return true; return true;
...@@ -313,8 +326,11 @@ class StructuralElement extends \SimpleORMap ...@@ -313,8 +326,11 @@ class StructuralElement extends \SimpleORMap
switch ($this->range_type) { switch ($this->range_type) {
case 'user': case 'user':
// Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen. if ($this->range_id === $user->id) {
return $this->range_id === $user->id; return true;
}
return $this->hasReadApproval($user);
case 'course': case 'course':
if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) { if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) {
...@@ -367,59 +383,133 @@ class StructuralElement extends \SimpleORMap ...@@ -367,59 +383,133 @@ class StructuralElement extends \SimpleORMap
private function hasReadApproval($user): bool private function hasReadApproval($user): bool
{ {
if (!count($this->read_approval)) { // this property is shared between all range types.
if ($this->read_approval['all']) {
return true; return true;
} }
if ($this->read_approval['all']) { // now we also check against the perms for contents.
return true; if ($this->range_type === 'user') {
return $this->hasUserReadApproval($user);
} else {
if (!count($this->read_approval)) {
return true;
}
// find user in users
$users = $this->read_approval['users'];
foreach ($users as $approvedUserId) {
if ($approvedUserId === $user->id) {
return true;
}
}
// find user in groups
$groups = $this->read_approval['groups'];
foreach ($groups as $groupId) {
/** @var ?\Statusgruppen $group */
$group = \Statusgruppen::find($groupId);
if ($group && $group->isMember($user->id)) {
return true;
}
}
}
return false;
}
private function hasUserReadApproval($user): bool
{
if (!count($this->read_approval)) {
if ($this->isRootNode()) {
return false;
}
return $this->parent->hasUserReadApproval($user);
} }
// find user in users // find user in users
$users = $this->read_approval['users']; $users = $this->read_approval['users'];
foreach ($users as $approvedUserId) { foreach ($users as $listedUserPerm) {
if ($approvedUserId == $user->id) { // now for contents, there is an expiry date defined.
if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) {
if ($this->isRootNode()) {
return false;
}
return $this->parent->hasUserReadApproval($user);
}
// In order to have a record of the users in the perms list of contents,
// we keep a full perm record in read_approval column, and set read property to true or false,
// this won't apply to write_approval column.
if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read'] == true) {
return true; return true;
} }
} }
}
private function hasWriteApproval($user): bool
{
// this property is shared between all range types.
if ($this->write_approval['all']) {
return true;
}
// find user in groups // now we also check against the perms for contents.
$groups = $this->read_approval['groups']; if ($this->range_type === 'user') {
foreach ($groups as $groupId) { return $this->hasUserWriteApproval($user);
/** @var ?\Statusgruppen $group */ } else {
$group = \Statusgruppen::find($groupId); if (!count($this->write_approval)) {
if ($group && $group->isMember($user->id)) { return false;
}
if ($this->write_approval['all']) {
return true;
}
// find user in users
$users = $this->write_approval['users']->getArrayCopy();
if (in_array($user->id, $users)) {
return true; return true;
} }
// find user in groups
foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) {
if ($group->isMember($user->id)) {
return true;
}
}
} }
return false; return false;
} }
private function hasWriteApproval($user): bool private function hasUserWriteApproval($user): bool
{ {
if (!count($this->write_approval)) { if (!count($this->write_approval)) {
return false; if ($this->isRootNode()) {
} return false;
}
if ($this->write_approval['all']) { return $this->parent->hasUserWriteApproval($user);
return true;
} }
// find user in users // find user in users
$users = $this->write_approval['users']->getArrayCopy(); $users = $this->write_approval['users'];
if (in_array($user->id, $users)) { foreach ($users as $listedUserPerm) {
return true; // now for contents, there is an expiry date defined.
} if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) {
if ($this->isRootNode()) {
// find user in groups return false;
foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) { }
if ($group->isMember($user->id)) { return $this->parent->hasUserWriteApproval($user);
}
if ($listedUserPerm['id'] == $user->id) {
return true; return true;
} }
} }
return false; if ($this->isRootNode()) {
return false;
}
return $this->parent->hasUserWriteApproval($user);
} }
/** /**
......
...@@ -47,30 +47,36 @@ class ContentsNavigation extends Navigation ...@@ -47,30 +47,36 @@ class ContentsNavigation extends Navigation
$courseware->setDescription(_('Erstellen und Sammeln von Lernmaterialien')); $courseware->setDescription(_('Erstellen und Sammeln von Lernmaterialien'));
$courseware->setImage(Icon::create('courseware')); $courseware->setImage(Icon::create('courseware'));
$courseware->addSubNavigation( $courseware = new Navigation(_('Courseware'));
'overview', $courseware->setDescription(_('Erstellen und Sammeln von Lernmaterialien'));
new Navigation(_('Übersicht'), 'dispatch.php/contents/courseware/index') $courseware->setImage(Icon::create('courseware'));
);
$courseware->addSubNavigation( $courseware->addSubNavigation(
'courseware', 'overview',
new Navigation(_('Persönliche Lernmaterialien'), 'dispatch.php/contents/courseware/courseware') new Navigation(_('Übersicht'), 'dispatch.php/contents/courseware/index')
); );
$courseware->addSubNavigation( $courseware->addSubNavigation(
'courseware_manager', 'courseware',
new Navigation(_('Verwaltung persönlicher Lernmaterialien'), 'dispatch.php/contents/courseware/courseware_manager') new Navigation(_('Persönliche Lernmaterialien'), 'dispatch.php/contents/courseware/courseware')
); );
$courseware->addSubNavigation( $courseware->addSubNavigation(
'releases', 'courseware_manager',
new Navigation(_('Freigaben'), 'dispatch.php/contents/courseware/releases') new Navigation(_('Verwaltung persönlicher Lernmaterialien'), 'dispatch.php/contents/courseware/courseware_manager')
); );
$courseware->addSubNavigation( $courseware->addSubNavigation(
'bookmarks', 'releases',
new Navigation(_('Lesezeichen'), 'dispatch.php/contents/courseware/bookmarks') new Navigation(_('Freigaben'), 'dispatch.php/contents/courseware/releases')
); );
$courseware->addSubNavigation( $courseware->addSubNavigation(
'courses_overview', 'bookmarks',
new Navigation(_('Meine Veranstaltungen'), 'dispatch.php/contents/courseware/courses_overview') new Navigation(_('Lesezeichen'), 'dispatch.php/contents/courseware/bookmarks')
); );
$courseware->addSubNavigation(
'courses_overview',
new Navigation(_('Meine Veranstaltungen'), 'dispatch.php/contents/courseware/courses_overview')
);
$this->addSubNavigation('courseware', $courseware);
$this->addSubNavigation('courseware', $courseware); $this->addSubNavigation('courseware', $courseware);
} }
......
...@@ -79,7 +79,7 @@ STUDIP.domReady(() => { ...@@ -79,7 +79,7 @@ STUDIP.domReady(() => {
if (document.getElementById('courseware-content-releases-app')) { if (document.getElementById('courseware-content-releases-app')) {
STUDIP.Vue.load().then(({ createApp }) => { STUDIP.Vue.load().then(({ createApp }) => {
import( import(
/* webpackChunkName: "courseware-content-links-app" */ /* webpackChunkName: "courseware-content-releases-app" */
'@/vue/courseware-content-releases-app.js' '@/vue/courseware-content-releases-app.js'
).then(({ default: mountApp }) => { ).then(({ default: mountApp }) => {
return mountApp(STUDIP, createApp, '#courseware-content-releases-app'); return mountApp(STUDIP, createApp, '#courseware-content-releases-app');
......
...@@ -94,6 +94,12 @@ c o n t e n t s ...@@ -94,6 +94,12 @@ c o n t e n t s
* * * * * * * * */ * * * * * * * * */
.cw-content-overview { .cw-content-overview {
max-width: 1100px; max-width: 1100px;
h2 {
margin: 0;
font-weight: 400;
padding: 5px 0;
font-size: 1.4em;
}
} }
.cw-contents-overview-teaser { .cw-contents-overview-teaser {
...@@ -193,6 +199,22 @@ c o n t e n t s ...@@ -193,6 +199,22 @@ c o n t e n t s
} }
} }
.cw-content-courses {
h2 {
margin: 0;
font-weight: 400;
padding: 5px 0;
font-size: 1.4em;
}
ul.cw-tiles {
margin-bottom: 20px;
}
}
.cw-contents-overview-personal {
margin-bottom: 2em;
}
/* * * * * * * * * * * /* * * * * * * * * * *
c o n t e n t s e n d c o n t e n t s e n d
* * * * * * * * * * */ * * * * * * * * * * */
...@@ -202,7 +224,8 @@ r i b b o n ...@@ -202,7 +224,8 @@ r i b b o n
* * * * * */ * * * * * */
$consum_ribbon_width: calc(100% - 58px); $consum_ribbon_width: calc(100% - 58px);
#course-courseware-index, #course-courseware-index,
#contents-courseware-courseware { #contents-courseware-courseware,
#contents-courseware-shared_content_courseware {
&.consume { &.consume {
overflow: hidden; overflow: hidden;
} }
...@@ -725,6 +748,23 @@ ribbon end ...@@ -725,6 +748,23 @@ ribbon end
padding: 0; padding: 0;
font-size: 1.25em; font-size: 1.25em;
} }
td.perm {
input.right, input.date {
cursor: pointer !important;
}
}
}
button.cw-add-persons {
margin-left: 4px;
}
button.cw-permission-delete {
width: 24px;
height: 24px;
border: none;
background-color: transparent;
@include background-icon(trash, clickable);
background-repeat: no-repeat;
cursor: pointer;
} }
} }
...@@ -3117,6 +3157,26 @@ a u d i o b l o c k ...@@ -3117,6 +3157,26 @@ a u d i o b l o c k
a u d i o b l o c k e n d a u d i o b l o c k e n d
* * * * * * * * * * * * * */ * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * * * *
f o r m u l t i m e d i a b l o c k s
* * * * * * * * * * * * * * * * * * * */
.cw-file-empty {
@include background-icon(file, info, 96);
border: solid thin $content-color-40;
background-position: center 1em;
background-repeat: no-repeat;
min-height: 140px;
padding: 1em;
p {
text-align: center;
padding-top: 106px;
}
}
/* * * * * * * * * * * * * * * * * * * * * * * *
f o r m u l t i m e d i a b l o c k s e n d
* * * * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * /* * * * * * * * * *
v i d e o b l o c k v i d e o b l o c k
* * * * * * * * * * */ * * * * * * * * * * */
...@@ -4808,17 +4868,6 @@ cw tiles end ...@@ -4808,17 +4868,6 @@ cw tiles end
} }
/* courseware template preview end*/ /* courseware template preview end*/
/* contents courseware courses */
.cw-content-courses {
h2 {
margin-top: 0;
}
ul.cw-tiles {
margin-bottom: 20px;
}
}
/* contents courseware courses end*/
/* * * * * * * * * * /* * * * * * * * * *
i n p u t f i l e i n p u t f i l e
* * * * * * * * * */ * * * * * * * * * */
......
.studip-msp-vue {
a.msp-btn {
margin-left: 5px;
img {
vertical-align: middle;
}
}
}
...@@ -99,6 +99,7 @@ ...@@ -99,6 +99,7 @@
@import "scss/typography"; @import "scss/typography";
@import "scss/user-administration"; @import "scss/user-administration";
@import "scss/wiki"; @import "scss/wiki";
@import "scss/multi_person_search";
// Class for DOM elements that should only be visible to Screen readers // Class for DOM elements that should only be visible to Screen readers
......
...@@ -19,6 +19,7 @@ import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue'; ...@@ -19,6 +19,7 @@ import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue';
import StudipProxiedCheckbox from './components/StudipProxiedCheckbox.vue'; import StudipProxiedCheckbox from './components/StudipProxiedCheckbox.vue';
import StudipTooltipIcon from './components/StudipTooltipIcon.vue'; import StudipTooltipIcon from './components/StudipTooltipIcon.vue';
import StudipSelect from './components/StudipSelect.vue'; import StudipSelect from './components/StudipSelect.vue';
import StudipMultiPersonSearch from './components/StudipMultiPersonSearch.vue';
const BaseComponents = { const BaseComponents = {
Multiselect, Multiselect,
...@@ -41,7 +42,8 @@ const BaseComponents = { ...@@ -41,7 +42,8 @@ const BaseComponents = {
StudipProxiedCheckbox, StudipProxiedCheckbox,
StudipTooltipIcon, StudipTooltipIcon,
StudipSelect, StudipSelect,
TextareaWithToolbar TextareaWithToolbar,
StudipMultiPersonSearch
}; };
export default BaseComponents; export default BaseComponents;
...@@ -28,7 +28,8 @@ ...@@ -28,7 +28,8 @@
if (this.shape.indexOf("http") === 0) { if (this.shape.indexOf("http") === 0) {
return this.shape; return this.shape;
} }
return `${STUDIP.ASSETS_URL}images/icons/${this.color}/${this.shape}.svg`; var path = this.shape.split('+').reverse().join('/');
return `${STUDIP.ASSETS_URL}images/icons/${this.color}/${path}.svg`;
}, },
color: function () { color: function () {
switch (this.role) { switch (this.role) {
......
<template>
<div class="mpscontainer studip-msp-vue">
<form method="post" class="default" @submit.prevent="search">
<label class="with-action">
<input type="text" ref="searchInputField" v-model="searchTerm" :placeholder="$gettext('Suchen')" style="width: 260px;">
<a href="#" class="msp-btn" @click.prevent="search" :title="$gettext('Suche starten')">
<studip-icon shape="search" role="clickable" size="16"></studip-icon>
</a>
<a href="#" class="msp-btn" @click.prevent="resetSearch" :title="$gettext('Suche zurücksetzen')">
<studip-icon shape="decline" role="clickable" size="16"></studip-icon>
</a>
</label>
<select multiple="multiple" :id="select_box_id" name="selectbox[]"></select>
</form>
</div>
</template>
<script>
export default {
name: 'studip-multi-person-search',
props: {
name: String,
withDetail: {
type: Boolean,
default: true
}
},
data() {
return {
searchTerm: '',
count: 0,
users: []
}
},
mounted () {
this.$nextTick(() => {
this.init();
setTimeout(() => {
this.$refs.searchInputField.focus();
}, 100);
});
},
computed: {
id() {
return this._uid;
},
count_text_id() {
return this.id + '_count';
},
select_box_id() {
return this.id + '_selectbox';
},
},
methods: {
init() {
let select_all_btn = document.createElement('a');
select_all_btn.setAttribute('id', `${this.id}-select-all`);
select_all_btn.setAttribute('href', '#');
select_all_btn.innerText = this.$gettext('Alle hinzufügen');
select_all_btn.addEventListener('click', (e) => {
e.preventDefault();
this.selectAll();
});
let unselect_all_btn = document.createElement('a');
unselect_all_btn.setAttribute('id', `${this.id}-unselect-all`);
unselect_all_btn.setAttribute('href', '#');
unselect_all_btn.innerText = this.$gettext('Alle entfernen');
unselect_all_btn.addEventListener('click', (e) => {
e.preventDefault();
this.unselectAll();
});
let selection_header = document.createElement('div');
selection_header.setAttribute('id', this.count_text_id);
selection_header.innerText = this.$gettextInterpolate('Sie haben %{ count } Personen ausgewählt', {count: this.count});
$('#' + this.select_box_id).multiSelect({
selectableHeader: '<div>' + this.$gettext('Suchergebnisse') + '</div>',
selectionHeader: selection_header,
selectableFooter: select_all_btn,
selectionFooter: unselect_all_btn,
afterSelect: () => this.updateSelection(),
afterDeselect: () => this.updateSelection()
});
},
search() {
this.users = [];
let view = this;
$.getJSON(
STUDIP.URLHelper.getURL('dispatch.php/multipersonsearch/ajax_search_vue/' + this.name, { s: this.searchTerm }),
function(data) {
view.removeAllNotSelected();
var searchcount = 0;
$.each(data, function(i, item) {
searchcount += view.append(
item.id,
item.avatar + ' -- ' + item.text,
item.selected
);
delete item.selected;
view.users.push(item);
});
view.refresh();
if (searchcount === 0) {
view.append(
'--',
view.$gettextInterpolate('Es wurden keine neuen Ergebnisse für "%{ needle }" gefunden.', {needle: view.searchTerm}),
true
);
view.refresh();
}
}
);
},
selectAll: function() {
$('#' + this.select_box_id).multiSelect('select_all');
this.updateSelection();
},
unselectAll: function() {
$('#' + this.select_box_id).multiSelect('deselect_all');
this.updateSelection();
},
removeAll: function() {
$('#' + this.select_box_id + ' option').remove();
this.refresh();
},
removeAllNotSelected() {
$('#' + this.select_box_id + ' option:not(:selected)').remove();
this.refresh();
},
resetSearch() {
this.searchTerm = '';
this.removeAllNotSelected();
},
append(id, text, selected = false) {
if ($('#' + this.select_box_id + ' option[value=' + id + ']').length === 0) {
$('#' + this.select_box_id).multiSelect('addOption', {
value: id,
text: text,
disabled: selected
});
return 1;
}
return 0;
},
refresh() {
$('#' + this.select_box_id).multiSelect('refresh');
this.updateSelection();
},
updateCount(){
this.count = $('#' + this.select_box_id + ' option:enabled:selected').length;
$('#' + this.count_text_id).text(this.$gettextInterpolate('Sie haben %{ count } Personen ausgewählt', {count: this.count}));
},
async updateSelection() {
this.updateCount();
let selected_options = $('#' + this.select_box_id + ' option:enabled:selected');
let user_ids = [];
if (selected_options.length) {
for (const option of selected_options) {
user_ids.push(option.value);
}
}
let return_value = [];
if (this.withDetail && this.users.length) {
for (const user_id of user_ids) {
let existing_index = this.users.findIndex(user => {
return user.id === user_id;
});
if (existing_index !== -1) {
return_value.push(this.users[existing_index]);
}
}
} else {
return_value = user_ids;
}
this.$emit('input', return_value);
}
},
}
</script>
<template> <template>
<div class="cw-content-overview"> <div class="cw-content-overview">
<courseware-content-overview-elements /> <courseware-content-overview-elements />
<MountingPortal mountTo="#courseware-content-overview-action-widget" name="sidebar-views"> <MountingPortal mountTo="#courseware-content-overview-action-widget" name="sidebar-actions">
<courseware-content-overview-action-widget /> <courseware-content-overview-action-widget />
</MountingPortal> </MountingPortal>
<MountingPortal mountTo="#courseware-content-overview-filter-widget" name="sidebar-views"> <MountingPortal mountTo="#courseware-content-overview-filter-widget" name="sidebar-filters">
<courseware-content-overview-filter-widget /> <courseware-content-overview-filter-widget />
</MountingPortal> </MountingPortal>
</div> </div>
......
<template> <template>
<div class="cw-content-releases"> <div class="cw-content-releases">
<courseware-content-links /> <courseware-content-links />
<courseware-content-shared />
<courseware-companion-overlay /> <courseware-companion-overlay />
</div> </div>
</template> </template>
<script> <script>
import CoursewareContentLinks from './CoursewareContentLinks.vue'; import CoursewareContentLinks from './CoursewareContentLinks.vue';
import CoursewareContentShared from './CoursewareContentShared.vue';
import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue'; import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
export default { export default {
components: { components: {
CoursewareContentLinks, CoursewareContentLinks,
CoursewareContentShared,
CoursewareCompanionOverlay CoursewareCompanionOverlay
} },
} }
</script> </script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment