Skip to content
Snippets Groups Projects
Commit 06bc1d02 authored by Ron Lucke's avatar Ron Lucke Committed by David Siegfried
Browse files

StEP00365: Suchfunktion auf Courswareseiten

Merge request !636
parent bb5570a7
Branches
No related tags found
No related merge requests found
Showing
with 504 additions and 9 deletions
...@@ -123,6 +123,12 @@ class Contents_CoursewareController extends AuthenticatedController ...@@ -123,6 +123,12 @@ class Contents_CoursewareController extends AuthenticatedController
); );
$sidebar->addWidget($actions)->addLayoutCSSClass('courseware-action-widget'); $sidebar->addWidget($actions)->addLayoutCSSClass('courseware-action-widget');
$views = new TemplateWidget(
_('Suche'),
$this->get_template_factory()->open('course/courseware/search_widget')
);
$sidebar->addWidget($views)->addLayoutCSSClass('courseware-search-widget');
$views = new \TemplateWidget( $views = new \TemplateWidget(
_('Ansichten'), _('Ansichten'),
$this->get_template_factory()->open('course/courseware/view_widget') $this->get_template_factory()->open('course/courseware/view_widget')
......
...@@ -116,6 +116,12 @@ class Course_CoursewareController extends AuthenticatedController ...@@ -116,6 +116,12 @@ class Course_CoursewareController extends AuthenticatedController
); );
$sidebar->addWidget($actions)->addLayoutCSSClass('courseware-action-widget'); $sidebar->addWidget($actions)->addLayoutCSSClass('courseware-action-widget');
$views = new TemplateWidget(
_('Suche'),
$this->get_template_factory()->open('course/courseware/search_widget')
);
$sidebar->addWidget($views)->addLayoutCSSClass('courseware-search-widget');
$views = new TemplateWidget( $views = new TemplateWidget(
_('Ansichten'), _('Ansichten'),
$this->get_template_factory()->open('course/courseware/view_widget') $this->get_template_factory()->open('course/courseware/view_widget')
......
<aside id="courseware-search-widget" class="widget-sidebar"></aside>
<?php
final class CoursewareSearch extends Migration
{
public function description()
{
return "Add Courseware to global search.";
}
public function up()
{
$statement = DBManager::get()->prepare("
SELECT *
FROM config
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute();
$config = $statement->fetch(PDO::FETCH_ASSOC);
$config['value'] = json_decode($config['value'], true);
$config['value']['GlobalSearchCourseware'] = [
'order' => 14,
'active' => true,
'fulltext' => true
];
$statement = DBManager::get()->prepare("
UPDATE config
SET `value` = :json
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute([
'json' => json_encode($config['value'])
]);
$statement = DBManager::get()->prepare("
SELECT *
FROM config_values
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute();
$config = $statement->fetch(PDO::FETCH_ASSOC);
if ($config) {
$config['value'] = json_decode($config['value'], true);
$config['value']['GlobalSearchCourseware'] = [
'order' => 14,
'active' => true,
'fulltext' => true
];
$statement = DBManager::get()->prepare("
UPDATE config_values
SET `value` = :json
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute([
'json' => json_encode($config['value'])
]);
}
}
public function down()
{
$statement = DBManager::get()->prepare("
SELECT *
FROM config_values
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute();
$config = $statement->fetch(PDO::FETCH_ASSOC);
if ($config) {
$config['value'] = json_decode($config['value'], true);
unset($config['value']['GlobalSearchCourseware']);
$statement = DBManager::get()->prepare("
UPDATE config_values
SET `value` = :json
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute([
'json' => json_encode($config['value'])
]);
}
$statement = DBManager::get()->prepare("
SELECT *
FROM config
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute();
$config = $statement->fetch(PDO::FETCH_ASSOC);
$config['value'] = json_decode($config['value'], true);
unset($config['value']['GlobalSearchCourseware']);
$statement = DBManager::get()->prepare("
UPDATE config
SET `value` = :json
WHERE field = 'GLOBALSEARCH_MODULES'
");
$statement->execute([
'json' => json_encode($config['value'])
]);
}
}
<?php
use Courseware\StructuralElement;
use Courseware\Block;
use Courseware\Container;
/**
* Global search module for files
*
* @author Ron Lucke <lucke@elan-ev.de>
* @category Stud.IP
* @since 5.2
*/
class GlobalSearchCourseware extends GlobalSearchModule implements GlobalSearchFulltext
{
/**
* Returns the displayname for this module
*
* @return string
*/
public static function getName()
{
return _('Courseware');
}
/**
* Returns the filters that are displayed in the sidebar of the global search.
*
* @return array Filters for this class.
*/
public static function getFilters()
{
return [];
}
public static function getSQL($search, $filter, $limit)
{
if (!$search) {
return null;
}
$query = DBManager::get()->quote("%{$search}%");
if ($filter['rangeId']) {
$range_id = $filter['rangeId'];
$sql = "(SELECT `cw_structural_elements` . `id` AS id, CONCAT('', 'cw_structural_elements') AS type
FROM `cw_structural_elements`
WHERE (`title` LIKE {$query} OR `payload` LIKE {$query})
AND `range_id` = '{$range_id}'
ORDER BY `cw_structural_elements`.`mkdate` DESC)
UNION (
SELECT se . `id` AS id, CONCAT('', 'cw_containers') AS type
FROM `cw_containers` c
JOIN cw_structural_elements se
ON se . `id` = c . `structural_element_id`
WHERE c. `payload` LIKE {$query}
AND `container_type` != 'list'
AND se . `range_id` = '{$range_id}'
ORDER BY c . `mkdate` DESC)
UNION (
SELECT se . `id` AS id, CONCAT('', 'cw_blocks') AS type
FROM `cw_blocks` b
JOIN cw_containers c
ON c.id = b.container_id
JOIN cw_structural_elements se
ON se . `id` = c . `structural_element_id`
WHERE b.payload LIKE {$query}
AND se . `range_id` = '{$range_id}'
ORDER BY b . `mkdate` DESC
) LIMIT {$limit}";
} else {
$sql = "(SELECT `cw_structural_elements` . `id` AS id, CONCAT('', 'cw_structural_elements') AS type
FROM `cw_structural_elements`
WHERE (`title` LIKE {$query} OR `payload` LIKE {$query})
ORDER BY `cw_structural_elements`.`mkdate` DESC)
UNION (
SELECT se . `id` AS id, CONCAT('', 'cw_containers') AS type
FROM `cw_containers` c
JOIN cw_structural_elements se
ON se . `id` = c . `structural_element_id`
WHERE c. `payload` LIKE {$query}
AND `container_type` != 'list'
ORDER BY c . `mkdate` DESC)
UNION (
SELECT se . `id` AS id, CONCAT('', 'cw_blocks') AS type
FROM `cw_blocks` b
JOIN cw_containers c
ON c.id = b.container_id
JOIN cw_structural_elements se
ON se . `id` = c . `structural_element_id`
WHERE b.payload LIKE {$query}
ORDER BY b . `mkdate` DESC
) LIMIT {$limit}";
}
return $sql;
}
public static function filter($data, $search)
{
$user = $GLOBALS['user'];
$structural_element = StructuralElement::find($data['id']);
if ($structural_element->canRead($user)) {
if ($data['type'] === 'cw_structural_elements') {
$description = self::mark($structural_element->payload['description'], $search, true);
}
if ($data['type'] === 'cw_containers') {
$description = _('Suchbegriff wurde in einem Abschnitt gefunden');
}
if ($data['type'] === 'cw_blocks') {
$description = _('Suchbegriff wurde in einem Block gefunden');
}
$pageData = self::getPageData($structural_element);
$date = new DateTime();
$date->setTimestamp($structural_element->chdate);
return [
'name' => self::mark($structural_element->title, $search, true),
'description' => $description,
'url' => $pageData['url'],
'img' => $structural_element->image ? $structural_element->getImageUrl(): null,
'additional' => '<a href="' . $pageData['originUrl'] . '" title="' . $pageData['originName'] . '">' . $pageData['originName'] . '</a>',
'date' => $date->format('d.m.Y H:i'),
'structural-element-id' => $structural_element->id
];
}
return [];
}
private static function getPageData(StructuralElement $structural_element): Array
{
$url = '';
$originUrl = '';
$originName = '';
if ($structural_element->range_type === 'course') {
$url = URLHelper::getURL(
"dispatch.php/course/courseware?cid={$structural_element->range_id}#/structural_element/{$structural_element->id}",
[],
true);
$originUrl = URLHelper::getURL(
"dispatch.php/course/overview?cid={$structural_element->range_id}",
[],
true);
$originName = Course::find($structural_element->range_id)->name;
}
if ($structural_element->range_type === 'user') {
$url = URLHelper::getURL(
"dispatch.php/contents/courseware/courseware#/structural_element/{$structural_element->id}",
[],
true);
$originUrl = URLHelper::getURL(
"dispatch.php/contents/courseware/index",
[],
true);
$originName = _('Persönliche Lernmaterialien');
}
return array(
'url' => $url,
'originUrl' => $originUrl,
'originName' => $originName
);
}
public static function enable()
{
}
public static function disable()
{
}
public static function getSearchURL($searchterm)
{
return null;
}
}
...@@ -280,6 +280,10 @@ $consum_ribbon_width: calc(100% - 58px); ...@@ -280,6 +280,10 @@ $consum_ribbon_width: calc(100% - 58px);
.cw-ribbon-nav { .cw-ribbon-nav {
display: flex; display: flex;
min-width: 75px; min-width: 75px;
&.single-icon {
min-width: 45px;
}
} }
.cw-ribbon-breadcrumb { .cw-ribbon-breadcrumb {
...@@ -326,6 +330,12 @@ $consum_ribbon_width: calc(100% - 58px); ...@@ -326,6 +330,12 @@ $consum_ribbon_width: calc(100% - 58px);
.cw-ribbon-wrapper-right { .cw-ribbon-wrapper-right {
display: flex; display: flex;
button {
border: none;
background-color: transparent;
cursor: pointer;
}
} }
.cw-ribbon-button { .cw-ribbon-button {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div v-if="stickyRibbon" class="cw-ribbon-sticky-top"></div> <div v-if="stickyRibbon" class="cw-ribbon-sticky-top"></div>
<header class="cw-ribbon" :class="{ 'cw-ribbon-sticky': stickyRibbon, 'cw-ribbon-consume': consumeMode }"> <header class="cw-ribbon" :class="{ 'cw-ribbon-sticky': stickyRibbon, 'cw-ribbon-consume': consumeMode }">
<div class="cw-ribbon-wrapper-left"> <div class="cw-ribbon-wrapper-left">
<nav class="cw-ribbon-nav"> <nav class="cw-ribbon-nav" :class="buttonsClass">
<slot name="buttons" /> <slot name="buttons" />
</nav> </nav>
<nav class="cw-ribbon-breadcrumb"> <nav class="cw-ribbon-breadcrumb">
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<button <button
class="cw-ribbon-button cw-ribbon-button-menu" class="cw-ribbon-button cw-ribbon-button-menu"
:title="textRibbon.toolbar" :title="textRibbon.toolbar"
@click="activeToolbar" @click.prevent="activeToolbar"
> >
</button> </button>
<button <button
...@@ -54,6 +54,15 @@ export default { ...@@ -54,6 +54,15 @@ export default {
}, },
props: { props: {
canEdit: Boolean, canEdit: Boolean,
showToolbarButton: {
default: true,
type: Boolean
},
showModeSwitchButton: {
default: true,
type: Boolean
},
buttonsClass: String,
}, },
data() { data() {
return { return {
......
<template>
<div role="region" id="search" aria-live="polite">
<courseware-ribbon
:showToolbarButton="false"
:showModeSwitchButton="false"
buttonsClass="single-icon"
>
<template #buttons>
<studip-icon shape="search" size="24" />
</template>
<template #breadcrumbList>
<translate>Suchergebnisse</translate>
</template>
<template #menu>
<button :title="$gettext('Suchergebnisse schließen')" @click="closeResults">
<studip-icon shape="decline" size="24"/>
</button>
</template>
</courseware-ribbon>
<div id="search-results">
<article v-if="searchResults.length > 0">
<section v-for="result in searchResults" :key="result['structural-element-id']">
<router-link
:to="'/structural_element/' + result['structural-element-id']"
@click.native="closeResults"
>
<div v-show="result.img !== null" class="search-result-img hidden-tiny-down">
<img :src="result.img" />
</div>
<div class="search-result-data">
<div class="search-result-title" v-html="result.name"></div>
<div class="search-result-details">
<div class="search-result-description" v-html="result.description"></div>
</div>
</div>
<div class="search-result-information">
<div class="search-result-time" v-html="result.date"></div>
</div>
</router-link>
</section>
</article>
<courseware-companion-box
v-else
:msgCompanion="$gettext('Es wurden keine Suchergebnisse gefunden.')"
mood="sad"
/>
</div>
</div>
</template>
<script>
import CoursewareRibbon from './CoursewareRibbon.vue';
import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
import StudipIcon from '../StudipIcon.vue';
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'courseware-search-results',
components: {
CoursewareRibbon,
CoursewareCompanionBox,
StudipIcon
},
computed: {
...mapGetters({
searchResults: 'searchResults'
}),
},
methods: {
...mapActions({
setShowSearchResults: 'setShowSearchResults',
setSearchResults: 'setSearchResults',
}),
closeResults() {
this.setShowSearchResults(false);
this.setSearchResults([]);
},
}
}
</script>
<template>
<form class="sidebar-search" @submit.prevent="">
<ul class="needles">
<li>
<form @submit.prevent="">
<input type="text" v-model="searchTerm"/>
<input
type="submit"
:value="$gettext('Suchen')"
aria-controls="search"
@click="loadResults"
/>
</form>
</li>
</ul>
</form>
</template>
<script>
import axios from 'axios';
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'courseware-search-widget',
data() {
return {
searchTerm: '',
}
},
computed: {
...mapGetters({
courseware: 'courseware',
context: 'context',
}),
},
methods: {
...mapActions({
setShowSearchResults: 'setShowSearchResults',
setSearchResults: 'setSearchResults',
companionWarning: 'companionWarning'
}),
loadResults() {
if (this.searchTerm.length < 3) {
this.companionWarning({ info: this.$gettext('Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.')});
return;
}
const limit = 100;
let params = {
search: this.searchTerm,
filters: { category: 'GlobalSearchCourseware', contextType: this.context.type, rangeId: this.context.id}
};
axios({
method: 'get',
url: STUDIP.URLHelper.getURL('dispatch.php/globalsearch/find/' + limit),
params: params,
}).then( result => {
this.setShowSearchResults(true);
if (result.data.GlobalSearchCourseware) {
this.setSearchResults((result.data.GlobalSearchCourseware.content));
} else {
this.setSearchResults([]);
}
}).catch(error => {
console.debug(error);
});
}
}
}
</script>
...@@ -170,8 +170,6 @@ ...@@ -170,8 +170,6 @@
</div> </div>
</div> </div>
<courseware-companion-overlay />
<studip-dialog <studip-dialog
v-if="showEditDialog" v-if="showEditDialog"
:title="textEdit.title" :title="textEdit.title"
...@@ -581,7 +579,6 @@ import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue'; ...@@ -581,7 +579,6 @@ import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue';
import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
import CoursewareWellcomeScreen from './CoursewareWellcomeScreen.vue'; import CoursewareWellcomeScreen from './CoursewareWellcomeScreen.vue';
import CoursewareEmptyElementBox from './CoursewareEmptyElementBox.vue'; import CoursewareEmptyElementBox from './CoursewareEmptyElementBox.vue';
import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
import CoursewareListContainer from './CoursewareListContainer.vue'; import CoursewareListContainer from './CoursewareListContainer.vue';
import CoursewareTabsContainer from './CoursewareTabsContainer.vue'; import CoursewareTabsContainer from './CoursewareTabsContainer.vue';
import CoursewareRibbon from './CoursewareRibbon.vue'; import CoursewareRibbon from './CoursewareRibbon.vue';
...@@ -605,7 +602,6 @@ export default { ...@@ -605,7 +602,6 @@ export default {
CoursewareAccordionContainer, CoursewareAccordionContainer,
CoursewareTabsContainer, CoursewareTabsContainer,
CoursewareCompanionBox, CoursewareCompanionBox,
CoursewareCompanionOverlay,
CoursewareWellcomeScreen, CoursewareWellcomeScreen,
CoursewareEmptyElementBox, CoursewareEmptyElementBox,
CoursewareTabs, CoursewareTabs,
......
<template> <template>
<div> <div>
<div v-if="structureLoadingState === 'done'"> <div v-if="structureLoadingState === 'done'">
<courseware-search-results v-show="showSearchResults" />
<courseware-structural-element <courseware-structural-element
v-show="!showSearchResults"
:canVisit="canVisit" :canVisit="canVisit"
:structural-element="selected" :structural-element="selected"
:ordered-structural-elements="orderedStructuralElements" :ordered-structural-elements="orderedStructuralElements"
...@@ -10,12 +12,15 @@ ...@@ -10,12 +12,15 @@
<MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions"> <MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions">
<courseware-action-widget :structural-element="selected" :canVisit="canVisit"></courseware-action-widget> <courseware-action-widget :structural-element="selected" :canVisit="canVisit"></courseware-action-widget>
</MountingPortal> </MountingPortal>
<MountingPortal mountTo="#courseware-export-widget" name="sidebar-actions"> <MountingPortal mountTo="#courseware-search-widget" name="sidebar-search">
<courseware-export-widget :structural-element="selected" :canVisit="canVisit"></courseware-export-widget> <courseware-search-widget></courseware-search-widget>
</MountingPortal> </MountingPortal>
<MountingPortal mountTo="#courseware-view-widget" name="sidebar-views"> <MountingPortal mountTo="#courseware-view-widget" name="sidebar-views">
<courseware-view-widget :structural-element="selected" :canVisit="canVisit"></courseware-view-widget> <courseware-view-widget :structural-element="selected" :canVisit="canVisit"></courseware-view-widget>
</MountingPortal> </MountingPortal>
<MountingPortal mountTo="#courseware-export-widget" name="sidebar-export">
<courseware-export-widget :structural-element="selected" :canVisit="canVisit"></courseware-export-widget>
</MountingPortal>
</div> </div>
<studip-progress-indicator <studip-progress-indicator
v-if="structureLoadingState === 'loading'" v-if="structureLoadingState === 'loading'"
...@@ -27,26 +32,34 @@ ...@@ -27,26 +32,34 @@
mood="sad" mood="sad"
:msgCompanion="loadingErrorMessage" :msgCompanion="loadingErrorMessage"
/> />
<courseware-companion-overlay />
</div> </div>
</template> </template>
<script> <script>
import CoursewareStructuralElement from './CoursewareStructuralElement.vue'; import CoursewareStructuralElement from './CoursewareStructuralElement.vue';
import CoursewareSearchResults from './CoursewareSearchResults.vue';
import CoursewareViewWidget from './CoursewareViewWidget.vue'; import CoursewareViewWidget from './CoursewareViewWidget.vue';
import CoursewareActionWidget from './CoursewareActionWidget.vue'; import CoursewareActionWidget from './CoursewareActionWidget.vue';
import CoursewareExportWidget from './CoursewareExportWidget.vue'; import CoursewareExportWidget from './CoursewareExportWidget.vue';
import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
import CoursewareSearchWidget from './CoursewareSearchWidget.vue';
import CoursewareCompanionOverlay from './CoursewareCompanionOverlay.vue';
import StudipProgressIndicator from '../StudipProgressIndicator.vue'; import StudipProgressIndicator from '../StudipProgressIndicator.vue';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
export default { export default {
components: { components: {
CoursewareStructuralElement, CoursewareStructuralElement,
CoursewareSearchResults,
CoursewareViewWidget, CoursewareViewWidget,
CoursewareActionWidget, CoursewareActionWidget,
CoursewareCompanionBox, CoursewareCompanionBox,
StudipProgressIndicator, StudipProgressIndicator,
CoursewareExportWidget CoursewareExportWidget,
CoursewareSearchWidget,
CoursewareCompanionOverlay,
}, },
data: () => ({ data: () => ({
canVisit: null, canVisit: null,
...@@ -61,6 +74,7 @@ export default { ...@@ -61,6 +74,7 @@ export default {
courseware: 'courseware', courseware: 'courseware',
orderedStructuralElements: 'courseware-structure/ordered', orderedStructuralElements: 'courseware-structure/ordered',
relatedStructuralElement: 'courseware-structural-elements/related', relatedStructuralElement: 'courseware-structural-elements/related',
showSearchResults: 'showSearchResults',
structuralElementLastMeta: 'courseware-structural-elements/lastMeta', structuralElementLastMeta: 'courseware-structural-elements/lastMeta',
structuralElements: 'courseware-structural-elements/all', structuralElements: 'courseware-structural-elements/all',
structuralElementById: 'courseware-structural-elements/byId', structuralElementById: 'courseware-structural-elements/byId',
......
...@@ -54,6 +54,9 @@ const getDefaultState = () => { ...@@ -54,6 +54,9 @@ const getDefaultState = () => {
showOverviewElementAddDialog: false, showOverviewElementAddDialog: false,
bookmarkFilter: 'all', bookmarkFilter: 'all',
showSearchResults: false,
searchResults: [],
}; };
}; };
...@@ -204,6 +207,12 @@ const getters = { ...@@ -204,6 +207,12 @@ const getters = {
bookmarkFilter(state) { bookmarkFilter(state) {
return state.bookmarkFilter; return state.bookmarkFilter;
}, },
showSearchResults(state) {
return state.showSearchResults;
},
searchResults(state) {
return state.searchResults;
},
}; };
export const state = { ...initialState }; export const state = { ...initialState };
...@@ -844,6 +853,14 @@ export const actions = { ...@@ -844,6 +853,14 @@ export const actions = {
commit('setExportProgress', percent); commit('setExportProgress', percent);
}, },
setShowSearchResults({ commit }, state) {
commit('setShowSearchResults', state);
},
setSearchResults({ commit }, state) {
commit('setSearchResults', state);
},
addBookmark({ dispatch, rootGetters }, structuralElement) { addBookmark({ dispatch, rootGetters }, structuralElement) {
const cw = rootGetters['courseware']; const cw = rootGetters['courseware'];
...@@ -1342,6 +1359,12 @@ export const mutations = { ...@@ -1342,6 +1359,12 @@ export const mutations = {
setBookmarkFilter(state, course) { setBookmarkFilter(state, course) {
state.bookmarkFilter = course; state.bookmarkFilter = course;
}, },
setShowSearchResults(state, searchState) {
state.showSearchResults = searchState;
},
setSearchResults(state, results) {
state.searchResults = results;
},
}; };
export default { export default {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment