Merge pull request #1278 from albertgasset/MOBILE-2319

Mobile 2319
main
Juan Leyva 2018-03-22 16:46:48 +01:00 committed by GitHub
commit e026563934
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1453 additions and 97 deletions

View File

@ -514,7 +514,7 @@ export class AddonCalendarProvider {
notificationtime: e.notificationtime || -1
};
return db.insertOrUpdateRecord(this.EVENTS_TABLE, eventRecord, { id: eventRecord.id });
return db.insertRecord(this.EVENTS_TABLE, eventRecord);
}));
});
@ -539,7 +539,7 @@ export class AddonCalendarProvider {
event.notificationtime = time;
return site.getDb().insertOrUpdateRecord(this.EVENTS_TABLE, event, { id: event.id }).then(() => {
return site.getDb().insertRecord(this.EVENTS_TABLE, event).then(() => {
return this.scheduleEventNotification(event, time);
});
});

View File

@ -22,10 +22,6 @@ import { AddonCompetencyComponentsModule } from './components/components.module'
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreUserDelegate } from '@core/user/providers/user-delegate';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
@NgModule({
declarations: [
@ -44,24 +40,10 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses';
export class AddonCompetencyModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: AddonCompetencyMainMenuHandler,
courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonCompetencyCourseOptionHandler,
userDelegate: CoreUserDelegate, userHandler: AddonCompetencyUserHandler,
eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) {
userDelegate: CoreUserDelegate, userHandler: AddonCompetencyUserHandler) {
mainMenuDelegate.registerHandler(mainMenuHandler);
courseOptionsDelegate.registerHandler(courseOptionHandler);
userDelegate.registerHandler(userHandler);
eventsProvider.on(CoreEventsProvider.LOGOUT, () => {
courseOptionHandler.clearCoursesNavCache();
userHandler.clearUsersNavCache();
}, sitesProvider.getCurrentSiteId());
eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED, () => {
courseOptionHandler.clearCoursesNavCache();
}, sitesProvider.getCurrentSiteId());
eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, () => {
userHandler.clearUsersNavCache();
}, sitesProvider.getCurrentSiteId());
}
}

View File

@ -26,17 +26,8 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand
name = 'AddonCompetency';
priority = 700;
protected coursesNavEnabledCache = {};
constructor(private competencyProvider: AddonCompetencyProvider) {}
/**
* Clear courses nav cache.
*/
clearCoursesNavCache(): void {
this.coursesNavEnabledCache = {};
}
/**
* Whether or not the handler is enabled ona site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
@ -63,15 +54,8 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand
return navOptions.competencies;
}
if (typeof this.coursesNavEnabledCache[courseId] != 'undefined') {
return this.coursesNavEnabledCache[courseId];
}
return this.competencyProvider.isPluginForCourseEnabled(courseId).then((competencies) => {
const enabled = competencies ? !competencies.canmanagecoursecompetencies : false;
this.coursesNavEnabledCache[courseId] = enabled;
return enabled;
return competencies ? !competencies.canmanagecoursecompetencies : false;
});
}

View File

@ -16,6 +16,8 @@ import { Injectable } from '@angular/core';
import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
import { CoreSitesProvider } from '@providers/sites';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreEventsProvider } from '@providers/events';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonCompetencyProvider } from './competency';
/**
@ -30,13 +32,15 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler {
usersNavEnabledCache = {};
constructor(private linkHelper: CoreContentLinksHelperProvider, protected sitesProvider: CoreSitesProvider,
private competencyProvider: AddonCompetencyProvider) {
private competencyProvider: AddonCompetencyProvider, eventsProvider: CoreEventsProvider) {
eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearUsersNavCache.bind(this));
eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, this.clearUsersNavCache.bind(this));
}
/**
* Clear users nav cache.
*/
clearUsersNavCache(): void {
private clearUsersNavCache(): void {
this.participantsNavEnabledCache = {};
this.usersNavEnabledCache = {};
}

View File

@ -148,11 +148,7 @@ export class AddonMessagesOfflineProvider {
deviceoffline: this.appProvider.isOnline() ? 0 : 1
};
return site.getDb().insertOrUpdateRecord(this.MESSAGES_TABLE, entry, {
touserid: toUserId,
smallmessage: message,
timecreated: entry.timecreated
}).then(() => {
return site.getDb().insertRecord(this.MESSAGES_TABLE, entry).then(() => {
return entry;
});
});
@ -173,11 +169,7 @@ export class AddonMessagesOfflineProvider {
data = { deviceoffline: value ? 1 : 0 };
messages.forEach((message) => {
promises.push(db.insertOrUpdateRecord(this.MESSAGES_TABLE, data, {
touserid: message.touserid,
smallmessage: message.smallmessage,
timecreated: message.timecreated
}));
promises.push(db.insertRecord(this.MESSAGES_TABLE, data));
});
return Promise.all(promises);

View File

@ -0,0 +1,45 @@
// (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 { AddonNotesListComponent } from './list/list';
@NgModule({
declarations: [
AddonNotesListComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule
],
providers: [
],
exports: [
AddonNotesListComponent
],
entryComponents: [
AddonNotesListComponent
]
})
export class AddonNotesComponentsModule {}

View File

@ -0,0 +1,43 @@
<core-navbar-buttons end>
<core-context-menu>
<core-context-menu-item [hidden]="!(notesLoaded && !hasOffline)" [priority]="100" [content]="'core.refresh' | translate" (action)="refreshNotes(false)" [iconAction]="refreshIcon" [closeOnClick]="true"></core-context-menu-item>
<core-context-menu-item [hidden]="!(notesLoaded && hasOffline)" [priority]="100" [content]="'core.settings.synchronizenow' | translate" (action)="refreshNotes(true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-refresher [enabled]="notesLoaded" (ionRefresh)="refreshNotes(false, $event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="notesLoaded" class="core-loading-center">
<ion-select [(ngModel)]="type" (ngModelChange)="typeChanged()" interface="popover">
<ion-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-option>
<ion-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-option>
<ion-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-option>
</ion-select>
<div class="core-warning-card" icon-start *ngIf="hasOffline">
<ion-icon name="warning"></ion-icon>
{{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }}
</div>
<core-empty-box *ngIf="notes && notes.length == 0" icon="list" [message]="'addon.notes.nonotes' | translate"></core-empty-box>
<ion-list *ngIf="notes && notes.length > 0">
<ion-card *ngFor="let note of notes">
<ion-item text-wrap>
<ion-avatar item-start>
<img [src]="note.userprofileimageurl || 'assets/img/user-avatar.png'" core-external-content core-user-link [userId]="note.userid" [courseId]="note.courseid" [alt]="'core.pictureof' | translate:{$a: note.userfullname}" role="presentation">
</ion-avatar>
<h2>{{note.userfullname}}</h2>
<p *ngIf="!note.offline" item-end>{{note.lastmodified | coreDateDayOrTime}}</p>
<p *ngIf="note.offline" item-end><ion-icon name="clock"></ion-icon> {{ 'core.notsent' | translate }}</p>
</ion-item>
<ion-item text-wrap>
<core-format-text [clean]="true" [text]="note.content"></core-format-text>
</ion-item>
</ion-card>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,171 @@
// (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, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Content } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonNotesProvider } from '../../providers/notes';
import { AddonNotesSyncProvider } from '../../providers/notes-sync';
/**
* Component that displays the notes of a course.
*/
@Component({
selector: 'addon-notes-list',
templateUrl: 'list.html',
})
export class AddonNotesListComponent implements OnInit, OnDestroy {
@Input() courseId: number;
@ViewChild(Content) content: Content;
protected syncObserver: any;
type = 'course';
refreshIcon = 'spinner';
syncIcon = 'spinner';
notes: any[];
hasOffline = false;
notesLoaded = false;
constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider) {
// Refresh data if notes are synchronized automatically.
this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => {
if (data.courseId == this.courseId) {
// Show the sync warnings.
this.showSyncWarnings(data.warnings);
// Refresh the data.
this.notesLoaded = false;
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
this.content.scrollToTop();
this.fetchNotes(false);
}
}, sitesProvider.getCurrentSiteId());
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.fetchNotes(true).then(() => {
this.notesProvider.logView(this.courseId);
});
}
/**
* Fetch notes.
*
* @param {boolean} sync When to resync notes.
* @param {boolean} [showErrors] When to display errors or not.
* @return {Promise<any>} Promise with the notes.
*/
private fetchNotes(sync: boolean, showErrors?: boolean): Promise<any> {
const promise = sync ? this.syncNotes(showErrors) : Promise.resolve();
return promise.catch(() => {
// Ignore errors.
}).then(() => {
return this.notesProvider.getNotes(this.courseId).then((notes) => {
notes = notes[this.type + 'notes'] || [];
this.hasOffline = notes.some((note) => note.offline);
return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => {
this.notes = notes;
});
});
}).catch((message) => {
this.domUtils.showErrorModal(message);
}).finally(() => {
this.notesLoaded = true;
this.refreshIcon = 'refresh';
this.syncIcon = 'sync';
});
}
/**
* Refresh notes on PTR.
*
* @param {boolean} showErrors Whether to display errors or not.
* @param {any} refresher Refresher instance.
*/
refreshNotes(showErrors: boolean, refresher?: any): void {
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
this.notesProvider.invalidateNotes(this.courseId).finally(() => {
this.fetchNotes(true, showErrors).finally(() => {
if (refresher) {
refresher.complete();
}
});
});
}
/**
* Function called when the type has changed.
*/
typeChanged(): void {
this.notesLoaded = false;
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
this.fetchNotes(true).then(() => {
this.notesProvider.logView(this.courseId);
});
}
/**
* Tries to synchronize course notes.
*
* @param {boolean} showErrors Whether to display errors or not.
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
*/
private syncNotes(showErrors: boolean): Promise<any> {
return this.notesSync.syncNotes(this.courseId).then((warnings) => {
this.showSyncWarnings(warnings);
}).catch((error) => {
if (showErrors) {
this.domUtils.showErrorModalDefault(error, 'core.errorsync', true);
}
return Promise.reject(null);
});
}
/**
* Show sync warnings if any.
*
* @param {string[]} warnings the warnings
*/
private showSyncWarnings(warnings: string[]): void {
const message = this.textUtils.buildMessage(warnings);
if (message) {
this.domUtils.showErrorModal(message);
}
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.syncObserver && this.syncObserver.off();
}
}

View File

@ -0,0 +1,13 @@
{
"addnewnote": "Add a new note",
"coursenotes": "Course notes",
"eventnotecreated": "Note created",
"nonotes": "There are no notes of this type yet.",
"note": "Note",
"notes": "Notes",
"personalnotes": "Personal notes",
"publishstate": "Context",
"sitenotes": "Site notes",
"userwithid": "User with ID {{id}}",
"warningnotenotsent": "Couldn't add note(s) to course {{course}}. {{error}}"
}

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 { AddonNotesProvider } from './providers/notes';
import { AddonNotesOfflineProvider } from './providers/notes-offline';
import { AddonNotesSyncProvider } from './providers/notes-sync';
import { AddonNotesCourseOptionHandler } from './providers/course-option-handler';
import { AddonNotesSyncCronHandler } from './providers/sync-cron-handler';
import { AddonNotesUserHandler } from './providers/user-handler';
import { AddonNotesComponentsModule } from './components/components.module';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { CoreCronDelegate } from '@providers/cron';
import { CoreUserDelegate } from '@core/user/providers/user-delegate';
@NgModule({
declarations: [
],
imports: [
AddonNotesComponentsModule
],
providers: [
AddonNotesProvider,
AddonNotesOfflineProvider,
AddonNotesSyncProvider,
AddonNotesCourseOptionHandler,
AddonNotesSyncCronHandler,
AddonNotesUserHandler
]
})
export class AddonNotesModule {
constructor(courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonNotesCourseOptionHandler,
userDelegate: CoreUserDelegate, userHandler: AddonNotesUserHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonNotesSyncCronHandler) {
// Register handlers.
courseOptionsDelegate.registerHandler(courseOptionHandler);
userDelegate.registerHandler(userHandler);
cronDelegate.register(syncHandler);
}
}

View File

@ -0,0 +1,28 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'addon.notes.addnewnote' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding>
<form name="itemEdit" (ngSubmit)="addNote()">
<ion-item>
<ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label>
<ion-select [(ngModel)]="publishState" name="publishState">
<ion-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-option>
<ion-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-option>
<ion-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-textarea placeholder="{{ 'addon.notes.note' | translate }}" rows="5" [(ngModel)]="text" name="text" required="required"></ion-textarea>
</ion-item>
<button ion-button block margin-vertical type="submit" [disabled]="processing || text.length < 2">
{{ 'addon.notes.addnewnote' | translate }}
</button>
</form>
</ion-content>

View File

@ -0,0 +1,31 @@
// (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 { AddonNotesAddPage } from './add';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonNotesAddPage
],
imports: [
CoreDirectivesModule,
IonicPageModule.forChild(AddonNotesAddPage),
TranslateModule.forChild()
]
})
export class AddonNotesAddPageModule {}

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 { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonNotesProvider } from '../../providers/notes';
/**
* Component that displays a text area for composing a note.
*/
@IonicPage({ segment: 'addon-notes-add' })
@Component({
selector: 'page-addon-notes-add',
templateUrl: 'add.html',
})
export class AddonNotesAddPage {
userId: number;
courseId: number;
publishState = 'personal';
text = '';
processing = false;
constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider,
private domUtils: CoreDomUtilsProvider, private notesProvider: AddonNotesProvider) {
this.userId = params.get('userId');
this.courseId = params.get('courseId');
}
/**
* Send the note or store it offline.
*/
addNote(): void {
this.appProvider.closeKeyboard();
const loadingModal = this.domUtils.showModalLoading('core.sending', true);
// Freeze the add note button.
this.processing = true;
this.notesProvider.addNote(this.userId, this.courseId, this.publishState, this.text).then((sent) => {
this.viewCtrl.dismiss().finally(() => {
const message = sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline';
this.domUtils.showAlertTranslated('core.success', message);
});
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.processing = false;
}).finally(() => {
loadingModal.dismiss();
});
}
/**
* Close modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
}

View File

@ -0,0 +1,74 @@
// (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 { AddonNotesProvider } from './notes';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate';
import { AddonNotesListComponent } from '../components/list/list';
/**
* Handler to inject an option into the course main menu.
*/
@Injectable()
export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler {
name = 'AddonNotes';
priority = 200;
constructor(private notesProvider: AddonNotesProvider) {
}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.notesProvider.isPluginEnabled();
}
/**
* Whether or not the handler is enabled for a certain course.
*
* @param {number} courseId The course ID.
* @param {any} accessData Access type and data. Default, guest, ...
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guests.
}
if (navOptions && typeof navOptions.notes != 'undefined') {
return navOptions.notes;
}
return this.notesProvider.isPluginViewNotesEnabledForCourse(courseId);
}
/**
* Returns the data needed to render the handler.
*
* @param {number} courseId The course ID.
* @return {CoreCourseOptionsHandlerData} Data.
*/
getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData {
return {
title: 'addon.notes.notes',
class: 'addon-notes-course-handler',
component: AddonNotesListComponent,
};
}
}

View File

@ -0,0 +1,227 @@
// (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 { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
/**
* Service to handle offline notes.
*/
@Injectable()
export class AddonNotesOfflineProvider {
protected logger;
// Variables for database.
protected NOTES_TABLE = 'addon_notes_offline_notes';
protected tablesSchema = [
{
name: this.NOTES_TABLE,
columns: [
{
name: 'userid',
type: 'INTEGER'
},
{
name: 'courseid',
type: 'INTEGER'
},
{
name: 'publishstate',
type: 'TEXT',
},
{
name: 'content',
type: 'TEXT'
},
{
name: 'format',
type: 'INTEGER'
},
{
name: 'created',
type: 'INTEGER'
},
{
name: 'lastmodified',
type: 'INTEGER'
}
],
primaryKeys: ['userid', 'content', 'created']
}
];
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) {
this.logger = logger.getInstance('AddonNotesOfflineProvider');
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
}
/**
* Delete a note.
*
* @param {number} userId User ID the note is about.
* @param {string} content The note content.
* @param {number} timecreated The time the note was created.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
*/
deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(this.NOTES_TABLE, {
userid: userId,
content: content,
created: timecreated
});
});
}
/**
* Get all offline notes.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with notes.
*/
getAllNotes(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(this.NOTES_TABLE);
});
}
/**
* Get an offline note.
*
* @param {number} userId User ID the note is about.
* @param {string} content The note content.
* @param {number} timecreated The time the note was created.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the notes.
*/
getNote(userId: number, content: string, timecreated: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecord(this.NOTES_TABLE, {
userid: userId,
content: content,
created: timecreated
});
});
}
/**
* Get offline notes for a certain course.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with notes.
*/
getNotesForCourse(courseId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(this.NOTES_TABLE, {courseid: courseId});
});
}
/**
* Get offline notes for a certain user.
*
* @param {number} userId User ID the notes are about.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with notes.
*/
getNotesForUser(userId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(this.NOTES_TABLE, {userid: userId});
});
}
/**
* Get offline notes with a certain publish state (Personal, Site or Course).
*
* @param {string} state Publish state ('personal', 'site' or 'course').
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with notes.
*/
getNotesWithPublishState(state: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(this.NOTES_TABLE, {publishstate: state});
});
}
/**
* Check if there are offline notes for a certain course.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline notes, false otherwise.
*/
hasNotesForCourse(courseId: number, siteId?: string): Promise<boolean> {
return this.getNotesForCourse(courseId, siteId).then((notes) => {
return !!notes.length;
});
}
/**
* Check if there are offline notes for a certain user.
*
* @param {number} userId User ID the notes are about.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline notes, false otherwise.
*/
hasNotesForUser(userId: number, siteId?: string): Promise<boolean> {
return this.getNotesForUser(userId, siteId).then((notes) => {
return !!notes.length;
});
}
/**
* Check if there are offline notes with a certain publish state (Personal, Site or Course).
*
* @param {string} state Publish state ('personal', 'site' or 'course').
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline notes, false otherwise.
*/
hasNotesWithPublishState(state: string, siteId?: string): Promise<boolean> {
return this.getNotesWithPublishState(state, siteId).then((notes) => {
return !!notes.length;
});
}
/**
* Save a note to be sent later.
*
* @param {number} userId User ID the note is about.
* @param {number} courseId Course ID.
* @param {string} state Publish state ('personal', 'site' or 'course').
* @param {string} content The note content.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
*/
saveNote(userId: number, courseId: number, state: string, content: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const now = this.timeUtils.timestamp();
const data = {
userid: userId,
courseid: courseId,
publishstate: state,
content: content,
format: 1,
created: now,
lastmodified: now
};
return site.getDb().insertRecord(this.NOTES_TABLE, data).then(() => {
return data;
});
});
}
}

View File

@ -0,0 +1,198 @@
// (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 { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreAppProvider } from '@providers/app';
import { AddonNotesOfflineProvider } from './notes-offline';
import { AddonNotesProvider } from './notes';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreEventsProvider } from '@providers/events';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { TranslateService } from '@ngx-translate/core';
import { CoreSyncProvider } from '@providers/sync';
/**
* Service to sync notes.
*/
@Injectable()
export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_notes_autom_synced';
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
protected appProvider: CoreAppProvider, private notesOffline: AddonNotesOfflineProvider,
private eventsProvider: CoreEventsProvider, private notesProvider: AddonNotesProvider,
private coursesProvider: CoreCoursesProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) {
super('AddonNotesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils);
}
/**
* Try to synchronize all the notes 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.
*/
syncAllNotes(siteId?: string): Promise<any> {
return this.syncOnSites('all notes', this.syncAllNotesFunc.bind(this), [], siteId);
}
/**
* Synchronize all the notes in a certain site
*
* @param {string} siteId Site ID to sync.
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
private syncAllNotesFunc(siteId: string): Promise<any> {
return this.notesOffline.getAllNotes(siteId).then((notes) => {
// Get all the courses to be synced.
const courseIds = [];
notes.forEach((note) => {
if (courseIds.indexOf(note.courseid) == -1) {
courseIds.push(note.courseid);
}
});
// Sync all courses.
const promises = courseIds.map((courseId) => {
return this.syncNotesIfNeeded(courseId, siteId).then((warnings) => {
if (typeof warnings != 'undefined') {
// Sync successful, send event.
this.eventsProvider.trigger(AddonNotesSyncProvider.AUTO_SYNCED, {
courseId: courseId,
warnings: warnings
}, siteId);
}
});
});
return Promise.all(promises);
});
}
/**
* Sync course notes only if a certain time has passed since the last time.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the notes are synced or if they don't need to be synced.
*/
private syncNotesIfNeeded(courseId: number, siteId?: string): Promise<void> {
return this.isSyncNeeded(courseId, siteId).then((needed) => {
if (needed) {
return this.syncNotes(courseId, siteId);
}
});
}
/**
* Synchronize notes of a course.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
*/
syncNotes(courseId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (this.isSyncing(courseId, siteId)) {
// There's already a sync ongoing for notes, return the promise.
return this.getOngoingSync(courseId, siteId);
}
this.logger.debug('Try to sync notes for course ' + courseId);
const warnings = [];
// Get offline notes to be sent.
const syncPromise = this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => {
if (!notes.length) {
// Nothing to sync.
return;
} else if (!this.appProvider.isOnline()) {
// Cannot sync in offline.
return Promise.reject(this.translate.instant('core.networkerrormsg'));
}
const errors = [];
// Format the notes to be sent.
const notesToSend = notes.map((note) => {
return {
userid: note.userid,
publishstate: note.publishstate,
courseid: note.courseid,
text: note.content,
format: note.format
};
});
// Send the notes.
return this.notesProvider.addNotesOnline(notesToSend, siteId).then((response) => {
// Search errors in the response.
response.forEach((entry) => {
if (entry.noteid === -1 && errors.indexOf(entry.errormessage) == -1) {
errors.push(entry.errormessage);
}
});
// Fetch the notes from server to be sure they're up to date.
return this.notesProvider.invalidateNotes(courseId, siteId).then(() => {
return this.notesProvider.getNotes(courseId, false, true, siteId);
}).catch(() => {
// Ignore errors.
});
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// It's a WebService error, this means the user cannot send notes.
errors.push(error);
} else {
// Not a WebService error, reject the synchronization to try again.
return Promise.reject(error);
}
}).then(() => {
// Notes were sent, delete them from local DB.
const promises = notes.map((note) => {
return this.notesOffline.deleteNote(note.userid, note.content, note.created, siteId);
});
return Promise.all(promises);
}).then(() => {
if (errors && errors.length) {
// At least an error occurred, get course name and add errors to warnings array.
return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => {
// Ignore errors.
return {};
}).then((course) => {
errors.forEach((error) => {
warnings.push(this.translate.instant('addon.notes.warningnotenotsent', {
course: course.fullname ? course.fullname : courseId,
error: error
}));
});
});
}
});
}).then(() => {
// All done, return the warnings.
return warnings;
});
return this.addOngoingSync(courseId, syncPromise, siteId);
}
}

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 { CoreAppProvider } from '@providers/app';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSiteWSPreSets } from '@classes/site';
import { TranslateService } from '@ngx-translate/core';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonNotesOfflineProvider } from './notes-offline';
/**
* Service to handle notes.
*/
@Injectable()
export class AddonNotesProvider {
protected ROOT_CACHE_KEY = 'mmaNotes:';
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider,
private utils: CoreUtilsProvider, private translate: TranslateService, private userProvider: CoreUserProvider,
private notesOffline: AddonNotesOfflineProvider) {
this.logger = logger.getInstance('AddonNotesProvider');
}
/**
* Add a note.
*
* @param {number} userId User ID of the person to add the note.
* @param {number} courseId Course ID where the note belongs.
* @param {string} publishState Personal, Site or Course.
* @param {string} noteText The note text.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if note was sent to server, false if stored in device.
*/
addNote(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise<boolean> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Convenience function to store a note to be synchronized later.
const storeOffline = (): Promise<any> => {
return this.notesOffline.saveNote(userId, courseId, publishState, noteText, siteId).then(() => {
return false;
});
};
if (!this.appProvider.isOnline()) {
// App is offline, store the note.
return storeOffline();
}
// Send note to server.
return this.addNoteOnline(userId, courseId, publishState, noteText, siteId).then(() => {
return true;
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// It's a WebService error, the user cannot send the message so don't store it.
return Promise.reject(error);
}
// Error sending note, store it to retry later.
return storeOffline();
});
}
/**
* Add a note. It will fail if offline or cannot connect.
*
* @param {number} userId User ID of the person to add the note.
* @param {number} courseId Course ID where the note belongs.
* @param {string} publishState Personal, Site or Course.
* @param {string} noteText The note text.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when added, rejected otherwise.
*/
addNoteOnline(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise<any> {
const notes = [
{
courseid: courseId,
format: 1,
publishstate: publishState,
text: noteText,
userid: userId
}
];
return this.addNotesOnline(notes, siteId).then((response) => {
if (response && response[0] && response[0].noteid === -1) {
// There was an error, and it should be translated already.
return Promise.reject(this.utils.createFakeWSError(response[0].errormessage));
}
// A note was added, invalidate the course notes.
return this.invalidateNotes(courseId, siteId).catch(() => {
// Ignore errors.
});
});
}
/**
* Add several notes. It will fail if offline or cannot connect.
*
* @param {any[]} notes Notes to save.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that notes
* have been added, the resolve param can contain errors for notes not sent.
*/
addNotesOnline(notes: any[], siteId?: string): Promise<any> {
if (!notes || !notes.length) {
return Promise.resolve();
}
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
notes: notes
};
return site.write('core_notes_create_notes', data);
});
}
/**
* Returns whether or not the notes plugin is enabled for a certain site.
*
* This method is called quite often and thus should only perform a quick
* check, we should not be calling WS from here.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false or rejected otherwise.
*/
isPluginEnabled(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.canUseAdvancedFeature('enablenotes');
});
}
/**
* Returns whether or not the add note plugin is enabled for a certain course.
*
* @param {number} courseId ID of the course.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false or rejected otherwise.
*/
isPluginAddNoteEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
// The only way to detect if it's enabled is to perform a WS call.
// We use an invalid user ID (-1) to avoid saving the note if the user has permissions.
const data = {
notes: [
{
userid: -1,
publishstate: 'personal',
courseid: courseId,
text: '',
format: 1
}
]
};
/* Use .read to cache data and be able to check it in offline. This means that, if a user loses the capabilities
to add notes, he'll still see the option in the app. */
return this.utils.promiseWorks(site.read('core_notes_create_notes', data));
});
}
/**
* Returns whether or not the read notes plugin is enabled for a certain course.
*
* @param {number} courseId ID of the course.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false or rejected otherwise.
*/
isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> {
return this.utils.promiseWorks(this.getNotes(courseId, false, true, siteId));
}
/**
* Get the cache key for the get notes call.
*
* @param {number} courseId ID of the course to get the notes from.
* @return {string} Cache key.
*/
getNotesCacheKey(courseId: number): string {
return this.ROOT_CACHE_KEY + 'notes:' + courseId;
}
/**
* Get users notes for a certain site, course and personal notes.
*
* @param {number} courseId ID of the course to get the notes from.
* @param {boolean} [ignoreCache] True when we should not get the value from the cache.
* @param {boolean} [onlyOnline] True to return only online notes, false to return both online and offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise to be resolved when the notes are retrieved.
*/
getNotes(courseId: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise<any> {
this.logger.debug('Get notes for course ' + courseId);
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
courseid: courseId
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getNotesCacheKey(courseId)
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('core_notes_get_course_notes', data, preSets).then((notes) => {
if (onlyOnline) {
return notes;
}
// Get offline notes and add them to the list.
return this.notesOffline.getNotesForCourse(courseId, siteId).then((offlineNotes) => {
offlineNotes.forEach((note) => {
const fieldName = note.publishstate + 'notes';
if (!notes[fieldName]) {
notes[fieldName] = [];
}
note.offline = true;
// Add note to the start of array since last notes are shown first.
notes[fieldName].unshift(note);
});
return notes;
});
});
});
}
/**
* Get user data for notes since they only have userid.
*
* @param {any[]} notes Notes to get the data for.
* @param {number} courseId ID of the course the notes belong to.
* @return {Promise<any>} Promise always resolved. Resolve param is the formatted notes.
*/
getNotesUserData(notes: any[], courseId: number): Promise<any> {
const promises = notes.map((note) => {
// Get the user profile to retrieve the user image.
return this.userProvider.getProfile(note.userid, note.courseid, true).then((user) => {
note.userfullname = user.fullname;
note.userprofileimageurl = user.profileimageurl || null;
}).catch(() => {
note.userfullname = this.translate.instant('addon.notes.userwithid', {id: note.userid});
});
});
return Promise.all(promises).then(() => {
return notes;
});
}
/**
* Invalidate get notes WS call.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when data is invalidated.
*/
invalidateNotes(courseId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId));
});
}
/**
* Report notes as being viewed.
*
* @param {number} courseId ID of the course.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logView(courseId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
courseid: courseId,
userid: 0
};
return site.write('core_notes_view_notes', params);
});
}
}

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 { AddonNotesSyncProvider } from './notes-sync';
/**
* Synchronization cron handler.
*/
@Injectable()
export class AddonNotesSyncCronHandler implements CoreCronHandler {
name = 'AddonNotesSyncCronHandler';
constructor(private notesSync: AddonNotesSyncProvider) {}
/**
* 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.notesSync.syncAllNotes(siteId);
}
/**
* Get the time between consecutive executions.
*
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
}
}

View File

@ -0,0 +1,107 @@
// (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 { ModalController } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
import { CoreSitesProvider } from '@providers/sites';
import { AddonNotesProvider } from './notes';
/**
* Profile notes handler.
*/
@Injectable()
export class AddonNotesUserHandler implements CoreUserProfileHandler {
name = 'AddonNotes';
priority = 200;
type = CoreUserDelegate.TYPE_COMMUNICATION;
addNoteEnabledCache = {};
constructor(private modalCtrl: ModalController, private sitesProvider: CoreSitesProvider,
private notesProvider: AddonNotesProvider, eventsProvider: CoreEventsProvider) {
eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearAddNoteCache.bind(this));
eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
this.clearAddNoteCache(data.courseId);
});
}
/**
* Clear add note cache.
* If a courseId is specified, it will only delete the entry for that course.
*
* @param {number} [courseId] Course ID.
*/
private clearAddNoteCache(courseId?: number): void {
if (courseId) {
delete this.addNoteEnabledCache[courseId];
} else {
this.addNoteEnabledCache = {};
}
}
/**
* Whether or not the handler is enabled on a site level.
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.notesProvider.isPluginEnabled();
}
/**
* Check if handler is enabled for this user in this context.
*
* @param {any} user User to check.
* @param {number} courseId Course ID.
* @param {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions.
* @param {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions.
* @return {boolean|Promise<boolean>} Promise resolved with true if enabled, resolved with false otherwise.
*/
isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
// Active course required.
if (!courseId || user.id == this.sitesProvider.getCurrentSiteUserId()) {
return Promise.resolve(false);
}
if (typeof this.addNoteEnabledCache[courseId] != 'undefined') {
return this.addNoteEnabledCache[courseId];
}
return this.notesProvider.isPluginAddNoteEnabledForCourse(courseId).then((enabled) => {
this.addNoteEnabledCache[courseId] = enabled;
return enabled;
});
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreUserProfileHandlerData} Data needed to render the handler.
*/
getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData {
return {
icon: 'list',
title: 'addon.notes.addnewnote',
class: 'addon-notes-handler',
action: (event, navCtrl, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: user.id, courseId });
modal.present();
}
};
}
}

View File

@ -326,8 +326,8 @@ export class AddonPushNotificationsProvider {
this.onMessageReceived(notification);
});
pushObject.on('registration').subscribe((registrationId: any) => {
this.pushID = registrationId;
pushObject.on('registration').subscribe((data: any) => {
this.pushID = data.registrationId;
this.registerDeviceOnMoodle().catch((error) => {
this.logger.warn('Can\'t register device', error);
});
@ -402,7 +402,7 @@ export class AddonPushNotificationsProvider {
number: value
};
return this.appDB.insertOrUpdateRecord(this.BADGE_TABLE, entry, {siteid: siteId, addon: addon}).then(() => {
return this.appDB.insertRecord(this.BADGE_TABLE, entry).then(() => {
return value;
});
}

View File

@ -81,6 +81,7 @@ import { AddonModFolderModule } from '@addon/mod/folder/folder.module';
import { AddonModPageModule } from '@addon/mod/page/page.module';
import { AddonModUrlModule } from '@addon/mod/url/url.module';
import { AddonMessagesModule } from '@addon/messages/messages.module';
import { AddonNotesModule } from '../addon/notes/notes.module';
import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module';
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
@ -165,6 +166,7 @@ export const CORE_PROVIDERS: any[] = [
AddonModPageModule,
AddonModUrlModule,
AddonMessagesModule,
AddonNotesModule,
AddonPushNotificationsModule,
AddonRemoteThemesModule
],

View File

@ -151,11 +151,11 @@ export class CoreSyncBaseProvider {
/**
* Check if a sync is needed: if a certain time has passed since the last time.
*
* @param {string} id Unique sync identifier per component.
* @param {string | number} id Unique sync identifier per component.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: whether sync is needed.
*/
isSyncNeeded(id: string, siteId?: string): Promise<boolean> {
isSyncNeeded(id: string | number, siteId?: string): Promise<boolean> {
return this.getSyncTime(id, siteId).then((time) => {
return Date.now() - this.syncInterval >= time;
});
@ -178,12 +178,12 @@ export class CoreSyncBaseProvider {
/**
* Set the synchronization warnings.
*
* @param {string} id Unique sync identifier per component.
* @param {string | number} id Unique sync identifier per component.
* @param {string[]} warnings Warnings to set.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise<any> {
setSyncWarnings(id: string | number, warnings: string[], siteId?: string): Promise<any> {
const warningsText = JSON.stringify(warnings || []);
return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId);

View File

@ -780,7 +780,7 @@ export class CoreSite {
entry.key = preSets.cacheKey;
}
return this.db.insertOrUpdateRecord(this.WS_CACHE_TABLE, entry, { id: id });
return this.db.insertRecord(this.WS_CACHE_TABLE, entry);
});
}

View File

@ -625,7 +625,7 @@ export class SQLiteDB {
questionMarks = ',?'.repeat(keys.length).substr(1);
return [
`INSERT INTO ${table} (${fields}) VALUES (${questionMarks})`,
`INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`,
keys.map((key) => data[key])
];
}
@ -644,24 +644,6 @@ export class SQLiteDB {
});
}
/**
* Insert or update a record.
*
* @param {string} table The database table.
* @param {object} data An object with the fields to insert/update: fieldname=>fieldvalue.
* @param {object} conditions The conditions to check if the record already exists (and to update it).
* @return {Promise<any>} Promise resolved with done.
*/
insertOrUpdateRecord(table: string, data: object, conditions: object): Promise<any> {
return this.getRecord(table, conditions).then(() => {
// It exists, update it.
return this.updateRecords(table, data, conditions);
}).catch(() => {
// Doesn't exist, insert it.
return this.insertRecord(table, data);
});
}
/**
* Insert a record into a table and return the "rowId" field.
*

View File

@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { Component, Input, OnInit, OnDestroy, ElementRef, Optional } from '@angular/core';
import { PopoverController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreContextMenuItemComponent } from './context-menu-item';
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
import { CoreTabComponent } from '@components/tabs/tab';
import { Subject } from 'rxjs';
/**
@ -40,7 +41,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy {
protected parentContextMenu: CoreContextMenuComponent;
constructor(private translate: TranslateService, private popoverCtrl: PopoverController, elementRef: ElementRef,
private domUtils: CoreDomUtilsProvider) {
private domUtils: CoreDomUtilsProvider, @Optional() public coreTab: CoreTabComponent) {
// Create the stream and subscribe to it. We ignore successive changes during 250ms.
this.itemsChangedStream = new Subject<void>();
this.itemsChangedStream.auditTime(250).subscribe(() => {

View File

@ -134,10 +134,12 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
}
// Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
const mainContextMenuInstance = this.domUtils.getInstanceByElement(mainContextMenu),
secondaryContextMenuInstance = this.domUtils.getInstanceByElement(secondaryContextMenu);
const mainContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(mainContextMenu),
secondaryContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(secondaryContextMenu);
if (mainContextMenuInstance && secondaryContextMenuInstance) {
// Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs.
if (mainContextMenuInstance && secondaryContextMenuInstance &&
mainContextMenuInstance.coreTab === secondaryContextMenuInstance.coreTab) {
this.mergedContextMenu = secondaryContextMenuInstance;
this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance);

View File

@ -9,7 +9,7 @@
"languages": {"ar": "عربي", "bg": "Български", "ca": "Català", "cs": "Čeština", "da": "Dansk", "de": "Deutsch", "de-du": "Deutsch - Du", "el": "Ελληνικά", "en": "English", "es": "Español", "es-mx": "Español - México", "eu": "Euskara", "fa": "فارسی", "fr" : "Français", "he" : "עברית", "hu": "magyar", "it": "Italiano", "lt" : "Lietuvių", "ja": "日本語","nl": "Nederlands", "pl": "Polski", "pt-br": "Português - Brasil", "pt": "Português - Portugal", "ro": "Română", "ru": "Русский", "sr-cr": "Српски", "sr-lt": "Srpski", "sv": "Svenska", "tr" : "Türkçe", "uk" : "Українська", "zh-cn" : "简体中文", "zh-tw" : "正體中文"},
"wsservice" : "moodle_mobile_app",
"wsextservice" : "local_mobile",
"demo_sites": {"student": {"url": "http://school.demo.moodle.net", "username": "student", "password": "moodle"}, "teacher": {"url": "http://school.demo.moodle.net", "username": "teacher", "password": "moodle"}},
"demo_sites": {"student": {"url": "https://school.demo.moodle.net", "username": "student", "password": "moodle"}, "teacher": {"url": "https://school.demo.moodle.net", "username": "teacher", "password": "moodle"}},
"gcmpn": "694767596569",
"customurlscheme": "moodlemobile",
"siteurl": "",

View File

@ -699,7 +699,7 @@ export class CoreCourseProvider {
previousDownloadTime: previousDownloadTime
};
return site.getDb().insertOrUpdateRecord(this.COURSE_STATUS_TABLE, data, { id: courseId });
return site.getDb().insertRecord(this.COURSE_STATUS_TABLE, data);
}
}).then(() => {
// Success inserting, trigger event.

View File

@ -417,7 +417,9 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
courseId: courseId,
time: this.timeUtils.timestamp()
};
site.getDb().insertOrUpdateRecord(this.CHECK_UPDATES_TIMES_TABLE, entry, { courseId: courseId });
site.getDb().insertRecord(this.CHECK_UPDATES_TIMES_TABLE, entry).catch(() => {
// Ignore errors.
});
return this.treatCheckUpdatesResult(data.toCheck, response, result);
}).catch((error) => {

View File

@ -671,7 +671,7 @@ export class LocalNotificationsMock extends LocalNotifications {
notification = Object.assign({}, notification); // Clone the object.
notification.triggered = !!triggered;
return this.appDB.insertOrUpdateRecord(this.DESKTOP_NOTIFS_TABLE, notification, { id: notification.id });
return this.appDB.insertRecord(this.DESKTOP_NOTIFS_TABLE, notification);
}
/**

View File

@ -383,7 +383,7 @@ export class CoreUserProvider {
profileimageurl: avatar
};
return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, { id: userId });
return site.getDb().insertRecord(this.USERS_TABLE, userRecord);
});
}

View File

@ -81,6 +81,6 @@ export class CoreConfigProvider {
* @return {Promise<any>} Promise resolved when done.
*/
set(name: string, value: boolean | number | string): Promise<any> {
return this.appDB.insertOrUpdateRecord(this.TABLE_NAME, { name: name, value: value }, { name: name });
return this.appDB.insertRecord(this.TABLE_NAME, { name: name, value: value });
}
}

View File

@ -459,7 +459,7 @@ export class CoreCronDelegate {
value: time
};
return this.appDB.insertOrUpdateRecord(this.CRON_TABLE, entry, { id: id });
return this.appDB.insertRecord(this.CRON_TABLE, entry);
}
/**

View File

@ -477,7 +477,7 @@ export class CoreFilepoolProvider {
componentId: componentId || ''
};
return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, { fileId: fileId });
return db.insertRecord(this.LINKS_TABLE, newEntry);
});
}
@ -544,7 +544,7 @@ export class CoreFilepoolProvider {
values.fileId = fileId;
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.insertOrUpdateRecord(this.FILES_TABLE, values, { fileId: fileId });
return db.insertRecord(this.FILES_TABLE, values);
});
}
@ -2766,7 +2766,7 @@ export class CoreFilepoolProvider {
// The package already has this status, no need to change it.
promise = Promise.resolve();
} else {
promise = site.getDb().insertOrUpdateRecord(this.PACKAGES_TABLE, packageEntry, { id: packageId });
promise = site.getDb().insertRecord(this.PACKAGES_TABLE, packageEntry);
}
return promise.then(() => {

View File

@ -500,6 +500,6 @@ export class CoreLocalNotificationsProvider {
at: parseInt(notification.at, 10)
};
return this.appDB.insertOrUpdateRecord(this.TRIGGERED_TABLE, entry, { id: notification.id });
return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry);
}
}

View File

@ -623,7 +623,7 @@ export class CoreSitesProvider {
loggedOut: 0
};
return this.appDB.insertOrUpdateRecord(this.SITES_TABLE, entry, { id: id });
return this.appDB.insertRecord(this.SITES_TABLE, entry);
}
/**
@ -895,7 +895,7 @@ export class CoreSitesProvider {
siteId: siteId
};
return this.appDB.insertOrUpdateRecord(this.CURRENT_SITE_TABLE, entry, { id: 1 }).then(() => {
return this.appDB.insertRecord(this.CURRENT_SITE_TABLE, entry).then(() => {
this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {}, siteId);
});
}

View File

@ -142,7 +142,7 @@ export class CoreSyncProvider {
data.component = component;
data.id = id;
return db.insertOrUpdateRecord(this.SYNC_TABLE, data, { component: component, id: id });
return db.insertRecord(this.SYNC_TABLE, data);
});
}