forked from EVOgeek/Vmeda.Online
		
	
						commit
						980af8d852
					
				| @ -12,10 +12,13 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| import { Component, Input, OnInit, Injector } from '@angular/core'; | import { Component, Input, OnInit, Injector } from '@angular/core'; | ||||||
|  | import { NavController } from 'ionic-angular'; | ||||||
| import { CoreEventsProvider } from '@providers/events'; | import { CoreEventsProvider } from '@providers/events'; | ||||||
| import { AddonModDataProvider } from '../../providers/data'; | import { AddonModDataProvider } from '../../providers/data'; | ||||||
|  | import { AddonModDataHelperProvider } from '../../providers/helper'; | ||||||
| import { AddonModDataOfflineProvider } from '../../providers/offline'; | import { AddonModDataOfflineProvider } from '../../providers/offline'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider } from '@providers/sites'; | ||||||
|  | import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; | ||||||
| import { CoreUserProvider } from '@core/user/providers/user'; | import { CoreUserProvider } from '@core/user/providers/user'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -30,6 +33,8 @@ export class AddonModDataActionComponent implements OnInit { | |||||||
|     @Input() action: string; // The field to render.
 |     @Input() action: string; // The field to render.
 | ||||||
|     @Input() entry?: any; // The value of the field.
 |     @Input() entry?: any; // The value of the field.
 | ||||||
|     @Input() database: any; // Database object.
 |     @Input() database: any; // Database object.
 | ||||||
|  |     @Input() module: any; // Module object.
 | ||||||
|  |     @Input() group: number; // Module object.
 | ||||||
|     @Input() offset?: number; // Offset of the entry.
 |     @Input() offset?: number; // Offset of the entry.
 | ||||||
| 
 | 
 | ||||||
|     siteId: string; |     siteId: string; | ||||||
| @ -39,11 +44,72 @@ export class AddonModDataActionComponent implements OnInit { | |||||||
| 
 | 
 | ||||||
|     constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider, |     constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider, | ||||||
|             protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider, |             protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider, | ||||||
|             sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider) { |             sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider, private navCtrl: NavController, | ||||||
|  |             protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider) { | ||||||
|         this.rootUrl = sitesProvider.getCurrentSite().getURL(); |         this.rootUrl = sitesProvider.getCurrentSite().getURL(); | ||||||
|         this.siteId = sitesProvider.getCurrentSiteId(); |         this.siteId = sitesProvider.getCurrentSiteId(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         if (this.action == 'userpicture') { | ||||||
|  |             this.userProvider.getProfile(this.entry.userid, this.database.courseid).then((profile) => { | ||||||
|  |                 this.userPicture = profile.profileimageurl; | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Approve the entry. | ||||||
|  |      */ | ||||||
|  |     approveEntry(): void { | ||||||
|  |         this.dataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, true, this.database.courseid); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show confirmation modal for deleting the entry. | ||||||
|  |      */ | ||||||
|  |     deleteEntry(): void { | ||||||
|  |        this.dataHelper.showDeleteEntryModal(this.database.id, this.entry.id, this.database.courseid); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Disapprove the entry. | ||||||
|  |      */ | ||||||
|  |     disapproveEntry(): void { | ||||||
|  |         this.dataHelper.approveOrDisapproveEntry(this.database.id, this.entry.id, false, this.database.courseid); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Go to the edit page of the entry. | ||||||
|  |      */ | ||||||
|  |     editEntry(): void { | ||||||
|  |         const pageParams = { | ||||||
|  |             courseId: this.database.course, | ||||||
|  |             module: this.module, | ||||||
|  |             entryId: this.entry.id | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         this.linkHelper.goInSite(this.navCtrl, 'AddonModDataEditPage', pageParams); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Go to the view page of the entry. | ||||||
|  |      */ | ||||||
|  |     viewEntry(): void { | ||||||
|  |         const pageParams: any = { | ||||||
|  |             courseId: this.database.course, | ||||||
|  |             module: this.module, | ||||||
|  |             entryId: this.entry.id, | ||||||
|  |             group: this.group, | ||||||
|  |             offset: this.offset | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         this.linkHelper.goInSite(this.navCtrl, 'AddonModDataEntryPage', pageParams); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Undo delete action. |      * Undo delete action. | ||||||
|      * |      * | ||||||
| @ -60,37 +126,4 @@ export class AddonModDataActionComponent implements OnInit { | |||||||
|             this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, this.siteId); |             this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, this.siteId); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Component being initialized. |  | ||||||
|      */ |  | ||||||
|     ngOnInit(): void { |  | ||||||
|         switch (this.action) { |  | ||||||
|             case 'more': |  | ||||||
|                 this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; |  | ||||||
|                 if (typeof this.offset == 'number') { |  | ||||||
|                     this.url += '&mode=single&page=' + this.offset; |  | ||||||
|                 } |  | ||||||
|                 break; |  | ||||||
|             case 'edit': |  | ||||||
|                 this.url = this.rootUrl + '/mod/data/edit.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; |  | ||||||
|                 break; |  | ||||||
|             case 'delete': |  | ||||||
|                 this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&delete=' + this.entry.id; |  | ||||||
|                 break; |  | ||||||
|             case 'approve': |  | ||||||
|                 this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&approve=' + this.entry.id; |  | ||||||
|                 break; |  | ||||||
|             case 'disapprove': |  | ||||||
|                 this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&disapprove=' + this.entry.id; |  | ||||||
|                 break; |  | ||||||
|             case 'userpicture': |  | ||||||
|                 this.userProvider.getProfile(this.entry.userid, this.database.courseid).then((profile) => { |  | ||||||
|                     this.userPicture = profile.profileimageurl; |  | ||||||
|                 }); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,12 +1,12 @@ | |||||||
| <a *ngIf="action == 'more'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'addon.mod_data.more' | translate"> | <a *ngIf="action == 'more'" ion-button icon-only clear (click)="viewEntry()" [title]="'addon.mod_data.more' | translate"> | ||||||
|     <ion-icon name="search"></ion-icon> |     <ion-icon name="search"></ion-icon> | ||||||
| </a> | </a> | ||||||
| 
 | 
 | ||||||
| <a *ngIf="action == 'edit'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'core.edit' | translate"> | <a *ngIf="action == 'edit'" ion-button icon-only clear (click)="editEntry()"  [title]="'core.edit' | translate"> | ||||||
|     <ion-icon name="cog"></ion-icon> |     <ion-icon name="cog"></ion-icon> | ||||||
| </a> | </a> | ||||||
| 
 | 
 | ||||||
| <a *ngIf="action == 'delete' && !entry.deleted" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'core.delete' | translate"> | <a *ngIf="action == 'delete' && !entry.deleted" ion-button icon-only clear (click)="deleteEntry()" [title]="'core.delete' | translate"> | ||||||
|     <ion-icon name="trash"></ion-icon> |     <ion-icon name="trash"></ion-icon> | ||||||
| </a> | </a> | ||||||
| 
 | 
 | ||||||
| @ -14,11 +14,11 @@ | |||||||
|     <ion-icon name="undo"></ion-icon> |     <ion-icon name="undo"></ion-icon> | ||||||
| </a> | </a> | ||||||
| 
 | 
 | ||||||
| <a *ngIf="action == 'approve'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'addon.mod_data.approve' | translate"> | <a *ngIf="action == 'approve'" ion-button icon-only clear (click)="approveEntry()" [title]="'addon.mod_data.approve' | translate"> | ||||||
|     <ion-icon name="thumbs-up"></ion-icon> |     <ion-icon name="thumbs-up"></ion-icon> | ||||||
| </a> | </a> | ||||||
| 
 | 
 | ||||||
| <a *ngIf="action == 'disapprove'" ion-button icon-only clear [href]="url" core-link capture="true" [title]="'addon.mod_data.disapprove' | translate"> | <a *ngIf="action == 'disapprove'" ion-button icon-only clear (click)="disapproveEntry()" [title]="'addon.mod_data.disapprove' | translate"> | ||||||
|     <ion-icon name="thumbs-down"></ion-icon> |     <ion-icon name="thumbs-down"></ion-icon> | ||||||
| </a> | </a> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -20,11 +20,9 @@ import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | |||||||
| import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; | import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; | ||||||
| import { CoreCommentsProvider } from '@core/comments/providers/comments'; | import { CoreCommentsProvider } from '@core/comments/providers/comments'; | ||||||
| import { CoreRatingProvider } from '@core/rating/providers/rating'; | import { CoreRatingProvider } from '@core/rating/providers/rating'; | ||||||
| import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; |  | ||||||
| import { CoreRatingSyncProvider } from '@core/rating/providers/sync'; | import { CoreRatingSyncProvider } from '@core/rating/providers/sync'; | ||||||
| import { AddonModDataProvider } from '../../providers/data'; | import { AddonModDataProvider } from '../../providers/data'; | ||||||
| import { AddonModDataHelperProvider } from '../../providers/helper'; | import { AddonModDataHelperProvider } from '../../providers/helper'; | ||||||
| import { AddonModDataOfflineProvider } from '../../providers/offline'; |  | ||||||
| import { AddonModDataSyncProvider } from '../../providers/sync'; | import { AddonModDataSyncProvider } from '../../providers/sync'; | ||||||
| import { AddonModDataComponentsModule } from '../components.module'; | import { AddonModDataComponentsModule } from '../components.module'; | ||||||
| import { AddonModDataPrefetchHandler } from '../../providers/prefetch-handler'; | import { AddonModDataPrefetchHandler } from '../../providers/prefetch-handler'; | ||||||
| @ -65,8 +63,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|         advanced: [] |         advanced: [] | ||||||
|     }; |     }; | ||||||
|     hasNextPage = false; |     hasNextPage = false; | ||||||
|     offlineActions: any; |  | ||||||
|     offlineEntries: any; |  | ||||||
|     entriesRendered = ''; |     entriesRendered = ''; | ||||||
|     extraImports = [AddonModDataComponentsModule]; |     extraImports = [AddonModDataComponentsModule]; | ||||||
|     jsData; |     jsData; | ||||||
| @ -81,12 +77,19 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|     protected ratingOfflineObserver: any; |     protected ratingOfflineObserver: any; | ||||||
|     protected ratingSyncObserver: any; |     protected ratingSyncObserver: any; | ||||||
| 
 | 
 | ||||||
|     constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, |     constructor( | ||||||
|             private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content, |             injector: Injector, | ||||||
|             private prefetchHandler: AddonModDataPrefetchHandler, private timeUtils: CoreTimeUtilsProvider, |             @Optional() content: Content, | ||||||
|             private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, |             private dataProvider: AddonModDataProvider, | ||||||
|             private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController, |             private dataHelper: AddonModDataHelperProvider, | ||||||
|             private ratingOffline: CoreRatingOfflineProvider) { |             private prefetchHandler: AddonModDataPrefetchHandler, | ||||||
|  |             private timeUtils: CoreTimeUtilsProvider, | ||||||
|  |             private groupsProvider: CoreGroupsProvider, | ||||||
|  |             private commentsProvider: CoreCommentsProvider, | ||||||
|  |             private modalCtrl: ModalController, | ||||||
|  |             private utils: CoreUtilsProvider, | ||||||
|  |             protected navCtrl: NavController) { | ||||||
|  | 
 | ||||||
|         super(injector, content); |         super(injector, content); | ||||||
| 
 | 
 | ||||||
|         // Refresh entries on change.
 |         // Refresh entries on change.
 | ||||||
| @ -233,8 +236,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|                         this.selectedGroup = groupInfo.groups[0].id; |                         this.selectedGroup = groupInfo.groups[0].id; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 return this.fetchOfflineEntries(); |  | ||||||
|             }); |             }); | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             return this.dataProvider.getFields(this.data.id).then((fields) => { |             return this.dataProvider.getFields(this.data.id).then((fields) => { | ||||||
| @ -270,21 +271,19 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|             // Update values for current group.
 |             // Update values for current group.
 | ||||||
|             this.access.canaddentry = accessData.canaddentry; |             this.access.canaddentry = accessData.canaddentry; | ||||||
| 
 | 
 | ||||||
|             if (this.search.searching) { |             const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; | ||||||
|                 const text = this.search.searchingAdvanced ? undefined : this.search.text, |             const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined; | ||||||
|                     advanced = this.search.searchingAdvanced ? this.search.advanced : undefined; |  | ||||||
| 
 | 
 | ||||||
|                 return this.dataProvider.searchEntries(this.data.id, this.selectedGroup, text, advanced, this.search.sortBy, |             return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, search, advSearch, | ||||||
|                     this.search.sortDirection, this.search.page); |                     this.search.sortBy, this.search.sortDirection, this.search.page); | ||||||
|             } else { |  | ||||||
|                 return this.dataProvider.getEntries(this.data.id, this.selectedGroup, this.search.sortBy, this.search.sortDirection, |  | ||||||
|                     this.search.page); |  | ||||||
|             } |  | ||||||
|         }).then((entries) => { |         }).then((entries) => { | ||||||
|             const numEntries = (entries && entries.entries && entries.entries.length) || 0; |             const numEntries = entries.entries.length; | ||||||
|             this.isEmpty = !numEntries && !Object.keys(this.offlineActions).length && !Object.keys(this.offlineEntries).length; |             const numOfflineEntries = entries.offlineEntries.length; | ||||||
|  |             this.isEmpty = !numEntries && !entries.offlineEntries.length; | ||||||
|             this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) * |             this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) * | ||||||
|                 AddonModDataProvider.PER_PAGE) < entries.totalcount; |                 AddonModDataProvider.PER_PAGE) < entries.totalcount; | ||||||
|  |             this.hasOffline = entries.hasOfflineActions; | ||||||
|  |             this.hasOfflineRatings = entries.hasOfflineRatings; | ||||||
|             this.entriesRendered = ''; |             this.entriesRendered = ''; | ||||||
| 
 | 
 | ||||||
|             if (typeof entries.maxcount != 'undefined') { |             if (typeof entries.maxcount != 'undefined') { | ||||||
| @ -298,79 +297,40 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (!this.isEmpty) { |             if (!this.isEmpty) { | ||||||
|                 const siteInfo = this.sitesProvider.getCurrentSite().getInfo(), |                 this.entries = entries.offlineEntries.concat(entries.entries); | ||||||
|                     promises = []; |  | ||||||
| 
 | 
 | ||||||
|                 this.utils.objectToArray(this.offlineEntries).forEach((offlineActions) => { |                 let entriesHTML = this.data.listtemplateheader || ''; | ||||||
|                     const offlineEntry = offlineActions.find((offlineEntry) => offlineEntry.action == 'add'); |  | ||||||
| 
 | 
 | ||||||
|                     if (offlineEntry) { |                 // Get first entry from the whole list.
 | ||||||
|                         const entry = { |                 if (!this.search.searching || !this.firstEntry) { | ||||||
|                             id: offlineEntry.entryid, |                     this.firstEntry = this.entries[0].id; | ||||||
|                             canmanageentry: true, |                 } | ||||||
|                             approved: !this.data.approval || this.data.manageapproved, |  | ||||||
|                             dataid: offlineEntry.dataid, |  | ||||||
|                             groupid: offlineEntry.groupid, |  | ||||||
|                             timecreated: -offlineEntry.entryid, |  | ||||||
|                             timemodified: -offlineEntry.entryid, |  | ||||||
|                             userid: siteInfo.userid, |  | ||||||
|                             fullname: siteInfo.fullname, |  | ||||||
|                             contents: {} |  | ||||||
|                         }; |  | ||||||
| 
 | 
 | ||||||
|                         if (offlineActions.length > 0) { |                 const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray); | ||||||
|                             promises.push(this.dataHelper.applyOfflineActions(entry, offlineActions, this.fieldsArray)); | 
 | ||||||
|                         } else { |                 const entriesById = {}; | ||||||
|                             promises.push(Promise.resolve(entry)); |                 this.entries.forEach((entry, index) => { | ||||||
|                         } |                     entriesById[entry.id] = entry; | ||||||
|                     } | 
 | ||||||
|  |                     const actions = this.dataHelper.getActions(this.data, this.access, entry); | ||||||
|  |                     const offset = this.search.searching ? undefined : | ||||||
|  |                             this.search.page * AddonModDataProvider.PER_PAGE + index - numOfflineEntries; | ||||||
|  | 
 | ||||||
|  |                     entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, offset, 'list',  actions); | ||||||
|                 }); |                 }); | ||||||
|  |                 entriesHTML += this.data.listtemplatefooter || ''; | ||||||
| 
 | 
 | ||||||
|                 entries.entries.forEach((entry) => { |                 this.entriesRendered = entriesHTML; | ||||||
|                     // Index contents by fieldid.
 |  | ||||||
|                     entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); |  | ||||||
| 
 | 
 | ||||||
|                     if (typeof this.offlineActions[entry.id] != 'undefined') { |                 // Pass the input data to the component.
 | ||||||
|                         promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fieldsArray)); |                 this.jsData = { | ||||||
|                     } else { |                     fields: this.fields, | ||||||
|                         promises.push(Promise.resolve(entry)); |                     entries: entriesById, | ||||||
|                     } |                     data: this.data, | ||||||
|                 }); |                     module: this.module, | ||||||
| 
 |                     group: this.selectedGroup, | ||||||
|                 return Promise.all(promises).then((entries) => { |                     gotoEntry: this.gotoEntry.bind(this) | ||||||
|                     this.entries = entries; |                 }; | ||||||
| 
 |  | ||||||
|                     let entriesHTML = this.data.listtemplateheader || ''; |  | ||||||
| 
 |  | ||||||
|                     // Get first entry from the whole list.
 |  | ||||||
|                     if (entries && entries[0] && (!this.search.searching || !this.firstEntry)) { |  | ||||||
|                         this.firstEntry = entries[0].id; |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray); |  | ||||||
| 
 |  | ||||||
|                     const entriesById = {}; |  | ||||||
|                     entries.forEach((entry, index) => { |  | ||||||
|                         entriesById[entry.id] = entry; |  | ||||||
| 
 |  | ||||||
|                         const actions = this.dataHelper.getActions(this.data, this.access, entry); |  | ||||||
|                         const offset = this.search.page * AddonModDataProvider.PER_PAGE + index; |  | ||||||
| 
 |  | ||||||
|                         entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, offset, 'list', |  | ||||||
|                                 actions); |  | ||||||
|                     }); |  | ||||||
|                     entriesHTML += this.data.listtemplatefooter || ''; |  | ||||||
| 
 |  | ||||||
|                     this.entriesRendered = entriesHTML; |  | ||||||
| 
 |  | ||||||
|                     // Pass the input data to the component.
 |  | ||||||
|                     this.jsData = { |  | ||||||
|                         fields: this.fields, |  | ||||||
|                         entries: entriesById, |  | ||||||
|                         data: this.data, |  | ||||||
|                         gotoEntry: this.gotoEntry.bind(this) |  | ||||||
|                     }; |  | ||||||
|                 }); |  | ||||||
|             } else if (!this.search.searching) { |             } else if (!this.search.searching) { | ||||||
|                 // Empty and no searching.
 |                 // Empty and no searching.
 | ||||||
|                 this.canSearch = false; |                 this.canSearch = false; | ||||||
| @ -435,6 +395,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|      */ |      */ | ||||||
|     setGroup(groupId: number): Promise<any> { |     setGroup(groupId: number): Promise<any> { | ||||||
|         this.selectedGroup = groupId; |         this.selectedGroup = groupId; | ||||||
|  |         this.search.page = 0; | ||||||
| 
 | 
 | ||||||
|         return this.fetchEntriesData().catch((message) => { |         return this.fetchEntriesData().catch((message) => { | ||||||
|             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); |             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); | ||||||
| @ -479,42 +440,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | |||||||
|         this.navCtrl.push('AddonModDataEntryPage', params); |         this.navCtrl.push('AddonModDataEntryPage', params); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Fetch offline entries. |  | ||||||
|      * |  | ||||||
|      * @return {Promise<any>} Resolved then done. |  | ||||||
|      */ |  | ||||||
|     protected fetchOfflineEntries(): Promise<any> { |  | ||||||
|         // Check if there are entries stored in offline.
 |  | ||||||
|         return this.dataOffline.getDatabaseEntries(this.data.id).then((offlineEntries) => { |  | ||||||
|             this.hasOffline = !!offlineEntries.length; |  | ||||||
| 
 |  | ||||||
|             this.offlineActions = {}; |  | ||||||
|             this.offlineEntries = {}; |  | ||||||
| 
 |  | ||||||
|             // Only show offline entries on first page.
 |  | ||||||
|             if (this.search.page == 0 && this.hasOffline) { |  | ||||||
|                 offlineEntries.forEach((entry) => { |  | ||||||
|                     if (entry.entryid > 0) { |  | ||||||
|                         if (typeof this.offlineActions[entry.entryid] == 'undefined') { |  | ||||||
|                             this.offlineActions[entry.entryid] = []; |  | ||||||
|                         } |  | ||||||
|                         this.offlineActions[entry.entryid].push(entry); |  | ||||||
|                     } else { |  | ||||||
|                         if (typeof this.offlineActions[entry.entryid] == 'undefined') { |  | ||||||
|                             this.offlineEntries[entry.entryid] = []; |  | ||||||
|                         } |  | ||||||
|                         this.offlineEntries[entry.entryid].push(entry); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         }).then(() => { |  | ||||||
|             return this.ratingOffline.hasRatings('mod_data', 'entry', 'module', this.data.coursemodule).then((hasRatings) => { |  | ||||||
|                 this.hasOfflineRatings = hasRatings; |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Performs the sync of the activity. |      * Performs the sync of the activity. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -45,7 +45,6 @@ export class AddonModDataEditPage { | |||||||
|     protected data: any; |     protected data: any; | ||||||
|     protected entryId: number; |     protected entryId: number; | ||||||
|     protected entry: any; |     protected entry: any; | ||||||
|     protected offlineActions = []; |  | ||||||
|     protected fields = {}; |     protected fields = {}; | ||||||
|     protected fieldsArray = []; |     protected fieldsArray = []; | ||||||
|     protected siteId: string; |     protected siteId: string; | ||||||
| @ -145,31 +144,14 @@ export class AddonModDataEditPage { | |||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             return this.dataOffline.getEntryActions(this.data.id, this.entryId); |  | ||||||
|         }).then((actions) => { |  | ||||||
|             this.offlineActions = actions; |  | ||||||
| 
 |  | ||||||
|             return this.dataProvider.getFields(this.data.id); |             return this.dataProvider.getFields(this.data.id); | ||||||
|         }).then((fieldsData) => { |         }).then((fieldsData) => { | ||||||
|             this.fieldsArray = fieldsData; |             this.fieldsArray = fieldsData; | ||||||
|             this.fields = this.utils.arrayToObject(fieldsData, 'id'); |             this.fields = this.utils.arrayToObject(fieldsData, 'id'); | ||||||
| 
 | 
 | ||||||
|             return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions); |             return this.dataHelper.fetchEntry(this.data, fieldsData, this.entryId); | ||||||
|         }).then((entry) => { |         }).then((entry) => { | ||||||
|              if (entry) { |             this.entry = entry.entry; | ||||||
|                 entry = entry.entry; |  | ||||||
| 
 |  | ||||||
|                 // Index contents by fieldid.
 |  | ||||||
|                 entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); |  | ||||||
|             } else { |  | ||||||
|                 entry = { |  | ||||||
|                     contents: {} |  | ||||||
|                 }; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return this.dataHelper.applyOfflineActions(entry, this.offlineActions, this.fieldsArray); |  | ||||||
|         }).then((entryData) => { |  | ||||||
|             this.entry = entryData; |  | ||||||
| 
 | 
 | ||||||
|             this.editFormRender = this.displayEditFields(); |             this.editFormRender = this.displayEditFields(); | ||||||
|         }).catch((message) => { |         }).catch((message) => { | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ | |||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="entryLoaded && (isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)"> |     <core-loading [hideUntil]="entryLoaded && (isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)"> | ||||||
|         <!-- Database entries found to be synchronized --> |         <!-- Database entries found to be synchronized --> | ||||||
|         <div class="core-warning-card" icon-start *ngIf="hasOffline"> |         <div class="core-warning-card" icon-start *ngIf="entry && entry.hasOffline"> | ||||||
|             <ion-icon name="warning"></ion-icon> |             <ion-icon name="warning"></ion-icon> | ||||||
|             {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} |             {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} | ||||||
|         </div> |         </div> | ||||||
| @ -31,7 +31,7 @@ | |||||||
|         <core-rating-rate *ngIf="data && entry && ratingInfo && (!data.approval || entry.approved)" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale" [userId]="entry.userid" (onLoading)="setLoadingRating($event)" (onUpdate)="ratingUpdated()"></core-rating-rate> |         <core-rating-rate *ngIf="data && entry && ratingInfo && (!data.approval || entry.approved)" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale" [userId]="entry.userid" (onLoading)="setLoadingRating($event)" (onUpdate)="ratingUpdated()"></core-rating-rate> | ||||||
|         <core-rating-aggregate *ngIf="data && entry && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale"></core-rating-aggregate> |         <core-rating-aggregate *ngIf="data && entry && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale"></core-rating-aggregate> | ||||||
| 
 | 
 | ||||||
|         <ion-item *ngIf="data && entry"> |         <ion-item *ngIf="data && entry && entry.id > 0"> | ||||||
|             <core-comments contextLevel="module" [instanceId]="data.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry" [displaySpinner]="false" (onLoading)="setLoadingComments($event)"></core-comments> |             <core-comments contextLevel="module" [instanceId]="data.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry" [displaySpinner]="false" (onLoading)="setLoadingComments($event)"></core-comments> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -23,7 +23,6 @@ import { CoreCourseProvider } from '@core/course/providers/course'; | |||||||
| import { CoreRatingInfo } from '@core/rating/providers/rating'; | import { CoreRatingInfo } from '@core/rating/providers/rating'; | ||||||
| import { AddonModDataProvider } from '../../providers/data'; | import { AddonModDataProvider } from '../../providers/data'; | ||||||
| import { AddonModDataHelperProvider } from '../../providers/helper'; | import { AddonModDataHelperProvider } from '../../providers/helper'; | ||||||
| import { AddonModDataOfflineProvider } from '../../providers/offline'; |  | ||||||
| import { AddonModDataSyncProvider } from '../../providers/sync'; | import { AddonModDataSyncProvider } from '../../providers/sync'; | ||||||
| import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; | import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; | ||||||
| import { AddonModDataComponentsModule } from '../../components/components.module'; | import { AddonModDataComponentsModule } from '../../components/components.module'; | ||||||
| @ -46,6 +45,7 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|     protected syncObserver: any; // It will observe the sync auto event.
 |     protected syncObserver: any; // It will observe the sync auto event.
 | ||||||
|     protected entryChangedObserver: any; // It will observe the changed entry event.
 |     protected entryChangedObserver: any; // It will observe the changed entry event.
 | ||||||
|     protected fields = {}; |     protected fields = {}; | ||||||
|  |     protected fieldsArray = []; | ||||||
| 
 | 
 | ||||||
|     title = ''; |     title = ''; | ||||||
|     moduleName = 'data'; |     moduleName = 'data'; | ||||||
| @ -56,8 +56,6 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|     loadingRating = false; |     loadingRating = false; | ||||||
|     selectedGroup = 0; |     selectedGroup = 0; | ||||||
|     entry: any; |     entry: any; | ||||||
|     offlineActions = []; |  | ||||||
|     hasOffline = false; |  | ||||||
|     previousOffset: number; |     previousOffset: number; | ||||||
|     nextOffset: number; |     nextOffset: number; | ||||||
|     access: any; |     access: any; | ||||||
| @ -74,7 +72,7 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|     constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, |     constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, | ||||||
|             protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, |             protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, | ||||||
|             protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, |             protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, | ||||||
|             protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider, |             protected dataHelper: AddonModDataHelperProvider, | ||||||
|             sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected eventsProvider: CoreEventsProvider, |             sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected eventsProvider: CoreEventsProvider, | ||||||
|             private cdr: ChangeDetectorRef) { |             private cdr: ChangeDetectorRef) { | ||||||
|         this.module = params.get('module') || {}; |         this.module = params.get('module') || {}; | ||||||
| @ -131,16 +129,19 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|      * @return {Promise<any>} Resolved when done. |      * @return {Promise<any>} Resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchEntryData(refresh?: boolean, isPtr?: boolean): Promise<any> { |     protected fetchEntryData(refresh?: boolean, isPtr?: boolean): Promise<any> { | ||||||
|         let fieldsArray; |  | ||||||
| 
 |  | ||||||
|         this.isPullingToRefresh = isPtr; |         this.isPullingToRefresh = isPtr; | ||||||
| 
 | 
 | ||||||
|         return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { |         return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { | ||||||
|             this.title = data.name || this.title; |             this.title = data.name || this.title; | ||||||
|             this.data = data; |             this.data = data; | ||||||
| 
 | 
 | ||||||
|             return this.setEntryIdFromOffset(data.id, this.offset, this.selectedGroup).then(() => { |             return this.dataProvider.getFields(this.data.id).then((fieldsData) => { | ||||||
|                 return this.dataProvider.getDatabaseAccessInformation(data.id); |                 this.fields = this.utils.arrayToObject(fieldsData, 'id'); | ||||||
|  |                 this.fieldsArray = fieldsData; | ||||||
|  |             }); | ||||||
|  |         }).then(() => { | ||||||
|  |             return this.setEntryFromOffset().then(() => { | ||||||
|  |                 return this.dataProvider.getDatabaseAccessInformation(this.data.id); | ||||||
|             }); |             }); | ||||||
|         }).then((accessData) => { |         }).then((accessData) => { | ||||||
|             this.access = accessData; |             this.access = accessData; | ||||||
| @ -155,35 +156,13 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|                         this.selectedGroup = groupInfo.groups[0].id; |                         this.selectedGroup = groupInfo.groups[0].id; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 return this.dataOffline.getEntryActions(this.data.id, this.entryId); |  | ||||||
|             }); |             }); | ||||||
|         }).then((actions) => { |         }).then(() => { | ||||||
|             this.offlineActions = actions; |  | ||||||
|             this.hasOffline = !!actions.length; |  | ||||||
| 
 |  | ||||||
|             return this.dataProvider.getFields(this.data.id).then((fieldsData) => { |  | ||||||
|                 this.fields = this.utils.arrayToObject(fieldsData, 'id'); |  | ||||||
| 
 |  | ||||||
|                 return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions); |  | ||||||
|             }); |  | ||||||
|         }).then((entry) => { |  | ||||||
|             this.ratingInfo = entry.ratinginfo; |  | ||||||
|             entry = entry.entry; |  | ||||||
| 
 |  | ||||||
|             // Index contents by fieldid.
 |  | ||||||
|             entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); |  | ||||||
| 
 |  | ||||||
|             fieldsArray = this.utils.objectToArray(this.fields); |  | ||||||
| 
 |  | ||||||
|             return this.dataHelper.applyOfflineActions(entry, this.offlineActions, fieldsArray); |  | ||||||
|         }).then((entryData) => { |  | ||||||
|             this.entry = entryData; |  | ||||||
| 
 |  | ||||||
|             const actions = this.dataHelper.getActions(this.data, this.access, this.entry); |             const actions = this.dataHelper.getActions(this.data, this.access, this.entry); | ||||||
| 
 | 
 | ||||||
|             const templte = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', fieldsArray); |             const template = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', this.fieldsArray); | ||||||
|             this.entryHtml = this.dataHelper.displayShowFields(templte, fieldsArray, this.entry, this.offset, 'show', actions); |             this.entryHtml = this.dataHelper.displayShowFields(template, this.fieldsArray, this.entry, this.offset, 'show', | ||||||
|  |                     actions); | ||||||
|             this.showComments = actions.comments; |             this.showComments = actions.comments; | ||||||
| 
 | 
 | ||||||
|             const entries = {}; |             const entries = {}; | ||||||
| @ -193,7 +172,9 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|             this.jsData = { |             this.jsData = { | ||||||
|                 fields: this.fields, |                 fields: this.fields, | ||||||
|                 entries: entries, |                 entries: entries, | ||||||
|                 data: this.data |                 data: this.data, | ||||||
|  |                 module: this.module, | ||||||
|  |                 group: this.selectedGroup | ||||||
|             }; |             }; | ||||||
|         }).catch((message) => { |         }).catch((message) => { | ||||||
|             if (!refresh) { |             if (!refresh) { | ||||||
| @ -266,7 +247,7 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|      */ |      */ | ||||||
|     setGroup(groupId: number): Promise<any> { |     setGroup(groupId: number): Promise<any> { | ||||||
|         this.selectedGroup = groupId; |         this.selectedGroup = groupId; | ||||||
|         this.offset = 0; |         this.offset = null; | ||||||
|         this.entry = null; |         this.entry = null; | ||||||
|         this.entryId = null; |         this.entryId = null; | ||||||
|         this.entryLoaded = false; |         this.entryLoaded = false; | ||||||
| @ -275,46 +256,73 @@ export class AddonModDataEntryPage implements OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Convenience function to translate offset to entry identifier and set next/previous entries. |      * Convenience function to fetch the entry and set next/previous entries. | ||||||
|      * |      * | ||||||
|      * @param {number} dataId Data Id. |  | ||||||
|      * @param {number} [offset] Offset of the entry. |  | ||||||
|      * @param {number} [groupId] Group Id to get the entry. |  | ||||||
|      * @return {Promise<any>} Resolved when done. |      * @return {Promise<any>} Resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected setEntryIdFromOffset(dataId: number, offset?: number, groupId?: number): Promise<any> { |     protected setEntryFromOffset(): Promise<any> { | ||||||
|         if (typeof offset != 'number') { |         const emptyOffset = typeof this.offset != 'number'; | ||||||
|  | 
 | ||||||
|  |         if (emptyOffset && typeof this.entryId == 'number') { | ||||||
|             // Entry id passed as navigation parameter instead of the offset.
 |             // Entry id passed as navigation parameter instead of the offset.
 | ||||||
|             // We don't display next/previous buttons in this case.
 |             // We don't display next/previous buttons in this case.
 | ||||||
|             this.nextOffset = null; |             this.nextOffset = null; | ||||||
|             this.previousOffset = null; |             this.previousOffset = null; | ||||||
| 
 | 
 | ||||||
|             return Promise.resolve(); |             return this.dataHelper.fetchEntry(this.data, this.fieldsArray, this.entryId).then((entry) => { | ||||||
|  |                 this.entry = entry.entry; | ||||||
|  |                 this.ratingInfo = entry.ratinginfo; | ||||||
|  |             }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const perPage = AddonModDataProvider.PER_PAGE; |         const perPage = AddonModDataProvider.PER_PAGE; | ||||||
|         const page = Math.floor(offset / perPage); |         const page = !emptyOffset && this.offset >= 0 ? Math.floor(this.offset / perPage) : 0; | ||||||
|         const pageOffset = offset % perPage; |  | ||||||
| 
 | 
 | ||||||
|         return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page, perPage).then((entries) => { |         return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, undefined, undefined, '0', 'DESC', | ||||||
|             if (!entries || !entries.entries || !entries.entries.length || pageOffset >= entries.entries.length) { |                 page, perPage).then((entries) => { | ||||||
|                 return Promise.reject(null); | 
 | ||||||
|  |             const pageEntries = entries.offlineEntries.concat(entries.entries); | ||||||
|  |             let pageIndex; // Index of the entry when concatenating offline and online page entries.
 | ||||||
|  |             if (emptyOffset) { | ||||||
|  |                 // No offset passed, display the first entry.
 | ||||||
|  |                 pageIndex = 0; | ||||||
|  |             } else if (this.offset > 0) { | ||||||
|  |                 // Online entry.
 | ||||||
|  |                 pageIndex = this.offset % perPage + entries.offlineEntries.length; | ||||||
|  |             } else { | ||||||
|  |                 // Offline entry.
 | ||||||
|  |                 pageIndex = this.offset + entries.offlineEntries.length; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             this.entryId = entries.entries[pageOffset].id; |             this.entry = pageEntries[pageIndex]; | ||||||
|             this.previousOffset = offset > 0 ? offset - 1 : null; |             this.entryId = this.entry.id; | ||||||
|             if (pageOffset + 1 < entries.entries.length) { | 
 | ||||||
|  |             this.previousOffset = page > 0 || pageIndex > 0 ? this.offset - 1 : null; | ||||||
|  | 
 | ||||||
|  |             let promise; | ||||||
|  | 
 | ||||||
|  |             if (pageIndex + 1 < pageEntries.length) { | ||||||
|                 // Not the last entry on the page;
 |                 // Not the last entry on the page;
 | ||||||
|                 this.nextOffset = offset + 1; |                 this.nextOffset = this.offset + 1; | ||||||
|             } else if (entries.entries.length < perPage) { |             } else if (pageEntries.length < perPage) { | ||||||
|                 // Last entry of the last page.
 |                 // Last entry of the last page.
 | ||||||
|                 this.nextOffset = null; |                 this.nextOffset = null; | ||||||
|             } else { |             } else { | ||||||
|                 // Last entry of the page, check if there are more pages.
 |                 // Last entry of the page, check if there are more pages.
 | ||||||
|                 return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page + 1, perPage).then((entries) => { |                 promise = this.dataProvider.getEntries(this.data.id, this.selectedGroup, '0', 'DESC', page + 1, perPage) | ||||||
|                     this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? offset + 1 : null; |                         .then((entries) => { | ||||||
|  |                     this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? this.offset + 1 : null; | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             return Promise.resolve(promise).then(() => { | ||||||
|  |                 if (this.entryId > 0) { | ||||||
|  |                     // Online entry, we need to fetch the the rating info.
 | ||||||
|  |                     return this.dataProvider.getEntry(this.data.id, this.entryId).then((entry) => { | ||||||
|  |                         this.ratingInfo = entry.ratinginfo; | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,9 +16,7 @@ import { Injectable } from '@angular/core'; | |||||||
| import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; | import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; | ||||||
| import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||||
| import { AddonModDataProvider } from './data'; | import { AddonModDataProvider } from './data'; | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { AddonModDataHelperProvider } from './helper'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; |  | ||||||
| import { CoreEventsProvider } from '@providers/events'; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Content links handler for database approve/disapprove entry. |  * Content links handler for database approve/disapprove entry. | ||||||
| @ -30,29 +28,10 @@ export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase | |||||||
|     featureName = 'CoreCourseModuleDelegate_AddonModData'; |     featureName = 'CoreCourseModuleDelegate_AddonModData'; | ||||||
|     pattern = /\/mod\/data\/view\.php.*([\?\&](d|approve|disapprove)=\d+)/; |     pattern = /\/mod\/data\/view\.php.*([\?\&](d|approve|disapprove)=\d+)/; | ||||||
| 
 | 
 | ||||||
|     constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, |     constructor(private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider) { | ||||||
|             private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider) { |  | ||||||
|         super(); |         super(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Convenience function to help get courseId. |  | ||||||
|      * |  | ||||||
|      * @param {number} dataId   Database Id. |  | ||||||
|      * @param {string} siteId   Site Id, if not set, current site will be used. |  | ||||||
|      * @param {number} courseId Course Id if already set. |  | ||||||
|      * @return {Promise<number>}   Resolved with course Id when done. |  | ||||||
|      */ |  | ||||||
|     protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise<number> { |  | ||||||
|         if (courseId) { |  | ||||||
|             return Promise.resolve(courseId); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { |  | ||||||
|             return module.course; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Get the list of actions for a link (url). |      * Get the list of actions for a link (url). | ||||||
|      * |      * | ||||||
| @ -66,34 +45,11 @@ export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase | |||||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { |             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||||
|         return [{ |         return [{ | ||||||
|             action: (siteId, navCtrl?): void => { |             action: (siteId, navCtrl?): void => { | ||||||
|                 const modal = this.domUtils.showModalLoading(), |                 const dataId = parseInt(params.d, 10), | ||||||
|                     dataId = parseInt(params.d, 10), |  | ||||||
|                     entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10), |                     entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10), | ||||||
|                     approve = parseInt(params.approve, 10) ? true : false; |                     approve = parseInt(params.approve, 10) ? true : false; | ||||||
| 
 | 
 | ||||||
|                 this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => { |                 this.dataHelper.approveOrDisapproveEntry(dataId, entryId, approve, courseId, siteId); | ||||||
|                     courseId = cId; |  | ||||||
| 
 |  | ||||||
|                     // Approve/disapprove entry.
 |  | ||||||
|                     return this.dataProvider.approveEntry(dataId, entryId, approve, courseId, siteId).catch((message) => { |  | ||||||
|                         this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errorapproving', true); |  | ||||||
| 
 |  | ||||||
|                         return Promise.reject(null); |  | ||||||
|                     }); |  | ||||||
|                 }).then(() => { |  | ||||||
|                     const promises = []; |  | ||||||
|                     promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); |  | ||||||
|                     promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); |  | ||||||
| 
 |  | ||||||
|                     return Promise.all(promises); |  | ||||||
|                 }).then(() => { |  | ||||||
|                     this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, siteId); |  | ||||||
| 
 |  | ||||||
|                     this.domUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true, |  | ||||||
|                         3000); |  | ||||||
|                 }).finally(() => { |  | ||||||
|                     modal.dismiss(); |  | ||||||
|                 }); |  | ||||||
|             } |             } | ||||||
|         }]; |         }]; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -21,6 +21,67 @@ import { CoreFilepoolProvider } from '@providers/filepool'; | |||||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||||
| import { AddonModDataOfflineProvider } from './offline'; | import { AddonModDataOfflineProvider } from './offline'; | ||||||
| import { AddonModDataFieldsDelegate } from './fields-delegate'; | import { AddonModDataFieldsDelegate } from './fields-delegate'; | ||||||
|  | import { CoreRatingInfo } from '@core/rating/providers/rating'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Database entry (online or offline). | ||||||
|  |  */ | ||||||
|  | export interface AddonModDataEntry { | ||||||
|  |     id: number; // Negative for offline entries.
 | ||||||
|  |     userid: number; | ||||||
|  |     groupid: number; | ||||||
|  |     dataid: number; | ||||||
|  |     timecreated: number; | ||||||
|  |     timemodified: number; | ||||||
|  |     approved: boolean; | ||||||
|  |     canmanageentry: boolean; | ||||||
|  |     fullname: string; | ||||||
|  |     contents: AddonModDataEntryFields; | ||||||
|  |     deleted?: boolean; // Entry is deleted offline.
 | ||||||
|  |     hasOffline?: boolean; // Entry has offline actions.
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Entry field content. | ||||||
|  |  */ | ||||||
|  | export interface AddonModDataEntryField { | ||||||
|  |     fieldid: number; | ||||||
|  |     content: string; | ||||||
|  |     content1: string; | ||||||
|  |     content2: string; | ||||||
|  |     content3: string; | ||||||
|  |     content4: string; | ||||||
|  |     files: any[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Entry contents indexed by field id. | ||||||
|  |  */ | ||||||
|  | export interface AddonModDataEntryFields { | ||||||
|  |     [fieldid: number]: AddonModDataEntryField; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * List of entries returned by web service and helper functions. | ||||||
|  |  */ | ||||||
|  | export interface AddonModDataEntries { | ||||||
|  |     entries: AddonModDataEntry[]; // Online entries.
 | ||||||
|  |     totalcount: number; // Total count of online entries or found entries.
 | ||||||
|  |     maxcount?: number; // Total count of online entries. Only returned when searching.
 | ||||||
|  |     offlineEntries?: AddonModDataEntry[]; // Offline entries.
 | ||||||
|  |     hasOfflineActions?: boolean; // Whether the database has offline data.
 | ||||||
|  |     hasOfflineRatings?: boolean; // Whether the database has offline ratings.
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Subfield form data. | ||||||
|  |  */ | ||||||
|  | export interface AddonModDataSubfieldData { | ||||||
|  |     fieldid: number; | ||||||
|  |     subfield?: string; | ||||||
|  |     value?: string; // Value encoded in JSON.
 | ||||||
|  |     files?: any[]; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service that provides some features for databases. |  * Service that provides some features for databases. | ||||||
| @ -49,13 +110,13 @@ export class AddonModDataProvider { | |||||||
|      * @param   {number}  courseId        Course ID. |      * @param   {number}  courseId        Course ID. | ||||||
|      * @param   {any}     contents        The fields data to be created. |      * @param   {any}     contents        The fields data to be created. | ||||||
|      * @param   {number}  [groupId]       Group id, 0 means that the function will determine the user group. |      * @param   {number}  [groupId]       Group id, 0 means that the function will determine the user group. | ||||||
|      * @param   {any}     fields          The fields that define the contents. |      * @param   {any[]}   fields          The fields that define the contents. | ||||||
|      * @param   {string}  [siteId]        Site ID. If not defined, current site. |      * @param   {string}  [siteId]        Site ID. If not defined, current site. | ||||||
|      * @param   {boolean} [forceOffline]  Force editing entry in offline. |      * @param   {boolean} [forceOffline]  Force editing entry in offline. | ||||||
|      * @return  {Promise<any>}            Promise resolved when the action is done. |      * @return  {Promise<any>}            Promise resolved when the action is done. | ||||||
|      */ |      */ | ||||||
|     addEntry(dataId: number, entryId: number, courseId: number, contents: any, groupId: number = 0, fields: any, siteId?: string, |     addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0, | ||||||
|             forceOffline: boolean = false): Promise<any> { |             fields: any, siteId?: string, forceOffline: boolean = false): Promise<any> { | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         // Convenience function to store a data to be synchronized later.
 |         // Convenience function to store a data to be synchronized later.
 | ||||||
| @ -76,6 +137,8 @@ export class AddonModDataProvider { | |||||||
|                     fieldnotifications: notifications |                     fieldnotifications: notifications | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             return storeOffline(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => { |         return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => { | ||||||
| @ -93,12 +156,12 @@ export class AddonModDataProvider { | |||||||
|      * Adds a new entry to a database. It does not cache calls. It will fail if offline or cannot connect. |      * Adds a new entry to a database. It does not cache calls. It will fail if offline or cannot connect. | ||||||
|      * |      * | ||||||
|      * @param   {number} dataId     Database ID. |      * @param   {number} dataId     Database ID. | ||||||
|      * @param   {any}    data       The fields data to be created. |      * @param   {any[]}  data       The fields data to be created. | ||||||
|      * @param   {number} [groupId]  Group id, 0 means that the function will determine the user group. |      * @param   {number} [groupId]  Group id, 0 means that the function will determine the user group. | ||||||
|      * @param   {string} [siteId]   Site ID. If not defined, current site. |      * @param   {string} [siteId]   Site ID. If not defined, current site. | ||||||
|      * @return  {Promise<any>}      Promise resolved when the action is done. |      * @return  {Promise<any>}      Promise resolved when the action is done. | ||||||
|      */ |      */ | ||||||
|     addEntryOnline(dataId: number, data: any, groupId?: number, siteId?: string): Promise<any> { |     addEntryOnline(dataId: number, data: AddonModDataSubfieldData[], groupId?: number, siteId?: string): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     databaseid: dataId, |                     databaseid: dataId, | ||||||
| @ -184,7 +247,7 @@ export class AddonModDataProvider { | |||||||
|      * @param   {any}    contents  The contents data of the fields. |      * @param   {any}    contents  The contents data of the fields. | ||||||
|      * @return  {any}            Array of notifications if any or false. |      * @return  {any}            Array of notifications if any or false. | ||||||
|      */ |      */ | ||||||
|     protected checkFields(fields: any, contents: any): any { |     protected checkFields(fields: any, contents: AddonModDataSubfieldData[]): any[] | false { | ||||||
|         const notifications = [], |         const notifications = [], | ||||||
|             contentsIndexed = {}; |             contentsIndexed = {}; | ||||||
| 
 | 
 | ||||||
| @ -289,13 +352,13 @@ export class AddonModDataProvider { | |||||||
|      * @param   {number}  dataId          Database ID. |      * @param   {number}  dataId          Database ID. | ||||||
|      * @param   {number}  entryId         Entry ID. |      * @param   {number}  entryId         Entry ID. | ||||||
|      * @param   {number}  courseId        Course ID. |      * @param   {number}  courseId        Course ID. | ||||||
|      * @param   {any}     contents        The contents data to be updated. |      * @param   {any[]}   contents        The contents data to be updated. | ||||||
|      * @param   {any}     fields          The fields that define the contents. |      * @param   {any}     fields          The fields that define the contents. | ||||||
|      * @param   {string}  [siteId]        Site ID. If not defined, current site. |      * @param   {string}  [siteId]        Site ID. If not defined, current site. | ||||||
|      * @param   {boolean} forceOffline    Force editing entry in offline. |      * @param   {boolean} forceOffline    Force editing entry in offline. | ||||||
|      * @return  {Promise<any>}            Promise resolved when the action is done. |      * @return  {Promise<any>}            Promise resolved when the action is done. | ||||||
|      */ |      */ | ||||||
|     editEntry(dataId: number, entryId: number, courseId: number, contents: any, fields: any, siteId?: string, |     editEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], fields: any, siteId?: string, | ||||||
|             forceOffline: boolean = false): Promise<any> { |             forceOffline: boolean = false): Promise<any> { | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
| @ -370,11 +433,11 @@ export class AddonModDataProvider { | |||||||
|      * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. |      * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. | ||||||
|      * |      * | ||||||
|      * @param   {number}  entryId  Entry ID. |      * @param   {number}  entryId  Entry ID. | ||||||
|      * @param   {any}     data     The fields data to be updated. |      * @param   {any[]}   data     The fields data to be updated. | ||||||
|      * @param   {string}  [siteId] Site ID. If not defined, current site. |      * @param   {string}  [siteId] Site ID. If not defined, current site. | ||||||
|      * @return  {Promise<any>}     Promise resolved when the action is done. |      * @return  {Promise<any>}     Promise resolved when the action is done. | ||||||
|      */ |      */ | ||||||
|     editEntryOnline(entryId: number, data: number, siteId?: string): Promise<any> { |     editEntryOnline(entryId: number, data: AddonModDataSubfieldData[], siteId?: string): Promise<any> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     entryid: entryId, |                     entryid: entryId, | ||||||
| @ -397,11 +460,11 @@ export class AddonModDataProvider { | |||||||
|      * @param  {boolean}   [forceCache]    True to always get the value from cache, false otherwise. Default false. |      * @param  {boolean}   [forceCache]    True to always get the value from cache, false otherwise. Default false. | ||||||
|      * @param  {boolean}   [ignoreCache]   True if it should ignore cached data (it will always fail in offline or server down). |      * @param  {boolean}   [ignoreCache]   True if it should ignore cached data (it will always fail in offline or server down). | ||||||
|      * @param  {string}    [siteId]        Site ID. If not defined, current site. |      * @param  {string}    [siteId]        Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}              Promise resolved when done. |      * @return {Promise<AddonModDataEntry[]>} Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', |     fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', | ||||||
|             perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, |             perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, | ||||||
|             siteId?: string): Promise<any> { |             siteId?: string): Promise<AddonModDataEntry[]> { | ||||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId); |         return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId); | ||||||
| @ -420,10 +483,10 @@ export class AddonModDataProvider { | |||||||
|      * @param  {any}       entries         Entries already fetch (just to concatenate them). |      * @param  {any}       entries         Entries already fetch (just to concatenate them). | ||||||
|      * @param  {number}    page            Page of records to return. |      * @param  {number}    page            Page of records to return. | ||||||
|      * @param  {string}    siteId          Site ID. |      * @param  {string}    siteId          Site ID. | ||||||
|      * @return {Promise<any>}              Promise resolved when done. |      * @return {Promise<AddonModDataEntry[]>} Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number, |     protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number, | ||||||
|             forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise<any> { |             forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise<AddonModDataEntry[]> { | ||||||
|         return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId) |         return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId) | ||||||
|                 .then((result) => { |                 .then((result) => { | ||||||
|             entries = entries.concat(result.entries); |             entries = entries.concat(result.entries); | ||||||
| @ -595,11 +658,11 @@ export class AddonModDataProvider { | |||||||
|      * @param   {boolean}   [forceCache=false]  True to always get the value from cache, false otherwise. Default false. |      * @param   {boolean}   [forceCache=false]  True to always get the value from cache, false otherwise. Default false. | ||||||
|      * @param   {boolean}   [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). |      * @param   {boolean}   [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). | ||||||
|      * @param   {string}    [siteId]            Site ID. If not defined, current site. |      * @param   {string}    [siteId]            Site ID. If not defined, current site. | ||||||
|      * @return  {Promise<any>}                  Promise resolved when the database is retrieved. |      * @return  {Promise<AddonModDataEntries>} Promise resolved when the database is retrieved. | ||||||
|      */ |      */ | ||||||
|     getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0, |     getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0, | ||||||
|             perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, |             perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, | ||||||
|             siteId?: string): Promise<any> { |             siteId?: string): Promise<AddonModDataEntries> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             // Always use sort and order params to improve cache usage (entries are identified by params).
 |             // Always use sort and order params to improve cache usage (entries are identified by params).
 | ||||||
|             const params = { |             const params = { | ||||||
| @ -622,7 +685,13 @@ export class AddonModDataProvider { | |||||||
|                 preSets['emergencyCache'] = false; |                 preSets['emergencyCache'] = false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_data_get_entries', params, preSets); |             return site.read('mod_data_get_entries', params, preSets).then((response) => { | ||||||
|  |                 response.entries.forEach((entry) => { | ||||||
|  |                     entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 return response; | ||||||
|  |             }); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -654,9 +723,10 @@ export class AddonModDataProvider { | |||||||
|      * @param   {number}    entryId   Entry ID. |      * @param   {number}    entryId   Entry ID. | ||||||
|      * @param   {boolean}   [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). |      * @param   {boolean}   [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). | ||||||
|      * @param   {string}    [siteId]  Site ID. If not defined, current site. |      * @param   {string}    [siteId]  Site ID. If not defined, current site. | ||||||
|      * @return  {Promise<any>}        Promise resolved when the database entry is retrieved. |      * @return {Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}>} Promise resolved when the entry is retrieved. | ||||||
|      */ |      */ | ||||||
|     getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): Promise<any> { |     getEntry(dataId: number, entryId: number, ignoreCache: boolean = false, siteId?: string): | ||||||
|  |              Promise<{entry: AddonModDataEntry, ratinginfo: CoreRatingInfo}> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     entryid: entryId, |                     entryid: entryId, | ||||||
| @ -671,7 +741,11 @@ export class AddonModDataProvider { | |||||||
|                 preSets['emergencyCache'] = false; |                 preSets['emergencyCache'] = false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_data_get_entry', params, preSets); |             return site.read('mod_data_get_entry', params, preSets).then((response) => { | ||||||
|  |                 response.entry.contents = this.utils.arrayToObject(response.entry.contents, 'fieldid'); | ||||||
|  | 
 | ||||||
|  |                 return response; | ||||||
|  |             }); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -871,16 +945,16 @@ export class AddonModDataProvider { | |||||||
|      * @param {number} dataId             The data instance id. |      * @param {number} dataId             The data instance id. | ||||||
|      * @param {number} [groupId=0]        Group id, 0 means that the function will determine the user group. |      * @param {number} [groupId=0]        Group id, 0 means that the function will determine the user group. | ||||||
|      * @param {string} [search]           Search text. It will be used if advSearch is not defined. |      * @param {string} [search]           Search text. It will be used if advSearch is not defined. | ||||||
|      * @param {any}    [advSearch]        Advanced search data. |      * @param {any[]}  [advSearch]        Advanced search data. | ||||||
|      * @param {string} [sort]             Sort by this field. |      * @param {string} [sort]             Sort by this field. | ||||||
|      * @param {string} [order]            The direction of the sorting. |      * @param {string} [order]            The direction of the sorting. | ||||||
|      * @param {number} [page=0]           Page of records to return. |      * @param {number} [page=0]           Page of records to return. | ||||||
|      * @param {number} [perPage=PER_PAGE] Records per page to return. Default on AddonModDataProvider.PER_PAGE. |      * @param {number} [perPage=PER_PAGE] Records per page to return. Default on AddonModDataProvider.PER_PAGE. | ||||||
|      * @param {string} [siteId]           Site ID. If not defined, current site. |      * @param {string} [siteId]           Site ID. If not defined, current site. | ||||||
|      * @return  {Promise<any>}            Promise resolved when the action is done. |      * @return {Promise<AddonModDataEntries>} Promise resolved when the action is done. | ||||||
|      */ |      */ | ||||||
|     searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string, |     searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string, | ||||||
|             page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise<any> { |             page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise<AddonModDataEntries> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             const params = { |             const params = { | ||||||
|                     databaseid: dataId, |                     databaseid: dataId, | ||||||
| @ -911,7 +985,13 @@ export class AddonModDataProvider { | |||||||
|                 params['advsearch'] = advSearch; |                 params['advsearch'] = advSearch; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return site.read('mod_data_search_entries', params, preSets); |             return site.read('mod_data_search_entries', params, preSets).then((response) => { | ||||||
|  |                 response.entries.forEach((entry) => { | ||||||
|  |                     entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 return response; | ||||||
|  |             }); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -13,13 +13,10 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; |  | ||||||
| import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; | import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; | ||||||
| import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||||
| import { AddonModDataProvider } from './data'; | import { AddonModDataProvider } from './data'; | ||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { AddonModDataHelperProvider } from './helper'; | ||||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; |  | ||||||
| import { CoreEventsProvider } from '@providers/events'; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Content links handler for database delete entry. |  * Content links handler for database delete entry. | ||||||
| @ -31,30 +28,10 @@ export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase { | |||||||
|     featureName = 'CoreCourseModuleDelegate_AddonModData'; |     featureName = 'CoreCourseModuleDelegate_AddonModData'; | ||||||
|     pattern = /\/mod\/data\/view\.php.*([\?\&](d|delete)=\d+)/; |     pattern = /\/mod\/data\/view\.php.*([\?\&](d|delete)=\d+)/; | ||||||
| 
 | 
 | ||||||
|     constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, |     constructor(private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider) { | ||||||
|             private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider, |  | ||||||
|             private translate: TranslateService) { |  | ||||||
|         super(); |         super(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Convenience function to help get courseId. |  | ||||||
|      * |  | ||||||
|      * @param {number} dataId   Database Id. |  | ||||||
|      * @param {string} siteId   Site Id, if not set, current site will be used. |  | ||||||
|      * @param {number} courseId Course Id if already set. |  | ||||||
|      * @return {Promise<number>}   Resolved with course Id when done. |  | ||||||
|      */ |  | ||||||
|     protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise<number> { |  | ||||||
|         if (courseId) { |  | ||||||
|             return Promise.resolve(courseId); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { |  | ||||||
|             return module.course; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Get the list of actions for a link (url). |      * Get the list of actions for a link (url). | ||||||
|      * |      * | ||||||
| @ -68,38 +45,10 @@ export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase { | |||||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { |             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||||
|         return [{ |         return [{ | ||||||
|             action: (siteId, navCtrl?): void => { |             action: (siteId, navCtrl?): void => { | ||||||
|  |                 const dataId = parseInt(params.d, 10); | ||||||
|  |                 const entryId = parseInt(params.delete, 10); | ||||||
| 
 | 
 | ||||||
|                 this.domUtils.showConfirm(this.translate.instant('addon.mod_data.confirmdeleterecord')).then(() => { |                 this.dataHelper.showDeleteEntryModal(dataId, entryId, courseId); | ||||||
|                     const modal = this.domUtils.showModalLoading(), |  | ||||||
|                         dataId = parseInt(params.d, 10), |  | ||||||
|                         entryId = parseInt(params.delete, 10); |  | ||||||
| 
 |  | ||||||
|                     return this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => { |  | ||||||
|                         courseId = cId; |  | ||||||
| 
 |  | ||||||
|                         // Delete entry.
 |  | ||||||
|                         return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId).catch((message) => { |  | ||||||
|                             this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true); |  | ||||||
| 
 |  | ||||||
|                             return Promise.reject(null); |  | ||||||
|                         }); |  | ||||||
|                     }).then(() => { |  | ||||||
|                         const promises = []; |  | ||||||
|                         promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); |  | ||||||
|                         promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); |  | ||||||
| 
 |  | ||||||
|                         return Promise.all(promises); |  | ||||||
|                     }).then(() => { |  | ||||||
|                         this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId, |  | ||||||
|                             deleted: true}, siteId); |  | ||||||
| 
 |  | ||||||
|                         this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); |  | ||||||
|                     }).finally(() => { |  | ||||||
|                         modal.dismiss(); |  | ||||||
|                     }); |  | ||||||
|                 }).catch(() => { |  | ||||||
|                     // Nothing to do.
 |  | ||||||
|                 }); |  | ||||||
|             } |             } | ||||||
|         }]; |         }]; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -14,12 +14,18 @@ | |||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { TranslateService } from '@ngx-translate/core'; | import { TranslateService } from '@ngx-translate/core'; | ||||||
|  | import { CoreEventsProvider } from '@providers/events'; | ||||||
| import { CoreSitesProvider } from '@providers/sites'; | import { CoreSitesProvider } from '@providers/sites'; | ||||||
|  | import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
|  | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
|  | import { CoreCourseProvider } from '@core/course/providers/course'; | ||||||
| import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | ||||||
| import { AddonModDataFieldsDelegate } from './fields-delegate'; | import { AddonModDataFieldsDelegate } from './fields-delegate'; | ||||||
| import { AddonModDataOfflineProvider } from './offline'; | import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; | ||||||
| import { AddonModDataProvider } from './data'; | import { AddonModDataProvider, AddonModDataEntry, AddonModDataEntryFields, AddonModDataEntries } from './data'; | ||||||
|  | import { CoreRatingInfo } from '@core/rating/providers/rating'; | ||||||
|  | import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service that provides helper functions for datas. |  * Service that provides helper functions for datas. | ||||||
| @ -30,20 +36,26 @@ export class AddonModDataHelperProvider { | |||||||
|     constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider, |     constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider, | ||||||
|         private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, |         private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, | ||||||
|         private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider, |         private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider, | ||||||
|         private textUtils: CoreTextUtilsProvider) { } |         private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider, | ||||||
|  |         private domUtils: CoreDomUtilsProvider, private courseProvider: CoreCourseProvider, | ||||||
|  |         private ratingOffline: CoreRatingOfflineProvider) {} | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns the record with the offline actions applied. |      * Returns the record with the offline actions applied. | ||||||
|      * |      * | ||||||
|      * @param  {any} record         Entry to modify. |      * @param {AddonModDataEntry} record Entry to modify. | ||||||
|      * @param  {any} offlineActions Offline data with the actions done. |      * @param {AddonModDataOfflineAction[]} offlineActions Offline data with the actions done. | ||||||
|      * @param  {any} fields         Entry defined fields indexed by fieldid. |      * @param {any[]} fields Entry defined fields indexed by fieldid. | ||||||
|      * @return {any}                Modified entry. |      * @return {Promise<AddonModDataEntry>} Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     applyOfflineActions(record: any, offlineActions: any[], fields: any[]): any { |     applyOfflineActions(record: AddonModDataEntry, offlineActions: AddonModDataOfflineAction[], fields: any[]): | ||||||
|  |             Promise<AddonModDataEntry> { | ||||||
|         const promises  = []; |         const promises  = []; | ||||||
| 
 | 
 | ||||||
|         offlineActions.forEach((action) => { |         offlineActions.forEach((action) => { | ||||||
|  |             record.timemodified = action.timemodified; | ||||||
|  |             record.hasOffline = true; | ||||||
|  | 
 | ||||||
|             switch (action.action) { |             switch (action.action) { | ||||||
|                 case 'approve': |                 case 'approve': | ||||||
|                     record.approved = true; |                     record.approved = true; | ||||||
| @ -56,6 +68,8 @@ export class AddonModDataHelperProvider { | |||||||
|                     break; |                     break; | ||||||
|                 case 'add': |                 case 'add': | ||||||
|                 case 'edit': |                 case 'edit': | ||||||
|  |                     record.groupid = action.groupid; | ||||||
|  | 
 | ||||||
|                     const offlineContents = {}; |                     const offlineContents = {}; | ||||||
| 
 | 
 | ||||||
|                     action.fields.forEach((offlineContent) => { |                     action.fields.forEach((offlineContent) => { | ||||||
| @ -77,10 +91,12 @@ export class AddonModDataHelperProvider { | |||||||
|                             promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => { |                             promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => { | ||||||
|                                 record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], |                                 record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], | ||||||
|                                         offlineContents[field.id], offlineFiles); |                                         offlineContents[field.id], offlineFiles); | ||||||
|  |                                 record.contents[field.id].fieldid = field.id; | ||||||
|                             })); |                             })); | ||||||
|                         } else { |                         } else { | ||||||
|                             record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], |                             record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], | ||||||
|                                     offlineContents[field.id]); |                                     offlineContents[field.id]); | ||||||
|  |                             record.contents[field.id].fieldid = field.id; | ||||||
|                         } |                         } | ||||||
|                     }); |                     }); | ||||||
|                     break; |                     break; | ||||||
| @ -94,18 +110,59 @@ export class AddonModDataHelperProvider { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Approve or disapprove a database entry. | ||||||
|  |      * | ||||||
|  |      * @param {number} dataId Database ID. | ||||||
|  |      * @param {number} entryId Entry ID. | ||||||
|  |      * @param {boolaen} approve True to approve, false to disapprove. | ||||||
|  |      * @param {number} [courseId] Course ID. It not defined, it will be fetched. | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      */ | ||||||
|  |     approveOrDisapproveEntry(dataId: number, entryId: number, approve: boolean, courseId?: number, siteId?: string): void { | ||||||
|  |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         const modal = this.domUtils.showModalLoading('core.sending', true); | ||||||
|  | 
 | ||||||
|  |         this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => { | ||||||
|  |             // Approve/disapprove entry.
 | ||||||
|  |             return this.dataProvider.approveEntry(dataId, entryId, approve, courseId, siteId).catch((message) => { | ||||||
|  |                 this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errorapproving', true); | ||||||
|  | 
 | ||||||
|  |                 return Promise.reject(null); | ||||||
|  |             }); | ||||||
|  |         }).then(() => { | ||||||
|  |             const promises = []; | ||||||
|  |             promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); | ||||||
|  |             promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); | ||||||
|  | 
 | ||||||
|  |             return Promise.all(promises).catch(() => { | ||||||
|  |                 // Ignore errors.
 | ||||||
|  |             }); | ||||||
|  |         }).then(() => { | ||||||
|  |             this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, siteId); | ||||||
|  | 
 | ||||||
|  |             this.domUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true, 3000); | ||||||
|  |         }).catch(() => { | ||||||
|  |             // Ignore error, it was already displayed.
 | ||||||
|  |         }).finally(() => { | ||||||
|  |             modal.dismiss(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Displays fields for being shown. |      * Displays fields for being shown. | ||||||
|      * |      * | ||||||
|      * @param {string} template   Template HMTL. |      * @param {string} template Template HMTL. | ||||||
|      * @param {any[]}  fields     Fields that defines every content in the entry. |      * @param {any[]} fields Fields that defines every content in the entry. | ||||||
|      * @param {any}    entry      Entry. |      * @param {any} entry Entry. | ||||||
|      * @param {number} offset     Entry offset. |      * @param {number} offset Entry offset. | ||||||
|      * @param {string} mode       Mode list or show. |      * @param {string} mode Mode list or show. | ||||||
|      * @param {any}    actions    Actions that can be performed to the record. |      * @param {AddonModDataOfflineAction[]} actions Actions that can be performed to the record. | ||||||
|      * @return {string}           Generated HTML. |      * @return {string} Generated HTML. | ||||||
|      */ |      */ | ||||||
|     displayShowFields(template: string, fields: any[], entry: any, offset: number, mode: string, actions: any): string { |     displayShowFields(template: string, fields: any[], entry: any, offset: number, mode: string, | ||||||
|  |             actions: AddonModDataOfflineAction[]): string { | ||||||
|         if (!template) { |         if (!template) { | ||||||
|             return ''; |             return ''; | ||||||
|         } |         } | ||||||
| @ -135,8 +192,8 @@ export class AddonModDataHelperProvider { | |||||||
|                 } else if (action == 'approvalstatus') { |                 } else if (action == 'approvalstatus') { | ||||||
|                     render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')); |                     render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')); | ||||||
|                 } else { |                 } else { | ||||||
|                     render = '<addon-mod-data-action action="' + action + '" [entry]="entries[' + entry.id + |                     render = '<addon-mod-data-action action="' + action + '" [entry]="entries[' + entry.id + ']" mode="' + mode + | ||||||
|                                 ']" mode="' + mode + '" [database]="data" [offset]="' + offset + '"></addon-mod-data-action>'; |                     '" [database]="data" [module]="module" [offset]="' + offset + '" [group]="group" ></addon-mod-data-action>'; | ||||||
|                 } |                 } | ||||||
|                 template = template.replace(replace, render); |                 template = template.replace(replace, render); | ||||||
|             } else { |             } else { | ||||||
| @ -147,6 +204,153 @@ export class AddonModDataHelperProvider { | |||||||
|         return template; |         return template; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Get online and offline entries, or search entries. | ||||||
|  |      * | ||||||
|  |      * @param   {any}       data               Database object. | ||||||
|  |      * @param   {any[]}     fields             The fields that define the contents. | ||||||
|  |      * @param   {number}    [groupId=0]        Group ID. | ||||||
|  |      * @param   {string}    [search]           Search text. It will be used if advSearch is not defined. | ||||||
|  |      * @param   {any[]}     [advSearch]        Advanced search data. | ||||||
|  |      * @param   {string}    [sort=0]           Sort the records by this field id, reserved ids are: | ||||||
|  |      *                                            0: timeadded | ||||||
|  |      *                                           -1: firstname | ||||||
|  |      *                                           -2: lastname | ||||||
|  |      *                                           -3: approved | ||||||
|  |      *                                           -4: timemodified. | ||||||
|  |      *                                          Empty for using the default database setting. | ||||||
|  |      * @param   {string}    [order=DESC]        The direction of the sorting: 'ASC' or 'DESC'. | ||||||
|  |      *                                          Empty for using the default database setting. | ||||||
|  |      * @param   {number}    [page=0]            Page of records to return. | ||||||
|  |      * @param   {number}    [perPage=PER_PAGE]  Records per page to return. Default on PER_PAGE. | ||||||
|  |      * @param   {string}    [siteId]            Site ID. If not defined, current site. | ||||||
|  |      * @return  {Promise<AddonModDataEntries>}  Promise resolved when the database is retrieved. | ||||||
|  |      */ | ||||||
|  |     fetchEntries(data: any, fields: any[], groupId: number = 0, search?: string, advSearch?: any[], sort: string = '0', | ||||||
|  |             order: string = 'DESC', page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): | ||||||
|  |             Promise<AddonModDataEntries> { | ||||||
|  |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|  |             const offlineActions = {}; | ||||||
|  |             const result: AddonModDataEntries = { | ||||||
|  |                 entries: [], | ||||||
|  |                 totalcount: 0, | ||||||
|  |                 offlineEntries: [] | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             const offlinePromise = this.dataOffline.getDatabaseEntries(data.id, site.id).then((actions) => { | ||||||
|  |                 result.hasOfflineActions = !!actions.length; | ||||||
|  | 
 | ||||||
|  |                 actions.forEach((action) => { | ||||||
|  |                     if (typeof offlineActions[action.entryid] == 'undefined') { | ||||||
|  |                         offlineActions[action.entryid] = []; | ||||||
|  |                     } | ||||||
|  |                     offlineActions[action.entryid].push(action); | ||||||
|  | 
 | ||||||
|  |                     // We only display new entries in the first page when not searching.
 | ||||||
|  |                     if (action.action == 'add' && page == 0 && !search && !advSearch && | ||||||
|  |                             (!action.groupid || !groupId || action.groupid == groupId)) { | ||||||
|  |                         result.offlineEntries.push({ | ||||||
|  |                             id: action.entryid, | ||||||
|  |                             canmanageentry: true, | ||||||
|  |                             approved: !data.approval || data.manageapproved, | ||||||
|  |                             dataid: data.id, | ||||||
|  |                             groupid: action.groupid, | ||||||
|  |                             timecreated: -action.entryid, | ||||||
|  |                             timemodified: -action.entryid, | ||||||
|  |                             userid: site.getUserId(), | ||||||
|  |                             fullname: site.getInfo().fullname, | ||||||
|  |                             contents: {} | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 // Sort offline entries by creation time.
 | ||||||
|  |                 result.offlineEntries.sort((entry1, entry2) => entry2.timecreated - entry1.timecreated); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             const ratingsPromise = this.ratingOffline.hasRatings('mod_data', 'entry', 'module', data.coursemodule) | ||||||
|  |                     .then((hasRatings) => { | ||||||
|  |                 result.hasOfflineRatings = hasRatings; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             let fetchPromise: Promise<void>; | ||||||
|  |             if (search || advSearch) { | ||||||
|  |                 fetchPromise = this.dataProvider.searchEntries(data.id, groupId, search, advSearch, sort, order, page, perPage, | ||||||
|  |                         site.id).then((fetchResult) => { | ||||||
|  |                     result.entries = fetchResult.entries; | ||||||
|  |                     result.totalcount = fetchResult.totalcount; | ||||||
|  |                     result.maxcount = fetchResult.maxcount; | ||||||
|  |                 }); | ||||||
|  |             } else { | ||||||
|  |                 fetchPromise = this.dataProvider.getEntries(data.id, groupId, sort, order, page, perPage, false, false, site.id) | ||||||
|  |                         .then((fetchResult) => { | ||||||
|  |                     result.entries = fetchResult.entries; | ||||||
|  |                     result.totalcount = fetchResult.totalcount; | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return Promise.all([offlinePromise, ratingsPromise, fetchPromise]).then(() => { | ||||||
|  |                 // Apply offline actions to online and offline entries.
 | ||||||
|  |                 const promises = []; | ||||||
|  |                 result.entries.forEach((entry) => { | ||||||
|  |                     promises.push(this.applyOfflineActions(entry, offlineActions[entry.id] || [], fields)); | ||||||
|  |                 }); | ||||||
|  |                 result.offlineEntries.forEach((entry) => { | ||||||
|  |                     promises.push(this.applyOfflineActions(entry, offlineActions[entry.id] || [], fields)); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 return Promise.all(promises); | ||||||
|  |             }).then(() => { | ||||||
|  |                 return result; | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Fetch an online or offline entry. | ||||||
|  |      * | ||||||
|  |      * @param {any} data Database. | ||||||
|  |      * @param {any[]} fields List of database fields. | ||||||
|  |      * @param {number} entryId Entry ID. | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      * @return {Promise<{entry: AddonModDataEntry, ratinginfo?: CoreRatingInfo}>} Promise resolved with the entry. | ||||||
|  |      */ | ||||||
|  |     fetchEntry(data: any, fields: any[], entryId: number, siteId?: string): | ||||||
|  |             Promise<{entry: AddonModDataEntry, ratinginfo?: CoreRatingInfo}> { | ||||||
|  |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|  |             return this.dataOffline.getEntryActions(data.id, entryId, site.id).then((offlineActions) => { | ||||||
|  |                 let promise: Promise<{entry: AddonModDataEntry, ratinginfo?: CoreRatingInfo}>; | ||||||
|  | 
 | ||||||
|  |                 if (entryId > 0) { | ||||||
|  |                     // Online entry.
 | ||||||
|  |                     promise = this.dataProvider.getEntry(data.id, entryId, false, site.id); | ||||||
|  |                 } else  { | ||||||
|  |                     // Offline entry or new entry.
 | ||||||
|  |                     promise = Promise.resolve({ | ||||||
|  |                         entry: { | ||||||
|  |                             id: entryId, | ||||||
|  |                             userid: site.getUserId(), | ||||||
|  |                             groupid: 0, | ||||||
|  |                             dataid: data.id, | ||||||
|  |                             timecreated: -entryId, | ||||||
|  |                             timemodified: -entryId, | ||||||
|  |                             approved: !data.approval || data.manageapproved, | ||||||
|  |                             canmanageentry: true, | ||||||
|  |                             fullname: site.getInfo().fullname, | ||||||
|  |                             contents: [], | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return promise.then((response) => { | ||||||
|  |                     return this.applyOfflineActions(response.entry, offlineActions, fields).then(() => { | ||||||
|  |                         return response; | ||||||
|  |                     }); | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns an object with all the actions that the user can do over the record. |      * Returns an object with all the actions that the user can do over the record. | ||||||
|      * |      * | ||||||
| @ -179,6 +383,24 @@ export class AddonModDataHelperProvider { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Convenience function to get the course id of the database. | ||||||
|  |      * | ||||||
|  |      * @param {number} dataId Database id. | ||||||
|  |      * @param {number} [courseId] Course id, if known. | ||||||
|  |      * @param {string} [siteId] Site id, if not set, current site will be used. | ||||||
|  |      * @return {Promise<number>} Resolved with course Id when done. | ||||||
|  |      */ | ||||||
|  |     protected getActivityCourseIdIfNotSet(dataId: number, courseId?: number, siteId?: string): Promise<number> { | ||||||
|  |         if (courseId) { | ||||||
|  |             return Promise.resolve(courseId); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { | ||||||
|  |             return module.course; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns the default template of a certain type. |      * Returns the default template of a certain type. | ||||||
|      * |      * | ||||||
| @ -256,17 +478,17 @@ export class AddonModDataHelperProvider { | |||||||
|      * Retrieve the entered data in the edit form. |      * Retrieve the entered data in the edit form. | ||||||
|      * We don't use ng-model because it doesn't detect changes done by JavaScript. |      * We don't use ng-model because it doesn't detect changes done by JavaScript. | ||||||
|      * |      * | ||||||
|      * @param  {any}     inputData    Array with the entered form values. |      * @param {any} inputData Array with the entered form values. | ||||||
|      * @param  {Array}   fields       Fields that defines every content in the entry. |      * @param {Array} fields Fields that defines every content in the entry. | ||||||
|      * @param  {number}  [dataId]     Database Id. If set, files will be uploaded and itemId set. |      * @param {number} [dataId] Database Id. If set, files will be uploaded and itemId set. | ||||||
|      * @param  {number}  entryId      Entry Id. |      * @param {number} entryId Entry Id. | ||||||
|      * @param  {any}  entryContents   Original entry contents indexed by field id. |      * @param {AddonModDataEntryFields} entryContents Original entry contents. | ||||||
|      * @param  {boolean} offline      True to prepare the data for an offline uploading, false otherwise. |      * @param {boolean} offline True to prepare the data for an offline uploading, false otherwise. | ||||||
|      * @param  {string}  [siteId]     Site ID. If not defined, current site. |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}         That contains object with the answers. |      * @return {Promise<any>} That contains object with the answers. | ||||||
|      */ |      */ | ||||||
|     getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: any, offline: boolean = false, |     getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: AddonModDataEntryFields, | ||||||
|             siteId?: string): Promise<any> { |             offline: boolean = false, siteId?: string): Promise<any> { | ||||||
|         if (!inputData) { |         if (!inputData) { | ||||||
|             return Promise.resolve({}); |             return Promise.resolve({}); | ||||||
|         } |         } | ||||||
| @ -322,13 +544,13 @@ export class AddonModDataHelperProvider { | |||||||
|     /** |     /** | ||||||
|      * Retrieve the temp files to be updated. |      * Retrieve the temp files to be updated. | ||||||
|      * |      * | ||||||
|      * @param  {any}     inputData    Array with the entered form values. |      * @param {any} inputData Array with the entered form values. | ||||||
|      * @param  {Array}   fields       Fields that defines every content in the entry. |      * @param {any[]} fields Fields that defines every content in the entry. | ||||||
|      * @param  {number}  [dataId]     Database Id. If set, fils will be uploaded and itemId set. |      * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. | ||||||
|      * @param  {any}   entryContents  Original entry contents indexed by field id. |      * @param {AddonModDataEntryFields} entryContents Original entry contents indexed by field id. | ||||||
|      * @return {Promise<any>}         That contains object with the files. |      * @return {Promise<any>} That contains object with the files. | ||||||
|      */ |      */ | ||||||
|     getEditTmpFiles(inputData: any, fields: any, dataId: number, entryContents: any): Promise<any> { |     getEditTmpFiles(inputData: any, fields: any[], dataId: number, entryContents: AddonModDataEntryFields): Promise<any> { | ||||||
|         if (!inputData) { |         if (!inputData) { | ||||||
|             return Promise.resolve([]); |             return Promise.resolve([]); | ||||||
|         } |         } | ||||||
| @ -343,45 +565,6 @@ export class AddonModDataHelperProvider { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Get an online or offline entry. |  | ||||||
|      * |  | ||||||
|      * @param  {any} data             Database. |  | ||||||
|      * @param  {number} entryId       Entry ID. |  | ||||||
|      * @param  {any} [offlineActions] Offline data with the actions done. Required for offline entries. |  | ||||||
|      * @param  {string} [siteId]      Site ID. If not defined, current site. |  | ||||||
|      * @return {Promise<any>}         Promise resolved with the entry. |  | ||||||
|      */ |  | ||||||
|     getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise<any> { |  | ||||||
|         if (entryId > 0) { |  | ||||||
|             // It's an online entry, get it from WS.
 |  | ||||||
|             return this.dataProvider.getEntry(data.id, entryId, false, siteId); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // It's an offline entry, search it in the offline actions.
 |  | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |  | ||||||
|             const offlineEntry = offlineActions.find((offlineAction) => offlineAction.action == 'add'); |  | ||||||
| 
 |  | ||||||
|             if (offlineEntry) { |  | ||||||
|                 const siteInfo = site.getInfo(); |  | ||||||
| 
 |  | ||||||
|                 return {entry: { |  | ||||||
|                         id: offlineEntry.entryid, |  | ||||||
|                         canmanageentry: true, |  | ||||||
|                         approved: !data.approval || data.manageapproved, |  | ||||||
|                         dataid: offlineEntry.dataid, |  | ||||||
|                         groupid: offlineEntry.groupid, |  | ||||||
|                         timecreated: -offlineEntry.entryid, |  | ||||||
|                         timemodified: -offlineEntry.entryid, |  | ||||||
|                         userid: siteInfo.userid, |  | ||||||
|                         fullname: siteInfo.fullname, |  | ||||||
|                         contents: {} |  | ||||||
|                     } |  | ||||||
|                 }; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles. |      * Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles. | ||||||
|      * |      * | ||||||
| @ -403,13 +586,13 @@ export class AddonModDataHelperProvider { | |||||||
|     /** |     /** | ||||||
|      * Check if data has been changed by the user. |      * Check if data has been changed by the user. | ||||||
|      * |      * | ||||||
|      * @param  {any}    inputData     Array with the entered form values. |      * @param {any} inputData Object with the entered form values. | ||||||
|      * @param  {any}  fields          Fields that defines every content in the entry. |      * @param {any[]} fields Fields that defines every content in the entry. | ||||||
|      * @param  {number} [dataId]      Database Id. If set, fils will be uploaded and itemId set. |      * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. | ||||||
|      * @param  {any}    entryContents Original entry contents indexed by field id. |      * @param {AddonModDataEntryFields} entryContents Original entry contents indexed by field id. | ||||||
|      * @return {Promise<boolean>}     True if changed, false if not. |      * @return {Promise<boolean>} True if changed, false if not. | ||||||
|      */ |      */ | ||||||
|     hasEditDataChanged(inputData: any, fields: any, dataId: number, entryContents: any): Promise<boolean> { |     hasEditDataChanged(inputData: any, fields: any[], dataId: number, entryContents: AddonModDataEntryFields): Promise<boolean> { | ||||||
|         const promises = fields.map((field) => { |         const promises = fields.map((field) => { | ||||||
|             return this.fieldsDelegate.hasFieldDataChanged(field, inputData, entryContents[field.id]); |             return this.fieldsDelegate.hasFieldDataChanged(field, inputData, entryContents[field.id]); | ||||||
|         }); |         }); | ||||||
| @ -424,6 +607,45 @@ export class AddonModDataHelperProvider { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Displays a confirmation modal for deleting an entry. | ||||||
|  |      * | ||||||
|  |      * @param {number} dataId Database ID. | ||||||
|  |      * @param {number} entryId Entry ID. | ||||||
|  |      * @param {number} [courseId] Course ID. It not defined, it will be fetched. | ||||||
|  |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|  |      */ | ||||||
|  |     showDeleteEntryModal(dataId: number, entryId: number, courseId?: number, siteId?: string): void { | ||||||
|  |         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||||
|  | 
 | ||||||
|  |         this.domUtils.showConfirm(this.translate.instant('addon.mod_data.confirmdeleterecord')).then(() => { | ||||||
|  |             const modal = this.domUtils.showModalLoading(); | ||||||
|  | 
 | ||||||
|  |             return this.getActivityCourseIdIfNotSet(dataId, courseId, siteId).then((courseId) => { | ||||||
|  |                 return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId); | ||||||
|  |             }).catch((message) => { | ||||||
|  |                 this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true); | ||||||
|  | 
 | ||||||
|  |                 return Promise.reject(null); | ||||||
|  |             }).then(() => { | ||||||
|  |                 return this.utils.allPromises([ | ||||||
|  |                     this.dataProvider.invalidateEntryData(dataId, entryId, siteId), | ||||||
|  |                     this.dataProvider.invalidateEntriesData(dataId, siteId) | ||||||
|  |                 ]).catch(() => { | ||||||
|  |                     // Ignore errors.
 | ||||||
|  |                 }); | ||||||
|  |             }).then(() => { | ||||||
|  |                 this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId, entryId,  deleted: true}, siteId); | ||||||
|  | 
 | ||||||
|  |                 this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); | ||||||
|  |             }).finally(() => { | ||||||
|  |                 modal.dismiss(); | ||||||
|  |             }); | ||||||
|  |         }).catch(() => { | ||||||
|  |             // Ignore error, it was already displayed.
 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Given a list of files (either online files or local files), store the local files in a local folder |      * Given a list of files (either online files or local files), store the local files in a local folder | ||||||
|      * to be submitted later. |      * to be submitted later. | ||||||
|  | |||||||
| @ -16,9 +16,24 @@ import { Injectable } from '@angular/core'; | |||||||
| import { CoreLoggerProvider } from '@providers/logger'; | import { CoreLoggerProvider } from '@providers/logger'; | ||||||
| import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; | import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
|  | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreFileProvider } from '@providers/file'; | import { CoreFileProvider } from '@providers/file'; | ||||||
| import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | ||||||
| import { SQLiteDB } from '@classes/sqlitedb'; | import { SQLiteDB } from '@classes/sqlitedb'; | ||||||
|  | import { AddonModDataSubfieldData } from './data'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Entry action stored offline. | ||||||
|  |  */ | ||||||
|  | export interface AddonModDataOfflineAction { | ||||||
|  |     dataid: number; | ||||||
|  |     courseid: number; | ||||||
|  |     groupid: number; | ||||||
|  |     action: string; | ||||||
|  |     entryid: number; // Negative for offline entries.
 | ||||||
|  |     fields: AddonModDataSubfieldData[]; | ||||||
|  |     timemodified: number; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Service to handle Offline data. |  * Service to handle Offline data. | ||||||
| @ -87,7 +102,8 @@ export class AddonModDataOfflineProvider { | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, |     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, | ||||||
|             private fileProvider: CoreFileProvider, private fileUploaderProvider: CoreFileUploaderProvider) { |             private fileProvider: CoreFileProvider, private fileUploaderProvider: CoreFileUploaderProvider, | ||||||
|  |             private utils: CoreUtilsProvider) { | ||||||
|         this.logger = logger.getInstance('AddonModDataOfflineProvider'); |         this.logger = logger.getInstance('AddonModDataOfflineProvider'); | ||||||
|         this.sitesProvider.registerSiteSchema(this.siteSchema); |         this.sitesProvider.registerSiteSchema(this.siteSchema); | ||||||
|     } |     } | ||||||
| @ -175,10 +191,10 @@ export class AddonModDataOfflineProvider { | |||||||
|     /** |     /** | ||||||
|      * Get all the stored entry data from all the databases. |      * Get all the stored entry data from all the databases. | ||||||
|      * |      * | ||||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}         Promise resolved with entries. |      * @return {Promise<AddonModDataOfflineAction[]>} Promise resolved with entries. | ||||||
|      */ |      */ | ||||||
|     getAllEntries(siteId?: string): Promise<any> { |     getAllEntries(siteId?: string): Promise<AddonModDataOfflineAction[]> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             return site.getDb().getAllRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE); |             return site.getDb().getAllRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE); | ||||||
|         }).then((entries) => { |         }).then((entries) => { | ||||||
| @ -187,15 +203,15 @@ export class AddonModDataOfflineProvider { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get all the stored entry data from a certain database. |      * Get all the stored entry actions from a certain database, sorted by modification time. | ||||||
|      * |      * | ||||||
|      * @param  {number} dataId     Database ID. |      * @param  {number} dataId Database ID. | ||||||
|      * @param  {string} [siteId]   Site ID. If not defined, current site. |      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}           Promise resolved with entries. |      * @return {Promise<AddonModDataOfflineAction[]>} Promise resolved with entries. | ||||||
|      */ |      */ | ||||||
|     getDatabaseEntries(dataId: number, siteId?: string): Promise<any> { |     getDatabaseEntries(dataId: number, siteId?: string): Promise<AddonModDataOfflineAction[]> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}); |             return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}, 'timemodified'); | ||||||
|         }).then((entries) => { |         }).then((entries) => { | ||||||
|             return entries.map(this.parseRecord.bind(this)); |             return entries.map(this.parseRecord.bind(this)); | ||||||
|         }); |         }); | ||||||
| @ -208,9 +224,9 @@ export class AddonModDataOfflineProvider { | |||||||
|      * @param  {number} entryId     Database entry Id. |      * @param  {number} entryId     Database entry Id. | ||||||
|      * @param  {string} action      Action to be done |      * @param  {string} action      Action to be done | ||||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. |      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}       Promise resolved with entry. |      * @return {Promise<AddonModDataOfflineAction>} Promise resolved with entry. | ||||||
|      */ |      */ | ||||||
|     getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise<any> { |     getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise<AddonModDataOfflineAction> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             return site.getDb().getRecord(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, |             return site.getDb().getRecord(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, | ||||||
|                     action: action}); |                     action: action}); | ||||||
| @ -225,9 +241,9 @@ export class AddonModDataOfflineProvider { | |||||||
|      * @param  {number} dataId      Database ID. |      * @param  {number} dataId      Database ID. | ||||||
|      * @param  {number} entryId     Database entry Id. |      * @param  {number} entryId     Database entry Id. | ||||||
|      * @param  {string} [siteId]    Site ID. If not defined, current site. |      * @param  {string} [siteId]    Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}            Promise resolved with entry actions. |      * @return {Promise<AddonModDataOfflineAction[]>} Promise resolved with entry actions. | ||||||
|      */ |      */ | ||||||
|     getEntryActions(dataId: number, entryId: number, siteId?: string): Promise<any> { |     getEntryActions(dataId: number, entryId: number, siteId?: string): Promise<AddonModDataOfflineAction[]> { | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId}); |             return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId}); | ||||||
|         }).then((entries) => { |         }).then((entries) => { | ||||||
| @ -243,11 +259,10 @@ export class AddonModDataOfflineProvider { | |||||||
|      * @return {Promise<any>}          Promise resolved with boolean: true if has offline answers, false otherwise. |      * @return {Promise<any>}          Promise resolved with boolean: true if has offline answers, false otherwise. | ||||||
|      */ |      */ | ||||||
|     hasOfflineData(dataId: number, siteId?: string): Promise<any> { |     hasOfflineData(dataId: number, siteId?: string): Promise<any> { | ||||||
|         return this.getDatabaseEntries(dataId, siteId).then((entries) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             return !!entries.length; |             return this.utils.promiseWorks( | ||||||
|         }).catch(() => { |                 site.getDb().recordExists(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}) | ||||||
|             // No offline data found, return false.
 |             ); | ||||||
|             return false; |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -286,10 +301,10 @@ export class AddonModDataOfflineProvider { | |||||||
|     /** |     /** | ||||||
|      * Parse "fields" of an offline record. |      * Parse "fields" of an offline record. | ||||||
|      * |      * | ||||||
|      * @param  {any} record Record object |      * @param {any} record Record object | ||||||
|      * @return {any}        Record object with columns parsed. |      * @return {AddonModDataOfflineAction} Record object with columns parsed. | ||||||
|      */ |      */ | ||||||
|     protected parseRecord(record: any): any { |     protected parseRecord(record: any): AddonModDataOfflineAction { | ||||||
|         record.fields = this.textUtils.parseJSON(record.fields); |         record.fields = this.textUtils.parseJSON(record.fields); | ||||||
| 
 | 
 | ||||||
|         return record; |         return record; | ||||||
| @ -308,8 +323,8 @@ export class AddonModDataOfflineProvider { | |||||||
|      * @param  {string} [siteId]        Site ID. If not defined, current site. |      * @param  {string} [siteId]        Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}           Promise resolved if stored, rejected if failure. |      * @return {Promise<any>}           Promise resolved if stored, rejected if failure. | ||||||
|      */ |      */ | ||||||
|     saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, fields?: any[], |     saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, | ||||||
|             timemodified?: number, siteId?: string): Promise<any> { |             fields?: AddonModDataSubfieldData[], timemodified?: number, siteId?: string): Promise<any> { | ||||||
| 
 | 
 | ||||||
|         return this.sitesProvider.getSite(siteId).then((site) => { |         return this.sitesProvider.getSite(siteId).then((site) => { | ||||||
|             timemodified = timemodified || new Date().getTime(); |             timemodified = timemodified || new Date().getTime(); | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ import { CoreCommentsProvider } from '@core/comments/providers/comments'; | |||||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | import { CoreCourseProvider } from '@core/course/providers/course'; | ||||||
| import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; | import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; | ||||||
| import { CoreRatingProvider } from '@core/rating/providers/rating'; | import { CoreRatingProvider } from '@core/rating/providers/rating'; | ||||||
| import { AddonModDataProvider } from './data'; | import { AddonModDataProvider, AddonModDataEntry } from './data'; | ||||||
| import { AddonModDataSyncProvider } from './sync'; | import { AddonModDataSyncProvider } from './sync'; | ||||||
| import { AddonModDataHelperProvider } from './helper'; | import { AddonModDataHelperProvider } from './helper'; | ||||||
| 
 | 
 | ||||||
| @ -57,10 +57,10 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|      * @param  {boolean} [forceCache]   True to always get the value from cache, false otherwise. Default false. |      * @param  {boolean} [forceCache]   True to always get the value from cache, false otherwise. Default false. | ||||||
|      * @param  {boolean} [ignoreCache]  True if it should ignore cached data (it will always fail in offline or server down). |      * @param  {boolean} [ignoreCache]  True if it should ignore cached data (it will always fail in offline or server down). | ||||||
|      * @param  {string}  [siteId]       Site ID. |      * @param  {string}  [siteId]       Site ID. | ||||||
|      * @return {Promise<any>}                All unique entries. |      * @return {Promise<AddonModDataEntry[]>} All unique entries. | ||||||
|      */ |      */ | ||||||
|     protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, |     protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, | ||||||
|             siteId?: string): Promise<any> { |             siteId?: string): Promise<AddonModDataEntry[]> { | ||||||
|         const promises = groups.map((group) => { |         const promises = groups.map((group) => { | ||||||
|             return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, |             return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, | ||||||
|                 siteId); |                 siteId); | ||||||
| @ -139,14 +139,14 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl | |||||||
|     /** |     /** | ||||||
|      * Returns the file contained in the entries. |      * Returns the file contained in the entries. | ||||||
|      * |      * | ||||||
|      * @param  {any[]} entries  List of entries to get files from. |      * @param {AddonModDataEntry[]} entries List of entries to get files from. | ||||||
|      * @return {any[]}          List of files. |      * @return {any[]} List of files. | ||||||
|      */ |      */ | ||||||
|     protected getEntriesFiles(entries: any[]): any[] { |     protected getEntriesFiles(entries: AddonModDataEntry[]): any[] { | ||||||
|         let files = []; |         let files = []; | ||||||
| 
 | 
 | ||||||
|         entries.forEach((entry) => { |         entries.forEach((entry) => { | ||||||
|             entry.contents.forEach((content) => { |             this.utils.objectToArray(entry.contents).forEach((content) => { | ||||||
|                 files = files.concat(content.files); |                 files = files.concat(content.files); | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ import { CoreAppProvider } from '@providers/app'; | |||||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||||
| import { AddonModDataOfflineProvider } from './offline'; | import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; | ||||||
| import { AddonModDataProvider } from './data'; | import { AddonModDataProvider } from './data'; | ||||||
| import { AddonModDataHelperProvider } from './helper'; | import { AddonModDataHelperProvider } from './helper'; | ||||||
| import { CoreEventsProvider } from '@providers/events'; | import { CoreEventsProvider } from '@providers/events'; | ||||||
| @ -174,7 +174,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|                 // No offline data found, return empty object.
 |                 // No offline data found, return empty object.
 | ||||||
|                 return []; |                 return []; | ||||||
|             }); |             }); | ||||||
|         }).then((offlineActions) => { |         }).then((offlineActions: AddonModDataOfflineAction[]) => { | ||||||
|             if (!offlineActions.length) { |             if (!offlineActions.length) { | ||||||
|                 // Nothing to sync.
 |                 // Nothing to sync.
 | ||||||
|                 return; |                 return; | ||||||
| @ -226,35 +226,41 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|     /** |     /** | ||||||
|      * Synchronize an entry. |      * Synchronize an entry. | ||||||
|      * |      * | ||||||
|      * @param  {any} data          Database. |      * @param {any} data Database. | ||||||
|      * @param  {any} entryActions  Entry actions. |      * @param {AddonModDataOfflineAction[]} entryActions  Entry actions. | ||||||
|      * @param  {any} result        Object with the result of the sync. |      * @param {any} result Object with the result of the sync. | ||||||
|      * @param  {string} [siteId]   Site ID. If not defined, current site. |      * @param {string} [siteId] Site ID. If not defined, current site. | ||||||
|      * @return {Promise<any>}      Promise resolved if success, rejected otherwise. |      * @return {Promise<any>} Promise resolved if success, rejected otherwise. | ||||||
|      */ |      */ | ||||||
|     protected syncEntry(data: any, entryActions: any[], result: any, siteId?: string): Promise<any> { |     protected syncEntry(data: any, entryActions: AddonModDataOfflineAction[], result: any, siteId?: string): Promise<any> { | ||||||
|         let discardError, |         let discardError, | ||||||
|             timePromise, |             timePromise, | ||||||
|             entryId = 0, |             entryId = entryActions[0].entryid, | ||||||
|             offlineId, |             offlineId, | ||||||
|             deleted = false; |             deleted = false; | ||||||
| 
 | 
 | ||||||
|         const promises = []; |         const editAction = entryActions.find((action) => action.action == 'add' || action.action == 'edit'); | ||||||
| 
 |         const approveAction = entryActions.find((action) => action.action == 'approve' || action.action == 'disapprove'); | ||||||
|         // Sort entries by timemodified.
 |         const deleteAction = entryActions.find((action) => action.action == 'delete'); | ||||||
|         entryActions = entryActions.sort((a: any, b: any) => a.timemodified - b.timemodified); |  | ||||||
| 
 |  | ||||||
|         entryId = entryActions[0].entryid; |  | ||||||
| 
 | 
 | ||||||
|         if (entryId > 0) { |         if (entryId > 0) { | ||||||
|             timePromise = this.dataProvider.getEntry(data.id, entryId, false, siteId).then((entry) => { |             timePromise = this.dataProvider.getEntry(data.id, entryId, true, siteId).then((entry) => { | ||||||
|                 return entry.entry.timemodified; |                 return entry.entry.timemodified; | ||||||
|             }).catch(() => { |             }).catch((error) => { | ||||||
|                 return -1; |                 if (error && this.utils.isWebServiceError(error)) { | ||||||
|  |                     // The WebService has thrown an error, this means the entry has been deleted.
 | ||||||
|  |                     return Promise.resolve(-1); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return Promise.reject(error); | ||||||
|             }); |             }); | ||||||
|         } else { |         } else if (editAction) { | ||||||
|  |             // New entry.
 | ||||||
|             offlineId = entryId; |             offlineId = entryId; | ||||||
|             timePromise = Promise.resolve(0); |             timePromise = Promise.resolve(0); | ||||||
|  |         } else { | ||||||
|  |             // New entry but the add action is missing, discard.
 | ||||||
|  |             timePromise = Promise.resolve(-1); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return timePromise.then((timemodified) => { |         return timePromise.then((timemodified) => { | ||||||
| @ -266,58 +272,11 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|                 return this.dataOffline.deleteAllEntryActions(data.id, entryId, siteId); |                 return this.dataOffline.deleteAllEntryActions(data.id, entryId, siteId); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             entryActions.forEach((action) => { |             if (deleteAction) { | ||||||
|                 let actionPromise; |                 return this.dataProvider.deleteEntryOnline(entryId, siteId).then(() => { | ||||||
|                 const proms = []; |                     deleted = true; | ||||||
| 
 |                 }).catch((error) => { | ||||||
|                 entryId = action.entryid > 0 ? action.entryid : entryId; |                     if (error && this.utils.isWebServiceError(error)) { | ||||||
| 
 |  | ||||||
|                 if (action.fields) { |  | ||||||
|                     action.fields.forEach((field) => { |  | ||||||
|                         // Upload Files if asked.
 |  | ||||||
|                         const value = this.textUtils.parseJSON(field.value); |  | ||||||
|                         if (value.online || value.offline) { |  | ||||||
|                             let files = value.online || []; |  | ||||||
|                             const fileProm = value.offline ? this.dataHelper.getStoredFiles(action.dataid, entryId, field.fieldid) : |  | ||||||
|                                     Promise.resolve([]); |  | ||||||
| 
 |  | ||||||
|                             proms.push(fileProm.then((offlineFiles) => { |  | ||||||
|                                 files = files.concat(offlineFiles); |  | ||||||
| 
 |  | ||||||
|                                 return this.dataHelper.uploadOrStoreFiles(action.dataid, 0, entryId, field.fieldid, files, false, |  | ||||||
|                                         siteId).then((filesResult) => { |  | ||||||
|                                     field.value = JSON.stringify(filesResult); |  | ||||||
|                                 }); |  | ||||||
|                             })); |  | ||||||
|                         } |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 actionPromise = Promise.all(proms).then(() => { |  | ||||||
|                     // Perform the action.
 |  | ||||||
|                     switch (action.action) { |  | ||||||
|                         case 'add': |  | ||||||
|                             return this.dataProvider.addEntryOnline(action.dataid, action.fields, data.groupid, siteId) |  | ||||||
|                                     .then((result) => { |  | ||||||
|                                 entryId = result.newentryid; |  | ||||||
|                             }); |  | ||||||
|                         case 'edit': |  | ||||||
|                             return this.dataProvider.editEntryOnline(entryId, action.fields, siteId); |  | ||||||
|                         case 'approve': |  | ||||||
|                             return this.dataProvider.approveEntryOnline(entryId, true, siteId); |  | ||||||
|                         case 'disapprove': |  | ||||||
|                             return this.dataProvider.approveEntryOnline(entryId, false, siteId); |  | ||||||
|                         case 'delete': |  | ||||||
|                             return this.dataProvider.deleteEntryOnline(entryId, siteId).then(() => { |  | ||||||
|                                 deleted = true; |  | ||||||
|                             }); |  | ||||||
|                         default: |  | ||||||
|                             break; |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|                 promises.push(actionPromise.catch((error) => { |  | ||||||
|                     if (error && error.wserror) { |  | ||||||
|                         // The WebService has thrown an error, this means it cannot be performed. Discard.
 |                         // The WebService has thrown an error, this means it cannot be performed. Discard.
 | ||||||
|                         discardError = this.textUtils.getErrorMessageFromError(error); |                         discardError = this.textUtils.getErrorMessageFromError(error); | ||||||
|                     } else { |                     } else { | ||||||
| @ -328,11 +287,79 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | |||||||
|                     // Delete the offline data.
 |                     // Delete the offline data.
 | ||||||
|                     result.updated = true; |                     result.updated = true; | ||||||
| 
 | 
 | ||||||
|                     return this.dataOffline.deleteEntry(action.dataid, action.entryid, action.action, siteId); |                     return this.dataOffline.deleteAllEntryActions(deleteAction.dataid, deleteAction.entryid, siteId); | ||||||
|                 })); |                 }); | ||||||
|             }); |             } | ||||||
|  | 
 | ||||||
|  |             let editPromise; | ||||||
|  | 
 | ||||||
|  |             if (editAction) { | ||||||
|  |                 editPromise = Promise.all(editAction.fields.map((field) => { | ||||||
|  |                     // Upload Files if asked.
 | ||||||
|  |                     const value = this.textUtils.parseJSON(field.value); | ||||||
|  |                     if (value.online || value.offline) { | ||||||
|  |                         let files = value.online || []; | ||||||
|  |                         const fileProm = value.offline ? | ||||||
|  |                                 this.dataHelper.getStoredFiles(editAction.dataid, entryId, field.fieldid) : | ||||||
|  |                                 Promise.resolve([]); | ||||||
|  | 
 | ||||||
|  |                         return fileProm.then((offlineFiles) => { | ||||||
|  |                             files = files.concat(offlineFiles); | ||||||
|  | 
 | ||||||
|  |                             return this.dataHelper.uploadOrStoreFiles(editAction.dataid, 0, entryId, field.fieldid, files, | ||||||
|  |                                     false, siteId).then((filesResult) => { | ||||||
|  |                                 field.value = JSON.stringify(filesResult); | ||||||
|  |                             }); | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 })).then(() => { | ||||||
|  |                     if (editAction.action == 'add') { | ||||||
|  |                         return this.dataProvider.addEntryOnline(editAction.dataid, editAction.fields, editAction.groupid, siteId) | ||||||
|  |                                 .then((result) => { | ||||||
|  |                             entryId = result.newentryid; | ||||||
|  |                         }); | ||||||
|  |                     } else { | ||||||
|  |                         return this.dataProvider.editEntryOnline(entryId, editAction.fields, siteId); | ||||||
|  |                     } | ||||||
|  |                 }).catch((error) => { | ||||||
|  |                     if (error && this.utils.isWebServiceError(error)) { | ||||||
|  |                         // The WebService has thrown an error, this means it cannot be performed. Discard.
 | ||||||
|  |                         discardError = this.textUtils.getErrorMessageFromError(error); | ||||||
|  |                     } else { | ||||||
|  |                         // Couldn't connect to server, reject.
 | ||||||
|  |                         return Promise.reject(error); | ||||||
|  |                     } | ||||||
|  |                 }).then(() => { | ||||||
|  |                     // Delete the offline data.
 | ||||||
|  |                     result.updated = true; | ||||||
|  | 
 | ||||||
|  |                     return this.dataOffline.deleteEntry(editAction.dataid, editAction.entryid, editAction.action, siteId); | ||||||
|  |                 }); | ||||||
|  |             } else { | ||||||
|  |                 editPromise = Promise.resolve(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (approveAction) { | ||||||
|  |                 editPromise = editPromise.then(() => { | ||||||
|  |                     return this.dataProvider.approveEntryOnline(entryId, approveAction.action == 'approve', siteId); | ||||||
|  |                 }).catch((error) => { | ||||||
|  |                     if (error && this.utils.isWebServiceError(error)) { | ||||||
|  |                         // The WebService has thrown an error, this means it cannot be performed. Discard.
 | ||||||
|  |                         discardError = this.textUtils.getErrorMessageFromError(error); | ||||||
|  |                     } else { | ||||||
|  |                         // Couldn't connect to server, reject.
 | ||||||
|  |                         return Promise.reject(error); | ||||||
|  |                     } | ||||||
|  |                 }).then(() => { | ||||||
|  |                     // Delete the offline data.
 | ||||||
|  |                     result.updated = true; | ||||||
|  | 
 | ||||||
|  |                     return this.dataOffline.deleteEntry(approveAction.dataid, approveAction.entryid, approveAction.action, siteId); | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return editPromise; | ||||||
| 
 | 
 | ||||||
|             return Promise.all(promises); |  | ||||||
|         }).then(() => { |         }).then(() => { | ||||||
|             if (discardError) { |             if (discardError) { | ||||||
|                 // Submission was discarded, add a warning.
 |                 // Submission was discarded, add a warning.
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user