commit
						980af8d852
					
				| @ -12,10 +12,13 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| import { Component, Input, OnInit, Injector } from '@angular/core'; | ||||
| import { NavController } from 'ionic-angular'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { AddonModDataProvider } from '../../providers/data'; | ||||
| import { AddonModDataHelperProvider } from '../../providers/helper'; | ||||
| import { AddonModDataOfflineProvider } from '../../providers/offline'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| 
 | ||||
| /** | ||||
| @ -30,6 +33,8 @@ export class AddonModDataActionComponent implements OnInit { | ||||
|     @Input() action: string; // The field to render.
 | ||||
|     @Input() entry?: any; // The value of the field.
 | ||||
|     @Input() database: any; // Database object.
 | ||||
|     @Input() module: any; // Module object.
 | ||||
|     @Input() group: number; // Module object.
 | ||||
|     @Input() offset?: number; // Offset of the entry.
 | ||||
| 
 | ||||
|     siteId: string; | ||||
| @ -39,11 +44,72 @@ export class AddonModDataActionComponent implements OnInit { | ||||
| 
 | ||||
|     constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider, | ||||
|             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.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. | ||||
|      * | ||||
| @ -60,37 +126,4 @@ export class AddonModDataActionComponent implements OnInit { | ||||
|             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> | ||||
| </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> | ||||
| </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> | ||||
| </a> | ||||
| 
 | ||||
| @ -14,11 +14,11 @@ | ||||
|     <ion-icon name="undo"></ion-icon> | ||||
| </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> | ||||
| </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> | ||||
| </a> | ||||
| 
 | ||||
|  | ||||
| @ -20,11 +20,9 @@ import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; | ||||
| import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; | ||||
| import { CoreCommentsProvider } from '@core/comments/providers/comments'; | ||||
| import { CoreRatingProvider } from '@core/rating/providers/rating'; | ||||
| import { CoreRatingOfflineProvider } from '@core/rating/providers/offline'; | ||||
| import { CoreRatingSyncProvider } from '@core/rating/providers/sync'; | ||||
| import { AddonModDataProvider } from '../../providers/data'; | ||||
| import { AddonModDataHelperProvider } from '../../providers/helper'; | ||||
| import { AddonModDataOfflineProvider } from '../../providers/offline'; | ||||
| import { AddonModDataSyncProvider } from '../../providers/sync'; | ||||
| import { AddonModDataComponentsModule } from '../components.module'; | ||||
| import { AddonModDataPrefetchHandler } from '../../providers/prefetch-handler'; | ||||
| @ -65,8 +63,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|         advanced: [] | ||||
|     }; | ||||
|     hasNextPage = false; | ||||
|     offlineActions: any; | ||||
|     offlineEntries: any; | ||||
|     entriesRendered = ''; | ||||
|     extraImports = [AddonModDataComponentsModule]; | ||||
|     jsData; | ||||
| @ -81,12 +77,19 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|     protected ratingOfflineObserver: any; | ||||
|     protected ratingSyncObserver: any; | ||||
| 
 | ||||
|     constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, | ||||
|             private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content, | ||||
|             private prefetchHandler: AddonModDataPrefetchHandler, private timeUtils: CoreTimeUtilsProvider, | ||||
|             private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, | ||||
|             private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController, | ||||
|             private ratingOffline: CoreRatingOfflineProvider) { | ||||
|     constructor( | ||||
|             injector: Injector, | ||||
|             @Optional() content: Content, | ||||
|             private dataProvider: AddonModDataProvider, | ||||
|             private dataHelper: AddonModDataHelperProvider, | ||||
|             private prefetchHandler: AddonModDataPrefetchHandler, | ||||
|             private timeUtils: CoreTimeUtilsProvider, | ||||
|             private groupsProvider: CoreGroupsProvider, | ||||
|             private commentsProvider: CoreCommentsProvider, | ||||
|             private modalCtrl: ModalController, | ||||
|             private utils: CoreUtilsProvider, | ||||
|             protected navCtrl: NavController) { | ||||
| 
 | ||||
|         super(injector, content); | ||||
| 
 | ||||
|         // Refresh entries on change.
 | ||||
| @ -233,8 +236,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|                         this.selectedGroup = groupInfo.groups[0].id; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 return this.fetchOfflineEntries(); | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             return this.dataProvider.getFields(this.data.id).then((fields) => { | ||||
| @ -270,21 +271,19 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|             // Update values for current group.
 | ||||
|             this.access.canaddentry = accessData.canaddentry; | ||||
| 
 | ||||
|             if (this.search.searching) { | ||||
|                 const text = this.search.searchingAdvanced ? undefined : this.search.text, | ||||
|                     advanced = this.search.searchingAdvanced ? this.search.advanced : undefined; | ||||
|             const search = this.search.searching && !this.search.searchingAdvanced ? this.search.text : undefined; | ||||
|             const advSearch = this.search.searching && this.search.searchingAdvanced ? this.search.advanced : undefined; | ||||
| 
 | ||||
|                 return this.dataProvider.searchEntries(this.data.id, this.selectedGroup, text, advanced, 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); | ||||
|             } | ||||
|             return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, search, advSearch, | ||||
|                     this.search.sortBy, this.search.sortDirection, this.search.page); | ||||
|         }).then((entries) => { | ||||
|             const numEntries = (entries && entries.entries && entries.entries.length) || 0; | ||||
|             this.isEmpty = !numEntries && !Object.keys(this.offlineActions).length && !Object.keys(this.offlineEntries).length; | ||||
|             const numEntries = entries.entries.length; | ||||
|             const numOfflineEntries = entries.offlineEntries.length; | ||||
|             this.isEmpty = !numEntries && !entries.offlineEntries.length; | ||||
|             this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) * | ||||
|                 AddonModDataProvider.PER_PAGE) < entries.totalcount; | ||||
|             this.hasOffline = entries.hasOfflineActions; | ||||
|             this.hasOfflineRatings = entries.hasOfflineRatings; | ||||
|             this.entriesRendered = ''; | ||||
| 
 | ||||
|             if (typeof entries.maxcount != 'undefined') { | ||||
| @ -298,79 +297,40 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|             } | ||||
| 
 | ||||
|             if (!this.isEmpty) { | ||||
|                 const siteInfo = this.sitesProvider.getCurrentSite().getInfo(), | ||||
|                     promises = []; | ||||
|                 this.entries = entries.offlineEntries.concat(entries.entries); | ||||
| 
 | ||||
|                 this.utils.objectToArray(this.offlineEntries).forEach((offlineActions) => { | ||||
|                     const offlineEntry = offlineActions.find((offlineEntry) => offlineEntry.action == 'add'); | ||||
|                 let entriesHTML = this.data.listtemplateheader || ''; | ||||
| 
 | ||||
|                     if (offlineEntry) { | ||||
|                         const entry = { | ||||
|                             id: offlineEntry.entryid, | ||||
|                             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: {} | ||||
|                         }; | ||||
|                 // Get first entry from the whole list.
 | ||||
|                 if (!this.search.searching || !this.firstEntry) { | ||||
|                     this.firstEntry = this.entries[0].id; | ||||
|                 } | ||||
| 
 | ||||
|                         if (offlineActions.length > 0) { | ||||
|                             promises.push(this.dataHelper.applyOfflineActions(entry, offlineActions, this.fieldsArray)); | ||||
|                         } else { | ||||
|                             promises.push(Promise.resolve(entry)); | ||||
|                         } | ||||
|                     } | ||||
|                 const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray); | ||||
| 
 | ||||
|                 const entriesById = {}; | ||||
|                 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) => { | ||||
|                     // Index contents by fieldid.
 | ||||
|                     entry.contents = this.utils.arrayToObject(entry.contents, 'fieldid'); | ||||
|                 this.entriesRendered = entriesHTML; | ||||
| 
 | ||||
|                     if (typeof this.offlineActions[entry.id] != 'undefined') { | ||||
|                         promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fieldsArray)); | ||||
|                     } else { | ||||
|                         promises.push(Promise.resolve(entry)); | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 return Promise.all(promises).then((entries) => { | ||||
|                     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) | ||||
|                     }; | ||||
|                 }); | ||||
|                 // Pass the input data to the component.
 | ||||
|                 this.jsData = { | ||||
|                     fields: this.fields, | ||||
|                     entries: entriesById, | ||||
|                     data: this.data, | ||||
|                     module: this.module, | ||||
|                     group: this.selectedGroup, | ||||
|                     gotoEntry: this.gotoEntry.bind(this) | ||||
|                 }; | ||||
|             } else if (!this.search.searching) { | ||||
|                 // Empty and no searching.
 | ||||
|                 this.canSearch = false; | ||||
| @ -435,6 +395,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|      */ | ||||
|     setGroup(groupId: number): Promise<any> { | ||||
|         this.selectedGroup = groupId; | ||||
|         this.search.page = 0; | ||||
| 
 | ||||
|         return this.fetchEntriesData().catch((message) => { | ||||
|             this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); | ||||
| @ -479,42 +440,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|         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. | ||||
|      * | ||||
|  | ||||
| @ -45,7 +45,6 @@ export class AddonModDataEditPage { | ||||
|     protected data: any; | ||||
|     protected entryId: number; | ||||
|     protected entry: any; | ||||
|     protected offlineActions = []; | ||||
|     protected fields = {}; | ||||
|     protected fieldsArray = []; | ||||
|     protected siteId: string; | ||||
| @ -145,31 +144,14 @@ export class AddonModDataEditPage { | ||||
|                 }); | ||||
|             } | ||||
|         }).then(() => { | ||||
|             return this.dataOffline.getEntryActions(this.data.id, this.entryId); | ||||
|         }).then((actions) => { | ||||
|             this.offlineActions = actions; | ||||
| 
 | ||||
|             return this.dataProvider.getFields(this.data.id); | ||||
|         }).then((fieldsData) => { | ||||
|             this.fieldsArray = fieldsData; | ||||
|             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) => { | ||||
|              if (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.entry = entry.entry; | ||||
| 
 | ||||
|             this.editFormRender = this.displayEditFields(); | ||||
|         }).catch((message) => { | ||||
|  | ||||
| @ -9,7 +9,7 @@ | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="entryLoaded && (isPullingToRefresh || !renderingEntry && !loadingRating && !loadingComments)"> | ||||
|         <!-- 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> | ||||
|             {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} | ||||
|         </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-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> | ||||
|         </ion-item> | ||||
| 
 | ||||
|  | ||||
| @ -23,7 +23,6 @@ import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreRatingInfo } from '@core/rating/providers/rating'; | ||||
| import { AddonModDataProvider } from '../../providers/data'; | ||||
| import { AddonModDataHelperProvider } from '../../providers/helper'; | ||||
| import { AddonModDataOfflineProvider } from '../../providers/offline'; | ||||
| import { AddonModDataSyncProvider } from '../../providers/sync'; | ||||
| import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; | ||||
| 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 entryChangedObserver: any; // It will observe the changed entry event.
 | ||||
|     protected fields = {}; | ||||
|     protected fieldsArray = []; | ||||
| 
 | ||||
|     title = ''; | ||||
|     moduleName = 'data'; | ||||
| @ -56,8 +56,6 @@ export class AddonModDataEntryPage implements OnDestroy { | ||||
|     loadingRating = false; | ||||
|     selectedGroup = 0; | ||||
|     entry: any; | ||||
|     offlineActions = []; | ||||
|     hasOffline = false; | ||||
|     previousOffset: number; | ||||
|     nextOffset: number; | ||||
|     access: any; | ||||
| @ -74,7 +72,7 @@ export class AddonModDataEntryPage implements OnDestroy { | ||||
|     constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, | ||||
|             protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, | ||||
|             protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, | ||||
|             protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider, | ||||
|             protected dataHelper: AddonModDataHelperProvider, | ||||
|             sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected eventsProvider: CoreEventsProvider, | ||||
|             private cdr: ChangeDetectorRef) { | ||||
|         this.module = params.get('module') || {}; | ||||
| @ -131,16 +129,19 @@ export class AddonModDataEntryPage implements OnDestroy { | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected fetchEntryData(refresh?: boolean, isPtr?: boolean): Promise<any> { | ||||
|         let fieldsArray; | ||||
| 
 | ||||
|         this.isPullingToRefresh = isPtr; | ||||
| 
 | ||||
|         return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { | ||||
|             this.title = data.name || this.title; | ||||
|             this.data = data; | ||||
| 
 | ||||
|             return this.setEntryIdFromOffset(data.id, this.offset, this.selectedGroup).then(() => { | ||||
|                 return this.dataProvider.getDatabaseAccessInformation(data.id); | ||||
|             return this.dataProvider.getFields(this.data.id).then((fieldsData) => { | ||||
|                 this.fields = this.utils.arrayToObject(fieldsData, 'id'); | ||||
|                 this.fieldsArray = fieldsData; | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             return this.setEntryFromOffset().then(() => { | ||||
|                 return this.dataProvider.getDatabaseAccessInformation(this.data.id); | ||||
|             }); | ||||
|         }).then((accessData) => { | ||||
|             this.access = accessData; | ||||
| @ -155,35 +156,13 @@ export class AddonModDataEntryPage implements OnDestroy { | ||||
|                         this.selectedGroup = groupInfo.groups[0].id; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 return this.dataOffline.getEntryActions(this.data.id, this.entryId); | ||||
|             }); | ||||
|         }).then((actions) => { | ||||
|             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; | ||||
| 
 | ||||
|         }).then(() => { | ||||
|             const actions = this.dataHelper.getActions(this.data, this.access, this.entry); | ||||
| 
 | ||||
|             const templte = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', fieldsArray); | ||||
|             this.entryHtml = this.dataHelper.displayShowFields(templte, fieldsArray, this.entry, this.offset, 'show', actions); | ||||
|             const template = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', this.fieldsArray); | ||||
|             this.entryHtml = this.dataHelper.displayShowFields(template, this.fieldsArray, this.entry, this.offset, 'show', | ||||
|                     actions); | ||||
|             this.showComments = actions.comments; | ||||
| 
 | ||||
|             const entries = {}; | ||||
| @ -193,7 +172,9 @@ export class AddonModDataEntryPage implements OnDestroy { | ||||
|             this.jsData = { | ||||
|                 fields: this.fields, | ||||
|                 entries: entries, | ||||
|                 data: this.data | ||||
|                 data: this.data, | ||||
|                 module: this.module, | ||||
|                 group: this.selectedGroup | ||||
|             }; | ||||
|         }).catch((message) => { | ||||
|             if (!refresh) { | ||||
| @ -266,7 +247,7 @@ export class AddonModDataEntryPage implements OnDestroy { | ||||
|      */ | ||||
|     setGroup(groupId: number): Promise<any> { | ||||
|         this.selectedGroup = groupId; | ||||
|         this.offset = 0; | ||||
|         this.offset = null; | ||||
|         this.entry = null; | ||||
|         this.entryId = null; | ||||
|         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. | ||||
|      */ | ||||
|     protected setEntryIdFromOffset(dataId: number, offset?: number, groupId?: number): Promise<any> { | ||||
|         if (typeof offset != 'number') { | ||||
|     protected setEntryFromOffset(): Promise<any> { | ||||
|         const emptyOffset = typeof this.offset != 'number'; | ||||
| 
 | ||||
|         if (emptyOffset && typeof this.entryId == 'number') { | ||||
|             // Entry id passed as navigation parameter instead of the offset.
 | ||||
|             // We don't display next/previous buttons in this case.
 | ||||
|             this.nextOffset = 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 page = Math.floor(offset / perPage); | ||||
|         const pageOffset = offset % perPage; | ||||
|         const page = !emptyOffset && this.offset >= 0 ? Math.floor(this.offset / perPage) : 0; | ||||
| 
 | ||||
|         return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page, perPage).then((entries) => { | ||||
|             if (!entries || !entries.entries || !entries.entries.length || pageOffset >= entries.entries.length) { | ||||
|                 return Promise.reject(null); | ||||
|         return this.dataHelper.fetchEntries(this.data, this.fieldsArray, this.selectedGroup, undefined, undefined, '0', 'DESC', | ||||
|                 page, perPage).then((entries) => { | ||||
| 
 | ||||
|             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.previousOffset = offset > 0 ? offset - 1 : null; | ||||
|             if (pageOffset + 1 < entries.entries.length) { | ||||
|             this.entry = pageEntries[pageIndex]; | ||||
|             this.entryId = this.entry.id; | ||||
| 
 | ||||
|             this.previousOffset = page > 0 || pageIndex > 0 ? this.offset - 1 : null; | ||||
| 
 | ||||
|             let promise; | ||||
| 
 | ||||
|             if (pageIndex + 1 < pageEntries.length) { | ||||
|                 // Not the last entry on the page;
 | ||||
|                 this.nextOffset = offset + 1; | ||||
|             } else if (entries.entries.length < perPage) { | ||||
|                 this.nextOffset = this.offset + 1; | ||||
|             } else if (pageEntries.length < perPage) { | ||||
|                 // Last entry of the last page.
 | ||||
|                 this.nextOffset = null; | ||||
|             } else { | ||||
|                 // Last entry of the page, check if there are more pages.
 | ||||
|                 return this.dataProvider.getEntries(dataId, groupId, undefined, undefined, page + 1, perPage).then((entries) => { | ||||
|                     this.nextOffset = entries && entries.entries && entries.entries.length > 0 ? offset + 1 : null; | ||||
|                 promise = this.dataProvider.getEntries(this.data.id, this.selectedGroup, '0', 'DESC', page + 1, perPage) | ||||
|                         .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 { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||
| import { AddonModDataProvider } from './data'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { AddonModDataHelperProvider } from './helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Content links handler for database approve/disapprove entry. | ||||
| @ -30,29 +28,10 @@ export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase | ||||
|     featureName = 'CoreCourseModuleDelegate_AddonModData'; | ||||
|     pattern = /\/mod\/data\/view\.php.*([\?\&](d|approve|disapprove)=\d+)/; | ||||
| 
 | ||||
|     constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, | ||||
|             private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider) { | ||||
|     constructor(private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider) { | ||||
|         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). | ||||
|      * | ||||
| @ -66,34 +45,11 @@ export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase | ||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||
|         return [{ | ||||
|             action: (siteId, navCtrl?): void => { | ||||
|                 const modal = this.domUtils.showModalLoading(), | ||||
|                     dataId = parseInt(params.d, 10), | ||||
|                 const dataId = parseInt(params.d, 10), | ||||
|                     entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10), | ||||
|                     approve = parseInt(params.approve, 10) ? true : false; | ||||
| 
 | ||||
|                 this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => { | ||||
|                     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(); | ||||
|                 }); | ||||
|                 this.dataHelper.approveOrDisapproveEntry(dataId, entryId, approve, courseId, siteId); | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
|  | ||||
| @ -21,6 +21,67 @@ import { CoreFilepoolProvider } from '@providers/filepool'; | ||||
| import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; | ||||
| import { AddonModDataOfflineProvider } from './offline'; | ||||
| 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. | ||||
| @ -49,13 +110,13 @@ export class AddonModDataProvider { | ||||
|      * @param   {number}  courseId        Course ID. | ||||
|      * @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   {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   {boolean} [forceOffline]  Force editing entry in offline. | ||||
|      * @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, | ||||
|             forceOffline: boolean = false): Promise<any> { | ||||
|     addEntry(dataId: number, entryId: number, courseId: number, contents: AddonModDataSubfieldData[], groupId: number = 0, | ||||
|             fields: any, siteId?: string, forceOffline: boolean = false): Promise<any> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Convenience function to store a data to be synchronized later.
 | ||||
| @ -76,6 +137,8 @@ export class AddonModDataProvider { | ||||
|                     fieldnotifications: notifications | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return storeOffline(); | ||||
|         } | ||||
| 
 | ||||
|         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. | ||||
|      * | ||||
|      * @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   {string} [siteId]   Site ID. If not defined, current site. | ||||
|      * @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) => { | ||||
|             const params = { | ||||
|                     databaseid: dataId, | ||||
| @ -184,7 +247,7 @@ export class AddonModDataProvider { | ||||
|      * @param   {any}    contents  The contents data of the fields. | ||||
|      * @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 = [], | ||||
|             contentsIndexed = {}; | ||||
| 
 | ||||
| @ -289,13 +352,13 @@ export class AddonModDataProvider { | ||||
|      * @param   {number}  dataId          Database ID. | ||||
|      * @param   {number}  entryId         Entry 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   {string}  [siteId]        Site ID. If not defined, current site. | ||||
|      * @param   {boolean} forceOffline    Force editing entry in offline. | ||||
|      * @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> { | ||||
|         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. | ||||
|      * | ||||
|      * @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. | ||||
|      * @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) => { | ||||
|             const params = { | ||||
|                     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}   [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. | ||||
|      * @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', | ||||
|             perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, | ||||
|             siteId?: string): Promise<any> { | ||||
|             siteId?: string): Promise<AddonModDataEntry[]> { | ||||
|         siteId = siteId || this.sitesProvider.getCurrentSiteId(); | ||||
| 
 | ||||
|         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  {number}    page            Page of records to return. | ||||
|      * @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, | ||||
|             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) | ||||
|                 .then((result) => { | ||||
|             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}   [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. | ||||
|      * @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, | ||||
|             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) => { | ||||
|             // Always use sort and order params to improve cache usage (entries are identified by params).
 | ||||
|             const params = { | ||||
| @ -622,7 +685,13 @@ export class AddonModDataProvider { | ||||
|                 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   {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. | ||||
|      * @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) => { | ||||
|             const params = { | ||||
|                     entryid: entryId, | ||||
| @ -671,7 +741,11 @@ export class AddonModDataProvider { | ||||
|                 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} [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 {any}    [advSearch]        Advanced search data. | ||||
|      * @param {any[]}  [advSearch]        Advanced search data. | ||||
|      * @param {string} [sort]             Sort by this field. | ||||
|      * @param {string} [order]            The direction of the sorting. | ||||
|      * @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 {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, | ||||
|             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) => { | ||||
|             const params = { | ||||
|                     databaseid: dataId, | ||||
| @ -911,7 +985,13 @@ export class AddonModDataProvider { | ||||
|                 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.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; | ||||
| import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; | ||||
| import { AddonModDataProvider } from './data'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { AddonModDataHelperProvider } from './helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Content links handler for database delete entry. | ||||
| @ -31,30 +28,10 @@ export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase { | ||||
|     featureName = 'CoreCourseModuleDelegate_AddonModData'; | ||||
|     pattern = /\/mod\/data\/view\.php.*([\?\&](d|delete)=\d+)/; | ||||
| 
 | ||||
|     constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, | ||||
|             private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider, | ||||
|             private translate: TranslateService) { | ||||
|     constructor(private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider) { | ||||
|         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). | ||||
|      * | ||||
| @ -68,38 +45,10 @@ export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase { | ||||
|             CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> { | ||||
|         return [{ | ||||
|             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(() => { | ||||
|                     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.
 | ||||
|                 }); | ||||
|                 this.dataHelper.showDeleteEntryModal(dataId, entryId, courseId); | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
|  | ||||
| @ -14,12 +14,18 @@ | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | ||||
| import { AddonModDataFieldsDelegate } from './fields-delegate'; | ||||
| import { AddonModDataOfflineProvider } from './offline'; | ||||
| import { AddonModDataProvider } from './data'; | ||||
| import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; | ||||
| 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. | ||||
| @ -30,20 +36,26 @@ export class AddonModDataHelperProvider { | ||||
|     constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider, | ||||
|         private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, | ||||
|         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. | ||||
|      * | ||||
|      * @param  {any} record         Entry to modify. | ||||
|      * @param  {any} offlineActions Offline data with the actions done. | ||||
|      * @param  {any} fields         Entry defined fields indexed by fieldid. | ||||
|      * @return {any}                Modified entry. | ||||
|      * @param {AddonModDataEntry} record Entry to modify. | ||||
|      * @param {AddonModDataOfflineAction[]} offlineActions Offline data with the actions done. | ||||
|      * @param {any[]} fields Entry defined fields indexed by fieldid. | ||||
|      * @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  = []; | ||||
| 
 | ||||
|         offlineActions.forEach((action) => { | ||||
|             record.timemodified = action.timemodified; | ||||
|             record.hasOffline = true; | ||||
| 
 | ||||
|             switch (action.action) { | ||||
|                 case 'approve': | ||||
|                     record.approved = true; | ||||
| @ -56,6 +68,8 @@ export class AddonModDataHelperProvider { | ||||
|                     break; | ||||
|                 case 'add': | ||||
|                 case 'edit': | ||||
|                     record.groupid = action.groupid; | ||||
| 
 | ||||
|                     const offlineContents = {}; | ||||
| 
 | ||||
|                     action.fields.forEach((offlineContent) => { | ||||
| @ -77,10 +91,12 @@ export class AddonModDataHelperProvider { | ||||
|                             promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => { | ||||
|                                 record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], | ||||
|                                         offlineContents[field.id], offlineFiles); | ||||
|                                 record.contents[field.id].fieldid = field.id; | ||||
|                             })); | ||||
|                         } else { | ||||
|                             record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], | ||||
|                                     offlineContents[field.id]); | ||||
|                             record.contents[field.id].fieldid = field.id; | ||||
|                         } | ||||
|                     }); | ||||
|                     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. | ||||
|      * | ||||
|      * @param {string} template   Template HMTL. | ||||
|      * @param {any[]}  fields     Fields that defines every content in the entry. | ||||
|      * @param {any}    entry      Entry. | ||||
|      * @param {number} offset     Entry offset. | ||||
|      * @param {string} mode       Mode list or show. | ||||
|      * @param {any}    actions    Actions that can be performed to the record. | ||||
|      * @return {string}           Generated HTML. | ||||
|      * @param {string} template Template HMTL. | ||||
|      * @param {any[]} fields Fields that defines every content in the entry. | ||||
|      * @param {any} entry Entry. | ||||
|      * @param {number} offset Entry offset. | ||||
|      * @param {string} mode Mode list or show. | ||||
|      * @param {AddonModDataOfflineAction[]} actions Actions that can be performed to the record. | ||||
|      * @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) { | ||||
|             return ''; | ||||
|         } | ||||
| @ -135,8 +192,8 @@ export class AddonModDataHelperProvider { | ||||
|                 } else if (action == 'approvalstatus') { | ||||
|                     render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')); | ||||
|                 } else { | ||||
|                     render = '<addon-mod-data-action action="' + action + '" [entry]="entries[' + entry.id + | ||||
|                                 ']" mode="' + mode + '" [database]="data" [offset]="' + offset + '"></addon-mod-data-action>'; | ||||
|                     render = '<addon-mod-data-action action="' + action + '" [entry]="entries[' + entry.id + ']" mode="' + mode + | ||||
|                     '" [database]="data" [module]="module" [offset]="' + offset + '" [group]="group" ></addon-mod-data-action>'; | ||||
|                 } | ||||
|                 template = template.replace(replace, render); | ||||
|             } else { | ||||
| @ -147,6 +204,153 @@ export class AddonModDataHelperProvider { | ||||
|         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. | ||||
|      * | ||||
| @ -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. | ||||
|      * | ||||
| @ -256,17 +478,17 @@ export class AddonModDataHelperProvider { | ||||
|      * Retrieve the entered data in the edit form. | ||||
|      * 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  {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}  entryId      Entry Id. | ||||
|      * @param  {any}  entryContents   Original entry contents indexed by field id. | ||||
|      * @param  {boolean} offline      True to prepare the data for an offline uploading, false otherwise. | ||||
|      * @param  {string}  [siteId]     Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}         That contains object with the answers. | ||||
|      * @param {any} inputData Array with the entered form values. | ||||
|      * @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} entryId Entry Id. | ||||
|      * @param {AddonModDataEntryFields} entryContents Original entry contents. | ||||
|      * @param {boolean} offline True to prepare the data for an offline uploading, false otherwise. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>} That contains object with the answers. | ||||
|      */ | ||||
|     getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: any, offline: boolean = false, | ||||
|             siteId?: string): Promise<any> { | ||||
|     getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: AddonModDataEntryFields, | ||||
|             offline: boolean = false, siteId?: string): Promise<any> { | ||||
|         if (!inputData) { | ||||
|             return Promise.resolve({}); | ||||
|         } | ||||
| @ -322,13 +544,13 @@ export class AddonModDataHelperProvider { | ||||
|     /** | ||||
|      * Retrieve the temp files to be updated. | ||||
|      * | ||||
|      * @param  {any}     inputData    Array with the entered form values. | ||||
|      * @param  {Array}   fields       Fields that defines every content in the entry. | ||||
|      * @param  {number}  [dataId]     Database Id. If set, fils will be uploaded and itemId set. | ||||
|      * @param  {any}   entryContents  Original entry contents indexed by field id. | ||||
|      * @return {Promise<any>}         That contains object with the files. | ||||
|      * @param {any} inputData Array with the entered form values. | ||||
|      * @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 {AddonModDataEntryFields} entryContents Original entry contents indexed by field id. | ||||
|      * @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) { | ||||
|             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. | ||||
|      * | ||||
| @ -403,13 +586,13 @@ export class AddonModDataHelperProvider { | ||||
|     /** | ||||
|      * Check if data has been changed by the user. | ||||
|      * | ||||
|      * @param  {any}    inputData     Array with the entered form values. | ||||
|      * @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  {any}    entryContents Original entry contents indexed by field id. | ||||
|      * @return {Promise<boolean>}     True if changed, false if not. | ||||
|      * @param {any} inputData Object with the entered form values. | ||||
|      * @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 {AddonModDataEntryFields} entryContents Original entry contents indexed by field id. | ||||
|      * @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) => { | ||||
|             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 | ||||
|      * to be submitted later. | ||||
|  | ||||
| @ -16,9 +16,24 @@ import { Injectable } from '@angular/core'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreFileProvider } from '@providers/file'; | ||||
| import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; | ||||
| 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. | ||||
| @ -87,7 +102,8 @@ export class AddonModDataOfflineProvider { | ||||
|     }; | ||||
| 
 | ||||
|     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.sitesProvider.registerSiteSchema(this.siteSchema); | ||||
|     } | ||||
| @ -175,10 +191,10 @@ export class AddonModDataOfflineProvider { | ||||
|     /** | ||||
|      * Get all the stored entry data from all the databases. | ||||
|      * | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}         Promise resolved with entries. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @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 site.getDb().getAllRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE); | ||||
|         }).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  {string} [siteId]   Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}           Promise resolved with entries. | ||||
|      * @param  {number} dataId Database ID. | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @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 site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}); | ||||
|             return site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}, 'timemodified'); | ||||
|         }).then((entries) => { | ||||
|             return entries.map(this.parseRecord.bind(this)); | ||||
|         }); | ||||
| @ -208,9 +224,9 @@ export class AddonModDataOfflineProvider { | ||||
|      * @param  {number} entryId     Database entry Id. | ||||
|      * @param  {string} action      Action to be done | ||||
|      * @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 site.getDb().getRecord(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, | ||||
|                     action: action}); | ||||
| @ -225,9 +241,9 @@ export class AddonModDataOfflineProvider { | ||||
|      * @param  {number} dataId      Database ID. | ||||
|      * @param  {number} entryId     Database entry Id. | ||||
|      * @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 site.getDb().getRecords(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId}); | ||||
|         }).then((entries) => { | ||||
| @ -243,11 +259,10 @@ export class AddonModDataOfflineProvider { | ||||
|      * @return {Promise<any>}          Promise resolved with boolean: true if has offline answers, false otherwise. | ||||
|      */ | ||||
|     hasOfflineData(dataId: number, siteId?: string): Promise<any> { | ||||
|         return this.getDatabaseEntries(dataId, siteId).then((entries) => { | ||||
|             return !!entries.length; | ||||
|         }).catch(() => { | ||||
|             // No offline data found, return false.
 | ||||
|             return false; | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             return this.utils.promiseWorks( | ||||
|                 site.getDb().recordExists(AddonModDataOfflineProvider.DATA_ENTRY_TABLE, {dataid: dataId}) | ||||
|             ); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -286,10 +301,10 @@ export class AddonModDataOfflineProvider { | ||||
|     /** | ||||
|      * Parse "fields" of an offline record. | ||||
|      * | ||||
|      * @param  {any} record Record object | ||||
|      * @return {any}        Record object with columns parsed. | ||||
|      * @param {any} record Record object | ||||
|      * @return {AddonModDataOfflineAction} Record object with columns parsed. | ||||
|      */ | ||||
|     protected parseRecord(record: any): any { | ||||
|     protected parseRecord(record: any): AddonModDataOfflineAction { | ||||
|         record.fields = this.textUtils.parseJSON(record.fields); | ||||
| 
 | ||||
|         return record; | ||||
| @ -308,8 +323,8 @@ export class AddonModDataOfflineProvider { | ||||
|      * @param  {string} [siteId]        Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}           Promise resolved if stored, rejected if failure. | ||||
|      */ | ||||
|     saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, fields?: any[], | ||||
|             timemodified?: number, siteId?: string): Promise<any> { | ||||
|     saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, | ||||
|             fields?: AddonModDataSubfieldData[], timemodified?: number, siteId?: string): Promise<any> { | ||||
| 
 | ||||
|         return this.sitesProvider.getSite(siteId).then((site) => { | ||||
|             timemodified = timemodified || new Date().getTime(); | ||||
|  | ||||
| @ -25,7 +25,7 @@ import { CoreCommentsProvider } from '@core/comments/providers/comments'; | ||||
| import { CoreCourseProvider } from '@core/course/providers/course'; | ||||
| import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; | ||||
| import { CoreRatingProvider } from '@core/rating/providers/rating'; | ||||
| import { AddonModDataProvider } from './data'; | ||||
| import { AddonModDataProvider, AddonModDataEntry } from './data'; | ||||
| import { AddonModDataSyncProvider } from './sync'; | ||||
| 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} [ignoreCache]  True if it should ignore cached data (it will always fail in offline or server down). | ||||
|      * @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, | ||||
|             siteId?: string): Promise<any> { | ||||
|             siteId?: string): Promise<AddonModDataEntry[]> { | ||||
|         const promises = groups.map((group) => { | ||||
|             return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, | ||||
|                 siteId); | ||||
| @ -139,14 +139,14 @@ export class AddonModDataPrefetchHandler extends CoreCourseActivityPrefetchHandl | ||||
|     /** | ||||
|      * Returns the file contained in the entries. | ||||
|      * | ||||
|      * @param  {any[]} entries  List of entries to get files from. | ||||
|      * @return {any[]}          List of files. | ||||
|      * @param {AddonModDataEntry[]} entries List of entries to get files from. | ||||
|      * @return {any[]} List of files. | ||||
|      */ | ||||
|     protected getEntriesFiles(entries: any[]): any[] { | ||||
|     protected getEntriesFiles(entries: AddonModDataEntry[]): any[] { | ||||
|         let files = []; | ||||
| 
 | ||||
|         entries.forEach((entry) => { | ||||
|             entry.contents.forEach((content) => { | ||||
|             this.utils.objectToArray(entry.contents).forEach((content) => { | ||||
|                 files = files.concat(content.files); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreTimeUtilsProvider } from '@providers/utils/time'; | ||||
| import { AddonModDataOfflineProvider } from './offline'; | ||||
| import { AddonModDataOfflineProvider, AddonModDataOfflineAction } from './offline'; | ||||
| import { AddonModDataProvider } from './data'; | ||||
| import { AddonModDataHelperProvider } from './helper'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| @ -174,7 +174,7 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | ||||
|                 // No offline data found, return empty object.
 | ||||
|                 return []; | ||||
|             }); | ||||
|         }).then((offlineActions) => { | ||||
|         }).then((offlineActions: AddonModDataOfflineAction[]) => { | ||||
|             if (!offlineActions.length) { | ||||
|                 // Nothing to sync.
 | ||||
|                 return; | ||||
| @ -226,35 +226,41 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | ||||
|     /** | ||||
|      * Synchronize an entry. | ||||
|      * | ||||
|      * @param  {any} data          Database. | ||||
|      * @param  {any} entryActions  Entry actions. | ||||
|      * @param  {any} result        Object with the result of the sync. | ||||
|      * @param  {string} [siteId]   Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}      Promise resolved if success, rejected otherwise. | ||||
|      * @param {any} data Database. | ||||
|      * @param {AddonModDataOfflineAction[]} entryActions  Entry actions. | ||||
|      * @param {any} result Object with the result of the sync. | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @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, | ||||
|             timePromise, | ||||
|             entryId = 0, | ||||
|             entryId = entryActions[0].entryid, | ||||
|             offlineId, | ||||
|             deleted = false; | ||||
| 
 | ||||
|         const promises = []; | ||||
| 
 | ||||
|         // Sort entries by timemodified.
 | ||||
|         entryActions = entryActions.sort((a: any, b: any) => a.timemodified - b.timemodified); | ||||
| 
 | ||||
|         entryId = entryActions[0].entryid; | ||||
|         const editAction = entryActions.find((action) => action.action == 'add' || action.action == 'edit'); | ||||
|         const approveAction = entryActions.find((action) => action.action == 'approve' || action.action == 'disapprove'); | ||||
|         const deleteAction = entryActions.find((action) => action.action == 'delete'); | ||||
| 
 | ||||
|         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; | ||||
|             }).catch(() => { | ||||
|                 return -1; | ||||
|             }).catch((error) => { | ||||
|                 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; | ||||
|             timePromise = Promise.resolve(0); | ||||
|         } else { | ||||
|             // New entry but the add action is missing, discard.
 | ||||
|             timePromise = Promise.resolve(-1); | ||||
|         } | ||||
| 
 | ||||
|         return timePromise.then((timemodified) => { | ||||
| @ -266,58 +272,11 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | ||||
|                 return this.dataOffline.deleteAllEntryActions(data.id, entryId, siteId); | ||||
|             } | ||||
| 
 | ||||
|             entryActions.forEach((action) => { | ||||
|                 let actionPromise; | ||||
|                 const proms = []; | ||||
| 
 | ||||
|                 entryId = action.entryid > 0 ? action.entryid : entryId; | ||||
| 
 | ||||
|                 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) { | ||||
|             if (deleteAction) { | ||||
|                 return this.dataProvider.deleteEntryOnline(entryId, siteId).then(() => { | ||||
|                     deleted = true; | ||||
|                 }).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 { | ||||
| @ -328,11 +287,79 @@ export class AddonModDataSyncProvider extends CoreSyncBaseProvider { | ||||
|                     // Delete the offline data.
 | ||||
|                     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(() => { | ||||
|             if (discardError) { | ||||
|                 // Submission was discarded, add a warning.
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user