MOBILE-2877 comments: Add comments

main
Pau Ferrer Ocaña 2019-07-02 14:04:13 +02:00
parent 797b0d7931
commit 79bdd4ed02
14 changed files with 912 additions and 43 deletions

View File

@ -1269,7 +1269,9 @@
"core.comments.comments": "Comments",
"core.comments.commentscount": "Comments ({{$a}})",
"core.comments.commentsnotworking": "Comments cannot be retrieved",
"core.comments.eventcommentcreated": "Comment created",
"core.comments.nocomments": "No comments",
"core.comments.savecomment": "Save comment",
"core.completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)",
"core.completion-alt-auto-n": "Not completed: {{$a}}",
"core.completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})",

View File

@ -13,8 +13,12 @@
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreCommentsProvider } from './providers/comments';
import { CoreEventsProvider } from '@providers/events';
import { CoreCronDelegate } from '@providers/cron';
import { CoreCommentsProvider } from './providers/comments';
import { CoreCommentsOfflineProvider } from './providers/offline';
import { CoreCommentsSyncCronHandler } from './providers/sync-cron-handler';
import { CoreCommentsSyncProvider } from './providers/sync';
@NgModule({
declarations: [
@ -22,15 +26,20 @@ import { CoreEventsProvider } from '@providers/events';
imports: [
],
providers: [
CoreCommentsProvider
CoreCommentsProvider,
CoreCommentsOfflineProvider,
CoreCommentsSyncProvider,
CoreCommentsSyncCronHandler
]
})
export class CoreCommentsModule {
constructor(eventsProvider: CoreEventsProvider) {
constructor(eventsProvider: CoreEventsProvider, cronDelegate: CoreCronDelegate, syncHandler: CoreCommentsSyncCronHandler) {
// Reset comments page size.
eventsProvider.on(CoreEventsProvider.LOGIN, () => {
CoreCommentsProvider.pageSize = null;
CoreCommentsProvider.pageSizeOK = false;
});
cronDelegate.register(syncHandler);
}
}

View File

@ -103,7 +103,7 @@ export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy {
this.navCtrl.push('CoreCommentsViewerPage', {
contextLevel: this.contextLevel,
instanceId: this.instanceId,
component: this.component,
componentName: this.component,
itemId: this.itemId,
area: this.area,
title: this.title,

View File

@ -3,5 +3,7 @@
"comments": "Comments",
"commentscount": "Comments ({{$a}})",
"commentsnotworking": "Comments cannot be retrieved",
"nocomments": "No comments"
"eventcommentcreated": "Comment created",
"nocomments": "No comments",
"savecomment": "Save comment"
}

View File

@ -0,0 +1,22 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'core.comments.addcomment' | 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>
<form name="itemEdit" (ngSubmit)="addComment($event)">
<ion-item>
<ion-textarea placeholder="{{ 'core.comments.addcomment' | translate }}" rows="5" [(ngModel)]="content" name="content" required="required"></ion-textarea>
</ion-item>
<div padding>
<button ion-button block type="submit" [disabled]="processing || content.length < 1">
{{ 'core.comments.savecomment' | translate }}
</button>
</div>
</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 { CoreCommentsAddPage } from './add';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
CoreCommentsAddPage
],
imports: [
CoreDirectivesModule,
IonicPageModule.forChild(CoreCommentsAddPage),
TranslateModule.forChild()
]
})
export class CoreCommentsAddPageModule {}

View File

@ -0,0 +1,82 @@
// (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 { CoreCommentsProvider } from '../../providers/comments';
/**
* Component that displays a text area for composing a comment.
*/
@IonicPage({ segment: 'core-comments-add' })
@Component({
selector: 'page-core-comments-add',
templateUrl: 'add.html',
})
export class CoreCommentsAddPage {
protected contextLevel: string;
protected instanceId: number;
protected componentName: string;
protected itemId: number;
protected area = '';
content = '';
processing = false;
constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider,
private domUtils: CoreDomUtilsProvider, private commentsProvider: CoreCommentsProvider) {
this.contextLevel = params.get('contextLevel');
this.instanceId = params.get('instanceId');
this.componentName = params.get('componentName');
this.itemId = params.get('itemId');
this.area = params.get('area') || '';
this.content = params.get('content') || '';
}
/**
* Send the comment or store it offline.
*
* @param {Event} e Event.
*/
addComment(e: Event): void {
e.preventDefault();
e.stopPropagation();
this.appProvider.closeKeyboard();
const loadingModal = this.domUtils.showModalLoading('core.sending', true);
// Freeze the add note button.
this.processing = true;
this.commentsProvider.addComment(this.content, this.contextLevel, this.instanceId, this.componentName, this.itemId,
this.area).then((commentsResponse) => {
this.viewCtrl.dismiss({comments: commentsResponse}).finally(() => {
this.domUtils.showToast(commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', true,
3000);
});
}).catch((error) => {
this.domUtils.showErrorModal(error);
this.processing = false;
}).finally(() => {
loadingModal.dismiss();
});
}
/**
* Close modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
}

View File

@ -1,20 +1,44 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end>
<core-context-menu>
<core-context-menu-item [hidden]="!(commentsLoaded && !hasOffline)" [priority]="100" [content]="'core.refresh' | translate" (action)="refreshComments(false)" [iconAction]="refreshIcon" [closeOnClick]="true"></core-context-menu-item>
<core-context-menu-item [hidden]="!(commentsLoaded && hasOffline)" [priority]="100" [content]="'core.settings.synchronizenow' | translate" (action)="refreshComments(true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="commentsLoaded" (ionRefresh)="refreshComments($event)">
<ion-refresher [enabled]="commentsLoaded" (ionRefresh)="refreshComments(false, $event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="commentsLoaded">
<core-empty-box *ngIf="!comments || !comments.length" icon="chatbubbles" [message]="'core.comments.nocomments' | translate"></core-empty-box>
<div class="core-warning-card" icon-start *ngIf="hasOffline">
<ion-icon name="warning"></ion-icon>
{{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
</div>
<ion-card *ngIf="hasOffline" (click)="addComment($event)">
<ion-item text-wrap>
<ion-avatar core-user-avatar [user]="offlineComment" item-start></ion-avatar>
<h2>{{ offlineComment.fullname }}</h2>
<p>
<ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}
</p>
</ion-item>
<ion-item text-wrap>
<core-format-text clean="true" [text]="offlineComment.content"></core-format-text>
</ion-item>
</ion-card>
<ion-card *ngFor="let comment of comments">
<ion-item text-wrap>
<ion-avatar core-user-avatar [user]="comment" item-start></ion-avatar>
<h2>{{ comment.fullname }}</h2>
<p>{{ comment.time }}</p>
<p>{{ comment.timecreated * 1000 | coreFormatDate: 'strftimerecentfull' }}</p>
</ion-item>
<ion-item text-wrap>
<core-format-text clean="true" [text]="comment.content"></core-format-text>
@ -25,7 +49,7 @@
</core-loading>
<ion-fab core-fab bottom end *ngIf="canAddComments">
<button ion-fab (click)="addComment()" [attr.aria-label]="'core.comments.addcomment' | translate">
<button ion-fab (click)="addComment($event)" [attr.aria-label]="'core.comments.addcomment' | translate">
<ion-icon name="add"></ion-icon>
</button>
</ion-fab>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreCommentsViewerPage } from './viewer';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCommentsComponentsModule } from '../../components/components.module';
@NgModule({
@ -27,6 +28,7 @@ import { CoreCommentsComponentsModule } from '../../components/components.module
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCommentsComponentsModule,
IonicPageModule.forChild(CoreCommentsViewerPage),
TranslateModule.forChild()

View File

@ -12,13 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { IonicPage, Content, NavParams } from 'ionic-angular';
import { Component, ViewChild, OnDestroy } from '@angular/core';
import { IonicPage, Content, NavParams, ModalController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreEventsProvider } from '@providers/events';
import { CoreUserProvider } from '@core/user/providers/user';
import { CoreCommentsProvider } from '../../providers/comments';
import { CoreCommentsOfflineProvider } from '../../providers/offline';
import { CoreCommentsSyncProvider } from '../../providers/sync';
/**
* Page that displays comments.
@ -28,14 +32,14 @@ import { CoreCommentsProvider } from '../../providers/comments';
selector: 'page-core-comments-viewer',
templateUrl: 'viewer.html',
})
export class CoreCommentsViewerPage {
export class CoreCommentsViewerPage implements OnDestroy {
@ViewChild(Content) content: Content;
comments = [];
commentsLoaded = false;
contextLevel: string;
instanceId: number;
component: string;
componentName: string;
itemId: number;
area: string;
page: number;
@ -43,20 +47,48 @@ export class CoreCommentsViewerPage {
canLoadMore = false;
loadMoreError = false;
canAddComments = false;
hasOffline = false;
refreshIcon = 'spinner';
syncIcon = 'spinner';
offlineComment: any;
protected addCommentsAvailable = false;
protected syncObserver: any;
protected currentUser: any;
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider,
private domUtils: CoreDomUtilsProvider, private translate: TranslateService,
private commentsProvider: CoreCommentsProvider) {
constructor(navParams: NavParams, private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider,
private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private modalCtrl: ModalController,
private commentsProvider: CoreCommentsProvider, private offlineComments: CoreCommentsOfflineProvider,
eventsProvider: CoreEventsProvider, private commentsSync: CoreCommentsSyncProvider,
private textUtils: CoreTextUtilsProvider) {
this.contextLevel = navParams.get('contextLevel');
this.instanceId = navParams.get('instanceId');
this.component = navParams.get('component');
this.componentName = navParams.get('componentName');
this.itemId = navParams.get('itemId');
this.area = navParams.get('area') || '';
this.title = navParams.get('title') || this.translate.instant('core.comments.comments');
this.page = 0;
// Refresh data if comments are synchronized automatically.
this.syncObserver = eventsProvider.on(CoreCommentsSyncProvider.AUTO_SYNCED, (data) => {
if (data.contextLevel == this.contextLevel && data.instanceId == this.instanceId &&
data.componentName == this.componentName && data.itemId == this.itemId && data.area == this.area) {
// Show the sync warnings.
this.showSyncWarnings(data.warnings);
// Refresh the data.
this.commentsLoaded = false;
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
this.domUtils.scrollToTop(this.content);
this.page = 0;
this.comments = [];
this.fetchComments(false);
}
}, sitesProvider.getCurrentSiteId());
}
/**
@ -67,46 +99,78 @@ export class CoreCommentsViewerPage {
this.addCommentsAvailable = enabled;
});
this.fetchComments().finally(() => {
this.commentsLoaded = true;
});
this.fetchComments(true);
}
/**
* Fetches the comments.
*
* @param {boolean} sync When to resync notes.
* @param {boolean} [showErrors] When to display errors or not.
* @return {Promise<any>} Resolved when done.
*/
protected fetchComments(): Promise<any> {
protected fetchComments(sync: boolean, showErrors?: boolean): Promise<any> {
this.loadMoreError = false;
// Get comments data.
return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.component, this.itemId,
this.area, this.page).then((response) => {
this.canAddComments = this.addCommentsAvailable && response.canpost;
const promise = sync ? this.syncComment(showErrors) : Promise.resolve();
const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated);
this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize;
return promise.catch(() => {
// Ignore errors.
}).then(() => {
return this.offlineComments.getComment(this.contextLevel, this.instanceId, this.componentName, this.itemId,
this.area).then((offlineComment) => {
this.hasOffline = !!offlineComment;
this.offlineComment = offlineComment;
this.comments.forEach((comment) => {
// Get the user profile image.
this.userProvider.getProfile(comment.userid, undefined, true).then((user) => {
comment.profileimageurl = user.profileimageurl;
}).catch(() => {
// Ignore errors.
});
if (this.hasOffline && !this.currentUser) {
return this.userProvider.getProfile(this.sitesProvider.getCurrentSiteUserId(), undefined, true).then((user) => {
this.currentUser = user;
this.offlineComment.profileimageurl = user.profileimageurl;
this.offlineComment.fullname = user.fullname;
this.offlineComment.userid = user.id;
}).catch(() => {
// Ignore errors.
});
} else if (this.hasOffline) {
this.offlineComment.profileimageurl = this.currentUser.profileimageurl;
this.offlineComment.fullname = this.currentUser.fullname;
this.offlineComment.userid = this.currentUser.id;
}
});
}).then(() => {
this.comments = this.comments.concat(comments);
// Get comments data.
return this.commentsProvider.getComments(this.contextLevel, this.instanceId, this.componentName, this.itemId,
this.area, this.page).then((response) => {
this.canAddComments = this.addCommentsAvailable && response.canpost;
const comments = response.comments.sort((a, b) => b.timecreated - a.timecreated);
this.canLoadMore = comments.length >= CoreCommentsProvider.pageSize;
this.comments.forEach((comment) => {
// Get the user profile image.
this.userProvider.getProfile(comment.userid, undefined, true).then((user) => {
comment.profileimageurl = user.profileimageurl;
}).catch(() => {
// Ignore errors.
});
});
this.comments = this.comments.concat(comments);
});
}).catch((error) => {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
if (error && this.component == 'assignsubmission_comments') {
if (error && this.componentName == 'assignsubmission_comments') {
this.domUtils.showAlertTranslated('core.notice', 'core.comments.commentsnotworking');
} else {
this.domUtils.showErrorModalDefault(error, this.translate.instant('core.error') + ': get_comments');
}
}).finally(() => {
this.commentsLoaded = true;
this.refreshIcon = 'refresh';
this.syncIcon = 'sync';
});
}
/**
@ -119,7 +183,7 @@ export class CoreCommentsViewerPage {
this.page++;
this.canLoadMore = false;
return this.fetchComments().finally(() => {
return this.fetchComments(true).finally(() => {
infiniteComplete && infiniteComplete();
});
}
@ -127,17 +191,89 @@ export class CoreCommentsViewerPage {
/**
* Refresh the comments.
*
* @param {any} refresher Refresher.
* @param {boolean} showErrors Whether to display errors or not.
* @param {any} [refresher] Refresher.
* @return {Promise<any>} Resolved when done.
*/
refreshComments(refresher: any): void {
this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.component,
refreshComments(showErrors: boolean, refresher?: any): Promise<any> {
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
return this.commentsProvider.invalidateCommentsData(this.contextLevel, this.instanceId, this.componentName,
this.itemId, this.area).finally(() => {
this.page = 0;
this.comments = [];
return this.fetchComments().finally(() => {
refresher.complete();
return this.fetchComments(true, showErrors).finally(() => {
refresher && refresher.complete();
});
});
}
/**
* 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);
}
}
/**
* Tries to synchronize comments.
*
* @param {boolean} showErrors Whether to display errors or not.
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
*/
private syncComment(showErrors: boolean): Promise<any> {
return this.commentsSync.syncComment(this.contextLevel, this.instanceId, this.componentName, this.itemId,
this.area).then((warnings) => {
this.showSyncWarnings(warnings);
}).catch((error) => {
if (showErrors) {
this.domUtils.showErrorModalDefault(error, 'core.errorsync', true);
}
return Promise.reject(null);
});
}
/**
* Add a new comment to the list.
*
* @param {Event} e Event.
*/
addComment(e: Event): void {
e.preventDefault();
e.stopPropagation();
const params = {
contextLevel: this.contextLevel,
instanceId: this.instanceId,
componentName: this.componentName,
itemId: this.itemId,
area: this.area,
content: this.hasOffline ? this.offlineComment.content : ''
};
const modal = this.modalCtrl.create('CoreCommentsAddPage', params);
modal.onDidDismiss((data) => {
if (data && data.comments) {
this.comments = data.comments.concat(this.comments);
} else if (data && !data.comments) {
this.fetchComments(false);
}
});
modal.present();
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.syncObserver && this.syncObserver.off();
}
}

View File

@ -13,8 +13,11 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreAppProvider } from '@providers/app';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSite } from '@classes/site';
import { CoreCommentsOfflineProvider } from './offline';
/**
* Service that provides some features regarding comments.
@ -26,7 +29,107 @@ export class CoreCommentsProvider {
static pageSize = null;
static pageSizeOK = false; // If true, the pageSize is definitive. If not, it's a temporal value to reduce WS calls.
constructor(private sitesProvider: CoreSitesProvider) {}
constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private appProvider: CoreAppProvider,
private commentsOffline: CoreCommentsOfflineProvider) {}
/**
* Add a comment.
*
* @param {string} content Comment text.
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if comment was sent to server, false if stored in device.
*/
addComment(content: string, contextLevel: string, instanceId: number, component: string, itemId: number, area: 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.commentsOffline.saveComment(content, contextLevel, instanceId, component, itemId, area, siteId).then(() => {
return Promise.resolve(false);
});
};
if (!this.appProvider.isOnline()) {
// App is offline, store the note.
return storeOffline();
}
// Send note to server.
return this.addCommentOnline(content, contextLevel, instanceId, component, itemId, area, siteId).then((comments) => {
return comments;
}).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 comment. It will fail if offline or cannot connect.
*
* @param {string} content Comment text.
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when added, rejected otherwise.
*/
addCommentOnline(content: string, contextLevel: string, instanceId: number, component: string, itemId: number,
area: string = '', siteId?: string): Promise<any> {
const comments = [
{
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
content: content
}
];
return this.addCommentsOnline(comments, siteId).then((commentsResponse) => {
// A cooment was added, invalidate them.
return this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId).catch(() => {
// Ignore errors.
}).then(() => {
return commentsResponse;
});
});
}
/**
* Add several comments. It will fail if offline or cannot connect.
*
* @param {any[]} comments Comments 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 comments
* have been added, the resolve param can contain errors for notes not sent.
*/
addCommentsOnline(comments: any[], siteId?: string): Promise<any> {
if (!comments || !comments.length) {
return Promise.resolve();
}
return this.sitesProvider.getSite(siteId).then((site) => {
const data = {
comments: comments
};
return site.write('core_comment_add_comments', data);
});
}
/**
* Check if Calendar is disabled in a certain site.

View File

@ -0,0 +1,187 @@
// (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 { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
/**
* Service to handle offline comments.
*/
@Injectable()
export class CoreCommentsOfflineProvider {
// Variables for database.
static COMMENTS_TABLE = 'core_comments_offline_comments';
protected siteSchema: CoreSiteSchema = {
name: 'CoreCommentsOfflineProvider',
version: 1,
tables: [
{
name: CoreCommentsOfflineProvider.COMMENTS_TABLE,
columns: [
{
name: 'contextlevel',
type: 'TEXT'
},
{
name: 'instanceid',
type: 'INTEGER'
},
{
name: 'component',
type: 'TEXT',
},
{
name: 'itemid',
type: 'INTEGER'
},
{
name: 'area',
type: 'TEXT'
},
{
name: 'content',
type: 'TEXT'
},
{
name: 'action',
type: 'TEXT'
},
{
name: 'lastmodified',
type: 'INTEGER'
}
],
primaryKeys: ['contextlevel', 'instanceid', 'component', 'itemid', 'area']
}
]
};
constructor( private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) {
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
/**
* Delete a comment.
*
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
*/
removeComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '',
siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE, {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area
});
});
}
/**
* Get all offline comments.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with comments.
*/
getAllComments(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(CoreCommentsOfflineProvider.COMMENTS_TABLE);
});
}
/**
* Get an offline comment.
*
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the comments.
*/
getComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '',
siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area
});
}).catch(() => {
return false;
});
}
/**
* Check if there are offline comments.
*
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline comments, false otherwise.
*/
hasComments(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '',
siteId?: string): Promise<boolean> {
return this.getComment(contextLevel, instanceId, component, itemId, area, siteId).then((comments) => {
return !!comments.length;
});
}
/**
* Save a comment to be sent later.
*
* @param {string} content Comment text.
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
*/
saveComment(content: string, contextLevel: string, instanceId: number, component: string, itemId: number,
area: string = '', siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const now = this.timeUtils.timestamp();
const data = {
contextlevel: contextLevel,
instanceid: instanceId,
component: component,
itemid: itemId,
area: area,
content: content,
action: 'add',
lastmodified: now
};
return site.getDb().insertRecord(CoreCommentsOfflineProvider.COMMENTS_TABLE, data).then(() => {
return data;
});
});
}
}

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 { Injectable } from '@angular/core';
import { CoreCronHandler } from '@providers/cron';
import { CoreCommentsSyncProvider } from './sync';
/**
* Synchronization cron handler.
*/
@Injectable()
export class CoreCommentsSyncCronHandler implements CoreCronHandler {
name = 'CoreCommentsSyncCronHandler';
constructor(private commentsSync: CoreCommentsSyncProvider) {}
/**
* 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.
* @param {boolean} [force] Wether the execution is forced (manual sync).
* @return {Promise<any>} Promise resolved when done, rejected if failure.
*/
execute(siteId?: string, force?: boolean): Promise<any> {
return this.commentsSync.syncAllComments(siteId, force);
}
/**
* Get the time between consecutive executions.
*
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 300000; // 5 minutes.
}
}

View File

@ -0,0 +1,221 @@
// (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 { CoreCommentsOfflineProvider } from './offline';
import { CoreCommentsProvider } from './comments';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreEventsProvider } from '@providers/events';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { TranslateService } from '@ngx-translate/core';
import { CoreSyncProvider } from '@providers/sync';
/**
* Service to sync omments.
*/
@Injectable()
export class CoreCommentsSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'core_comments_autom_synced';
constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider,
syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService,
private commentsOffline: CoreCommentsOfflineProvider, private utils: CoreUtilsProvider,
private eventsProvider: CoreEventsProvider, private commentsProvider: CoreCommentsProvider,
private coursesProvider: CoreCoursesProvider, timeUtils: CoreTimeUtilsProvider) {
super('CoreCommentsSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, timeUtils);
}
/**
* Try to synchronize all the comments in a certain site or in all sites.
*
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
* @param {boolean} [force] Wether to force sync not depending on last execution.
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllComments(siteId?: string, force?: boolean): Promise<any> {
return this.syncOnSites('all comments', this.syncAllCommentsFunc.bind(this), [force], siteId);
}
/**
* Synchronize all the comments in a certain site
*
* @param {string} siteId Site ID to sync.
* @param {boolean} force Wether to force sync not depending on last execution.
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
private syncAllCommentsFunc(siteId: string, force: boolean): Promise<any> {
return this.commentsOffline.getAllComments(siteId).then((comments) => {
// Sync all courses.
const promises = comments.map((comment) => {
const promise = force ? this.syncComment(comment.contextlevel, comment.instanceid, comment.component,
comment.itemid, comment.area, siteId) : this.syncCommentIfNeeded(comment.contextlevel, comment.instanceid,
comment.component, comment.itemid, comment.area, siteId);
return promise.then((warnings) => {
if (typeof warnings != 'undefined') {
// Sync successful, send event.
this.eventsProvider.trigger(CoreCommentsSyncProvider.AUTO_SYNCED, {
contextLevel: comment.contextlevel,
instanceId: comment.instanceid,
componentName: comment.component,
itemId: comment.itemid,
area: comment.area,
warnings: warnings
}, siteId);
}
});
});
return Promise.all(promises);
});
}
/**
* Sync course notes only if a certain time has passed since the last time.
*
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @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 syncCommentIfNeeded(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '',
siteId?: string): Promise<void> {
const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area);
return this.isSyncNeeded(syncId, siteId).then((needed) => {
if (needed) {
return this.syncComment(contextLevel, instanceId, component, itemId, area, siteId);
}
});
}
/**
* Synchronize notes of a course.
*
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
*/
syncComment(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = '',
siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const syncId = this.getSyncId(contextLevel, instanceId, component, itemId, area);
if (this.isSyncing(syncId, siteId)) {
// There's already a sync ongoing for notes, return the promise.
return this.getOngoingSync(syncId, siteId);
}
this.logger.debug('Try to sync comments ' + syncId);
const warnings = [];
// Get offline comments to be sent.
const syncPromise = this.commentsOffline.getComment(contextLevel, instanceId, component, itemId, area, siteId)
.then((comment) => {
if (!comment) {
// Nothing to sync.
return;
} else if (!this.appProvider.isOnline()) {
// Cannot sync in offline.
return Promise.reject(this.translate.instant('core.networkerrormsg'));
}
const errors = [];
let commentsResponse = [];
let promise;
if (comment.action == 'add') {
promise = this.commentsProvider.addCommentOnline(comment.content, contextLevel, instanceId, component, itemId, area,
siteId);
}
// Send the comments.
return promise.then((response) => {
commentsResponse = response;
// Fetch the comments from server to be sure they're up to date.
return this.commentsProvider.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId)
.then(() => {
return this.commentsProvider.getComments(contextLevel, instanceId, component, itemId, area, 0, siteId);
}).catch(() => {
// Ignore errors.
});
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// It's a WebService error, this means the user cannot send comments.
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 = commentsResponse.map((comment) => {
return this.commentsOffline.removeComment(contextLevel, instanceId, component, itemId, area, siteId);
});
return Promise.all(promises);
}).then(() => {
if (errors && errors.length) {
errors.forEach((error) => {
warnings.push(this.translate.instant('addon.notes.warningnotenotsent', {
contextLevel: contextLevel,
instanceId: instanceId,
componentName: component,
itemId: itemId,
area: area,
error: error
}));
});
}
});
}).then(() => {
// All done, return the warnings.
return warnings;
});
return this.addOngoingSync(syncId, syncPromise, siteId);
}
/**
* Get the ID of a comments sync.
*
* @param {string} contextLevel Contextlevel system, course, user...
* @param {number} instanceId The Instance id of item associated with the context level.
* @param {string} component Component name.
* @param {number} itemId Associated id.
* @param {string} [area=''] String comment area. Default empty.
* @return {string} Sync ID.
*/
protected getSyncId(contextLevel: string, instanceId: number, component: string, itemId: number, area: string = ''): string {
return contextLevel + '#' + instanceId + '#' + component + '#' + itemId + '#' + area;
}
}