Skip to content
Snippets Groups Projects
Commit 2a47c310 authored by Michaela Brückner's avatar Michaela Brückner :unicorn:
Browse files

Merge branch 'main' of gitlab.studip.de:studip/studip into main

parents eabd634b 1c318b04
No related branches found
No related tags found
No related merge requests found
Showing
with 210 additions and 40 deletions
......@@ -703,7 +703,7 @@ class Course_BasicdataController extends AuthenticatedController
} else {
$sem = Seminar::getInstance($course_id);
$deputy = Deputy::find([$deputy_id, $course_id]);
if ($deputy && $deputy->delete) {
if ($deputy && $deputy->delete()) {
// Remove user from subcourses as well.
if($sem->children) {
$children_ids = $sem->children->pluck('seminar_id');
......
......@@ -36,7 +36,7 @@ class Settings_CalendarController extends Settings_SettingsController
PageLayout::setHelpKeyword('Basis.MyStudIPTerminkalender');
PageLayout::setTitle(_('Einstellungen des Terminkalenders anpassen'));
Navigation::activateItem('/profile/settings/calendar_new');
SkipLinks::addIndex(_('Einstellungen des Terminkalenders anpassen'), 'main_content', 100);
SkipLinks::addIndex(_('Einstellungen des Terminkalenders anpassen'), 'layout_content', 100);
}
/**
......
<?
// add skip link
SkipLinks::addIndex(_('Tagesansicht'), 'main_content', 100);
SkipLinks::addIndex(_('Tagesansicht'), 'layout_content', 100);
?>
<div style="width: 100%; display: flex; flex-wrap: wrap;">
<div style="flex-grow:2; flex-basis: 60%;">
......
<?
// add skip link
SkipLinks::addIndex(_('Wochenansicht'), 'main_content', 100);
SkipLinks::addIndex(_('Wochenansicht'), 'layout_content', 100);
$at = date('G', $atime);
if ($at >= $settings['start']
&& $at <= $settings['end'] || !$atime) {
......
......@@ -111,7 +111,7 @@
</select>
</label>
<label class="file-upload">
<label class="file-upload enter-accessible" tabindex="0">
<?= _('Vorschaubild hochladen') ?>
<input id="previewfile" name="previewfile" type="file" accept="image/*">
</label>
......
......@@ -227,3 +227,12 @@ $(document).on('change', 'input[data-must-equal]', function() {
this.setCustomValidity('');
}
});
//Generalisation: The enter-accessible class allows an element to be accessible via keyboard
//by triggering the click event when the enter key is pressed.
$(document).on('keydown', '.enter-accessible', function(event) {
if (event.code == 'Enter') {
//The enter key has been pressed.
$(this).trigger('click');
}
});
......@@ -143,7 +143,8 @@ const Search = {
$(`a#search_category_${name}`)
.removeClass('no-result')
.text(`${value.name} (${counter}${value.plus ? '+' : ''})`);
.text(`${value.name} (${counter}${value.plus ? '+' : ''})`)
.attr('tabindex', '0');
// We have more search results than shown, provide link to
// full search if available.
......@@ -326,7 +327,7 @@ const Search = {
* Grey out all categories in the sidebar with no results.
*/
greyOutSearchCategories: function () {
$('a[id^="search_category_"]').addClass('no-result');
$('a[id^="search_category_"]').addClass('no-result').attr('tabindex', '-1');
},
/**
......@@ -371,6 +372,7 @@ const Search = {
$('#show_all_categories').closest('li').addClass('active');
} else {
$(`#search_category_${category}`).closest('li').addClass('active');
$(`#search_category_${category}`).attr('tabindex', '0');
}
STUDIP.Search.showFilter(category);
......
......@@ -4196,18 +4196,17 @@ vSelect end
margin: 0;
list-style: none;
}
button {
width: 100%;
border: solid thin $content-color-40;
background-color: $white;
padding: 1em;
margin-bottom: 4px;
.cw-manager-copy-selector-course {
color: $base-color;
cursor: pointer;
outline: none;
line-height: 2em;
&:hover {
color:$white;
background-color: $base-color;
color: $active-color;
}
img {
vertical-align: text-bottom;
}
}
}
......
......@@ -47,12 +47,12 @@
<div v-html="comment.html" class="html"></div>
<textarea class="edit"
v-html="comment.content"
@keyup.enter.exact="saveComment"
@keydown.enter.exact="saveComment"
@keyup.escape.exact="editComment"></textarea>
</div>
<div class="time">
<studip-date-time :timestamp="comment.mkdate" :relative="true"></studip-date-time>
<a href="" v-if="comment.writable" @click.prevent="editComment" class="edit_comment" :title="$gettext('Bearbeiten.')">
<a href="" v-if="comment.writable" @click.prevent.stop="editComment" class="edit_comment" :title="$gettext('Bearbeiten.')">
<studip-icon shape="edit" size="14" role="inactive"></studip-icon>
</a>
<a href="" @click.prevent="answerComment" class="answer_comment" :title="$gettext('Hierauf antworten.')">
......
......@@ -36,6 +36,7 @@ export default {
...mapGetters({
oerEnabled: 'oerEnabled',
oerTitle: 'oerTitle',
userId: 'userId',
}),
isRoot() {
if (!this.structuralElement) {
......@@ -53,6 +54,18 @@ export default {
currentId() {
return this.structuralElement?.id;
},
blocked() {
return this.structuralElement?.relationships['edit-blocker'].data !== null;
},
blockerId() {
return this.blocked ? this.structuralElement?.relationships['edit-blocker'].data?.id : null;
},
blockedByThisUser() {
return this.blocked && this.userId === this.blockerId;
},
blockedByAnotherUser() {
return this.blocked && this.userId !== this.blockerId;
},
},
methods: {
...mapActions({
......@@ -67,7 +80,22 @@ export default {
lockObject: 'lockObject',
}),
async editElement() {
if (this.blockedByAnotherUser) {
this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
return false;
}
try {
await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
} catch(error) {
if (error.status === 409) {
this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
} else {
console.log(error);
}
return false;
}
this.showElementEditDialog(true);
},
async deleteElement() {
......
......@@ -150,10 +150,10 @@ export default {
return this.blocked ? this.block?.relationships['edit-blocker'].data?.id : null;
},
blockedByThisUser() {
return this.userId === this.blockerId;
return this.blocked && this.userId === this.blockerId;
},
blockedByAnotherUser() {
return this.userId !== this.blockerId;
return this.blocked && this.userId !== this.blockerId;
},
blockTitle() {
const type = this.block.attributes['block-type'];
......@@ -181,6 +181,9 @@ export default {
loadContainer: 'loadContainer',
}),
async displayFeature(element) {
if (this.showEdit && element === 'Edit') {
return false;
}
this.showFeatures = false;
this.showFeedback = false;
this.showComments = false;
......@@ -190,8 +193,20 @@ export default {
this.showContent = true;
if (element) {
if (element === 'Edit') {
await this.loadContainer(this.block.relationships.container.data.id);
if (!this.blocked) {
try {
await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
} catch(error) {
if (error.status === 403) {
this.companionInfo({ info: this.$gettext('Dieser Block wird bereits bearbeitet.') });
} else {
console.log(error);
}
return false;
}
if (!this.preview) {
this.showContent = false;
}
......
......@@ -50,7 +50,7 @@
<script>
import CoursewareContainerActions from './CoursewareContainerActions.vue';
import StudipDialog from '../StudipDialog.vue';
import { mapActions } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'courseware-default-container',
......@@ -76,12 +76,27 @@ export default {
};
},
computed: {
...mapGetters({
userId: 'userId',
}),
showEditMode() {
return this.$store.getters.viewMode === 'edit';
},
colSpan() {
return this.container.attributes.payload.colspan ? this.container.attributes.payload.colspan : 'full';
},
blocked() {
return this.container?.relationships['edit-blocker'].data !== null;
},
blockerId() {
return this.blocked ? this.container?.relationships['edit-blocker'].data?.id : null;
},
blockedByThisUser() {
return this.blocked && this.userId === this.blockerId;
},
blockedByAnotherUser() {
return this.blocked && this.userId !== this.blockerId;
},
},
methods: {
...mapActions({
......@@ -90,7 +105,23 @@ export default {
unlockObject: 'unlockObject',
}),
async displayEditDialog() {
if (this.blockedByAnotherUser) {
this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
return false;
}
try {
await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
} catch(error) {
if (error.status === 409) {
this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
} else {
console.log(error);
}
return false;
}
this.showEditDialog = true;
},
async closeEdit() {
......
<template>
<div class="cw-manager-copy-selector">
<div v-if="sourceEmpty" class="cw-manager-copy-selector-source">
<button class="hugebutton" @click="selectSource('own'); loadOwnCourseware()"><translate>Aus meine Inhalte kopieren</translate></button>
<button class="hugebutton" @click="selectSource('remote')"><translate>Aus Veranstaltung kopieren</translate></button>
<button class="button" @click="selectSource('own'); loadOwnCourseware()"><translate>Aus meine Inhalte kopieren</translate></button>
<button class="button" @click="selectSource('remote')"><translate>Aus Veranstaltung kopieren</translate></button>
</div>
<div v-else>
<button class="button" @click="reset"><translate>Quelle auswählen</translate></button>
<button v-show="!sourceOwn && hasRemoteCid" class="button" @click="selectNewCourse"><translate>Veranstaltung auswählen</translate></button>
<div v-if="sourceRemote">
<h2 v-if="!hasRemoteCid"><translate>Veranstaltungen</translate></h2>
<ul v-if="!hasRemoteCid">
<li v-for="course in courses" :key="course.id" >
<button class="hugebutton" @click="loadRemoteCourseware(course.id)">{{course.attributes.title}}</button>
<li v-for="semester in semesterMap" :key="semester.id">
<h3>{{semester.attributes.title}}</h3>
<ul>
<li
v-for="course in coursesBySemester(semester)"
:key="course.id"
class="cw-manager-copy-selector-course"
@click="loadRemoteCourseware(course.id)"
>
<studip-icon :shape="getCourseIcon(course)" />
{{course.attributes.title}}
</li>
</ul>
</li>
</ul>
<courseware-manager-element
v-if="hasRemoteCid"
v-if="remoteId !== '' && hasRemoteCid"
type="remote"
:currentElement="remoteElement"
@selectElement="setRemoteId"
@loadSelf="loadSelf"
/>
<courseware-companion-box
v-if="remoteId === '' && hasRemoteCid"
:msgCompanion="$gettext('In dieser Veranstaltung wurden noch keine Inhalte angelegt')"
mood="sad"
/>
</div>
<div v-if="sourceOwn">
<courseware-manager-element
......@@ -61,12 +79,14 @@ export default {
ownCoursewareInstance: {},
ownId: '',
ownElement: {},
semesterMap: [],
}},
computed: {
...mapGetters({
userId: 'userId',
structuralElementById: 'courseware-structural-elements/byId',
semesterById: 'semesters/byId',
}),
sourceEmpty() {
return this.source === '';
......@@ -80,12 +100,16 @@ export default {
hasRemoteCid() {
return this.remoteCid !== '';
},
loadedCourses() {
return this.courses.sort((a, b) => a.attributes.title > b.attributes.title);
}
},
methods: {
...mapActions({
loadUsersCourses: 'loadUsersCourses',
loadStructuralElement: 'loadStructuralElement',
loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure',
loadSemester: 'semesters/loadById',
}),
selectSource(source) {
this.source = source;
......@@ -96,7 +120,7 @@ export default {
if (this.remoteCoursewareInstance !== null) {
this.setRemoteId(this.remoteCoursewareInstance.relationships.root.data.id);
} else {
console.debug('can not load');
this.remoteId = '';
}
},
......@@ -105,14 +129,17 @@ export default {
if (this.ownCoursewareInstance !== null) {
this.setOwnId(this.ownCoursewareInstance.relationships.root.data.id);
} else {
console.debug('can not load');
this.ownId = '';
}
},
reset() {
this.selectSource('');
this.remoteCid = '';
},
selectNewCourse() {
this.remoteCid = '';
this.remoteId = '';
},
async setRemoteId(target) {
this.remoteId = target;
await this.loadStructuralElement(this.remoteId);
......@@ -131,10 +158,41 @@ export default {
},
loadSelf(data) {
this.$emit('loadSelf', data);
},
loadSemesterMap() {
let view = this;
let semesters = [];
this.courses.every(course => {
let semId = course.relationships['start-semester'].data.id;
if(!semesters.includes(semId)) {
semesters.push(semId);
}
return true;
});
semesters.every(semester => {
view.loadSemester({id: semester}).then( () => {
view.semesterMap.push(view.semesterById({id: semester}));
view.semesterMap.sort((a, b) => a.attributes.start < b.attributes.start);
});
return true;
});
},
coursesBySemester(semester) {
return this.loadedCourses.filter(course => {
return course.relationships['start-semester'].data.id === semester.id}
);
},
getCourseIcon(course) {
if (course.attributes['course-type'] === 99) {
return 'studygroup';
}
return 'seminar';
}
},
async mounted() {
this.courses = await this.loadUsersCourses(this.userId);
this.loadSemesterMap();
}
}
......
......@@ -555,6 +555,7 @@ export default {
licenses: 'licenses',
exportState: 'exportState',
exportProgress: 'exportProgress',
userId: 'userId',
}),
currentId() {
......@@ -907,6 +908,18 @@ export default {
return '';
},
blocked() {
return this.structuralElement?.relationships['edit-blocker'].data !== null;
},
blockerId() {
return this.blocked ? this.structuralElement?.relationships['edit-blocker'].data?.id : null;
},
blockedByThisUser() {
return this.blocked && this.userId === this.blockerId;
},
blockedByAnotherUser() {
return this.blocked && this.userId !== this.blockerId;
},
},
methods: {
......@@ -936,7 +949,22 @@ export default {
async menuAction(action) {
switch (action) {
case 'editCurrentElement':
if (this.blockedByAnotherUser) {
this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
return false;
}
try {
await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
} catch(error) {
if (error.status === 409) {
this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
} else {
console.log(error);
}
return false;
}
this.showElementEditDialog(true);
break;
case 'addElement':
......
......@@ -10,7 +10,7 @@
tab.icon !== '' && tab.name === '' ? 'cw-tabs-nav-icon-solo-' + tab.icon : '',
]"
:href="tab.href"
:tabindex="index"
tabindex="0"
@click="selectTab(tab)"
@keydown.enter="selectTab(tab)"
@keydown.space="selectTab(tab)"
......
......@@ -16,7 +16,7 @@
<div class="contentbar-icons">
<? if ($toc->hasChildren()) : ?>
<input type="checkbox" id="cb-toc">
<label for="cb-toc" class="check-box" title="<?= _('Inhaltsverzeichnis') ?>" >
<label for="cb-toc" class="check-box enter-accessible" title="<?= _('Inhaltsverzeichnis') ?>" tabindex="0">
<?= Icon::create('table-of-contents')->asImg(24) ?>
</label>
<?= $ttpl->render() ?>
......
......@@ -2,7 +2,7 @@
<article class="toc_overview toc_transform" id="toc">
<header id="toc_header">
<h1 id="toc_h1"><?= sprintf(_('Inhalt (%u Elemente)'), htmlReady($root->countAllChildren())) ?></h1>
<label for="cb-toc" class="check-box" title="<?= _('Schließen')?>">
<label for="cb-toc" class="check-box enter-accessible" title="<?= _('Schließen')?>" tabindex="0">
<?= Icon::create('decline')->asImg(24) ?>
</label>
</header>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment