MOBILE-3930 course: Store and display modules viewed and last
parent
d10fa5db3c
commit
b72e247f81
|
@ -1559,6 +1559,7 @@
|
||||||
"core.course.highlighted": "moodle",
|
"core.course.highlighted": "moodle",
|
||||||
"core.course.insufficientavailablequota": "local_moodlemobileapp",
|
"core.course.insufficientavailablequota": "local_moodlemobileapp",
|
||||||
"core.course.insufficientavailablespace": "local_moodlemobileapp",
|
"core.course.insufficientavailablespace": "local_moodlemobileapp",
|
||||||
|
"core.course.lastaccessedactivity": "local_moodlemobileapp",
|
||||||
"core.course.manualcompletionnotsynced": "local_moodlemobileapp",
|
"core.course.manualcompletionnotsynced": "local_moodlemobileapp",
|
||||||
"core.course.modulenotfound": "local_moodlemobileapp",
|
"core.course.modulenotfound": "local_moodlemobileapp",
|
||||||
"core.course.nocontentavailable": "local_moodlemobileapp",
|
"core.course.nocontentavailable": "local_moodlemobileapp",
|
||||||
|
|
|
@ -49,17 +49,41 @@ export class AddonBlockRecentlyAccessedItemsProvider {
|
||||||
cacheKey: this.getRecentItemsCacheKey(),
|
cacheKey: this.getRecentItemsCacheKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: AddonBlockRecentlyAccessedItemsItem[] =
|
let items: AddonBlockRecentlyAccessedItemsItem[] =
|
||||||
await site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets);
|
await site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets);
|
||||||
|
|
||||||
return await Promise.all(items.map(async (item) => {
|
const cmIds: number[] = [];
|
||||||
|
|
||||||
|
items = await Promise.all(items.map(async (item) => {
|
||||||
const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src');
|
const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src');
|
||||||
|
|
||||||
item.iconUrl = await CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
|
item.iconUrl = await CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
|
||||||
item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
|
item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
|
||||||
|
cmIds.push(item.cmid);
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Check if the viewed module should be updated for each activity.
|
||||||
|
const lastViewedMap = await CoreCourse.getCertainModulesViewed(cmIds, site.getId());
|
||||||
|
|
||||||
|
items.forEach((recentItem) => {
|
||||||
|
const timeAccess = recentItem.timeaccess * 1000;
|
||||||
|
const lastViewed = lastViewedMap[recentItem.cmid];
|
||||||
|
|
||||||
|
if (lastViewed && lastViewed.timeaccess >= timeAccess) {
|
||||||
|
return; // No need to update.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update access.
|
||||||
|
CoreCourse.storeModuleViewed(recentItem.courseid, recentItem.cmid, {
|
||||||
|
timeaccess: recentItem.timeaccess * 1000,
|
||||||
|
sectionId: lastViewed && lastViewed.sectionId,
|
||||||
|
siteId: site.getId(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -391,7 +391,7 @@ export class AddonModBookProvider {
|
||||||
async storeLastChapterViewed(id: number, chapterId: number, courseId: number, siteId?: string): Promise<void> {
|
async storeLastChapterViewed(id: number, chapterId: number, courseId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, String(courseId));
|
await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, { data: String(courseId) });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,7 +204,10 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
if (this.logAfterFetch) {
|
if (this.logAfterFetch) {
|
||||||
this.logAfterFetch = false;
|
this.logAfterFetch = false;
|
||||||
this.logView();
|
await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
|
||||||
|
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.moduleId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!refresh) {
|
if (!refresh) {
|
||||||
|
@ -402,19 +405,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
|
||||||
AddonModData.invalidateEntryData(this.database!.id, this.entryId!);
|
AddonModData.invalidateEntryData(this.database!.id, this.entryId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Log viewing the activity.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async logView(): Promise<void> {
|
|
||||||
if (!this.database || !this.database.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being destroyed.
|
* Component being destroyed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -49,6 +50,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
protected attemptId: number;
|
protected attemptId: number;
|
||||||
|
protected fetchSuccess = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
|
@ -131,6 +133,11 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
|
||||||
return attemptItem;
|
return attemptItem;
|
||||||
}).filter((itemData) => itemData); // Filter items with errors.
|
}).filter((itemData) => itemData); // Filter items with errors.
|
||||||
|
|
||||||
|
if (!this.fetchSuccess) {
|
||||||
|
this.fetchSuccess = true;
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
|
}
|
||||||
} catch (message) {
|
} catch (message) {
|
||||||
// Some call failed on fetch, go back.
|
// Some call failed on fetch, go back.
|
||||||
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
|
import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
|
||||||
import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback';
|
import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays feedback attempts.
|
* Page that displays feedback attempts.
|
||||||
|
@ -37,14 +38,14 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
|
||||||
promisedAttempts: CorePromisedValue<CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource>>;
|
promisedAttempts: CorePromisedValue<AddonModFeedbackAttemptsManager>;
|
||||||
fetchFailed = false;
|
fetchFailed = false;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute) {
|
constructor(protected route: ActivatedRoute) {
|
||||||
this.promisedAttempts = new CorePromisedValue();
|
this.promisedAttempts = new CorePromisedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
get attempts(): CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> | null {
|
get attempts(): AddonModFeedbackAttemptsManager | null {
|
||||||
return this.promisedAttempts.value;
|
return this.promisedAttempts.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +96,7 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
source.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
|
source.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
|
||||||
|
|
||||||
this.promisedAttempts.resolve(new CoreListItemsManager(source, this.route.component));
|
this.promisedAttempts.resolve(new AddonModFeedbackAttemptsManager(source, this.route.component));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -181,3 +182,18 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts manager.
|
||||||
|
*/
|
||||||
|
class AddonModFeedbackAttemptsManager extends CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async logActivity(): Promise<void> {
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.getSource().COURSE_ID, this.getSource().CM_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
@ -34,6 +35,7 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
|
||||||
protected courseId!: number;
|
protected courseId!: number;
|
||||||
protected feedback?: AddonModFeedbackWSFeedback;
|
protected feedback?: AddonModFeedbackWSFeedback;
|
||||||
protected page = 0;
|
protected page = 0;
|
||||||
|
protected fetchSuccess = false;
|
||||||
|
|
||||||
selectedGroup!: number;
|
selectedGroup!: number;
|
||||||
groupInfo?: CoreGroupInfo;
|
groupInfo?: CoreGroupInfo;
|
||||||
|
@ -81,6 +83,12 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
|
||||||
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
|
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
|
||||||
|
|
||||||
await this.loadGroupUsers(this.selectedGroup);
|
await this.loadGroupUsers(this.selectedGroup);
|
||||||
|
|
||||||
|
if (!this.fetchSuccess) {
|
||||||
|
this.fetchSuccess = true;
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
|
}
|
||||||
} catch (message) {
|
} catch (message) {
|
||||||
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Opt
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
||||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||||
|
@ -118,6 +119,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
protected ratingOfflineObserver?: CoreEventObserver;
|
protected ratingOfflineObserver?: CoreEventObserver;
|
||||||
protected ratingSyncObserver?: CoreEventObserver;
|
protected ratingSyncObserver?: CoreEventObserver;
|
||||||
protected changeDiscObserver?: CoreEventObserver;
|
protected changeDiscObserver?: CoreEventObserver;
|
||||||
|
protected fetchSuccess = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Optional() protected splitView: CoreSplitViewComponent,
|
@Optional() protected splitView: CoreSplitViewComponent,
|
||||||
|
@ -547,6 +549,12 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
|
|
||||||
this.hasOfflineRatings =
|
this.hasOfflineRatings =
|
||||||
await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId);
|
await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId);
|
||||||
|
|
||||||
|
if (!this.fetchSuccess) {
|
||||||
|
this.fetchSuccess = true;
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
this.courseId && this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
||||||
import { CoreComments } from '@features/comments/services/comments';
|
import { CoreComments } from '@features/comments/services/comments';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||||
import { CoreTag } from '@features/tag/services/tag';
|
import { CoreTag } from '@features/tag/services/tag';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
@ -55,8 +56,10 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
tagsEnabled = false;
|
tagsEnabled = false;
|
||||||
commentsEnabled = false;
|
commentsEnabled = false;
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
|
cmId?: number;
|
||||||
|
|
||||||
protected entryId!: number;
|
protected entryId!: number;
|
||||||
|
protected fetchSuccess = false;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute) {}
|
constructor(protected route: ActivatedRoute) {}
|
||||||
|
|
||||||
|
@ -72,15 +75,17 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
||||||
|
|
||||||
if (routeData.swipeEnabled ?? true) {
|
if (routeData.swipeEnabled ?? true) {
|
||||||
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModGlossaryEntriesSource,
|
AddonModGlossaryEntriesSource,
|
||||||
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
|
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
);
|
);
|
||||||
|
|
||||||
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
|
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
|
||||||
|
|
||||||
await this.entries.start();
|
await this.entries.start();
|
||||||
|
} else {
|
||||||
|
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
@ -143,6 +148,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
this.entry = result.entry;
|
this.entry = result.entry;
|
||||||
this.ratingInfo = result.ratinginfo;
|
this.ratingInfo = result.ratinginfo;
|
||||||
|
|
||||||
|
if (!this.fetchSuccess) {
|
||||||
|
this.fetchSuccess = true;
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.glossary) {
|
if (this.glossary) {
|
||||||
// Glossary already loaded, nothing else to load.
|
// Glossary already loaded, nothing else to load.
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
AddonModH5PActivityData,
|
AddonModH5PActivityData,
|
||||||
AddonModH5PActivityAttemptResults,
|
AddonModH5PActivityAttemptResults,
|
||||||
} from '../../services/h5pactivity';
|
} from '../../services/h5pactivity';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays results of an attempt.
|
* Page that displays results of an attempt.
|
||||||
|
@ -99,6 +100,9 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
|
||||||
this.h5pActivity.name,
|
this.h5pActivity.name,
|
||||||
{ attemptId: this.attemptId },
|
{ attemptId: this.attemptId },
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
|
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
AddonModH5PActivityData,
|
AddonModH5PActivityData,
|
||||||
AddonModH5PActivityUserAttempts,
|
AddonModH5PActivityUserAttempts,
|
||||||
} from '../../services/h5pactivity';
|
} from '../../services/h5pactivity';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays user attempts of a certain user.
|
* Page that displays user attempts of a certain user.
|
||||||
|
@ -101,6 +102,9 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
|
||||||
this.h5pActivity.name,
|
this.h5pActivity.name,
|
||||||
{ userId: this.userId },
|
{ userId: this.userId },
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
|
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
|
@ -92,6 +93,9 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
|
||||||
if (!this.fetchSuccess) {
|
if (!this.fetchSuccess) {
|
||||||
this.fetchSuccess = true;
|
this.fetchSuccess = true;
|
||||||
CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name));
|
CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name));
|
||||||
|
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
|
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
|
||||||
|
|
|
@ -303,7 +303,7 @@ export class AddonModImscpProvider {
|
||||||
async storeLastItemViewed(id: number, href: string, courseId: number, siteId?: string): Promise<void> {
|
async storeLastItemViewed(id: number, href: string, courseId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, String(courseId));
|
await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, { data: String(courseId) });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
} from '../../services/lesson';
|
} from '../../services/lesson';
|
||||||
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
|
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a retake made by a certain user.
|
* Page that displays a retake made by a certain user.
|
||||||
|
@ -59,6 +60,7 @@ export class AddonModLessonUserRetakePage implements OnInit {
|
||||||
protected userId?: number; // User ID to see the retakes.
|
protected userId?: number; // User ID to see the retakes.
|
||||||
protected retakeNumber?: number; // Number of the initial retake to see.
|
protected retakeNumber?: number; // Number of the initial retake to see.
|
||||||
protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
|
protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
|
||||||
|
protected fetchSuccess = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
|
@ -160,6 +162,12 @@ export class AddonModLessonUserRetakePage implements OnInit {
|
||||||
this.student.profileimageurl = user?.profileimageurl;
|
this.student.profileimageurl = user?.profileimageurl;
|
||||||
|
|
||||||
await this.setRetake(this.selectedRetake);
|
await this.setRetake(this.selectedRetake);
|
||||||
|
|
||||||
|
if (!this.fetchSuccess) {
|
||||||
|
this.fetchSuccess = true;
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true);
|
CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { makeSingleton } from '@singletons';
|
||||||
import { AddonModLtiHelper } from '../lti-helper';
|
import { AddonModLtiHelper } from '../lti-helper';
|
||||||
import { AddonModLtiIndexComponent } from '../../components/index';
|
import { AddonModLtiIndexComponent } from '../../components/index';
|
||||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
|
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to support LTI modules.
|
* Handler to support LTI modules.
|
||||||
|
@ -67,6 +68,8 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
|
||||||
action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
|
action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
|
||||||
// Launch the LTI.
|
// Launch the LTI.
|
||||||
AddonModLtiHelper.getDataAndLaunch(courseId, module);
|
AddonModLtiHelper.getDataAndLaunch(courseId, module);
|
||||||
|
|
||||||
|
CoreCourse.storeModuleViewed(courseId, module.id);
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreQuestionQuestionParsed } from '@features/question/services/question';
|
import { CoreQuestionQuestionParsed } from '@features/question/services/question';
|
||||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
import { CoreQuestionHelper } from '@features/question/services/question-helper';
|
||||||
import { IonContent, IonRefresher } from '@ionic/angular';
|
import { IonContent, IonRefresher } from '@ionic/angular';
|
||||||
|
@ -163,6 +164,9 @@ export class AddonModQuizReviewPage implements OnInit {
|
||||||
CoreUtils.ignoreErrors(
|
CoreUtils.ignoreErrors(
|
||||||
AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id, this.quiz.name),
|
AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id, this.quiz.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Store module viewed. It's done in this page because it can be reached using a link.
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.cmId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
|
||||||
|
|
|
@ -90,6 +90,8 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
|
||||||
const hide = await this.hideOpenButton(module);
|
const hide = await this.hideOpenButton(module);
|
||||||
if (!hide) {
|
if (!hide) {
|
||||||
AddonModResourceHelper.openModuleFile(module, courseId);
|
AddonModResourceHelper.openModuleFile(module, courseId);
|
||||||
|
|
||||||
|
CoreCourse.storeModuleViewed(courseId, module.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
|
|
|
@ -63,7 +63,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
|
||||||
* @param module The module object.
|
* @param module The module object.
|
||||||
* @param courseId The course ID.
|
* @param courseId The course ID.
|
||||||
*/
|
*/
|
||||||
const openUrl = async (module: CoreCourseModuleData): Promise<void> => {
|
const openUrl = async (module: CoreCourseModuleData, courseId: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (module.instance) {
|
if (module.instance) {
|
||||||
await AddonModUrl.logView(module.instance, module.name);
|
await AddonModUrl.logView(module.instance, module.name);
|
||||||
|
@ -73,6 +73,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CoreCourse.storeModuleViewed(courseId, module.id);
|
||||||
|
|
||||||
const contents = await CoreCourse.getModuleContents(module);
|
const contents = await CoreCourse.getModuleContents(module);
|
||||||
AddonModUrlHelper.open(contents[0].fileurl);
|
AddonModUrlHelper.open(contents[0].fileurl);
|
||||||
};
|
};
|
||||||
|
@ -89,7 +91,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
|
||||||
const shouldOpen = await this.shouldOpenLink(module);
|
const shouldOpen = await this.shouldOpenLink(module);
|
||||||
|
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
openUrl(module);
|
openUrl(module, courseId);
|
||||||
} else {
|
} else {
|
||||||
this.openActivityPage(module, module.course, options);
|
this.openActivityPage(module, module.course, options);
|
||||||
}
|
}
|
||||||
|
@ -101,8 +103,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
|
||||||
hidden: true, // Hide it until we calculate if it should be displayed or not.
|
hidden: true, // Hide it until we calculate if it should be displayed or not.
|
||||||
icon: 'fas-link',
|
icon: 'fas-link',
|
||||||
label: 'core.openmodinbrowser',
|
label: 'core.openmodinbrowser',
|
||||||
action: (event: Event, module: CoreCourseModuleData): void => {
|
action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
|
||||||
openUrl(module);
|
openUrl(module, courseId);
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
|
@ -160,7 +160,7 @@ export class CoreSite {
|
||||||
this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.LAST_VIEWED_TABLE, {
|
this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.LAST_VIEWED_TABLE, {
|
||||||
siteId: this.getId(),
|
siteId: this.getId(),
|
||||||
database: this.getDb(),
|
database: this.getDb(),
|
||||||
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
|
||||||
primaryKeyColumns: ['component', 'id'],
|
primaryKeyColumns: ['component', 'id'],
|
||||||
}));
|
}));
|
||||||
this.setInfo(infos);
|
this.setInfo(infos);
|
||||||
|
@ -2000,21 +2000,55 @@ export class CoreSite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get several last viewed for a certain component.
|
||||||
|
*
|
||||||
|
* @param component The component.
|
||||||
|
* @param ids IDs. If not provided or empty, return all last viewed for a component.
|
||||||
|
* @return Resolves with last viewed records, undefined if error.
|
||||||
|
*/
|
||||||
|
async getComponentLastViewed(component: string, ids: number[] = []): Promise<CoreSiteLastViewedDBRecord[] | undefined> {
|
||||||
|
try {
|
||||||
|
if (!ids.length) {
|
||||||
|
return await this.lastViewedTable.getMany({ component });
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereAndParams = SQLiteDB.getInOrEqual(ids);
|
||||||
|
|
||||||
|
whereAndParams.sql = 'id ' + whereAndParams.sql + ' AND component = ?';
|
||||||
|
whereAndParams.params.push(component);
|
||||||
|
|
||||||
|
return await this.lastViewedTable.getManyWhere({
|
||||||
|
sql: whereAndParams.sql,
|
||||||
|
sqlParams: whereAndParams.params,
|
||||||
|
js: (record) => record.component === component && ids.includes(record.id),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Not found.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a last viewed record.
|
* Store a last viewed record.
|
||||||
*
|
*
|
||||||
* @param component The component.
|
* @param component The component.
|
||||||
* @param id ID.
|
* @param id ID.
|
||||||
* @param value Last viewed item value.
|
* @param value Last viewed item value.
|
||||||
* @param data Other data.
|
* @param options Options.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async storeLastViewed(component: string, id: number, value: string | number, data?: string): Promise<void> {
|
async storeLastViewed(
|
||||||
|
component: string,
|
||||||
|
id: number,
|
||||||
|
value: string | number,
|
||||||
|
options: CoreSiteStoreLastViewedOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
await this.lastViewedTable.insert({
|
await this.lastViewedTable.insert({
|
||||||
component,
|
component,
|
||||||
id,
|
id,
|
||||||
value: String(value),
|
value: String(value),
|
||||||
data,
|
data: options.data,
|
||||||
|
timeaccess: options.timeaccess ?? Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2349,5 +2383,14 @@ export type CoreSiteLastViewedDBRecord = {
|
||||||
component: string;
|
component: string;
|
||||||
id: number;
|
id: number;
|
||||||
value: string;
|
value: string;
|
||||||
|
timeaccess: number;
|
||||||
data?: string;
|
data?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for storeLastViewed.
|
||||||
|
*/
|
||||||
|
export type CoreSiteStoreLastViewedOptions = {
|
||||||
|
data?: string; // Other data.
|
||||||
|
timeaccess?: number; // Accessed time. If not set, current time.
|
||||||
|
};
|
||||||
|
|
|
@ -442,6 +442,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchSuccess = true;
|
this.fetchSuccess = true;
|
||||||
|
CoreCourse.storeModuleViewed(this.courseId, this.module.id, { sectionId: this.module.section });
|
||||||
|
|
||||||
// Log activity now.
|
// Log activity now.
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -97,7 +97,9 @@
|
||||||
|
|
||||||
<ng-container *ngFor="let module of section.modules">
|
<ng-container *ngFor="let module of section.modules">
|
||||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
|
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
|
||||||
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions">
|
[showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
|
||||||
|
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
|
||||||
|
!viewedModules[module.id] && (!module.completiondata || module.completiondata.state === completionStatusIncomplete)">
|
||||||
</core-course-module>
|
</core-course-module>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp
|
||||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
import {
|
import {
|
||||||
CoreCourse,
|
CoreCourse,
|
||||||
|
CoreCourseModuleCompletionStatus,
|
||||||
CoreCourseProvider,
|
CoreCourseProvider,
|
||||||
} from '@features/course/services/course';
|
} from '@features/course/services/course';
|
||||||
import {
|
import {
|
||||||
|
@ -42,6 +43,7 @@ import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from
|
||||||
import { CoreBlockHelper } from '@features/block/services/block-helper';
|
import { CoreBlockHelper } from '@features/block/services/block-helper';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
|
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||||
|
@ -90,9 +92,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
highlighted?: string;
|
highlighted?: string;
|
||||||
|
lastModuleViewed?: CoreCourseViewedModulesDBRecord;
|
||||||
|
viewedModules: Record<number, boolean> = {};
|
||||||
|
completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
|
||||||
|
|
||||||
protected selectTabObserver?: CoreEventObserver;
|
protected selectTabObserver?: CoreEventObserver;
|
||||||
|
protected modViewedObserver?: CoreEventObserver;
|
||||||
protected lastCourseFormat?: string;
|
protected lastCourseFormat?: string;
|
||||||
|
protected viewedModulesInitialized = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected content: IonContent,
|
protected content: IonContent,
|
||||||
|
@ -133,6 +140,16 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.modViewedObserver = CoreEvents.on(CoreEvents.COURSE_MODULE_VIEWED, (data) => {
|
||||||
|
if (data.courseId !== this.course.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.viewedModules[data.cmId] = true;
|
||||||
|
if (!this.lastModuleViewed || data.timeaccess > this.lastModuleViewed.timeaccess) {
|
||||||
|
this.lastModuleViewed = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,8 +196,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.lastCourseFormat = this.course.format;
|
this.lastCourseFormat = this.course.format;
|
||||||
|
|
||||||
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
|
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
|
||||||
const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
|
const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
|
||||||
currentSection.highlighted = true;
|
currentSectionData.section.highlighted = true;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadCourseFormatComponent(),
|
this.loadCourseFormatComponent(),
|
||||||
|
@ -236,6 +253,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
|
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
|
||||||
const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
||||||
|
|
||||||
|
await this.initializeViewedModules();
|
||||||
|
|
||||||
if (this.selectedSection) {
|
if (this.selectedSection) {
|
||||||
const selectedSection = this.selectedSection;
|
const selectedSection = this.selectedSection;
|
||||||
// We have a selected section, but the list has changed. Search the section in the list.
|
// We have a selected section, but the list has changed. Search the section in the list.
|
||||||
|
@ -243,7 +262,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
|
||||||
if (!newSection) {
|
if (!newSection) {
|
||||||
// Section not found, calculate which one to use.
|
// Section not found, calculate which one to use.
|
||||||
newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
|
const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
|
||||||
|
newSection = currentSectionData.section;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sectionChanged(newSection);
|
this.sectionChanged(newSection);
|
||||||
|
@ -269,16 +289,60 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.loaded) {
|
if (!this.loaded) {
|
||||||
// No section specified, not found or not visible, get current section.
|
// No section specified, not found or not visible, load current section or the section with last module viewed.
|
||||||
const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
|
const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
|
||||||
|
|
||||||
|
const lastModuleViewed = this.lastModuleViewed;
|
||||||
|
let section = currentSectionData.section;
|
||||||
|
let moduleId: number | undefined;
|
||||||
|
|
||||||
|
if (!currentSectionData.forceSelected && lastModuleViewed) {
|
||||||
|
// Search the section with the last module viewed.
|
||||||
|
let lastModuleSection: CoreCourseSection | undefined;
|
||||||
|
|
||||||
|
if (lastModuleViewed.sectionId) {
|
||||||
|
lastModuleSection = sections.find(section => section.id === lastModuleViewed.sectionId);
|
||||||
|
}
|
||||||
|
if (!lastModuleSection) {
|
||||||
|
// No sectionId or section not found. Search the module.
|
||||||
|
lastModuleSection = sections.find(
|
||||||
|
section => section.modules.some(module => module.id === lastModuleViewed.cmId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
section = lastModuleSection || section;
|
||||||
|
moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined;
|
||||||
|
} else if (lastModuleViewed && currentSectionData.section.modules.some(module => module.id === lastModuleViewed.cmId)) {
|
||||||
|
// Last module viewed is inside the highlighted section.
|
||||||
|
moduleId = lastModuleViewed.cmId;
|
||||||
|
}
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.sectionChanged(section);
|
this.sectionChanged(section, moduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize viewed modules.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async initializeViewedModules(): Promise<void> {
|
||||||
|
if (this.viewedModulesInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewedModules = await CoreCourse.getViewedModules(this.course.id);
|
||||||
|
|
||||||
|
this.viewedModulesInitialized = true;
|
||||||
|
this.lastModuleViewed = viewedModules[0];
|
||||||
|
viewedModules.forEach(entry => {
|
||||||
|
this.viewedModules[entry.cmId] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the course index modal.
|
* Display the course index modal.
|
||||||
*/
|
*/
|
||||||
|
@ -345,8 +409,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
* Function called when selected section changes.
|
* Function called when selected section changes.
|
||||||
*
|
*
|
||||||
* @param newSection The new selected section.
|
* @param newSection The new selected section.
|
||||||
|
* @param moduleId The module to scroll to.
|
||||||
*/
|
*/
|
||||||
sectionChanged(newSection: CoreCourseSection): void {
|
sectionChanged(newSection: CoreCourseSection, moduleId?: number): void {
|
||||||
const previousValue = this.selectedSection;
|
const previousValue = this.selectedSection;
|
||||||
this.selectedSection = newSection;
|
this.selectedSection = newSection;
|
||||||
this.data.section = this.selectedSection;
|
this.data.section = this.selectedSection;
|
||||||
|
@ -377,12 +442,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.showMoreActivities();
|
this.showMoreActivities();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.moduleId && previousValue === undefined) {
|
// Scroll to module if needed. Give more priority to the input.
|
||||||
|
moduleId = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
|
||||||
|
if (moduleId) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
CoreDomUtils.scrollToElementBySelector(
|
CoreDomUtils.scrollToElementBySelector(
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
this.content,
|
this.content,
|
||||||
'#core-course-module-' + this.moduleId,
|
'#core-course-module-' + moduleId,
|
||||||
);
|
);
|
||||||
}, 200);
|
}, 200);
|
||||||
} else {
|
} else {
|
||||||
|
@ -502,7 +569,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.selectTabObserver && this.selectTabObserver.off();
|
this.selectTabObserver?.off();
|
||||||
|
this.modViewedObserver?.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -68,11 +68,11 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
||||||
completionEnabled = this.course.showcompletionconditions;
|
completionEnabled = this.course.showcompletionconditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
|
const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
|
||||||
|
|
||||||
if (this.selectedId === undefined) {
|
if (this.selectedId === undefined) {
|
||||||
// Highlight current section if none is selected.
|
// Highlight current section if none is selected.
|
||||||
this.selectedId = currentSection.id;
|
this.selectedId = currentSectionData.section.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone sections to add information.
|
// Clone sections to add information.
|
||||||
|
@ -104,7 +104,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
|
||||||
name: section.name,
|
name: section.name,
|
||||||
availabilityinfo: !!section.availabilityinfo,
|
availabilityinfo: !!section.availabilityinfo,
|
||||||
expanded: section.id === this.selectedId,
|
expanded: section.id === this.selectedId,
|
||||||
highlighted: currentSection?.id === section.id,
|
highlighted: currentSectionData.section.id === section.id,
|
||||||
hasVisibleModules: modules.length > 0,
|
hasVisibleModules: modules.length > 0,
|
||||||
modules: modules,
|
modules: modules,
|
||||||
};
|
};
|
||||||
|
|
|
@ -99,6 +99,11 @@
|
||||||
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<div class="core-course-last-module-viewed" *ngIf="isLastViewed">
|
||||||
|
<ion-icon name="fas-eye" aria-hidden="true"></ion-icon>
|
||||||
|
{{ 'core.course.lastaccessedactivity' | translate }}
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Loading. -->
|
<!-- Loading. -->
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
:host {
|
:host {
|
||||||
--horizontal-margin: 10px;
|
--horizontal-margin: 10px;
|
||||||
--vertical-margin: 10px;
|
--vertical-margin: 10px;
|
||||||
|
--core-course-module-not-viewed-border-color: var(--gray-500);
|
||||||
|
|
||||||
ion-card {
|
ion-card {
|
||||||
margin: var(--vertical-margin) var(--horizontal-margin);
|
margin: var(--vertical-margin) var(--horizontal-margin);
|
||||||
|
@ -91,4 +92,18 @@
|
||||||
.core-course-module-info ::ng-deep core-course-module-completion .core-module-automatic-completion-conditions .completioninfo.completion_complete {
|
.core-course-module-info ::ng-deep core-course-module-completion .core-module-automatic-completion-conditions .completioninfo.completion_complete {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.core-course-module-not-viewed {
|
||||||
|
--ion-card-border-color: var(--core-course-module-not-viewed-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-course-last-module-viewed {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: var(--subdued-text-color);
|
||||||
|
border-top: 1px solid var(--stroke);
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
||||||
@Input() showActivityDates = false; // Whether to show activity dates.
|
@Input() showActivityDates = false; // Whether to show activity dates.
|
||||||
@Input() showCompletionConditions = false; // Whether to show activity completion conditions.
|
@Input() showCompletionConditions = false; // Whether to show activity completion conditions.
|
||||||
@Input() showLegacyCompletion?: boolean; // Whether to show module completion in the old format.
|
@Input() showLegacyCompletion?: boolean; // Whether to show module completion in the old format.
|
||||||
|
@Input() isLastViewed = false; // Whether it's the last module viewed in a course.
|
||||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
|
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
|
||||||
|
|
||||||
modNameTranslated = '';
|
modNameTranslated = '';
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
|
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatHandler } from '@features/course/services/format-delegate';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
import { CoreCourseWSSection } from '@features/course/services/course';
|
import { CoreCourseWSSection } from '@features/course/services/course';
|
||||||
|
@ -41,12 +41,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
|
async getCurrentSection(
|
||||||
|
course: CoreCourseAnyCourseData,
|
||||||
|
sections: CoreCourseSection[],
|
||||||
|
): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
|
||||||
const now = CoreTimeUtils.timestamp();
|
const now = CoreTimeUtils.timestamp();
|
||||||
|
|
||||||
if ((course.startdate && now < course.startdate) || (course.enddate && now > course.enddate)) {
|
if ((course.startdate && now < course.startdate) || (course.enddate && now > course.enddate)) {
|
||||||
// Course hasn't started yet or it has ended already. Return all sections.
|
// Course hasn't started yet or it has ended already. Return all sections.
|
||||||
return sections[0];
|
return {
|
||||||
|
section: sections[0],
|
||||||
|
forceSelected: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < sections.length; i++) {
|
for (let i = 0; i < sections.length; i++) {
|
||||||
|
@ -57,12 +63,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
|
||||||
|
|
||||||
const dates = this.getSectionDates(section, course.startdate || 0);
|
const dates = this.getSectionDates(section, course.startdate || 0);
|
||||||
if (now >= dates.start && now < dates.end) {
|
if (now >= dates.start && now < dates.end) {
|
||||||
return section;
|
return {
|
||||||
|
section,
|
||||||
|
forceSelected: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The section wasn't found, return all sections.
|
// The section wasn't found, return all sections.
|
||||||
return sections[0];
|
return {
|
||||||
|
section: sections[0],
|
||||||
|
forceSelected: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"highlighted": "Highlighted",
|
"highlighted": "Highlighted",
|
||||||
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
|
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
|
||||||
"insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
|
"insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
|
||||||
|
"lastaccessedactivity": "Last accessed activity",
|
||||||
"manualcompletionnotsynced": "Manual completion not synchronised.",
|
"manualcompletionnotsynced": "Manual completion not synchronised.",
|
||||||
"modulenotfound": "Resource or activity not found, please make sure you're online and it's still available.",
|
"modulenotfound": "Resource or activity not found, please make sure you're online and it's still available.",
|
||||||
"nocontentavailable": "No content available at the moment.",
|
"nocontentavailable": "No content available at the moment.",
|
||||||
|
|
|
@ -2022,6 +2022,7 @@ export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completi
|
||||||
isStealth?: boolean;
|
isStealth?: boolean;
|
||||||
handlerData?: CoreCourseModuleHandlerData;
|
handlerData?: CoreCourseModuleHandlerData;
|
||||||
completiondata?: CoreCourseModuleCompletionData;
|
completiondata?: CoreCourseModuleCompletionData;
|
||||||
|
section: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,7 +26,9 @@ import { CoreConstants } from '@/core/constants';
|
||||||
import { makeSingleton, Platform, Translate } from '@singletons';
|
import { makeSingleton, Platform, Translate } from '@singletons';
|
||||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||||
|
|
||||||
import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course';
|
import {
|
||||||
|
CoreCourseStatusDBRecord, CoreCourseViewedModulesDBRecord, COURSE_STATUS_TABLE, COURSE_VIEWED_MODULES_TABLE ,
|
||||||
|
} from './database/course';
|
||||||
import { CoreCourseOffline } from './course-offline';
|
import { CoreCourseOffline } from './course-offline';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import {
|
import {
|
||||||
|
@ -49,6 +51,7 @@ import { lazyMap, LazyMap } from '@/core/utils/lazy-map';
|
||||||
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
|
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
|
||||||
import { CoreDatabaseTable } from '@classes/database/database-table';
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
||||||
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
||||||
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmCourse:';
|
const ROOT_CACHE_KEY = 'mmCourse:';
|
||||||
|
|
||||||
|
@ -140,6 +143,7 @@ export class CoreCourseProvider {
|
||||||
|
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
protected statusTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseStatusDBRecord>>>;
|
protected statusTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseStatusDBRecord>>>;
|
||||||
|
protected viewedModulesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseViewedModulesDBRecord, 'courseId' | 'cmId'>>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.logger = CoreLogger.getInstance('CoreCourseProvider');
|
this.logger = CoreLogger.getInstance('CoreCourseProvider');
|
||||||
|
@ -152,6 +156,17 @@ export class CoreCourseProvider {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.viewedModulesTables = lazyMap(
|
||||||
|
siteId => asyncInstance(
|
||||||
|
() => CoreSites.getSiteTable<CoreCourseViewedModulesDBRecord, 'courseId' | 'cmId'>(COURSE_VIEWED_MODULES_TABLE, {
|
||||||
|
siteId,
|
||||||
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
||||||
|
primaryKeyColumns: ['courseId', 'cmId'],
|
||||||
|
onDestroy: () => delete this.viewedModulesTables[siteId],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -340,6 +355,31 @@ export class CoreCourseProvider {
|
||||||
return ROOT_CACHE_KEY + 'activitiescompletion:' + courseId + ':' + userId;
|
return ROOT_CACHE_KEY + 'activitiescompletion:' + courseId + ':' + userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get certain module viewed records in the app.
|
||||||
|
*
|
||||||
|
* @param ids Module IDs.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with map of last module viewed data.
|
||||||
|
*/
|
||||||
|
async getCertainModulesViewed(ids: number[] = [], siteId?: string): Promise<Record<number, CoreCourseViewedModulesDBRecord>> {
|
||||||
|
if (!ids.length) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const whereAndParams = SQLiteDB.getInOrEqual(ids);
|
||||||
|
|
||||||
|
const entries = await this.viewedModulesTables[site.getId()].getManyWhere({
|
||||||
|
sql: 'cmId ' + whereAndParams.sql,
|
||||||
|
sqlParams: whereAndParams.params,
|
||||||
|
js: (record) => ids.includes(record.cmId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return CoreUtils.arrayToObject(entries, 'cmId');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get course blocks.
|
* Get course blocks.
|
||||||
*
|
*
|
||||||
|
@ -425,6 +465,19 @@ export class CoreCourseProvider {
|
||||||
return entries.map((entry) => entry.id);
|
return entries.map((entry) => entry.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last module viewed in the app for a course.
|
||||||
|
*
|
||||||
|
* @param id Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with last module viewed data, undefined if none.
|
||||||
|
*/
|
||||||
|
async getLastModuleViewed(id: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord | undefined> {
|
||||||
|
const viewedModules = await this.getViewedModules(id, siteId);
|
||||||
|
|
||||||
|
return viewedModules[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a module from Moodle.
|
* Get a module from Moodle.
|
||||||
*
|
*
|
||||||
|
@ -548,7 +601,7 @@ export class CoreCourseProvider {
|
||||||
|
|
||||||
let foundModule: CoreCourseGetContentsWSModule | undefined;
|
let foundModule: CoreCourseGetContentsWSModule | undefined;
|
||||||
|
|
||||||
const foundSection = sections.some((section) => {
|
const foundSection = sections.find((section) => {
|
||||||
if (section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID &&
|
if (section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID &&
|
||||||
sectionId !== undefined &&
|
sectionId !== undefined &&
|
||||||
sectionId != section.id
|
sectionId != section.id
|
||||||
|
@ -562,7 +615,7 @@ export class CoreCourseProvider {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (foundSection && foundModule) {
|
if (foundSection && foundModule) {
|
||||||
return this.addAdditionalModuleData(foundModule, courseId);
|
return this.addAdditionalModuleData(foundModule, courseId, foundSection.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new CoreError(Translate.instant('core.course.modulenotfound'));
|
throw new CoreError(Translate.instant('core.course.modulenotfound'));
|
||||||
|
@ -573,11 +626,13 @@ export class CoreCourseProvider {
|
||||||
*
|
*
|
||||||
* @param module Module.
|
* @param module Module.
|
||||||
* @param courseId Course ID of the module.
|
* @param courseId Course ID of the module.
|
||||||
|
* @param sectionId Section ID of the module.
|
||||||
* @return Module with additional info.
|
* @return Module with additional info.
|
||||||
*/
|
*/
|
||||||
protected addAdditionalModuleData(
|
protected addAdditionalModuleData(
|
||||||
module: CoreCourseGetContentsWSModule,
|
module: CoreCourseGetContentsWSModule,
|
||||||
courseId: number,
|
courseId: number,
|
||||||
|
sectionId: number,
|
||||||
): CoreCourseModuleData {
|
): CoreCourseModuleData {
|
||||||
let completionData: CoreCourseModuleCompletionData | undefined = undefined;
|
let completionData: CoreCourseModuleCompletionData | undefined = undefined;
|
||||||
|
|
||||||
|
@ -590,13 +645,12 @@ export class CoreCourseProvider {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleWithCourse: CoreCourseModuleData = {
|
return {
|
||||||
...module,
|
...module,
|
||||||
course: courseId,
|
course: courseId,
|
||||||
|
section: sectionId,
|
||||||
completiondata: completionData,
|
completiondata: completionData,
|
||||||
};
|
};
|
||||||
|
|
||||||
return moduleWithCourse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -873,7 +927,7 @@ export class CoreCourseProvider {
|
||||||
// Add course to all modules.
|
// Add course to all modules.
|
||||||
return sections.map((section) => ({
|
return sections.map((section) => ({
|
||||||
...section,
|
...section,
|
||||||
modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId)),
|
modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -901,6 +955,23 @@ export class CoreCourseProvider {
|
||||||
return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
|
return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all viewed modules in a course, ordered by timeaccess in descending order.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the list of viewed modules.
|
||||||
|
*/
|
||||||
|
async getViewedModules(courseId: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord[]> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return await this.viewedModulesTables[site.getId()].getMany({ courseId }, {
|
||||||
|
sorting: [
|
||||||
|
{ timeaccess: 'desc' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidates course blocks WS call.
|
* Invalidates course blocks WS call.
|
||||||
*
|
*
|
||||||
|
@ -1348,6 +1419,34 @@ export class CoreCourseProvider {
|
||||||
this.triggerCourseStatusChanged(courseId, status, siteId);
|
this.triggerCourseStatusChanged(courseId, status, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store activity as viewed.
|
||||||
|
*
|
||||||
|
* @param courseId Chapter ID.
|
||||||
|
* @param cmId Module ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with last chapter viewed, undefined if none.
|
||||||
|
*/
|
||||||
|
async storeModuleViewed(courseId: number, cmId: number, options: CoreCourseStoreModuleViewedOptions = {}): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const timeaccess = options.timeaccess ?? Date.now();
|
||||||
|
|
||||||
|
await this.viewedModulesTables[site.getId()].insert({
|
||||||
|
courseId,
|
||||||
|
cmId,
|
||||||
|
sectionId: options.sectionId,
|
||||||
|
timeaccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.COURSE_MODULE_VIEWED, {
|
||||||
|
courseId,
|
||||||
|
cmId,
|
||||||
|
timeaccess,
|
||||||
|
sectionId: options.sectionId,
|
||||||
|
}, site.getId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate a module name to current language.
|
* Translate a module name to current language.
|
||||||
*
|
*
|
||||||
|
@ -1770,3 +1869,12 @@ type CoreCompletionUpdateActivityCompletionStatusManuallyWSParams = {
|
||||||
export type CoreCourseAnyModuleData = CoreCourseModuleData | CoreCourseModuleBasicInfo & {
|
export type CoreCourseAnyModuleData = CoreCourseModuleData | CoreCourseModuleBasicInfo & {
|
||||||
contents?: CoreCourseModuleContentFile[]; // If needed, calculated in the app in loadModuleContents.
|
contents?: CoreCourseModuleContentFile[]; // If needed, calculated in the app in loadModuleContents.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for storeModuleViewed.
|
||||||
|
*/
|
||||||
|
export type CoreCourseStoreModuleViewedOptions = {
|
||||||
|
sectionId?: number;
|
||||||
|
timeaccess?: number;
|
||||||
|
siteId?: string;
|
||||||
|
};
|
||||||
|
|
|
@ -18,9 +18,10 @@ import { CoreSiteSchema } from '@services/sites';
|
||||||
* Database variables for CoreCourse service.
|
* Database variables for CoreCourse service.
|
||||||
*/
|
*/
|
||||||
export const COURSE_STATUS_TABLE = 'course_status';
|
export const COURSE_STATUS_TABLE = 'course_status';
|
||||||
|
export const COURSE_VIEWED_MODULES_TABLE = 'course_viewed_modules';
|
||||||
export const SITE_SCHEMA: CoreSiteSchema = {
|
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
name: 'CoreCourseProvider',
|
name: 'CoreCourseProvider',
|
||||||
version: 1,
|
version: 2,
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
name: COURSE_STATUS_TABLE,
|
name: COURSE_STATUS_TABLE,
|
||||||
|
@ -53,6 +54,29 @@ export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: COURSE_VIEWED_MODULES_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'courseId',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cmId',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timeaccess',
|
||||||
|
type: 'INTEGER',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sectionId',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ['courseId', 'cmId'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -85,7 +109,7 @@ export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||||
type: 'TEXT',
|
type: 'TEXT',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timecompleted',
|
name: 'timeaccess',
|
||||||
type: 'INTEGER',
|
type: 'INTEGER',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -102,6 +126,13 @@ export type CoreCourseStatusDBRecord = {
|
||||||
previousDownloadTime: number;
|
previousDownloadTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CoreCourseViewedModulesDBRecord = {
|
||||||
|
courseId: number;
|
||||||
|
cmId: number;
|
||||||
|
timeaccess: number;
|
||||||
|
sectionId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type CoreCourseManualCompletionDBRecord = {
|
export type CoreCourseManualCompletionDBRecord = {
|
||||||
cmid: number;
|
cmid: number;
|
||||||
completed: number;
|
completed: number;
|
||||||
|
|
|
@ -98,9 +98,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
|
||||||
*
|
*
|
||||||
* @param course The course to get the title.
|
* @param course The course to get the title.
|
||||||
* @param sections List of sections.
|
* @param sections List of sections.
|
||||||
* @return Promise resolved with current section.
|
* @return Promise resolved with current section and whether the section should be selected. If only the section is returned,
|
||||||
|
* forceSelected will default to false.
|
||||||
*/
|
*/
|
||||||
getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection>;
|
getCurrentSection?(
|
||||||
|
course: CoreCourseAnyCourseData,
|
||||||
|
sections: CoreCourseSection[],
|
||||||
|
): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection> | CoreCourseSection>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name for the highlighted section.
|
* Returns the name for the highlighted section.
|
||||||
|
@ -299,21 +303,37 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
||||||
*
|
*
|
||||||
* @param course The course to get the title.
|
* @param course The course to get the title.
|
||||||
* @param sections List of sections.
|
* @param sections List of sections.
|
||||||
* @return Promise resolved with current section.
|
* @return Promise.
|
||||||
*/
|
*/
|
||||||
async getCurrentSection<T = CoreCourseSection>(course: CoreCourseAnyCourseData, sections: T[]): Promise<T> {
|
async getCurrentSection<T = CoreCourseSection>(
|
||||||
|
course: CoreCourseAnyCourseData,
|
||||||
|
sections: T[],
|
||||||
|
): Promise<CoreCourseFormatCurrentSectionData<T>> {
|
||||||
try {
|
try {
|
||||||
const section = await this.executeFunctionOnEnabled<T>(
|
const sectionData = await this.executeFunctionOnEnabled<CoreCourseFormatCurrentSectionData<T> | T>(
|
||||||
course.format || '',
|
course.format || '',
|
||||||
'getCurrentSection',
|
'getCurrentSection',
|
||||||
[course, sections],
|
[course, sections],
|
||||||
);
|
);
|
||||||
|
|
||||||
return section || sections[0];
|
if (sectionData && 'forceSelected' in sectionData) {
|
||||||
|
return sectionData;
|
||||||
|
} else if (sectionData) {
|
||||||
|
// Function just returned the section, don't force selecting it.
|
||||||
|
return {
|
||||||
|
section: sectionData,
|
||||||
|
forceSelected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// This function should never fail. Just return the first section (usually, "All sections").
|
// This function should never fail.
|
||||||
return sections[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the first section (usually, "All sections").
|
||||||
|
return {
|
||||||
|
section: sections[0],
|
||||||
|
forceSelected: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,3 +400,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoreCourseFormatDelegate = makeSingleton(CoreCourseFormatDelegateService);
|
export const CoreCourseFormatDelegate = makeSingleton(CoreCourseFormatDelegateService);
|
||||||
|
|
||||||
|
export type CoreCourseFormatCurrentSectionData<T = CoreCourseSection> = {
|
||||||
|
section: T; // Current section.
|
||||||
|
forceSelected: boolean; // If true, the app will force selecting the section when opening the course.
|
||||||
|
};
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreCourseSection } from '../course-helper';
|
import { CoreCourseSection } from '../course-helper';
|
||||||
import { CoreCourseFormatHandler } from '../format-delegate';
|
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatHandler } from '../format-delegate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default handler used when the course format doesn't have a specific implementation.
|
* Default handler used when the course format doesn't have a specific implementation.
|
||||||
|
@ -74,7 +74,10 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
|
async getCurrentSection(
|
||||||
|
course: CoreCourseAnyCourseData,
|
||||||
|
sections: CoreCourseSection[],
|
||||||
|
): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
|
||||||
let marker: number | undefined;
|
let marker: number | undefined;
|
||||||
|
|
||||||
// We need the "marker" to determine the current section.
|
// We need the "marker" to determine the current section.
|
||||||
|
@ -93,12 +96,18 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
||||||
const section = sections.find((sect) => sect.section == marker);
|
const section = sections.find((sect) => sect.section == marker);
|
||||||
|
|
||||||
if (section) {
|
if (section) {
|
||||||
return section;
|
return {
|
||||||
|
section,
|
||||||
|
forceSelected: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marked section not found or we couldn't retrieve the marker. Return all sections.
|
// Marked section not found or we couldn't retrieve the marker. Return all sections.
|
||||||
return sections[0];
|
return {
|
||||||
|
section: sections[0],
|
||||||
|
forceSelected: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -145,6 +145,10 @@ export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
name: 'data',
|
name: 'data',
|
||||||
type: 'TEXT',
|
type: 'TEXT',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'timeaccess',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
primaryKeys: ['component', 'id'],
|
primaryKeys: ['component', 'id'],
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,6 +60,7 @@ export interface CoreEventsData {
|
||||||
[CoreEvents.FILE_SHARED]: CoreEventFileSharedData;
|
[CoreEvents.FILE_SHARED]: CoreEventFileSharedData;
|
||||||
[CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData;
|
[CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData;
|
||||||
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
|
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
|
||||||
|
[CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -110,6 +111,7 @@ export class CoreEvents {
|
||||||
static readonly FORM_ACTION = 'form_action';
|
static readonly FORM_ACTION = 'form_action';
|
||||||
static readonly ACTIVITY_DATA_SENT = 'activity_data_sent';
|
static readonly ACTIVITY_DATA_SENT = 'activity_data_sent';
|
||||||
static readonly DEVICE_REGISTERED_IN_MOODLE = 'device_registered_in_moodle';
|
static readonly DEVICE_REGISTERED_IN_MOODLE = 'device_registered_in_moodle';
|
||||||
|
static readonly COURSE_MODULE_VIEWED = 'course_module_viewed';
|
||||||
|
|
||||||
protected static logger = CoreLogger.getInstance('CoreEvents');
|
protected static logger = CoreLogger.getInstance('CoreEvents');
|
||||||
protected static observables: { [eventName: string]: Subject<unknown> } = {};
|
protected static observables: { [eventName: string]: Subject<unknown> } = {};
|
||||||
|
@ -427,3 +429,13 @@ export type CoreEventAppLaunchedData = {
|
||||||
export type CoreEventOrientationData = {
|
export type CoreEventOrientationData = {
|
||||||
orientation: CoreScreenOrientation;
|
orientation: CoreScreenOrientation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data passed to COURSE_MODULE_VIEWED event.
|
||||||
|
*/
|
||||||
|
export type CoreEventCourseModuleViewed = {
|
||||||
|
courseId: number;
|
||||||
|
cmId: number;
|
||||||
|
timeaccess: number;
|
||||||
|
sectionId?: number;
|
||||||
|
};
|
||||||
|
|
|
@ -22,7 +22,8 @@ information provided here is intended especially for developers.
|
||||||
- The parameters of the following functions in CoreCourseHelper have changed: navigateToModuleByInstance, navigateToModule, openModule.
|
- The parameters of the following functions in CoreCourseHelper have changed: navigateToModuleByInstance, navigateToModule, openModule.
|
||||||
- fillContextMenu, expandDescription, gotoBlog, prefetch and removeFiles functions have been removed from CoreCourseModuleMainResourceComponent.
|
- fillContextMenu, expandDescription, gotoBlog, prefetch and removeFiles functions have been removed from CoreCourseModuleMainResourceComponent.
|
||||||
- contextMenuPrefetch and fillContextMenu have been removed from CoreCourseHelper.
|
- contextMenuPrefetch and fillContextMenu have been removed from CoreCourseHelper.
|
||||||
-The variable "loaded" in CoreCourseModuleMainResourceComponent has been changed to "showLoading" to reflect its purpose better.
|
- The variable "loaded" in CoreCourseModuleMainResourceComponent has been changed to "showLoading" to reflect its purpose better.
|
||||||
|
- The function getCurrentSection of course formats can now return a forceSelected boolean along with the section (defaults to false if not returned).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue