Merge pull request #1324 from albertgasset/MOBILE-2342

Mobile 2342
main
Juan Leyva 2018-05-30 11:05:39 +02:00 committed by GitHub
commit 5dd048cff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 3309 additions and 11 deletions

View File

@ -0,0 +1,51 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModGlossaryIndexComponent } from './index/index';
import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker';
@NgModule({
declarations: [
AddonModGlossaryIndexComponent,
AddonModGlossaryModePickerPopoverComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonModGlossaryIndexComponent,
AddonModGlossaryModePickerPopoverComponent
],
entryComponents: [
AddonModGlossaryIndexComponent,
AddonModGlossaryModePickerPopoverComponent
]
})
export class AddonModGlossaryComponentsModule {}

View File

@ -0,0 +1,64 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons end>
<button *ngIf="glossary" ion-button icon-only (click)="openModePicker($event)" [attr.aria-label]="'addon.mod_glossary.browsemode' | translate">
<ion-icon name="funnel"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="650" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="canAdd" [priority]="600" [content]="'addon.mod_glossary.addentry' | translate" (action)="openNewEntry()" iconAction="add"></core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-split-view>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
<!-- Glossary entries found to be synchronized -->
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
</ion-card>
<core-search-box *ngIf="viewMode == 'search'" (onSubmit)="search($event)" [placeholder]="'addon.mod_glossary.searchquery' | translate" [autoFocus]="true" [lengthCheck]="2" [showClear]="false"></core-search-box>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-list *ngIf="viewMode != 'search' && offlineEntries.length > 0">
<ion-item-divider color="light">
{{ 'addon.mod_glossary.entriestobesynced' | translate }}
</ion-item-divider>
<ion-item *ngFor="let entry of offlineEntries" (click)="openNewEntry(entry)">
<p>{{entry.concept}}</p>
</ion-item>
</ion-list>
<ion-list *ngIf="entries.length > 0">
<ng-container *ngFor="let entry of entries; let index = index">
<ion-item-divider color="light" *ngIf="showDivider(entry, entries[index - 1])">
{{getDivider(entry)}}
</ion-item-divider>
<ion-item (click)="openEntry(entry.id)" [class.core-split-item-selected]="entry.id == selectedEntry">
<p>{{entry.concept}}</p>
</ion-item>
</ng-container>
</ion-list>
<core-empty-box *ngIf="!entries.length && !offlineEntries.length" icon="list" [message]="'addon.mod_glossary.noentriesfound' | translate"></core-empty-box>
<ion-infinite-scroll [enabled]="canLoadMore" (ionInfinite)="$event.waitFor(loadMoreEntries())">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</core-loading>
</ion-content>
</core-split-view>

View File

@ -0,0 +1,400 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Injector, ViewChild } from '@angular/core';
import { Content, PopoverController } from 'ionic-angular';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { AddonModGlossaryProvider } from '../../providers/glossary';
import { AddonModGlossaryOfflineProvider } from '../../providers/offline';
import { AddonModGlossarySyncProvider } from '../../providers/sync';
import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker';
type FetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'search' | 'letter_all';
/**
* Component that displays a glossary entry page.
*/
@Component({
selector: 'addon-mod-glossary-index',
templateUrl: 'index.html',
})
export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
@ViewChild(Content) content: Content;
component = AddonModGlossaryProvider.COMPONENT;
moduleName = 'glossary';
fetchMode: FetchMode;
viewMode: string;
isSearch = false;
entries = [];
offlineEntries = [];
canAdd = false;
canLoadMore = false;
loadingMessage = this.translate.instant('core.loading');
selectedEntry: number;
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
protected glossary: any;
protected fetchFunction: Function;
protected fetchInvalidate: Function;
protected fetchArguments: any[];
protected showDivider: (entry: any, previous?: any) => boolean;
protected getDivider: (entry: any) => string;
protected addEntryObserver: any;
constructor(injector: Injector,
private popoverCtrl: PopoverController,
private glossaryProvider: AddonModGlossaryProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider,
private glossarySync: AddonModGlossarySyncProvider) {
super(injector);
}
/**
* Component being initialized.
*/
ngOnInit(): void {
super.ngOnInit();
// When an entry is added, we reload the data.
this.addEntryObserver = this.eventsProvider.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, this.eventReceived.bind(this));
this.loadContent(false, true).then(() => {
if (!this.glossary) {
return;
}
if (this.splitviewCtrl.isOn()) {
// Load the first entry.
if (this.entries.length > 0) {
this.openEntry(this.entries[0].id);
}
}
this.glossaryProvider.logView(this.glossary.id, this.viewMode).then(() => {
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
}).catch((error) => {
// Ignore errors.
});
});
}
/**
* Download the component contents.
*
* @param {boolean} [refresh=false] Whether we're refreshing data.
* @param {boolean} [sync=false] If the refresh needs syncing.
* @param {boolean} [showErrors=false] Wether to show errors to the user or hide them.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
return this.glossaryProvider.getGlossary(this.courseId, this.module.id).then((glossary) => {
this.glossary = glossary;
this.description = glossary.intro || this.description;
this.canAdd = (this.glossaryProvider.isPluginEnabledForEditing() && glossary.canaddentry) || false;
if (!this.fetchMode) {
this.switchMode('letter_all');
}
if (sync) {
// Try to synchronize the glossary.
return this.syncActivity(showErrors);
}
}).then(() => {
return this.fetchEntries().then(() => {
// Check if there are responses stored in offline.
return this.glossaryOffline.getGlossaryNewEntries(this.glossary.id).then((offlineEntries) => {
offlineEntries.sort((a, b) => a.concept.localeCompare(b.fullname));
this.hasOffline = !!offlineEntries.length;
this.offlineEntries = offlineEntries || [];
});
});
}).then(() => {
// All data obtained, now fill the context menu.
this.fillContextMenu(refresh);
});
}
/**
* Convenience function to fetch entries.
*
* @param {boolean} [append=false] True if fetched entries are appended to exsiting ones.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchEntries(append: boolean = false): Promise<any> {
if (!this.fetchFunction || !this.fetchArguments) {
// This happens in search mode with an empty query.
return Promise.resolve({entries: [], count: 0});
}
const limitFrom = append ? this.entries.length : 0;
const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES;
return this.glossaryProvider.fetchEntries(this.fetchFunction, this.fetchArguments, limitFrom, limitNum).then((result) => {
if (append) {
Array.prototype.push.apply(this.entries, result.entries);
} else {
this.entries = result.entries;
}
this.canLoadMore = this.entries.length < result.count;
}).catch((error) => {
this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading.
return Promise.reject(error);
});
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
const promises = [];
if (this.fetchInvalidate && this.fetchArguments) {
promises.push(this.fetchInvalidate.apply(this.glossaryProvider, this.fetchArguments));
}
promises.push(this.glossaryProvider.invalidateCourseGlossaries(this.courseId));
if (this.glossary && this.glossary.id) {
promises.push(this.glossaryProvider.invalidateCategories(this.glossary.id));
}
return Promise.all(promises);
}
/**
* Performs the sync of the activity.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected sync(): Promise<boolean> {
return this.glossarySync.syncGlossaryEntries(this.glossary.id);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param {any} result Data returned on the sync function.
* @return {boolean} Whether it succeed or not.
*/
protected hasSyncSucceed(result: any): boolean {
return result.updated;
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*
* @param {any} syncEventData Data receiven on sync observer.
* @return {boolean} True if refresh is needed, false otherwise.
*/
protected isRefreshSyncNeeded(syncEventData: any): boolean {
return this.glossary && syncEventData.glossaryId == this.glossary.id &&
syncEventData.userId == this.sitesProvider.getCurrentSiteUserId();
}
/**
* Change fetch mode.
*
* @param {FetchMode} mode New mode.
*/
protected switchMode(mode: FetchMode): void {
this.fetchMode = mode;
switch (mode) {
case 'author_all':
// Browse by author.
this.viewMode = 'author';
this.fetchFunction = this.glossaryProvider.getEntriesByAuthor;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByAuthor;
this.fetchArguments = [this.glossary.id, 'ALL', 'LASTNAME', 'ASC'];
this.getDivider = (entry: any): string => entry.userfullname;
this.showDivider = (entry: any, previous?: any): boolean => {
return typeof previous === 'undefined' || entry.userid != previous.userid;
};
break;
case 'cat_all':
// Browse by category.
this.viewMode = 'cat';
this.fetchFunction = this.glossaryProvider.getEntriesByCategory;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByCategory;
this.fetchArguments = [this.glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES];
this.getDivider = (entry: any): string => entry.categoryname;
this.showDivider = (entry?: any, previous?: any): boolean => {
return !previous || this.getDivider(entry) != this.getDivider(previous);
};
break;
case 'newest_first':
// Newest first.
this.viewMode = 'date';
this.fetchFunction = this.glossaryProvider.getEntriesByDate;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByDate;
this.fetchArguments = [this.glossary.id, 'CREATION', 'DESC'];
this.getDivider = null;
this.showDivider = (): boolean => false;
break;
case 'recently_updated':
// Recently updated.
this.viewMode = 'date';
this.fetchFunction = this.glossaryProvider.getEntriesByDate;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByDate;
this.fetchArguments = [this.glossary.id, 'UPDATE', 'DESC'];
this.getDivider = null;
this.showDivider = (): boolean => false;
break;
case 'search':
// Search for entries.
this.viewMode = 'search';
this.fetchFunction = this.glossaryProvider.getEntriesBySearch;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesBySearch;
this.fetchArguments = null; // Dynamically set later.
this.getDivider = null;
this.showDivider = (): boolean => false;
break;
case 'letter_all':
default:
// Consider it is 'letter_all'.
this.viewMode = 'letter';
this.fetchMode = 'letter_all';
this.fetchFunction = this.glossaryProvider.getEntriesByLetter;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByLetter;
this.fetchArguments = [this.glossary.id, 'ALL'];
this.getDivider = (entry: any): string => entry.concept.substr(0, 1).toUpperCase();
this.showDivider = (entry?: any, previous?: any): boolean => {
return !previous || this.getDivider(entry) != this.getDivider(previous);
};
break;
}
}
/**
* Convenience function to load more forum discussions.
*
* @return {Promise<any>} Promise resolved when done.
*/
loadMoreEntries(): Promise<any> {
return this.fetchEntries(true).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
});
}
/**
* Show the mode picker menu.
*
* @param {MouseEvent} event Event.
*/
openModePicker(event: MouseEvent): void {
const popover = this.popoverCtrl.create(AddonModGlossaryModePickerPopoverComponent, {
glossary: this.glossary,
selectedMode: this.fetchMode
});
popover.onDidDismiss((newMode: FetchMode) => {
if (newMode === this.fetchMode) {
return;
}
this.loadingMessage = this.translate.instant('core.loading');
this.content.scrollToTop();
this.switchMode(newMode);
if (this.fetchMode === 'search') {
// If it's not an instant search, then we reset the values.
this.entries = [];
this.canLoadMore = false;
} else {
this.loaded = false;
this.loadContent();
}
});
popover.present({
ev: event
});
}
/**
* Opens an entry.
*
* @param {number} entryId Entry id.
*/
openEntry(entryId: number): void {
const params = {
courseId: this.courseId,
entryId: entryId,
};
this.splitviewCtrl.push('AddonModGlossaryEntryPage', params);
this.selectedEntry = entryId;
}
/**
* Opens new entry editor.
*
* @param {any} [entry] Offline entry to edit.
*/
openNewEntry(entry?: any): void {
const params = {
courseId: this.courseId,
module: this.module,
glossary: this.glossary,
entry: entry,
};
this.splitviewCtrl.getMasterNav().push('AddonModGlossaryEditPage', params);
this.selectedEntry = 0;
}
/**
* Search entries.
*
* @param {string} query Text entered on the search box.
*/
search(query: string): void {
this.loadingMessage = this.translate.instant('core.searching');
this.fetchArguments = [this.glossary.id, query, 1, 'CONCEPT', 'ASC'];
this.loaded = false;
this.loadContent();
}
/**
* Function called when we receive an event of new entry.
*
* @param {any} data Event data.
*/
protected eventReceived(data: any): void {
if (this.glossary && this.glossary.id === data.glossaryId) {
this.loaded = false;
this.loadContent();
// Check completion since it could be configured to complete once the user adds a new discussion or replies.
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
}
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.addEntryObserver && this.addEntryObserver.off();
}
}

View File

@ -0,0 +1,6 @@
<ion-list radio-group [(ngModel)]="selectedMode">
<ion-item text-wrap *ngFor="let mode of modes" >
<ion-label>{{mode.langkey | translate}}</ion-label>
<ion-radio [value]="mode.key" (ionSelect)="modePicked($event, mode.key)" ></ion-radio>
</ion-item>
</ion-list>

View File

@ -0,0 +1,69 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { NavParams, ViewController } from 'ionic-angular';
/**
* Component to display the mode picker.
*/
@Component({
selector: 'addon-mod-glossary-mode-picker-popover',
templateUrl: 'mode-picker.html'
})
export class AddonModGlossaryModePickerPopoverComponent {
modes = [];
selectedMode: string;
constructor(navParams: NavParams, private viewCtrl: ViewController) {
this.selectedMode = navParams.get('selectedMode');
const glossary = navParams.get('glossary');
// Preparing browse modes.
this.modes = [
{key: 'search', langkey: 'addon.mod_glossary.bysearch'}
];
glossary.browsemodes.forEach((mode) => {
switch (mode) {
case 'letter' :
this.modes.push({key: 'letter_all', langkey: 'addon.mod_glossary.byalphabet'});
break;
case 'cat' :
this.modes.push({key: 'cat_all', langkey: 'addon.mod_glossary.bycategory'});
break;
case 'date' :
this.modes.push({key: 'newest_first', langkey: 'addon.mod_glossary.bynewestfirst'});
this.modes.push({key: 'recently_updated', langkey: 'addon.mod_glossary.byrecentlyupdated'});
break;
case 'author' :
this.modes.push({key: 'author_all', langkey: 'addon.mod_glossary.byauthor'});
break;
default:
}
});
}
/**
* Function called when a mode is clicked.
*
* @param {Event} event Click event.
* @param {string} key Clicked mode key.
* @return {boolean} Return true if success, false if error.
*/
modePicked(event: Event, key: string): boolean {
this.viewCtrl.dismiss(key);
return true;
}
}

View File

@ -0,0 +1,60 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreCronDelegate } from '@providers/cron';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { AddonModGlossaryProvider } from './providers/glossary';
import { AddonModGlossaryOfflineProvider } from './providers/offline';
import { AddonModGlossaryHelperProvider } from './providers/helper';
import { AddonModGlossarySyncProvider } from './providers/sync';
import { AddonModGlossaryModuleHandler } from './providers/module-handler';
import { AddonModGlossaryPrefetchHandler } from './providers/prefetch-handler';
import { AddonModGlossarySyncCronHandler } from './providers/sync-cron-handler';
import { AddonModGlossaryIndexLinkHandler } from './providers/index-link-handler';
import { AddonModGlossaryEntryLinkHandler } from './providers/entry-link-handler';
import { AddonModGlossaryComponentsModule } from './components/components.module';
@NgModule({
declarations: [
],
imports: [
AddonModGlossaryComponentsModule,
],
providers: [
AddonModGlossaryProvider,
AddonModGlossaryOfflineProvider,
AddonModGlossaryHelperProvider,
AddonModGlossarySyncProvider,
AddonModGlossaryModuleHandler,
AddonModGlossaryPrefetchHandler,
AddonModGlossarySyncCronHandler,
AddonModGlossaryIndexLinkHandler,
AddonModGlossaryEntryLinkHandler,
]
})
export class AddonModGlossaryModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModGlossaryModuleHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModGlossaryPrefetchHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler) {
moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler);
cronDelegate.register(syncHandler);
linksDelegate.registerHandler(indexHandler);
linksDelegate.registerHandler(discussionHandler);
}
}

View File

@ -0,0 +1,29 @@
{
"addentry": "Add a new entry",
"aliases": "Keyword(s)",
"attachment": "Attachment",
"browsemode": "Browse entries",
"byalphabet": "Alphabetically",
"byauthor": "Group by author",
"bycategory": "Group by category",
"bynewestfirst": "Newest first",
"byrecentlyupdated": "Recently updated",
"bysearch": "Search",
"cannoteditentry": "Cannot edit entry",
"casesensitive": "This entry is case sensitive",
"categories": "Categories",
"concept": "Concept",
"definition": "Definition",
"entriestobesynced": "Entries to be synced",
"entrypendingapproval": "This entry is pending approval.",
"entryusedynalink": "This entry should be automatically linked",
"errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.",
"errorloadingentries": "An error occurred while loading entries.",
"errorloadingentry": "An error occurred while loading the entry.",
"errorloadingglossary": "An error occurred while loading the glossary.",
"fillfields": "Concept and definition are mandatory fields.",
"fullmatch": "Match whole words only",
"linking": "Auto-linking",
"noentriesfound": "No entries were found.",
"searchquery": "Search query"
}

View File

@ -0,0 +1,51 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="module.name"></core-format-text></ion-title>
<ion-buttons end>
<button ion-button (click)="save()"> {{ 'core.save' | translate }}</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list>
<ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="definitionControl" (contentChanged)="onDefinitionChange($event)" [placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="glossary.cmid" -->
</ion-item>
<ion-item *ngIf="categories.length > 0">
<ion-label stacked id="addon-mod-glossary-categories-label">{{ 'addon.mod_glossary.categories' | translate }}</ion-label>
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label" interface="popover">
<ion-option *ngFor="let category of categories" [value]="category.id">{{ category.name }}</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label stacked id="addon-mod-glossary-aliases-label">{{ 'addon.mod_glossary.aliases' | translate }}</ion-label>
<ion-textarea [(ngModel)]="options.aliases" rows="1" core-auto-rows aria-labelledby="addon-mod-glossary-aliases-label"></ion-textarea>
</ion-item>
<ion-item-divider color="light">{{ 'addon.mod_glossary.attachment' | translate }}</ion-item-divider>
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.cmid" [allowOffline]="true"></core-attachments>
<ng-container *ngIf="glossary.usedynalink">
<ion-item-divider color="light">{{ 'addon.mod_glossary.linking' | translate }}</ion-item-divider>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
<ion-toggle [(ngModel)]="options.usedynalink"></ion-toggle>
</ion-item>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive"></ion-toggle>
</ion-item>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch"></ion-toggle>
</ion-item>
</ng-container>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModGlossaryEditPage } from './edit';
@NgModule({
declarations: [
AddonModGlossaryEditPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(AddonModGlossaryEditPage),
TranslateModule.forChild()
],
})
export class AddonModGlossaryNewDiscussionPageModule {}

View File

@ -0,0 +1,253 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { AddonModGlossaryProvider } from '../../providers/glossary';
import { AddonModGlossaryOfflineProvider } from '../../providers/offline';
import { AddonModGlossaryHelperProvider } from '../../providers/helper';
/**
* Page that displays the edit form.
*/
@IonicPage({ segment: 'addon-mod-glossary-edit' })
@Component({
selector: 'page-addon-mod-glossary-edit',
templateUrl: 'edit.html',
})
export class AddonModGlossaryEditPage implements OnInit {
component = AddonModGlossaryProvider.COMPONENT;
loaded = false;
entry = {
concept: '',
definition: '',
timecreated: 0,
};
options = {
categories: [],
aliases: '',
usedynalink: false,
casesensitive: false,
fullmatch: false
};
attachments = [];
definitionControl = new FormControl();
categories = [];
protected courseId: number;
protected module: any;
protected glossary: any;
protected syncId: string;
protected syncObserver: any;
protected isDestroyed = false;
protected originalData: any;
protected saved = false;
constructor(private navParams: NavParams,
private navCtrl: NavController,
private translate: TranslateService,
private domUtils: CoreDomUtilsProvider,
private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider,
private uploaderProvider: CoreFileUploaderProvider,
private textUtils: CoreTextUtilsProvider,
private glossaryProvider: AddonModGlossaryProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider,
private glossaryHelper: AddonModGlossaryHelperProvider) {
this.courseId = navParams.get('courseId');
this.module = navParams.get('module');
this.glossary = navParams.get('glossary');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
const entry = this.navParams.get('entry');
let promise;
if (entry) {
this.entry.concept = entry.concept || '';
this.entry.definition = entry.definition || '';
this.originalData = {
concept: this.entry.concept,
definition: this.entry.definition,
files: [],
};
if (entry.options) {
this.options.categories = entry.options.categories || [];
this.options.aliases = entry.options.aliases || '';
this.options.usedynalink = !!entry.options.usedynalink;
if (this.options.usedynalink) {
this.options.casesensitive = !!entry.options.casesensitive;
this.options.fullmatch = !!entry.options.fullmatch;
}
}
// Treat offline attachments if any.
if (entry.attachments && entry.attachments.offline) {
promise = this.glossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated).then((files) => {
this.attachments = files;
this.originalData.files = files.slice();
});
}
}
this.definitionControl.setValue(this.entry.definition);
Promise.resolve(promise).then(() => {
this.glossaryProvider.getAllCategories(this.glossary.id).then((categories) => {
this.categories = categories;
}).finally(() => {
this.loaded = true;
});
});
}
/**
* Definition changed.
*
* @param {string} text The new text.
*/
onDefinitionChange(text: string): void {
this.entry.definition = text;
}
/**
* Check if we can leave the page or not.
*
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
let promise: any;
if (!this.saved && this.glossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) {
// Show confirmation if some data has been modified.
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
promise = Promise.resolve();
}
return promise.then(() => {
// Delete the local files from the tmp folder.
this.uploaderProvider.clearTmpFiles(this.attachments);
});
}
/**
* Save the entry.
*/
save(): void {
let definition = this.entry.definition;
const timecreated = this.entry.timecreated || Date.now();
let saveOffline = false;
if (!this.entry.concept || !definition) {
this.domUtils.showErrorModal('addon.mod_glossary.fillfields', true);
return;
}
const modal = this.domUtils.showModalLoading('core.sending', true);
// Check if rich text editor is enabled or not.
this.domUtils.isRichTextEditorEnabled().then((enabled) => {
if (!enabled) {
// Rich text editor not enabled, add some HTML to the definition if needed.
definition = this.textUtils.formatHtmlLines(definition);
}
// Upload attachments first if any.
if (this.attachments.length > 0) {
return this.glossaryHelper.uploadOrStoreFiles(this.glossary.id, this.entry.concept, timecreated, this.attachments,
false).catch(() => {
// Cannot upload them in online, save them in offline.
saveOffline = true;
return this.glossaryHelper.uploadOrStoreFiles(this.glossary.id, this.entry.concept, timecreated,
this.attachments, true);
});
}
}).then((attach) => {
const options: any = {
aliases: this.options.aliases,
categories: this.options.categories.join(',')
};
if (this.glossary.usedynalink) {
options.usedynalink = this.options.usedynalink ? 1 : 0;
if (this.options.usedynalink) {
options.casesensitive = this.options.casesensitive ? 1 : 0;
options.fullmatch = this.options.fullmatch ? 1 : 0;
}
}
if (saveOffline) {
let promise;
if (this.entry && !this.glossary.allowduplicatedentries) {
// Check if the entry is duplicated in online or offline mode.
promise = this.glossaryProvider.isConceptUsed(this.glossary.id, this.entry.concept, this.entry.timecreated)
.then((used) => {
if (used) {
// There's a entry with same name, reject with error message.
return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists'));
}
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
// Save entry in offline.
return this.glossaryOffline.addNewEntry(this.glossary.id, this.entry.concept, definition, this.courseId,
options, attach, timecreated, undefined, undefined, this.entry).then(() => {
// Don't return anything.
});
});
} else {
// Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine.
return this.glossaryProvider.addEntry(this.glossary.id, this.entry.concept, definition, this.courseId, options,
attach, timecreated, undefined, this.entry, !this.attachments.length, !this.glossary.allowduplicatedentries);
}
}).then((entryId) => {
if (entryId) {
// Data sent to server, delete stored files (if any).
this.glossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated);
}
const data = {
glossaryId: this.glossary.id,
};
this.eventsProvider.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.saved = true;
this.navCtrl.pop();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true);
}).finally(() => {
modal.dismiss();
});
}
}

View File

@ -0,0 +1,42 @@
<ion-header>
<ion-navbar>
<ion-title *ngIf="entry"><core-format-text [text]="entry.concept"></core-format-text></ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ng-container *ngIf="entry">
<ion-item text-wrap *ngIf="showAuthor">
<ion-avatar item-start (click)="openUserProfile(post.userid)">
<img [src]="entry.userpictureurl" onError="this.src='assets/img/user-avatar.png'" core-external-content [alt]="'core.pictureof' | translate:{$a: entry.userfullname}" role="presentation">
</ion-avatar>
<h2><core-format-text [text]="entry.concept"></core-format-text></h2>
<ion-note item-end *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
<p><core-format-text [text]="entry.userfullname"></core-format-text></p>
</ion-item>
<ion-item text-wrap *ngIf="!showAuthor">
<h2><core-format-text [text]="entry.concept"></core-format-text></h2>
<ion-note item-end *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
</ion-item>
<ion-item text-wrap>
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"></core-format-text>
</ion-item>
<ng-container *ngIf="entry.attachment">
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId"></core-file>
</ng-container>
<ion-item text-wrap *ngIf="entry.approved != 1">
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
</ion-item>
</ng-container>
<ion-card *ngIf="!entry">
<ion-item class="core-error-card">
{{ 'addon.mod_glossary.errorloadingentry' | translate }}
</ion-item>
</ion-card>
</core-loading>
</ion-content>

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { AddonModGlossaryEntryPage } from './entry';
@NgModule({
declarations: [
AddonModGlossaryEntryPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonModGlossaryEntryPage),
TranslateModule.forChild()
],
})
export class AddonModForumDiscussionPageModule {}

View File

@ -0,0 +1,110 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModGlossaryProvider } from '../../providers/glossary';
/**
* Page that displays a glossary entry.
*/
@IonicPage({ segment: 'addon-mod-glossary-entry' })
@Component({
selector: 'page-addon-mod-glossary-entry',
templateUrl: 'entry.html',
})
export class AddonModGlossaryEntryPage {
component = AddonModGlossaryProvider.COMPONENT;
componentId: number;
entry: any;
loaded = false;
showAuthor = false;
showDate = false;
protected courseId: number;
protected entryId: number;
constructor(navParams: NavParams,
private domUtils: CoreDomUtilsProvider,
private glossaryProvider: AddonModGlossaryProvider) {
this.courseId = navParams.get('courseId');
this.entryId = navParams.get('entryId');
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchEntry().then(() => {
this.glossaryProvider.logEntryView(this.entry.id);
}).finally(() => {
this.loaded = true;
});
}
/**
* Refresh the data.
*
* @param {any} [refresher] Refresher.
* @return {Promise<any>} Promise resolved when done.
*/
doRefresh(refresher?: any): Promise<any> {
return this.glossaryProvider.invalidateEntry(this.entry.id).catch(() => {
// Ignore errors.
}).then(() => {
return this.fetchEntry(true);
}).finally(() => {
refresher && refresher.complete();
});
}
/**
* Convenience function to get the glossary entry.
*
* @param {boolean} [refresh] Whether we're refreshing data.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchEntry(refresh?: boolean): Promise<any> {
return this.glossaryProvider.getEntry(this.entryId).then((result) => {
this.entry = result;
if (!refresh) {
// Load the glossary.
return this.glossaryProvider.getGlossaryById(this.courseId, this.entry.glossaryid).then((glossary) => {
this.componentId = glossary.coursemodule;
switch (glossary.displayformat) {
case 'fullwithauthor':
case 'encyclopedia':
this.showAuthor = true;
this.showDate = true;
break;
case 'fullwithoutauthor':
this.showAuthor = false;
this.showDate = true;
break;
default: // Default, and faq, simple, entrylist, continuous.
this.showAuthor = false;
this.showDate = false;
}
});
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
return Promise.reject(null);
});
}
}

View File

@ -0,0 +1,11 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end>
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-navbar>
</ion-header>
<addon-mod-glossary-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-glossary-index>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModGlossaryComponentsModule } from '../../components/components.module';
import { AddonModGlossaryIndexPage } from './index';
@NgModule({
declarations: [
AddonModGlossaryIndexPage,
],
imports: [
CoreDirectivesModule,
AddonModGlossaryComponentsModule,
IonicPageModule.forChild(AddonModGlossaryIndexPage),
TranslateModule.forChild()
],
})
export class AddonModGlossaryIndexPageModule {}

View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { AddonModGlossaryIndexComponent } from '../../components/index/index';
/**
* Page that displays a glossary.
*/
@IonicPage({ segment: 'addon-mod-glossary-index' })
@Component({
selector: 'page-addon-mod-glossary-index',
templateUrl: 'index.html',
})
export class AddonModGlossaryIndexPage {
@ViewChild(AddonModGlossaryIndexComponent) glossaryComponent: AddonModGlossaryIndexComponent;
title: string;
module: any;
courseId: number;
constructor(navParams: NavParams) {
this.module = navParams.get('module') || {};
this.courseId = navParams.get('courseId');
this.title = this.module.name;
}
/**
* Update some data based on the glossary instance.
*
* @param {any} glossary Glossary instance.
*/
updateData(glossary: any): void {
this.title = glossary.name || this.title;
}
}

View File

@ -0,0 +1,77 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { AddonModGlossaryProvider } from './glossary';
/**
* Handler to treat links to glossary entries.
*/
@Injectable()
export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonModGlossaryEntryLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModGlossary';
pattern = /\/mod\/glossary\/showentry\.php.*([\&\?]eid=\d+)/;
constructor(
private domUtils: CoreDomUtilsProvider,
private linkHelper: CoreContentLinksHelperProvider,
private glossaryProvider: AddonModGlossaryProvider,
private courseHelper: CoreCourseHelperProvider) {
super();
}
/**
* Get the list of actions for a link (url).
*
* @param {string[]} siteIds List of sites the URL belongs to.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
*/
getActions(siteIds: string[], url: string, params: any, courseId?: number):
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId, navCtrl?): void => {
const modal = this.domUtils.showModalLoading();
const entryId = parseInt(params.eid, 10);
let promise;
if (courseId) {
promise = Promise.resolve(courseId);
} else {
promise = this.glossaryProvider.getEntry(entryId, siteId).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
return Promise.reject(null);
}).then((entry) => {
return this.courseHelper.getModuleCourseIdByInstance(entry.glossaryid, 'glossary', siteId);
});
}
return promise.then((courseId) => {
this.linkHelper.goInSite(navCtrl, 'AddonModGlossaryEntryPage', {courseId, entryId}, siteId);
}).finally(() => {
modal.dismiss();
});
}
}];
}
}

View File

@ -0,0 +1,887 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSite } from '@classes/site';
import { CoreAppProvider } from '@providers/app';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreSitesProvider } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonModGlossaryOfflineProvider } from './offline';
/**
* Service that provides some features for glossaries.
*/
@Injectable()
export class AddonModGlossaryProvider {
static COMPONENT = 'mmaModGlossary';
static LIMIT_ENTRIES = 25;
static LIMIT_CATEGORIES = 10;
static SHOW_ALL_CATERGORIES = 0;
static SHOW_NOT_CATEGORISED = -1;
static ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry';
protected ROOT_CACHE_KEY = 'mmaModGlossary:';
constructor(private appProvider: CoreAppProvider,
private sitesProvider: CoreSitesProvider,
private filepoolProvider: CoreFilepoolProvider,
private translate: TranslateService,
private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider) {}
/**
* Get the course glossary cache key.
*
* @param {number} courseId Course Id.
* @return {string} Cache key.
*/
protected getCourseGlossariesCacheKey(courseId: number): string {
return this.ROOT_CACHE_KEY + 'courseGlossaries:' + courseId;
}
/**
* Get all the glossaries in a course.
*
* @param {number} courseId Course Id.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Resolved with the glossaries.
*/
getCourseGlossaries(courseId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
courseids: [courseId]
};
const preSets = {
cacheKey: this.getCourseGlossariesCacheKey(courseId)
};
return site.read('mod_glossary_get_glossaries_by_courses', params, preSets).then((result) => {
return result.glossaries;
});
});
}
/**
* Invalidate all glossaries in a course.
*
* @param {number} courseId Course Id.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Resolved when data is invalidated.
*/
invalidateCourseGlossaries(courseId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getCourseGlossariesCacheKey(courseId);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get the entries by author cache key.
*
* @param {number} glossaryId Glossary Id.
* @param {string} letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
* @param {string} field Search and order using: FIRSTNAME or LASTNAME
* @param {string} sort The direction of the order: ASC or DESC
* @return {string} Cache key.
*/
protected getEntriesByAuthorCacheKey(glossaryId: number, letter: string, field: string, sort: string): string {
return this.ROOT_CACHE_KEY + 'entriesByAuthor:' + glossaryId + ':' + letter + ':' + field + ':' + sort;
}
/**
* Get entries by author.
*
* @param {number} glossaryId Glossary Id.
* @param {string} letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
* @param {string} field Search and order using: FIRSTNAME or LASTNAME
* @param {string} sort The direction of the order: ASC or DESC
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
* @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Resolved with the entries.
*/
getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, from: number, limit: number,
forceCache: boolean, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
letter: letter,
field: field,
sort: sort,
from: from,
limit: limit
};
const preSets = {
cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort),
omitExpires: forceCache
};
return site.read('mod_glossary_get_entries_by_author', params, preSets);
});
}
/**
* Invalidate cache of entries by author.
*
* @param {number} glossaryId Glossary Id.
* @param {string} letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
* @param {string} field Search and order using: FIRSTNAME or LASTNAME
* @param {string} sort The direction of the order: ASC or DESC
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when data is invalidated.
*/
invalidateEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get entries by category.
*
* @param {number} glossaryId Glossary Id.
* @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
* @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Resolved with the entries.
*/
getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, forceCache: boolean,
siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
categoryid: categoryId,
from: from,
limit: limit
};
const preSets = {
cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId),
omitExpires: forceCache
};
return site.read('mod_glossary_get_entries_by_category', params, preSets);
});
}
/**
* Invalidate cache of entries by category.
*
* @param {number} glossaryId Glossary Id.
* @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when data is invalidated.
*/
invalidateEntriesByCategory(glossaryId: number, categoryId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getEntriesByCategoryCacheKey(glossaryId, categoryId);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get the entries by category cache key.
*
* @param {number} glossaryId Glossary Id.
* @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @return {string} Cache key.
*/
getEntriesByCategoryCacheKey(glossaryId: number, categoryId: number): string {
return this.ROOT_CACHE_KEY + 'entriesByCategory:' + glossaryId + ':' + categoryId;
}
/**
* Get the entries by date cache key.
*
* @param {number} glossaryId Glossary Id.
* @param {string} order The way to order the records.
* @param {string} sort The direction of the order.
* @return {string} Cache key.
*/
getEntriesByDateCacheKey(glossaryId: number, order: string, sort: string): string {
return this.ROOT_CACHE_KEY + 'entriesByDate:' + glossaryId + ':' + order + ':' + sort;
}
/**
* Get entries by date.
*
* @param {number} glossaryId Glossary Id.
* @param {string} order The way to order the records.
* @param {string} sort The direction of the order.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
* @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Resolved with the entries.
*/
getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, forceCache: boolean,
siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
order: order,
sort: sort,
from: from,
limit: limit
};
const preSets = {
cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort),
omitExpires: forceCache
};
return site.read('mod_glossary_get_entries_by_date', params, preSets);
});
}
/**
* Invalidate cache of entries by date.
*
* @param {number} glossaryId Glossary Id.
* @param {string} order The way to order the records.
* @param {string} sort The direction of the order.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when data is invalidated.
*/
invalidateEntriesByDate(glossaryId: number, order: string, sort: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getEntriesByDateCacheKey(glossaryId, order, sort);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get the entries by letter cache key.
*
* @param {number} glossaryId Glossary Id.
* @param {string} letter A letter, or a special keyword.
* @return {string} Cache key.
*/
protected getEntriesByLetterCacheKey(glossaryId: number, letter: string): string {
return this.ROOT_CACHE_KEY + 'entriesByLetter:' + glossaryId + ':' + letter;
}
/**
* Get entries by letter.
*
* @param {number} glossaryId Glossary Id.
* @param {string} letter A letter, or a special keyword.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
* @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Resolved with the entries.
*/
getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, forceCache: boolean, siteId?: string):
Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
letter: letter,
from: from,
limit: limit
};
const preSets = {
cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter),
omitExpires: forceCache
};
return site.read('mod_glossary_get_entries_by_letter', params, preSets);
});
}
/**
* Invalidate cache of entries by letter.
*
* @param {number} glossaryId Glossary Id.
* @param {string} letter A letter, or a special keyword.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when data is invalidated.
*/
invalidateEntriesByLetter(glossaryId: number, letter: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getEntriesByLetterCacheKey(glossaryId, letter);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get the entries by search cache key.
*
* @param {number} glossaryId Glossary Id.
* @param {string} query The search query.
* @param {boolean} fullSearch Whether or not full search is required.
* @param {string} order The way to order the results.
* @param {string} sort The direction of the order.
* @return {string} Cache key.
*/
protected getEntriesBySearchCacheKey(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string):
string {
return this.ROOT_CACHE_KEY + 'entriesBySearch:' + glossaryId + ':' + fullSearch + ':' + order + ':' + sort + ':' + query;
}
/**
* Get entries by search.
*
* @param {number} glossaryId Glossary Id.
* @param {string} query The search query.
* @param {boolean} fullSearch Whether or not full search is required.
* @param {string} order The way to order the results.
* @param {string} sort The direction of the order.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
* @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Resolved with the entries.
*/
getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, from: number,
limit: number, forceCache: boolean, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
query: query,
fullsearch: fullSearch,
order: order,
sort: sort,
from: from,
limit: limit
};
const preSets = {
cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort),
omitExpires: forceCache,
};
return site.read('mod_glossary_get_entries_by_search', params, preSets);
});
}
/**
* Invalidate cache of entries by search.
*
* @param {number} glossaryId Glossary Id.
* @param {string} query The search query.
* @param {boolean} fullSearch Whether or not full search is required.
* @param {string} order The way to order the results.
* @param {string} sort The direction of the order.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when data is invalidated.
*/
invalidateEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, siteId?: string):
Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort);
return site.invalidateWsCacheForKey(key);
});
}
/**
* Get the glossary categories cache key.
*
* @param {number} glossaryId Glossary Id.
* @return {string} The cache key.
*/
protected getCategoriesCacheKey(glossaryId: number): string {
return this.ROOT_CACHE_KEY + 'categories:' + glossaryId;
}
/**
* Get all the categories related to the glossary.
*
* @param {number} glossaryId Glossary Id.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with the categories if supported or empty array if not.
*/
getAllCategories(glossaryId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.getCategories(glossaryId, 0, AddonModGlossaryProvider.LIMIT_CATEGORIES, [], site);
});
}
/**
* Get the categories related to the glossary by sections. It's a recursive function see initial call values.
*
* @param {number} glossaryId Glossary Id.
* @param {number} from Number of categories already fetched, so fetch will be done from this number. Initial value 0.
* @param {number} limit Number of categories to fetch. Initial value LIMIT_CATEGORIES.
* @param {any[]} categories Already fetched categories where to append the fetch. Initial value [].
* @param {any} site Site object.
* @return {Promise<any[]>} Promise resolved with the categories.
*/
protected getCategories(glossaryId: number, from: number, limit: number, categories: any[], site: CoreSite): Promise<any[]> {
const params = {
id: glossaryId,
from: from,
limit: limit
};
const preSets = {
cacheKey: this.getCategoriesCacheKey(glossaryId)
};
return site.read('mod_glossary_get_categories', params, preSets).then((response) => {
categories = categories.concat(response.categories);
const canLoadMore = (from + limit) < response.count;
if (canLoadMore) {
from += limit;
return this.getCategories(glossaryId, from, limit, categories, site);
}
return categories;
});
}
/**
* Invalidate cache of categories by glossary id.
*
* @param {number} glossaryId Glossary Id.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when categories data has been invalidated,
*/
invalidateCategories(glossaryId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getCategoriesCacheKey(glossaryId));
});
}
/**
* Get an entry by ID cache key.
*
* @param {number} entryId Entry Id.
* @return {string} Cache key.
*/
protected getEntryCacheKey(entryId: number): string {
return this.ROOT_CACHE_KEY + 'getEntry:' + entryId;
}
/**
* Get one entry by ID.
*
* @param {number} entryId Entry ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the entry.
*/
getEntry(entryId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: entryId
};
const preSets = {
cacheKey: this.getEntryCacheKey(entryId)
};
return site.read('mod_glossary_get_entry_by_id', params, preSets).then((response) => {
if (response && response.entry) {
return response.entry;
} else {
return Promise.reject(null);
}
});
});
}
/**
* Performs the fetch of the entries using the propper function and arguments.
*
* @param {Function} fetchFunction Function to fetch.
* @param {any[]} fetchArguments Arguments to call the fetching.
* @param {number} [limitFrom=0] Number of entries already fetched, so fetch will be done from this number.
* @param {number} [limitNum] Number of records to return. Defaults to LIMIT_ENTRIES.
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the response.
*/
fetchEntries(fetchFunction: Function, fetchArguments: any[], limitFrom: number = 0, limitNum?: number,
forceCache: boolean = false, siteId?: string): Promise<any> {
limitNum = limitNum || AddonModGlossaryProvider.LIMIT_ENTRIES;
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const args = fetchArguments.slice();
args.push(limitFrom);
args.push(limitNum);
args.push(forceCache);
args.push(siteId);
return fetchFunction.apply(this, args);
}
/**
* Performs the whole fetch of the entries using the propper function and arguments.
*
* @param {Function} fetchFunction Function to fetch.
* @param {any[]} fetchArguments Arguments to call the fetching.
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with all entrries.
*/
fetchAllEntries(fetchFunction: Function, fetchArguments: any[], forceCache: boolean = false, siteId?: string): Promise<any[]> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const entries = [];
const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES;
const fetchMoreEntries = (): Promise<any[]> => {
return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, forceCache, siteId).then((result) => {
Array.prototype.push.apply(entries, result.entries);
return entries.length < result.count ? fetchMoreEntries() : entries;
});
};
return fetchMoreEntries();
}
/**
* Invalidate cache of entry by ID.
*
* @param {number} entryId Entry Id.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when data is invalidated.
*/
invalidateEntry(entryId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getEntryCacheKey(entryId));
});
}
/**
* Invalidate cache of all entries in the array.
*
* @param {any[]} entries Entry objects to invalidate.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Resolved when data is invalidated.
*/
protected invalidateEntries(entries: any[], siteId?: string): Promise<any> {
const keys = [];
entries.forEach((entry) => {
keys.push(this.getEntryCacheKey(entry.id));
});
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateMultipleWsCacheForKey(keys);
});
}
/**
* Invalidate the prefetched content except files.
* To invalidate files, use AddonModGlossary#invalidateFiles.
*
* @param {number} moduleId The module ID.
* @param {number} courseId Course ID.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number): Promise<any> {
return this.getGlossary(courseId, moduleId).then((glossary) => {
return this.invalidateGlossaryEntries(glossary).finally(() => {
return this.utils.allPromises([
this.invalidateCourseGlossaries(courseId),
this.invalidateCategories(glossary.id)
]);
});
});
}
/**
* Invalidate the prefetched content for a given glossary, except files.
* To invalidate files, use AddonModGlossaryProvider#invalidateFiles.
*
* @param {any} glossary The glossary object.
* @param {boolean} [onlyEntriesList] If true, entries won't be invalidated.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateGlossaryEntries(glossary: any, onlyEntriesList?: boolean): Promise<any> {
const promises = [];
if (!onlyEntriesList) {
promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true).then((entries) => {
return this.invalidateEntries(entries);
}));
}
glossary.browsemodes.forEach((mode) => {
switch (mode) {
case 'letter':
promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL'));
break;
case 'cat':
promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES));
break;
case 'date':
promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC'));
promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC'));
break;
case 'author':
promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC'));
break;
default:
}
});
return this.utils.allPromises(promises);
}
/**
* Invalidate the prefetched files.
*
* @param {number} moduleId The module ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the files are invalidated.
*/
protected invalidateFiles(moduleId: number, siteId?: string): Promise<any> {
return this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModGlossaryProvider.COMPONENT, moduleId);
}
/**
* Get one glossary by cmid.
*
* @param {number} courseId Course Id.
* @param {number} cmId Course Module Id.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the glossary.
*/
getGlossary(courseId: number, cmId: number, siteId?: string): Promise<any> {
return this.getCourseGlossaries(courseId, siteId).then((glossaries) => {
const glossary = glossaries.find((glossary) => glossary.coursemodule == cmId);
if (glossary) {
return glossary;
}
return Promise.reject(null);
});
}
/**
* Get one glossary by glossary ID.
*
* @param {number} courseId Course Id.
* @param {number} glossaryId Glossary Id.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the glossary.
*/
getGlossaryById(courseId: number, glossaryId: number, siteId?: string): Promise<any> {
return this.getCourseGlossaries(courseId, siteId).then((glossaries) => {
const glossary = glossaries.find((glossary) => glossary.id == glossaryId);
if (glossary) {
return glossary;
}
return Promise.reject(null);
});
}
/**
* Create a new entry on a glossary
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Glossary entry concept.
* @param {string} definition Glossary entry concept definition.
* @param {number} courseId Course ID of the glossary.
* @param {any} [options] Array of options for the entry.
* @param {any} [attach] Attachments ID if sending online, result of CoreFileUploaderProvider#storeFilesToUpload
* otherwise.
* @param {number} [timeCreated] The time the entry was created. If not defined, current time.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {any} [discardEntry] The entry provided will be discarded if found.
* @param {boolean} [allowOffline] True if it can be stored in offline, false otherwise.
* @param {boolean} [checkDuplicates] Check for duplicates before storing offline. Only used if allowOffline is true.
* @return {Promise<number | false>} Promise resolved with entry ID if entry was created in server, false if stored in device.
*/
addEntry(glossaryId: number, concept: string, definition: string, courseId: number, options: any, attach: any,
timeCreated: number, siteId?: string, discardEntry?: any, allowOffline?: boolean, checkDuplicates?: boolean):
Promise<number | false> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Convenience function to store a new entry to be synchronized later.
const storeOffline = (): Promise<number | false> => {
const discardTime = discardEntry && discardEntry.timecreated;
let duplicatesPromise;
if (checkDuplicates) {
duplicatesPromise = this.isConceptUsed(glossaryId, concept, discardTime, siteId);
} else {
duplicatesPromise = Promise.resolve(false);
}
// Check if the entry is duplicated in online or offline mode.
return duplicatesPromise.then((used) => {
if (used) {
return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists'));
}
return this.glossaryOffline.addNewEntry(glossaryId, concept, definition, courseId, attach, options, timeCreated,
siteId, undefined, discardEntry).then(() => {
return false;
});
});
};
if (!this.appProvider.isOnline() && allowOffline) {
// App is offline, store the action.
return storeOffline();
}
// If we are editing an offline entry, discard previous first.
let discardPromise;
if (discardEntry) {
discardPromise = this.glossaryOffline.deleteNewEntry(
glossaryId, discardEntry.concept, discardEntry.timecreated, siteId);
} else {
discardPromise = Promise.resolve();
}
return discardPromise.then(() => {
// Try to add it in online.
return this.addEntryOnline(glossaryId, concept, definition, options, attach, siteId).then((entryId) => {
return entryId;
}).catch((error) => {
if (allowOffline && !this.utils.isWebServiceError(error)) {
// Couldn't connect to server, store in offline.
return storeOffline();
} else {
// The WebService has thrown an error or offline not supported, reject.
return Promise.reject(error);
}
});
});
}
/**
* Create a new entry on a glossary. It does not cache calls. It will fail if offline or cannot connect.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Glossary entry concept.
* @param {string} definition Glossary entry concept definition.
* @param {any} [options] Array of options for the entry.
* @param {number} [attachId] Attachments ID (if any attachment).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<number>} Promise resolved with the entry ID if created, rejected otherwise.
*/
addEntryOnline(glossaryId: number, concept: string, definition: string, options?: any, attachId?: number, siteId?: string):
Promise<number> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
glossaryid: glossaryId,
concept: concept,
definition: definition,
definitionformat: 1,
options: this.utils.objectToArrayOfObjects(options || {}, 'name', 'value')
};
if (attachId) {
params.options.push({
name: 'attachmentsid',
value: attachId
});
}
// Workaround for bug MDL-57737.
if (!site.isVersionGreaterEqualThan('3.2.2')) {
params.definition = this.textUtils.cleanTags(params.definition);
}
return site.write('mod_glossary_add_entry', params).then((response) => {
if (response && response.entryid) {
return response.entryid;
}
return this.utils.createFakeWSError('');
});
});
}
/**
* Check if a entry concept is already used.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Concept to check.
* @param {number} [timeCreated] Timecreated to check that is not the timecreated we are editing.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with true if used, resolved with false if not used or error.
*/
isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise<boolean> {
// Check offline first.
return this.glossaryOffline.isConceptUsed(glossaryId, concept, timeCreated, siteId).then((exists) => {
if (exists) {
return true;
}
// If we get here, there's no offline entry with this name, check online.
// Get entries from the cache.
return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, siteId).then((entries) => {
// Check if there's any entry with the same concept.
return entries.some((entry) => entry.concept == concept);
});
}).catch(() => {
// Error, assume not used.
return false;
});
}
/**
* Return whether or not the plugin is enabled for editing in the current site. Plugin is enabled if the glossary WS are
* available.
*
* @return {boolean} Whether the glossary editing is available or not.
*/
isPluginEnabledForEditing(): boolean {
return this.sitesProvider.getCurrentSite().wsAvailable('mod_glossary_add_entry');
}
/**
* Report a glossary as being viewed.
*
* @param {number} glossaryId Glossary ID.
* @param {string} mode The mode in which the glossary was viewed.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logView(glossaryId: number, mode: string): Promise<any> {
const params = {
id: glossaryId,
mode: mode
};
return this.sitesProvider.getCurrentSite().write('mod_glossary_view_glossary', params);
}
/**
* Report a glossary entry as being viewed.
*
* @param {number} entryId Entry ID.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logEntryView(entryId: number): Promise<any> {
const params = {
id: entryId
};
return this.sitesProvider.getCurrentSite().write('mod_glossary_view_entry', params);
}
}

View File

@ -0,0 +1,121 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { CoreFileProvider } from '@providers/file';
import { AddonModGlossaryProvider } from './glossary';
import { AddonModGlossaryOfflineProvider } from './offline';
/**
* Helper to gather some common functions for glossary.
*/
@Injectable()
export class AddonModGlossaryHelperProvider {
constructor(private fileProvider: CoreFileProvider,
private uploaderProvider: CoreFileUploaderProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider) {}
/**
* Delete stored attachment files for a new discussion.
*
* @param {number} glossaryId Glossary ID.
* @param {string} entryName The name of the entry.
* @param {number} timeCreated The time the entry was created.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when deleted.
*/
deleteStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise<any> {
return this.glossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId).then((folderPath) => {
return this.fileProvider.removeDir(folderPath).catch(() => {
// Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists.
});
});
}
/**
* Get a list of stored attachment files for a new entry. See AddonModGlossaryHelperProvider#storeFiles.
*
* @param {number} glossaryId lossary ID.
* @param {string} entryName The name of the entry.
* @param {number} [timeCreated] The time the entry was created.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with the files.
*/
getStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise<any[]> {
return this.glossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId).then((folderPath) => {
return this.uploaderProvider.getStoredFiles(folderPath);
});
}
/**
* Check if the data of an entry has changed.
*
* @param {any} entry Current data.
* @param {any[]} files Files attached.
* @param {any} original Original content.
* @return {boolean} True if data has changed, false otherwise.
*/
hasEntryDataChanged(entry: any, files: any[], original: any): boolean {
if (!original || typeof original.concept == 'undefined') {
// There is no original data.
return entry.definition || entry.concept || files.length > 0;
}
if (original.definition != entry.definition || original.concept != entry.concept) {
return true;
}
return this.uploaderProvider.areFileListDifferent(files, original.files);
}
/**
* Given a list of files (either online files or local files), store the local files in a local folder
* to be submitted later.
*
* @param {number} glossaryId Glossary ID.
* @param {string} entryName The name of the entry.
* @param {number} [timeCreated] The time the entry was created.
* @param {any[]} files List of files.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
*/
storeFiles(glossaryId: number, entryName: string, timeCreated: number, files: any[], siteId?: string): Promise<any> {
// Get the folder where to store the files.
return this.glossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId).then((folderPath) => {
return this.uploaderProvider.storeFilesToUpload(folderPath, files);
});
}
/**
* Upload or store some files, depending if the user is offline or not.
*
* @param {number} glossaryId Glossary ID.
* @param {string} entryName The name of the entry.
* @param {number} [timeCreated] The time the entry was created.
* @param {any[]} files List of files.
* @param {boolean} offline True if files sould be stored for offline, false to upload them.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if success.
*/
uploadOrStoreFiles(glossaryId: number, entryName: string, timeCreated: number, files: any[], offline: boolean,
siteId?: string): Promise<any> {
if (offline) {
return this.storeFiles(glossaryId, entryName, timeCreated, files, siteId);
} else {
return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModGlossaryProvider.COMPONENT, glossaryId, siteId);
}
}
}

View File

@ -0,0 +1,30 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { AddonModGlossaryProvider } from './glossary';
/**
* Handler to treat links to glossary index.
*/
@Injectable()
export class AddonModGlossaryIndexLinkHandler extends CoreContentLinksModuleIndexHandler {
name = 'AddonModGlossaryIndexLinkHandler';
constructor(courseHelper: CoreCourseHelperProvider, protected glossaryProvider: AddonModGlossaryProvider) {
super(courseHelper, 'AddonModGlossary', 'glossary');
}
}

View File

@ -0,0 +1,81 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { NavController, NavOptions } from 'ionic-angular';
import { AddonModGlossaryIndexComponent } from '../components/index/index';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
import { CoreCourseProvider } from '@core/course/providers/course';
/**
* Handler to support glossary modules.
*/
@Injectable()
export class AddonModGlossaryModuleHandler implements CoreCourseModuleHandler {
name = 'AddonModGlossary';
modName = 'glossary';
constructor(private courseProvider: CoreCourseProvider) { }
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean {
return true;
}
/**
* Get the data required to display the module in the course contents view.
*
* @param {any} module The module object.
* @param {number} courseId The course ID.
* @param {number} sectionId The section ID.
* @return {CoreCourseModuleHandlerData} Data to render the module.
*/
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
return {
icon: this.courseProvider.getModuleIconSrc('glossary'),
title: module.name,
class: 'addon-mod_glossary-handler',
showDownloadButton: true,
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
navCtrl.push('AddonModGlossaryIndexPage', {module: module, courseId: courseId}, options);
}
};
}
/**
* Get the component to render the module. This is needed to support singleactivity course format.
* The component returned must implement CoreCourseModuleMainComponent.
*
* @param {any} course The course object.
* @param {any} module The module object.
* @return {any} The component to use, undefined if not found.
*/
getMainComponent(course: any, module: any): any {
return AddonModGlossaryIndexComponent;
}
/**
* Whether to display the course refresher in single activity course format. If it returns false, a refresher must be
* included in the template that calls the doRefresh method of the component. Defaults to true.
*
* @return {boolean} Whether the refresher should be displayed.
*/
displayRefresherInSingleActivity(): boolean {
return false;
}
}

View File

@ -0,0 +1,280 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreFileProvider } from '@providers/file';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text';
/**
* Service to handle offline glossary.
*/
@Injectable()
export class AddonModGlossaryOfflineProvider {
// Variables for database.
protected ENTRIES_TABLE = 'addon_mod_glossary_entrues';
protected tablesSchema = [
{
name: this.ENTRIES_TABLE,
columns: [
{
name: 'glossaryid',
type: 'INTEGER',
},
{
name: 'courseid',
type: 'INTEGER',
},
{
name: 'concept',
type: 'TEXT',
},
{
name: 'definition',
type: 'TEXT',
},
{
name: 'definitionformat',
type: 'TEXT',
},
{
name: 'userid',
type: 'INTEGER',
},
{
name: 'timecreated',
type: 'INTEGER',
},
{
name: 'options',
type: 'TEXT',
},
{
name: 'attachments',
type: 'TEXT',
},
],
primaryKeys: ['glossaryid', 'concept', 'timecreated']
}
];
constructor(private fileProvider: CoreFileProvider,
private sitesProvider: CoreSitesProvider,
private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider) {
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
}
/**
* Delete a new entry.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Glossary entry concept.
* @param {number} timeCreated The time the entry was created.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<void>} Promise resolved if deleted, rejected if failure.
*/
deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions = {
glossaryid: glossaryId,
concept: concept,
timecreated: timeCreated,
};
return site.getDb().deleteRecords(this.ENTRIES_TABLE, conditions);
});
}
/**
* Get all the stored new entries from all the glossaries.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with entries.
*/
getAllNewEntries(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(this.ENTRIES_TABLE).then((records: any[]) => {
return records.map(this.parseRecord.bind(this));
});
});
}
/**
* Get a stored new entry.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Glossary entry concept.
* @param {number} timeCreated The time the entry was created.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with entry.
*/
getNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions = {
glossaryid: glossaryId,
concept: concept,
timecreated: timeCreated,
};
return site.getDb().getRecord(this.ENTRIES_TABLE, conditions).then(this.parseRecord.bind(this));
});
}
/**
* Get all the stored add entry data from a certain glossary.
*
* @param {number} glossaryId Glossary ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User the entries belong to. If not defined, current user in site.
* @return {Promise<any[]>} Promise resolved with entries.
*/
getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions = {
glossaryid: glossaryId,
userId: userId || site.getUserId(),
};
return site.getDb().getRecords(this.ENTRIES_TABLE, conditions).then((records: any[]) => {
return records.map(this.parseRecord.bind(this));
});
});
}
/**
* Check if a concept is used offline.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Concept to check.
* @param {number} [timeCreated] Time of the entry we are editing.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with true if concept is found, false otherwise.
*/
isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions = {
glossaryid: glossaryId,
concept: concept,
};
return site.getDb().getRecords(this.ENTRIES_TABLE, conditions).then((entries) => {
if (!entries.length) {
return false;
}
if (entries.length > 1 || !timeCreated) {
return true;
}
// If there's only one entry, check that is not the one we are editing.
return this.utils.promiseFails(this.getNewEntry(glossaryId, concept, timeCreated, siteId));
});
}).catch(() => {
// No offline data found, return false.
return false;
});
}
/**
* Save a new entry to be sent later.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Glossary entry concept.
* @param {string} definition Glossary entry concept definition.
* @param {number} courseId Course ID of the glossary.
* @param {any} [options] Options for the entry.
* @param {any} [attachments] Result of CoreFileUploaderProvider#storeFilesToUpload for attachments.
* @param {number} [timeCreated] The time the entry was created. If not defined, current time.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User the entry belong to. If not defined, current user in site.
* @param {any} [discardEntry] The entry provided will be discarded if found.
* @return {Promise<false>} Promise resolved if stored, rejected if failure.
*/
addNewEntry(glossaryId: number, concept: string, definition: string, courseId: number, options?: any, attachments?: any,
timeCreated?: number, siteId?: string, userId?: number, discardEntry?: any): Promise<false> {
return this.sitesProvider.getSite(siteId).then((site) => {
const entry = {
glossaryid: glossaryId,
courseid: courseId,
concept: concept,
definition: definition,
definitionformat: 'html',
options: JSON.stringify(options),
attachments: JSON.stringify(attachments),
userid: userId || site.getUserId(),
timecreated: timeCreated || new Date().getTime()
};
// If editing an offline entry, delete previous first.
let discardPromise;
if (discardEntry) {
discardPromise = this.deleteNewEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId());
} else {
discardPromise = Promise.resolve();
}
return discardPromise.then(() => {
return site.getDb().insertRecord(this.ENTRIES_TABLE, entry).then(() => false);
});
});
}
/**
* Get the path to the folder where to store files for offline attachments in a glossary.
*
* @param {number} glossaryId Glossary ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<string>} Promise resolved with the path.
*/
getGlossaryFolder(glossaryId: number, siteId?: string): Promise<string> {
return this.sitesProvider.getSite(siteId).then((site) => {
const siteFolderPath = this.fileProvider.getSiteFolder(site.getId());
const folderPath = 'offlineglossary/' + glossaryId;
return this.textUtils.concatenatePaths(siteFolderPath, folderPath);
});
}
/**
* Get the path to the folder where to store files for a new offline entry.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept The name of the entry.
* @param {number} timeCreated Time to allow duplicated entries.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<string>} Promise resolved with the path.
*/
getEntryFolder(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<string> {
return this.getGlossaryFolder(glossaryId, siteId).then((folderPath) => {
return this.textUtils.concatenatePaths(folderPath, 'newentry_' + concept + '_' + timeCreated);
});
}
/**
* Parse "options" and "attachments" columns of a fetched record.
*
* @param {any} records Record object
* @return {any} Record object with columns parsed.
*/
protected parseRecord(record: any): any {
record.options = this.textUtils.parseJSON(record.options);
record.attachments = this.textUtils.parseJSON(record.attachments);
return record;
}
}

View File

@ -0,0 +1,174 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModGlossaryProvider } from './glossary';
/**
* Handler to prefetch forums.
*/
@Injectable()
export class AddonModGlossaryPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
name = 'AddonModGlossary';
modName = 'glossary';
component = AddonModGlossaryProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$|^entries$/;
constructor(injector: Injector,
private userProvider: CoreUserProvider,
private glossaryProvider: AddonModGlossaryProvider) {
super(injector);
}
/**
* Download the module.
*
* @param {any} module The module object returned by WS.
* @param {number} courseId Course ID.
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
* @return {Promise<any>} Promise resolved when all content is downloaded.
*/
download(module: any, courseId: number, dirPath?: string): Promise<any> {
// Glossaries cannot be downloaded right away, only prefetched.
return this.prefetch(module, courseId);
}
/**
* Get list of files. If not defined, we'll assume they're in module.contents.
*
* @param {any} module Module.
* @param {Number} courseId Course ID the module belongs to.
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
* @return {Promise<any[]>} Promise resolved with the list of files.
*/
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {
return this.glossaryProvider.getGlossary(courseId, module.id).then((glossary) => {
return this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL'])
.then((entries) => {
return this.getFilesFromGlossaryAndEntries(module, glossary, entries);
});
}).catch(() => {
// Glossary not found, return empty list.
return [];
});
}
/**
* Get the list of downloadable files. It includes entry embedded files.
*
* @param {any} module Module to get the files.
* @param {any} glossary Glossary
* @param {any[]} entries Entries of the Glossary.
* @return {any[]} List of Files.
*/
protected getFilesFromGlossaryAndEntries(module: any, glossary: any, entries: any[]): any[] {
let files = this.getIntroFilesFromInstance(module, glossary);
// Get entries files.
entries.forEach((entry) => {
files = files.concat(this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(entry.definition));
files = files.concat(entry.attachments);
});
return files;
}
/**
* Invalidate the prefetched content.
*
* @param {number} moduleId The module ID.
* @param {number} courseId The course ID the module belongs to.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number): Promise<any> {
return this.glossaryProvider.invalidateContent(moduleId, courseId);
}
/**
* Prefetch a module.
*
* @param {any} module Module.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
* @return {Promise<any>} Promise resolved when done.
*/
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
return this.prefetchPackage(module, courseId, single, this.prefetchGlossary.bind(this));
}
/**
* Prefetch a glossary.
*
* @param {any} module The module object returned by WS.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
* @param {string} siteId Site ID.
* @return {Promise<any>} Promise resolved when done.
*/
protected prefetchGlossary(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Prefetch the glossary data.
return this.glossaryProvider.getGlossary(courseId, module.id, siteId).then((glossary) => {
const promises = [];
glossary.browsemodes.forEach((mode) => {
switch (mode) {
case 'letter': // Always done. Look bellow.
break;
case 'cat': // Not implemented.
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByCategory,
[glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES], false, siteId));
break;
case 'date':
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate,
[glossary.id, 'CREATION', 'DESC'], false, siteId));
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate,
[glossary.id, 'UPDATE', 'DESC'], false, siteId));
break;
case 'author':
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByAuthor,
[glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, siteId));
break;
default:
}
});
// Fetch all entries to get information from.
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter,
[glossary.id, 'ALL'], false, siteId).then((entries) => {
const promises = [];
const userIds = [];
// Fetch user avatars.
entries.forEach((entry) => {
// Fetch individual entries.
promises.push(this.glossaryProvider.getEntry(entry.id, siteId));
userIds.push(entry.userid);
});
// Prefetch user profiles.
promises.push(this.userProvider.prefetchProfiles(userIds, courseId, siteId));
const files = this.getFilesFromGlossaryAndEntries(module, glossary, entries);
promises.push(this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id));
return Promise.all(promises);
}));
});
}
}

View File

@ -0,0 +1,47 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreCronHandler } from '@providers/cron';
import { AddonModGlossarySyncProvider } from './sync';
/**
* Synchronization cron handler.
*/
@Injectable()
export class AddonModGlossarySyncCronHandler implements CoreCronHandler {
name = 'AddonModGlossarySyncCronHandler';
constructor(private glossarySync: AddonModGlossarySyncProvider) {}
/**
* Execute the process.
* Receives the ID of the site affected, undefined for all sites.
*
* @param {string} [siteId] ID of the site affected, undefined for all sites.
* @return {Promise<any>} Promise resolved when done, rejected if failure.
*/
execute(siteId?: string): Promise<any> {
return this.glossarySync.syncAllGlossaries(siteId);
}
/**
* Get the time between consecutive executions.
*
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return AddonModGlossarySyncProvider.SYNC_TIME;
}
}

View File

@ -0,0 +1,301 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { CoreAppProvider } from '@providers/app';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSyncProvider } from '@providers/sync';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonModGlossaryProvider } from './glossary';
import { AddonModGlossaryHelperProvider } from './helper';
import { AddonModGlossaryOfflineProvider } from './offline';
/**
* Service to sync glossaries.
*/
@Injectable()
export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_glossary_autom_synced';
static SYNC_TIME = 600000; // 10 minutes.
protected componentTranslate: string;
constructor(translate: TranslateService,
appProvider: CoreAppProvider,
courseProvider: CoreCourseProvider,
private eventsProvider: CoreEventsProvider,
loggerProvider: CoreLoggerProvider,
sitesProvider: CoreSitesProvider,
syncProvider: CoreSyncProvider,
textUtils: CoreTextUtilsProvider,
private uploaderProvider: CoreFileUploaderProvider,
private utils: CoreUtilsProvider,
private glossaryProvider: AddonModGlossaryProvider,
private glossaryHelper: AddonModGlossaryHelperProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider) {
super('AddonModGlossarySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
this.componentTranslate = courseProvider.translateModuleName('glossary');
}
/**
* Try to synchronize all the glossaries in a certain site or in all sites.
*
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllGlossaries(siteId?: string): Promise<any> {
return this.syncOnSites('all glossaries', this.syncAllGlossariesFunc.bind(this), [], siteId);
}
/**
* Sync all glossaries on a site.
*
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
protected syncAllGlossariesFunc(siteId?: string): Promise<any> {
// Sync all new entries
return this.glossaryOffline.getAllNewEntries(siteId).then((entries) => {
const promises = {};
// Do not sync same glossary twice.
for (const i in entries) {
const entry = entries[i];
if (typeof promises[entry.glossaryid] != 'undefined') {
continue;
}
promises[entry.glossaryid] = this.syncGlossaryEntriesIfNeeded(entry.glossaryid, entry.userid, siteId)
.then((result) => {
if (result && result.updated) {
// Sync successful, send event.
this.eventsProvider.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, {
glossaryId: entry.glossaryid,
userId: entry.userid,
warnings: result.warnings
}, siteId);
}
});
}
// Promises will be an object so, convert to an array first;
return Promise.all(this.utils.objectToArray(promises));
});
}
/**
* Sync a glossary only if a certain time has passed since the last time.
*
* @param {number} glossaryId Glossary ID.
* @param {number} userId User the entry belong to.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the glossary is synced or if it doesn't need to be synced.
*/
syncGlossaryEntriesIfNeeded(glossaryId: number, userId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const syncId = this.getGlossarySyncId(glossaryId, userId);
return this.isSyncNeeded(syncId, siteId).then((needed) => {
if (needed) {
return this.syncGlossaryEntries(glossaryId, userId, siteId);
}
});
}
/**
* Synchronize all offline entries of a glossary.
*
* @param {number} glossaryId Glossary ID to be synced.
* @param {number} [userId] User the entries belong to.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
*/
syncGlossaryEntries(glossaryId: number, userId?: number, siteId?: string): Promise<any> {
userId = userId || this.sitesProvider.getCurrentSiteUserId();
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const syncId = this.getGlossarySyncId(glossaryId, userId);
if (this.isSyncing(syncId, siteId)) {
// There's already a sync ongoing for this glossary, return the promise.
return this.getOngoingSync(syncId, siteId);
}
// Verify that glossary isn't blocked.
if (this.syncProvider.isBlocked(AddonModGlossaryProvider.COMPONENT, syncId, siteId)) {
this.logger.debug('Cannot sync glossary ' + glossaryId + ' because it is blocked.');
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
}
this.logger.debug('Try to sync glossary ' + glossaryId + ' for user ' + userId);
let courseId;
const result = {
warnings: [],
updated: false
};
// Get offline responses to be sent.
const syncPromise = this.glossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId).catch(() => {
// No offline data found, return empty object.
return [];
}).then((entries) => {
if (!entries.length) {
// Nothing to sync.
return;
} else if (!this.appProvider.isOnline()) {
// Cannot sync in offline.
return Promise.reject(null);
}
const promises = [];
entries.forEach((data) => {
let promise;
courseId = data.courseid;
// First of all upload the attachments (if any).
promise = this.uploadAttachments(glossaryId, data, siteId).then((itemId) => {
// Now try to add the entry.
return this.glossaryProvider.addEntryOnline(
glossaryId, data.concept, data.definition, data.options, itemId, siteId);
});
promises.push(promise.then(() => {
result.updated = true;
return this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId);
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
result.updated = true;
return this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId).then(() => {
// Responses deleted, add a warning.
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: data.concept,
error: error.error
}));
});
} else {
// Couldn't connect to server, reject.
return Promise.reject(error);
}
}));
});
return Promise.all(promises);
}).then(() => {
if (result.updated && courseId) {
// Data has been sent to server. Now invalidate the WS calls.
return this.glossaryProvider.getGlossaryById(courseId, glossaryId).then((glossary) => {
return this.glossaryProvider.invalidateGlossaryEntries(glossary, true);
}).catch(() => {
// Ignore errors.
});
}
}).then(() => {
// Sync finished, set sync time.
return this.setSyncTime(syncId, siteId).catch(() => {
// Ignore errors.
});
}).then(() => {
// All done, return the warnings.
return result;
});
return this.addOngoingSync(syncId, syncPromise, siteId);
}
/**
* Delete a new entry.
*
* @param {number} glossaryId Glossary ID.
* @param {string} concept Glossary entry concept.
* @param {number} timeCreated Time to allow duplicated entries.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when deleted.
*/
protected deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<any> {
const promises = [];
promises.push(this.glossaryOffline.deleteNewEntry(glossaryId, concept, timeCreated, siteId));
promises.push(this.glossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId).catch(() => {
// Ignore errors, maybe there are no files.
}));
return Promise.all(promises);
}
/**
* Upload attachments of an offline entry.
*
* @param {number} glossaryId Glossary ID.
* @param {any} entry Offline entry.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<number>} Promise resolved with draftid if uploaded, resolved with 0 if nothing to upload.
*/
protected uploadAttachments(glossaryId: number, entry: any, siteId?: string): Promise<number> {
if (entry.attachments) {
// Has some attachments to sync.
let files = entry.attachments.online || [];
let promise;
if (entry.attachments.offline) {
// Has offline files.
promise = this.glossaryHelper.getStoredFiles(glossaryId, entry.concept, entry.timecreated, siteId).then((atts) => {
files = files.concat(atts);
}).catch(() => {
// Folder not found, no files to add.
});
} else {
promise = Promise.resolve(0);
}
return promise.then(() => {
return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModGlossaryProvider.COMPONENT, glossaryId, siteId);
});
}
// No attachments, resolve.
return Promise.resolve(0);
}
/**
* Get the ID of a glossary sync.
*
* @param {number} glossaryId Glossary ID.
* @param {number} [userId] User the entries belong to.. If not defined, current user.
* @return {string} Sync ID.
*/
protected getGlossarySyncId(glossaryId: number, userId?: number): string {
userId = userId || this.sitesProvider.getCurrentSiteUserId();
return 'glossary#' + glossaryId + '#' + userId;
}
}

View File

@ -87,6 +87,7 @@ import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
import { AddonModFeedbackModule } from '@addon/mod/feedback/feedback.module';
import { AddonModFolderModule } from '@addon/mod/folder/folder.module';
import { AddonModForumModule } from '@addon/mod/forum/forum.module';
import { AddonModGlossaryModule } from '@addon/mod/glossary/glossary.module';
import { AddonModPageModule } from '@addon/mod/page/page.module';
import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module';
import { AddonModScormModule } from '@addon/mod/scorm/scorm.module';
@ -188,6 +189,7 @@ export const CORE_PROVIDERS: any[] = [
AddonModFeedbackModule,
AddonModFolderModule,
AddonModForumModule,
AddonModGlossaryModule,
AddonModLtiModule,
AddonModPageModule,
AddonModQuizModule,

View File

@ -22,17 +22,14 @@ import { Directive, ElementRef, HostListener, Output, EventEmitter } from '@angu
* <textarea class="core-textarea" [(ngModel)]="message" rows="1" core-auto-rows></textarea>
*/
@Directive({
selector: 'textarea[core-auto-rows]'
selector: 'textarea[core-auto-rows], ion-textarea[core-auto-rows]'
})
export class CoreAutoRowsDirective {
protected element: HTMLTextAreaElement;
protected height = 0;
@Output() onResize: EventEmitter<void>; // Emit when resizing the textarea.
constructor(element: ElementRef) {
this.element = element.nativeElement || element;
this.height = this.element.scrollHeight;
constructor(private element: ElementRef) {
this.onResize = new EventEmitter();
}
@ -48,9 +45,9 @@ export class CoreAutoRowsDirective {
}
/**
* Resize after init.
* Resize after content.
*/
ngAfterViewInit(): void {
ngAfterViewContent(): void {
this.resize();
}
@ -59,13 +56,19 @@ export class CoreAutoRowsDirective {
* @param {any} $event Event fired.
*/
protected resize($event?: any): void {
let nativeElement = this.element.nativeElement;
if (nativeElement.tagName == 'ION-TEXTAREA') {
// The first child of ion-textarea is the actual textarea element.
nativeElement = nativeElement.firstElementChild;
}
// Set height to 1px to force scroll height to calculate correctly.
this.element.style.height = '1px';
this.element.style.height = this.element.scrollHeight + 'px';
nativeElement.style.height = '1px';
nativeElement.style.height = nativeElement.scrollHeight + 'px';
// Emit event when resizing.
if (this.height != this.element.scrollHeight) {
this.height = this.element.scrollHeight;
if (this.height != nativeElement.scrollHeight) {
this.height = nativeElement.scrollHeight;
this.onResize.emit();
}
}