Merge pull request #3170 from dpalou/MOBILE-3930

Mobile 3930
main
Pau Ferrer Ocaña 2022-03-14 11:15:16 +01:00 committed by GitHub
commit 09b962cf04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1046 additions and 442 deletions

View File

@ -1557,6 +1557,7 @@
"core.course.highlighted": "moodle",
"core.course.insufficientavailablequota": "local_moodlemobileapp",
"core.course.insufficientavailablespace": "local_moodlemobileapp",
"core.course.lastaccessedactivity": "local_moodlemobileapp",
"core.course.manualcompletionnotsynced": "local_moodlemobileapp",
"core.course.modulenotfound": "local_moodlemobileapp",
"core.course.nocontentavailable": "local_moodlemobileapp",

View File

@ -49,17 +49,41 @@ export class AddonBlockRecentlyAccessedItemsProvider {
cacheKey: this.getRecentItemsCacheKey(),
};
const items: AddonBlockRecentlyAccessedItemsItem[] =
let items: AddonBlockRecentlyAccessedItemsItem[] =
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');
item.iconUrl = await CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
cmIds.push(item.cmid);
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;
}
/**

View File

@ -43,6 +43,7 @@ export class AddonBlogEntriesPage implements OnInit {
protected canLoadMoreEntries = false;
protected canLoadMoreUserEntries = true;
protected siteHomeId: number;
protected fetchSuccess = false;
loaded = false;
canLoadMore = false;
@ -123,8 +124,6 @@ export class AddonBlogEntriesPage implements OnInit {
deepLinkManager.treatLink();
await this.fetchEntries();
CoreUtils.ignoreErrors(AddonBlog.logView(this.filter));
}
/**
@ -176,13 +175,7 @@ export class AddonBlogEntriesPage implements OnInit {
entry.summary = CoreTextUtils.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
return CoreUser.getProfile(entry.userid, entry.courseid, true).then((user) => {
entry.user = user;
return;
}).catch(() => {
// Ignore errors.
});
entry.user = await CoreUtils.ignoreErrors(CoreUser.getProfile(entry.userid, entry.courseid, true));
});
if (refresh) {
@ -205,6 +198,11 @@ export class AddonBlogEntriesPage implements OnInit {
}
await Promise.all(promises);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
CoreUtils.ignoreErrors(AddonBlog.logView(this.filter));
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true);
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.

View File

@ -58,6 +58,8 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy {
contextLevel?: string;
contextInstanceId?: number;
protected fetchSuccess = false;
constructor() {
try {
const planId = CoreNavigator.getRouteNumberParam('planId');
@ -117,33 +119,6 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy {
await source.reload();
await this.competencies.start();
await this.fetchCompetency();
if (!this.competency) {
return;
}
const name = this.competency.competency.competency.shortname;
if (source instanceof AddonCompetencyPlanCompetenciesSource) {
this.planStatus && await CoreUtils.ignoreErrors(
AddonCompetency.logCompetencyInPlanView(
source.PLAN_ID,
this.requireCompetencyId(),
this.planStatus,
name,
source.user?.id,
),
);
} else {
await CoreUtils.ignoreErrors(
AddonCompetency.logCompetencyInCourseView(
source.COURSE_ID,
this.requireCompetencyId(),
name,
source.USER_ID,
),
);
}
} finally {
this.competencyLoaded = true;
}
@ -180,6 +155,32 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy {
evidence.description = Translate.instant(key, { $a: evidence.desca });
}
});
if (!this.fetchSuccess) {
this.fetchSuccess = true;
const name = this.competency.competency.competency.shortname;
if (source instanceof AddonCompetencyPlanCompetenciesSource) {
this.planStatus && await CoreUtils.ignoreErrors(
AddonCompetency.logCompetencyInPlanView(
source.PLAN_ID,
this.requireCompetencyId(),
this.planStatus,
name,
source.user?.id,
),
);
} else {
await CoreUtils.ignoreErrors(
AddonCompetency.logCompetencyInCourseView(
source.COURSE_ID,
this.requireCompetencyId(),
name,
source.USER_ID,
),
);
}
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting competency data.');
}

View File

@ -36,6 +36,8 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit {
contextLevel?: ContextLevel;
contextInstanceId?: number;
protected fetchSuccess = false; // Whether a fetch was finished successfully.
/**
* @inheritdoc
*/
@ -54,9 +56,6 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit {
try {
await this.fetchCompetency();
const name = this.competency!.competency && this.competency!.competency.shortname;
CoreUtils.ignoreErrors(AddonCompetency.logCompetencyView(this.competencyId, name));
} finally {
this.competencyLoaded = true;
}
@ -73,10 +72,15 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit {
if (!this.contextLevel || this.contextInstanceId === undefined) {
// Context not specified, use user context.
this.contextLevel = ContextLevel.USER;
this.contextInstanceId = result.usercompetency!.userid;
this.contextInstanceId = result.usercompetency?.userid;
}
this.competency = result.competency;
if (!this.fetchSuccess) {
this.fetchSuccess = true;
CoreUtils.ignoreErrors(AddonCompetency.logCompetencyView(this.competencyId, this.competency.competency.shortname));
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting competency summary data.');
}

View File

@ -17,7 +17,6 @@ import { Params } from '@angular/router';
import { CoreSite } from '@classes/site';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
@ -121,7 +120,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
(data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment submitted, check completion.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
// Reload data since it can have offline data now.
this.showLoadingAndRefresh(true, false);
@ -138,25 +137,6 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
}, this.siteId);
await this.loadContent(false, true);
if (!this.assign) {
return;
}
try {
await AddonModAssign.logView(this.assign.id, this.assign.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors. Just don't check Module completion.
}
if (this.canViewAllSubmissions) {
// User can see all submissions, log grading view.
CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id, this.assign.name));
} else if (this.canViewOwnSubmission) {
// User can only see their own submission, log view the user submission.
CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id, this.assign.name));
}
}
/**
@ -232,6 +212,25 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
}
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.assign) {
return; // Shouldn't happen.
}
await AddonModAssign.logView(this.assign.id, this.assign.name);
if (this.canViewAllSubmissions) {
// User can see all submissions, log grading view.
CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id, this.assign.name));
} else if (this.canViewOwnSubmission) {
// User can only see their own submission, log view the user submission.
CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id, this.assign.name));
}
}
/**
* Set group to see the summary.
*

View File

@ -15,7 +15,6 @@
import { Component, OnInit, Optional } from '@angular/core';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreDomUtils } from '@services/utils/dom';
@ -54,18 +53,6 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
super.ngOnInit();
await this.loadContent();
if (!this.bbb) {
return;
}
try {
await AddonModBBB.logView(this.bbb.id, this.bbb.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
@ -82,7 +69,6 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo);
await this.fetchMeetingInfo();
}
/**
@ -107,6 +93,17 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
}
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.bbb) {
return; // Shouldn't happen.
}
await AddonModBBB.logView(this.bbb.id, this.bbb.name);
}
/**
* Update meeting info.
*

View File

@ -18,7 +18,6 @@ import { AddonModBook, AddonModBookBookWSData, AddonModBookNumbering, AddonModBo
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
/**
* Component that displays a book entry page.
@ -36,6 +35,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
hasStartedBook = false;
protected book?: AddonModBookBookWSData;
protected checkCompletionAfterLog = false;
constructor( @Optional() courseContentsPage?: CoreCourseContentsPage) {
super('AddonModBookIndexComponent', courseContentsPage);
@ -48,9 +48,6 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
super.ngOnInit();
this.loadContent();
// Log book viewed.
await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance, undefined, this.module.name));
}
/**
@ -93,6 +90,13 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
this.chapters = AddonModBook.getTocList(contents);
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
AddonModBook.logView(this.module.instance, undefined, this.module.name);
}
/**
* Open the book in a certain chapter.
*

View File

@ -391,7 +391,7 @@ export class AddonModBookProvider {
async storeLastChapterViewed(id: number, chapterId: number, courseId: number, siteId?: string): Promise<void> {
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) });
}
}

View File

@ -15,7 +15,6 @@
import { Component, OnInit, Optional } from '@angular/core';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreTimeUtils } from '@services/utils/time';
@ -53,18 +52,6 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
super.ngOnInit();
await this.loadContent();
if (!this.chat) {
return;
}
try {
await AddonModChat.logView(this.chat.id, this.chat.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
@ -89,6 +76,17 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
this.dataRetrieved.emit(this.chat);
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.chat) {
return; // Shouldn't happen.
}
await AddonModChat.logView(this.chat.id, this.chat.name);
}
/**
* Enter the chat.
*/

View File

@ -15,7 +15,6 @@
import { Component, Optional, OnInit } from '@angular/core';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
@ -86,18 +85,6 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
this.userId = CoreSites.getCurrentSiteUserId();
await this.loadContent(false, true);
if (!this.choice) {
return;
}
try {
await AddonModChoice.logView(this.choice.id, this.choice.name);
await CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
@ -325,6 +312,17 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
this.canSeeResults = hasVotes || AddonModChoice.canStudentSeeResults(choice, this.hasAnsweredOnline);
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.choice) {
return; // Shouldn't happen.
}
await AddonModChoice.logView(this.choice.id, this.choice.name);
}
/**
* Check if a choice is open.
*
@ -384,7 +382,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
if (online) {
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: this.moduleName });
// Check completion since it could be configured to complete once the user answers the choice.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
await this.dataUpdated(online);

View File

@ -18,7 +18,6 @@ import { Params } from '@angular/router';
import { CoreCommentsProvider } from '@features/comments/services/comments';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { CoreRatingProvider } from '@features/rating/services/rating';
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
import { IonContent } from '@ionic/angular';
@ -154,7 +153,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
});
await this.loadContent(false, true);
await this.logView(true);
}
/**
@ -410,7 +408,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
try {
await this.fetchEntriesData();
// Log activity view for coherence with Moodle web.
await this.logView();
await this.logActivity();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
} finally {
@ -456,7 +454,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
await this.fetchEntriesData();
// Log activity view for coherence with Moodle web.
return this.logView();
return this.logActivity();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
}
@ -522,24 +520,14 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
}
/**
* Log viewing the activity.
*
* @param checkCompletion Whether to check completion.
* @return Promise resolved when done.
* @inheritdoc
*/
protected async logView(checkCompletion = false): Promise<void> {
protected async logActivity(): Promise<void> {
if (!this.database || !this.database.id) {
return;
}
try {
await AddonModData.logView(this.database.id, this.database.name);
if (checkCompletion) {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
} catch {
// Ignore errors, the user could be offline.
}
}
/**

View File

@ -55,6 +55,7 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
protected entryChangedObserver: CoreEventObserver; // It will observe the changed entry event.
protected fields: Record<number, AddonModDataField> = {};
protected fieldsArray: AddonModDataField[] = [];
protected logAfterFetch = true;
moduleId = 0;
courseId!: number;
@ -149,7 +150,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
await this.fetchEntryData();
this.logView();
}
/**
@ -201,6 +201,14 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
title: this.title,
group: this.selectedGroup,
};
if (this.logAfterFetch) {
this.logAfterFetch = false;
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) {
if (!refresh) {
// Some call failed, retry without using cache since it might be a new activity.
@ -225,9 +233,9 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
this.entryId = undefined;
this.entry = undefined;
this.entryLoaded = false;
this.logAfterFetch = true;
await this.fetchEntryData();
this.logView();
}
/**
@ -286,9 +294,9 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
this.entry = undefined;
this.entryId = undefined;
this.entryLoaded = false;
this.logAfterFetch = true;
await this.fetchEntryData();
this.logView();
}
/**
@ -397,19 +405,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
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.
*/

View File

@ -82,6 +82,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
protected submitObserver: CoreEventObserver;
protected syncEventName = AddonModFeedbackSyncProvider.AUTO_SYNCED;
protected checkCompletionAfterLog = false;
constructor(
protected content?: IonContent,
@ -125,15 +126,22 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
try {
await this.loadContent(false, true);
if (this.feedback) {
CoreUtils.ignoreErrors(AddonModFeedback.logView(this.feedback.id, this.feedback.name));
}
} finally {
this.tabsReady = true;
}
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.feedback) {
return; // Shouldn't happen.
}
await AddonModFeedback.logView(this.feedback.id, this.feedback.name);
}
/**
* @inheritdoc
*/

View File

@ -16,6 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
@ -49,6 +50,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
loaded = false;
protected attemptId: number;
protected fetchSuccess = false;
constructor() {
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
@ -131,6 +133,11 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
return attemptItem;
}).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) {
// Some call failed on fetch, go back.
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);

View File

@ -25,6 +25,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback';
import { CoreCourse } from '@features/course/services/course';
/**
* Page that displays feedback attempts.
@ -37,14 +38,14 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
promisedAttempts: CorePromisedValue<CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource>>;
promisedAttempts: CorePromisedValue<AddonModFeedbackAttemptsManager>;
fetchFailed = false;
constructor(protected route: ActivatedRoute) {
this.promisedAttempts = new CorePromisedValue();
}
get attempts(): CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> | null {
get attempts(): AddonModFeedbackAttemptsManager | null {
return this.promisedAttempts.value;
}
@ -95,7 +96,7 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
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) {
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);
}
}

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { IonRefresher } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
@ -34,6 +35,7 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
protected courseId!: number;
protected feedback?: AddonModFeedbackWSFeedback;
protected page = 0;
protected fetchSuccess = false;
selectedGroup!: number;
groupInfo?: CoreGroupInfo;
@ -81,6 +83,12 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
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) {
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);

View File

@ -62,13 +62,6 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
try {
await this.loadContent();
try {
await AddonModFolder.logView(this.module.instance, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
} finally {
this.showLoading = false;
}
@ -97,6 +90,13 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
this.contents = AddonModFolderHelper.formatContents(contents);
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await AddonModFolder.logView(this.module.instance, this.module.name);
}
/**
* Navigate to a subfolder.
*

View File

@ -99,6 +99,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver;
protected sourceUnsubscribe?: () => void;
protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
constructor(
public route: ActivatedRoute,
@ -570,7 +571,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
});
// Check completion since it could be configured to complete once the user adds a new discussion or replies.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
}
@ -692,15 +693,13 @@ class AddonModForumDiscussionsManager extends CoreListItemsManager<AddonModForum
return;
}
CoreUtils.ignoreErrors(
AddonModForum.instance
.logView(forum.id, forum.name)
.then(async () => {
CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
try {
await AddonModForum.instance.logView(forum.id, forum.name);
return;
}),
);
CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
} catch {
// Ignore errors.
}
}
}

View File

@ -17,6 +17,7 @@ import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Opt
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourse } from '@features/course/services/course';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
@ -118,6 +119,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver;
protected changeDiscObserver?: CoreEventObserver;
protected fetchSuccess = false;
constructor(
@Optional() protected splitView: CoreSplitViewComponent,
@ -547,6 +549,12 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
this.hasOfflineRatings =
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) {
CoreDomUtils.showErrorModal(error);
} finally {

View File

@ -81,12 +81,13 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
protected sourceUnsubscribe?: () => void;
protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver;
protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
getDivider?: (entry: AddonModGlossaryEntry) => string;
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
constructor(
protected route: ActivatedRoute,
public route: ActivatedRoute,
protected content?: IonContent,
@Optional() protected courseContentsPage?: CoreCourseContentsPage,
) {
@ -124,10 +125,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
);
this.promisedEntries.resolve(new AddonModGlossaryEntriesManager(
source,
this.route.component,
));
this.promisedEntries.resolve(new AddonModGlossaryEntriesManager(source, this));
this.sourceUnsubscribe = source.addListener({
onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)),
@ -139,7 +137,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
this.showLoadingAndRefresh(false);
// Check completion since it could be configured to complete once the user adds a new entry.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
});
@ -166,12 +164,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
await this.loadContent(false, true);
await entries.start(this.splitView);
try {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch (error) {
// Ignore errors.
}
}
/**
@ -437,6 +429,14 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
*/
class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
page: AddonModGlossaryIndexComponent;
constructor(source: AddonModGlossaryEntriesSource, page: AddonModGlossaryIndexComponent) {
super(source, page.route.component);
this.page = page;
}
get offlineEntries(): AddonModGlossaryOfflineEntry[] {
return this.getSource().offlineEntries;
}
@ -463,7 +463,13 @@ class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossa
return;
}
try {
await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
} catch (error) {
// Ignore errors.
}
}
}

View File

@ -17,6 +17,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
import { CoreComments } from '@features/comments/services/comments';
import { CoreCourse } from '@features/course/services/course';
import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreTag } from '@features/tag/services/tag';
import { IonRefresher } from '@ionic/angular';
@ -55,8 +56,10 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
tagsEnabled = false;
commentsEnabled = false;
courseId!: number;
cmId?: number;
protected entryId!: number;
protected fetchSuccess = false;
constructor(protected route: ActivatedRoute) {}
@ -72,15 +75,17 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
if (routeData.swipeEnabled ?? true) {
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonModGlossaryEntriesSource,
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
);
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
await this.entries.start();
} else {
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -143,6 +148,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
this.entry = result.entry;
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) {
// Glossary already loaded, nothing else to load.
return;

View File

@ -19,7 +19,6 @@ import { CoreConstants } from '@/core/constants';
import { CoreSite } from '@classes/site';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { CoreH5PDisplayOptions } from '@features/h5p/classes/core';
import { CoreH5PHelper } from '@features/h5p/classes/helper';
import { CoreH5P } from '@features/h5p/services/h5p';
@ -85,6 +84,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
protected site: CoreSite;
protected observer?: CoreEventObserver;
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
protected checkCompletionAfterLog = false; // It's called later, when the user plays the package.
constructor(
protected content?: IonContent,
@ -390,7 +390,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
// Mark the activity as viewed.
await AddonModH5PActivity.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
/**
@ -464,7 +464,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
// Check if the H5P has ended. Final statements don't include a subContentId.
const hasEnded = data.statements.some(statement => !statement.object.id.includes('subContentId='));
if (hasEnded) {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
}
} catch (error) {

View File

@ -25,6 +25,7 @@ import {
AddonModH5PActivityData,
AddonModH5PActivityAttemptResults,
} from '../../services/h5pactivity';
import { CoreCourse } from '@features/course/services/course';
/**
* Page that displays results of an attempt.
@ -45,6 +46,7 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
cmId!: number;
protected attemptId!: number;
protected fetchSuccess = false;
/**
* @inheritdoc
@ -62,17 +64,7 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
return;
}
try {
await this.fetchData();
if (this.h5pActivity) {
await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name, { attemptId: this.attemptId });
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
} finally {
this.loaded = true;
}
}
/**
@ -92,6 +84,7 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
* @return Promise resolved when done.
*/
protected async fetchData(): Promise<void> {
try {
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
this.attempt = await AddonModH5PActivity.getAttemptResults(this.h5pActivity.id, this.attemptId, {
@ -99,6 +92,23 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
});
await this.fetchUserProfile();
if (!this.fetchSuccess) {
this.fetchSuccess = true;
CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(
this.h5pActivity.id,
this.h5pActivity.name,
{ 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) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
} finally {
this.loaded = true;
}
}
/**

View File

@ -26,6 +26,7 @@ import {
AddonModH5PActivityData,
AddonModH5PActivityUserAttempts,
} from '../../services/h5pactivity';
import { CoreCourse } from '@features/course/services/course';
/**
* Page that displays user attempts of a certain user.
@ -46,6 +47,7 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
isCurrentUser = false;
protected userId!: number;
protected fetchSuccess = false;
/**
* @inheritdoc
@ -65,17 +67,7 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
this.isCurrentUser = this.userId == CoreSites.getCurrentSiteUserId();
try {
await this.fetchData();
if (this.h5pActivity) {
await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name, { userId: this.userId });
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
} finally {
this.loaded = true;
}
}
/**
@ -95,12 +87,30 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
* @return Promise resolved when done.
*/
protected async fetchData(): Promise<void> {
try {
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
await Promise.all([
this.fetchAttempts(),
this.fetchUserProfile(),
]);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(
this.h5pActivity.id,
this.h5pActivity.name,
{ 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) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
} finally {
this.loaded = true;
}
}
/**

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { IonRefresher } from '@ionic/angular';
@ -44,6 +45,7 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
canLoadMore = false;
protected page = 0;
protected fetchSuccess = false;
/**
* @inheritdoc
@ -60,17 +62,7 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
return;
}
try {
await this.fetchData();
if (this.h5pActivity) {
await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
} finally {
this.loaded = true;
}
this.fetchData();
}
/**
@ -91,11 +83,25 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
* @return Promise resolved when done.
*/
protected async fetchData(refresh?: boolean): Promise<void> {
try {
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
await Promise.all([
this.fetchUsers(refresh),
]);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
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) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
} finally {
this.loaded = true;
}
}
/**

View File

@ -20,6 +20,7 @@ import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonModH5PActivity } from '../h5pactivity';
import { AddonModH5PActivityModuleHandlerService } from './module';
@ -60,7 +61,7 @@ export class AddonModH5PActivityReportLinkHandlerService extends CoreContentLink
} else {
const userId = params.userid ? Number(params.userid) : undefined;
this.openUserAttempts(module.id, module.course, siteId, userId);
await this.openUserAttempts(module.id, module.course, instanceId, siteId, userId);
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error processing link.');
@ -100,13 +101,35 @@ export class AddonModH5PActivityReportLinkHandlerService extends CoreContentLink
*
* @param cmId Module ID.
* @param courseId Course ID.
* @param id Instance ID.
* @param siteId Site ID.
* @param userId User ID. If not defined, current user in site.
* @return Promise resolved when done.
*/
protected openUserAttempts(cmId: number, courseId: number, siteId: string, userId?: number): void {
userId = userId || CoreSites.getCurrentSiteUserId();
const path = AddonModH5PActivityModuleHandlerService.PAGE_NAME + `/${courseId}/${cmId}/userattempts/${userId}`;
protected async openUserAttempts(cmId: number, courseId: number, id: number, siteId: string, userId?: number): Promise<void> {
let canViewAllAttempts = false;
if (!userId) {
// No user ID specified. Check if current user can view all attempts.
userId = CoreSites.getCurrentSiteUserId();
canViewAllAttempts = await AddonModH5PActivity.canGetUsersAttempts(siteId);
if (canViewAllAttempts) {
const accessInfo = await CoreUtils.ignoreErrors(AddonModH5PActivity.getAccessInformation(id, {
cmId,
siteId,
}));
canViewAllAttempts = !!accessInfo?.canreviewattempts;
}
}
let path: string;
if (canViewAllAttempts) {
path = `${AddonModH5PActivityModuleHandlerService.PAGE_NAME}/${courseId}/${cmId}/users`;
} else {
path = `${AddonModH5PActivityModuleHandlerService.PAGE_NAME}/${courseId}/${cmId}/userattempts/${userId}`;
}
CoreNavigator.navigateToSitePath(path, {
siteId,

View File

@ -45,13 +45,6 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
super.ngOnInit();
await this.loadContent();
try {
await AddonModImscp.logView(this.module.instance, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
@ -103,6 +96,13 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
this.items = AddonModImscp.createItemList(contents);
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await AddonModImscp.logView(this.module.instance, this.module.name);
}
/**
* Open IMSCP book with a certain item.
*

View File

@ -303,7 +303,7 @@ export class AddonModImscpProvider {
async storeLastItemViewed(id: number, href: string, courseId: number, siteId?: string): Promise<void> {
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) });
}
}

View File

@ -18,7 +18,6 @@ import { Component, Input, ViewChild, ElementRef, OnInit, OnDestroy, Optional }
import { CoreTabsComponent } from '@components/tabs/tabs';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { CoreUser } from '@features/user/services/user';
import { IonContent, IonInput } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
@ -108,12 +107,6 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
this.selectedTab = this.action == 'report' ? 1 : 0;
await this.loadContent(false, true);
if (!this.lesson || this.preventReasons.length) {
return;
}
this.logView();
}
/**
@ -285,7 +278,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
protected hasSyncSucceed(result: AddonModLessonSyncResult): boolean {
if (result.updated || this.dataSent) {
// Check completion status if something was sent.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
this.dataSent = false;
@ -373,20 +366,14 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
}
/**
* Log viewing the lesson.
*
* @return Promise resolved when done.
* @inheritdoc
*/
protected async logView(): Promise<void> {
if (!this.lesson) {
protected async logActivity(): Promise<void> {
if (!this.lesson || this.preventReasons.length) {
return;
}
await CoreUtils.ignoreErrors(
AddonModLesson.logViewLesson(this.lesson.id, this.password, this.lesson.name),
);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
await AddonModLesson.logViewLesson(this.lesson.id, this.password, this.lesson.name);
}
/**
@ -631,7 +618,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
this.preventReasons = preventReason ? [preventReason] : [];
// Log view now that we have the password.
this.logView();
this.logActivity();
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {

View File

@ -35,6 +35,7 @@ import {
} from '../../services/lesson';
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreCourse } from '@features/course/services/course';
/**
* 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 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 fetchSuccess = false;
/**
* Component being initialized.
@ -160,6 +162,12 @@ export class AddonModLessonUserRetakePage implements OnInit {
this.student.profileimageurl = user?.profileimageurl;
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) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true);
}

View File

@ -21,6 +21,7 @@ import { makeSingleton } from '@singletons';
import { AddonModLtiHelper } from '../lti-helper';
import { AddonModLtiIndexComponent } from '../../components/index';
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
import { CoreCourse } from '@features/course/services/course';
/**
* Handler to support LTI modules.
@ -67,6 +68,8 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
// Launch the LTI.
AddonModLtiHelper.getDataAndLaunch(courseId, module);
CoreCourse.storeModuleViewed(courseId, module.id);
},
}];

View File

@ -52,13 +52,6 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
super.ngOnInit();
await this.loadContent();
try {
await AddonModPage.logView(this.module.instance, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
@ -118,4 +111,11 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
this.timemodified = 'timemodified' in this.page ? this.page.timemodified : undefined;
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await AddonModPage.logView(this.module.instance, this.module.name);
}
}

View File

@ -17,7 +17,6 @@ import { Component, OnDestroy, OnInit, Optional } from '@angular/core';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { CoreQuestionBehaviourDelegate } from '@features/question/services/behaviour-delegate';
import { IonContent } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
@ -121,18 +120,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
);
await this.loadContent(false, true);
if (!this.quiz) {
return;
}
try {
await AddonModQuiz.logViewQuiz(this.quiz.id, this.quiz.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
@ -387,6 +374,17 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.quiz) {
return; // Shouldn't happen.
}
await AddonModQuiz.logViewQuiz(this.quiz.id, this.quiz.name);
}
/**
* Go to review an attempt that has just been finished.
*
@ -398,7 +396,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
// If we go to auto review it means an attempt was finished. Check completion status.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
// Verify that user can see the review.
const attemptId = this.autoReview.attemptId;
@ -425,7 +423,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
protected hasSyncSucceed(result: AddonModQuizSyncResult): boolean {
if (result.attemptFinished) {
// An attempt was finished, check completion status.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
// If the sync call isn't rejected it means the sync was successful.
@ -508,7 +506,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
if (syncEventData.attemptFinished) {
// An attempt was finished, check completion status.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
if (this.quiz && syncEventData.quizId == this.quiz.id) {

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreQuestionQuestionParsed } from '@features/question/services/question';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { IonContent, IonRefresher } from '@ionic/angular';
@ -73,6 +74,7 @@ export class AddonModQuizReviewPage implements OnInit {
protected attemptId!: number; // The attempt being reviewed.
protected currentPage!: number; // The current page being reviewed.
protected options?: AddonModQuizCombinedReviewOptions; // Review options.
protected fetchSuccess = false;
constructor(
protected elementRef: ElementRef,
@ -99,10 +101,6 @@ export class AddonModQuizReviewPage implements OnInit {
try {
await this.fetchData();
CoreUtils.ignoreErrors(
AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz!.id, this.quiz!.name),
);
} finally {
this.loaded = true;
}
@ -160,6 +158,16 @@ export class AddonModQuizReviewPage implements OnInit {
// Load questions.
await this.loadPage(this.currentPage);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
CoreUtils.ignoreErrors(
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) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
}

View File

@ -90,12 +90,6 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
});
await this.loadContent();
try {
await AddonModResource.logView(this.module.instance, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
@ -189,6 +183,13 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
}
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await AddonModResource.logView(this.module.instance, this.module.name);
}
/**
* Opens a file.
*

View File

@ -90,6 +90,8 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
const hide = await this.hideOpenButton(module);
if (!hide) {
AddonModResourceHelper.openModuleFile(module, courseId);
CoreCourse.storeModuleViewed(courseId, module.id);
}
},
}];

View File

@ -16,7 +16,6 @@ import { CoreConstants } from '@/core/constants';
import { Component, OnInit, Optional } from '@angular/core';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreSync } from '@services/sync';
@ -114,21 +113,6 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
if (this.skip) {
this.open();
}
try {
await AddonModScorm.logView(this.scorm.id, this.scorm.name);
this.checkCompletion();
} catch {
// Ignore errors.
}
}
/**
* Check the completion.
*/
protected checkCompletion(): void {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
/**
@ -357,6 +341,17 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
this.gradeFormatted = AddonModScorm.formatGrade(scorm, this.grade);
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.scorm) {
return; // Shouldn't happen.
}
await AddonModScorm.logView(this.scorm.id, this.scorm.name);
}
/**
* Checks if sync has succeed from result sync data.
*

View File

@ -16,7 +16,6 @@ import { Component, OnInit, Optional } from '@angular/core';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
@ -75,13 +74,6 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
this.currentUserId = CoreSites.getCurrentSiteUserId();
await this.loadContent(false, true);
try {
await AddonModSurvey.logView(this.survey!.id, this.survey!.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors. Just don't check Module completion.
}
}
/**
@ -166,6 +158,17 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
});
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.survey) {
return; // Shouldn't happen.
}
await AddonModSurvey.logView(this.survey.id, this.survey.name);
}
/**
* Check if answers are valid to be submitted.
*

View File

@ -47,6 +47,8 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
mimetype?: string;
displayDescription = true;
protected checkCompletionAfterLog = false;
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
super('AddonModUrlIndexComponent', courseContentsPage);
}
@ -58,12 +60,6 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
super.ngOnInit();
await this.loadContent();
if ((this.shouldIframe ||
(this.shouldEmbed && this.isOther)) ||
(!this.shouldIframe && (!this.shouldEmbed || !this.isOther))) {
this.logView();
}
}
/**
@ -170,12 +166,24 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
protected async logView(): Promise<void> {
try {
await AddonModUrl.logView(this.module.instance, this.module.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
} catch {
// Ignore errors.
}
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if ((this.shouldIframe ||
(this.shouldEmbed && this.isOther)) ||
(!this.shouldIframe && (!this.shouldEmbed || !this.isOther))) {
this.logView();
}
}
/**
* Opens a file.
*/

View File

@ -63,7 +63,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
* @param module The module object.
* @param courseId The course ID.
*/
const openUrl = async (module: CoreCourseModuleData): Promise<void> => {
const openUrl = async (module: CoreCourseModuleData, courseId: number): Promise<void> => {
try {
if (module.instance) {
await AddonModUrl.logView(module.instance, module.name);
@ -73,6 +73,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
// Ignore errors.
}
CoreCourse.storeModuleViewed(courseId, module.id);
const contents = await CoreCourse.getModuleContents(module);
AddonModUrlHelper.open(contents[0].fileurl);
};
@ -89,7 +91,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
const shouldOpen = await this.shouldOpenLink(module);
if (shouldOpen) {
openUrl(module);
openUrl(module, courseId);
} else {
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.
icon: 'fas-link',
label: 'core.openmodinbrowser',
action: (event: Event, module: CoreCourseModuleData): void => {
openUrl(module);
action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
openUrl(module, courseId);
},
}],
};

View File

@ -148,24 +148,6 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
this.openMap();
}
}
if (!this.wiki) {
CoreNavigator.back();
return;
}
if (!this.pageId) {
try {
await AddonModWiki.logView(this.wiki.id, this.wiki.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
} else {
CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.pageId, this.wiki.id, this.wiki.name));
}
}
/**
@ -449,6 +431,22 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
}
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.wiki) {
return; // Shouldn't happen.
}
if (!this.pageId) {
await AddonModWiki.logView(this.wiki.id, this.wiki.name);
} else {
this.checkCompletionAfterLog = false;
CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.pageId, this.wiki.id, this.wiki.name));
}
}
/**
* Get path to the wiki home view. If cannot determine or it's current view, return undefined.
*

View File

@ -16,7 +16,6 @@ import { Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
@ -138,16 +137,6 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
super.ngOnInit();
await this.loadContent(false, true);
if (!this.workshop) {
return;
}
try {
await AddonModWorkshop.logView(this.workshop.id, this.workshop.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch (error) {
// Ignore errors.
}
}
/**
@ -164,7 +153,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
this.showLoadingAndRefresh(true);
// Check completion since it could be configured to complete once the user adds a new discussion or replies.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
this.checkCompletion();
}
}
@ -257,6 +246,17 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
await this.setPhaseInfo();
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
if (!this.workshop) {
return; // Shouldn't happen.
}
await AddonModWorkshop.logView(this.workshop.id, this.workshop.name);
}
/**
* Retrieves and shows submissions grade page.
*

View File

@ -102,6 +102,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
protected obsAssessmentSaved: CoreEventObserver;
protected syncObserver: CoreEventObserver;
protected isDestroyed = false;
protected fetchSuccess = false;
constructor(
protected fb: FormBuilder,
@ -157,12 +158,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
await this.fetchSubmissionData();
try {
await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
this.logView();
}
/**
@ -447,6 +443,8 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId);
await this.fetchSubmissionData();
this.logView();
}
}
@ -596,6 +594,24 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
});
}
/**
* Log submission viewed.
*/
protected async logView(): Promise<void> {
if (this.fetchSuccess) {
return; // Already done.
}
this.fetchSuccess = true;
try {
await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
* Component being destroyed.
*/

View File

@ -54,6 +54,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
currentUserId!: number;
protected syncObserver!: CoreEventObserver;
protected logAfterFetch = true;
constructor() {
try {
@ -91,8 +92,6 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
*/
async ngOnInit(): Promise<void> {
await this.fetchNotes(true);
CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId));
}
/**
@ -128,6 +127,11 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
} else {
this.notes = await AddonNotes.getNotesUserData(notesList);
}
if (this.logAfterFetch) {
this.logAfterFetch = false;
CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId));
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
} finally {
@ -172,9 +176,9 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
this.notesLoaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING;
this.logAfterFetch = true;
await this.fetchNotes(true);
CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId));
}
/**

View File

@ -34,6 +34,7 @@ export class CoreListItemsManager<
protected pageRouteLocator?: unknown | ActivatedRoute;
protected splitView?: CoreSplitViewComponent;
protected splitViewOutletSubscription?: Subscription;
protected fetchSuccess = false; // Whether a fetch was finished successfully.
constructor(source: Source, pageRouteLocator: unknown | ActivatedRoute) {
super(source);
@ -71,9 +72,6 @@ export class CoreListItemsManager<
// Calculate current selected item.
this.updateSelectedItem();
// Log activity.
await CoreUtils.ignoreErrors(this.logActivity());
}
/**
@ -145,6 +143,8 @@ export class CoreListItemsManager<
*/
async reload(): Promise<void> {
await this.getSource().reload();
this.finishSuccessfulFetch();
}
/**
@ -152,6 +152,21 @@ export class CoreListItemsManager<
*/
async load(): Promise<void> {
await this.getSource().load();
this.finishSuccessfulFetch();
}
/**
* Finish a successful fetch.
*/
protected async finishSuccessfulFetch(): Promise<void> {
if (this.fetchSuccess) {
return; // Already treated.
}
// Log activity.
this.fetchSuccess = true;
await CoreUtils.ignoreErrors(this.logActivity());
}
/**

View File

@ -160,7 +160,7 @@ export class CoreSite {
this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.LAST_VIEWED_TABLE, {
siteId: this.getId(),
database: this.getDb(),
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
primaryKeyColumns: ['component', 'id'],
}));
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.
*
* @param component The component.
* @param id ID.
* @param value Last viewed item value.
* @param data Other data.
* @param options Options.
* @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({
component,
id,
value: String(value),
data,
data: options.data,
timeaccess: options.timeaccess ?? Date.now(),
});
}
@ -2349,5 +2383,14 @@ export type CoreSiteLastViewedDBRecord = {
component: string;
id: number;
value: string;
timeaccess: number;
data?: string;
};
/**
* Options for storeLastViewed.
*/
export type CoreSiteStoreLastViewedOptions = {
data?: string; // Other data.
timeaccess?: number; // Accessed time. If not set, current time.
};

View File

@ -169,6 +169,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
}
await this.fetchContent(refresh, sync, showErrors);
this.finishSuccessfulFetch();
} catch (error) {
if (!refresh && !CoreSites.getCurrentSite()?.isOfflineDisabled() && this.isNotFoundError(error)) {
// Module not found, retry without using cache.

View File

@ -72,6 +72,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
protected showCompletion = false; // Whether to show completion inside the activity.
protected displayDescription = true; // Wether to show Module description on module page, and not on summary or the contrary.
protected isDestroyed = false; // Whether the component is destroyed.
protected fetchSuccess = false; // Whether a fetch was finished successfully.
protected checkCompletionAfterLog = true; // Whether to check if completion has changed after calling logActivity.
constructor(
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
@ -191,6 +193,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
try {
await this.fetchContent(refresh);
this.finishSuccessfulFetch();
} catch (error) {
if (!refresh && !CoreSites.getCurrentSite()?.isOfflineDisabled() && this.isNotFoundError(error)) {
// Module not found, retry without using cache.
@ -427,6 +431,47 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
}
}
/**
* Finish a successful fetch.
*
* @return Promise resolved when done.
*/
protected async finishSuccessfulFetch(): Promise<void> {
if (this.fetchSuccess) {
return; // Already treated.
}
this.fetchSuccess = true;
CoreCourse.storeModuleViewed(this.courseId, this.module.id, { sectionId: this.module.section });
// Log activity now.
try {
await this.logActivity();
if (this.checkCompletionAfterLog) {
this.checkCompletion();
}
} catch {
// Ignore errors.
}
}
/**
* Log activity.
*
* @return Promise resolved when done.
*/
protected async logActivity(): Promise<void> {
// To be overridden.
}
/**
* Check the module completion.
*/
protected checkCompletion(): void {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
/**
* Component being destroyed.
*/

View File

@ -97,7 +97,9 @@
<ng-container *ngFor="let module of section.modules">
<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>
</ng-container>
</section>

View File

@ -29,6 +29,7 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import {
CoreCourse,
CoreCourseModuleCompletionStatus,
CoreCourseProvider,
} from '@features/course/services/course';
import {
@ -42,6 +43,7 @@ import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from
import { CoreBlockHelper } from '@features/block/services/block-helper';
import { CoreNavigator } from '@services/navigator';
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.
@ -90,9 +92,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
loaded = false;
highlighted?: string;
lastModuleViewed?: CoreCourseViewedModulesDBRecord;
viewedModules: Record<number, boolean> = {};
completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
protected selectTabObserver?: CoreEventObserver;
protected modViewedObserver?: CoreEventObserver;
protected lastCourseFormat?: string;
protected viewedModulesInitialized = false;
constructor(
protected content: IonContent,
@ -133,6 +140,24 @@ 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;
if (this.selectedSection) {
// Change section to display the one with the last viewed module
const lastViewedSection = this.getViewedModuleSection(this.sections, data);
if (lastViewedSection && lastViewedSection.id !== this.selectedSection?.id) {
this.sectionChanged(lastViewedSection, data.cmId);
}
}
}
});
}
/**
@ -179,8 +204,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.lastCourseFormat = this.course.format;
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
currentSection.highlighted = true;
const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
currentSectionData.section.highlighted = true;
await Promise.all([
this.loadCourseFormatComponent(),
@ -236,6 +261,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
await this.initializeViewedModules();
if (this.selectedSection) {
const selectedSection = this.selectedSection;
// We have a selected section, but the list has changed. Search the section in the list.
@ -243,7 +270,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
if (!newSection) {
// 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);
@ -269,16 +297,75 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
if (!this.loaded) {
// No section specified, not found or not visible, get current section.
const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
// No section specified, not found or not visible, load current section or the section with last module viewed.
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.
const lastModuleSection = this.getViewedModuleSection(sections, lastModuleViewed);
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.sectionChanged(section);
this.sectionChanged(section, moduleId);
}
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;
});
}
/**
* Get the section of a viewed module.
*
* @param sections List of sections.
* @param viewedModule Viewed module.
* @return Section, undefined if not found.
*/
protected getViewedModuleSection(
sections: CoreCourseSection[],
viewedModule: CoreCourseViewedModulesDBRecord,
): CoreCourseSection | undefined {
if (viewedModule.sectionId) {
const lastModuleSection = sections.find(section => section.id === viewedModule.sectionId);
if (lastModuleSection) {
return lastModuleSection;
}
}
// No sectionId or section not found. Search the module.
return sections.find(
section => section.modules.some(module => module.id === viewedModule.cmId),
);
}
/**
* Display the course index modal.
*/
@ -345,8 +432,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* Function called when selected section changes.
*
* @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;
this.selectedSection = newSection;
this.data.section = this.selectedSection;
@ -377,13 +465,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.showMoreActivities();
}
if (this.moduleId && previousValue === undefined) {
// Scroll to module if needed. Give more priority to the input.
const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
if (moduleIdToScroll) {
setTimeout(() => {
CoreDomUtils.scrollToElementBySelector(
this.elementRef.nativeElement,
this.content,
'#core-course-module-' + this.moduleId,
);
this.scrollToModule(moduleIdToScroll);
}, 200);
} else {
this.content.scrollToTop(0);
@ -399,6 +485,19 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.invalidateSectionButtons();
}
/**
* Scroll to a certain module.
*
* @param moduleId Module ID.
*/
protected scrollToModule(moduleId: number): void {
CoreDomUtils.scrollToElementBySelector(
this.elementRef.nativeElement,
this.content,
'#core-course-module-' + moduleId,
);
}
/**
* Compare if two sections are equal.
*
@ -502,7 +601,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @inheritdoc
*/
ngOnDestroy(): void {
this.selectTabObserver && this.selectTabObserver.off();
this.selectTabObserver?.off();
this.modViewedObserver?.off();
}
/**

View File

@ -68,11 +68,11 @@ export class CoreCourseCourseIndexComponent implements OnInit {
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) {
// Highlight current section if none is selected.
this.selectedId = currentSection.id;
this.selectedId = currentSectionData.section.id;
}
// Clone sections to add information.
@ -104,7 +104,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
name: section.name,
availabilityinfo: !!section.availabilityinfo,
expanded: section.id === this.selectedId,
highlighted: currentSection?.id === section.id,
highlighted: currentSectionData.section.id === section.id,
hasVisibleModules: modules.length > 0,
modules: modules,
};

View File

@ -271,6 +271,10 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
{
replace: true,
animationDirection: 'back',
params: {
module: this.module,
openModule: false,
},
},
);
}

View File

@ -99,6 +99,11 @@
</ion-label>
</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>
<!-- Loading. -->

View File

@ -3,6 +3,7 @@
:host {
--horizontal-margin: 10px;
--vertical-margin: 10px;
--core-course-module-not-viewed-border-color: var(--gray-500);
ion-card {
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 {
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;
}
}
}

View File

@ -48,6 +48,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
@Input() showActivityDates = false; // Whether to show activity dates.
@Input() showCompletionConditions = false; // Whether to show activity completion conditions.
@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.
modNameTranslated = '';

View File

@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
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 { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreCourseWSSection } from '@features/course/services/course';
@ -41,12 +41,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
/**
* @inheritdoc
*/
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
async getCurrentSection(
course: CoreCourseAnyCourseData,
sections: CoreCourseSection[],
): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
const now = CoreTimeUtils.timestamp();
if ((course.startdate && now < course.startdate) || (course.enddate && now > course.enddate)) {
// 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++) {
@ -57,12 +63,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
const dates = this.getSectionDates(section, course.startdate || 0);
if (now >= dates.start && now < dates.end) {
return section;
return {
section,
forceSelected: false,
};
}
}
// The section wasn't found, return all sections.
return sections[0];
return {
section: sections[0],
forceSelected: false,
};
}
/**

View File

@ -44,6 +44,7 @@
"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.",
"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.",
"modulenotfound": "Resource or activity not found, please make sure you're online and it's still available.",
"nocontentavailable": "No content available at the moment.",

View File

@ -59,6 +59,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
protected module?: CoreCourseModuleData;
protected modNavOptions?: CoreNavigationOptions;
protected isGuest = false;
protected openModule = true;
protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = {
page: CONTENTS_PAGE_NAME,
title: 'core.course',
@ -138,6 +139,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module');
this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
this.modNavOptions = CoreNavigator.getRouteParam<CoreNavigationOptions>('modNavOptions');
this.openModule = CoreNavigator.getRouteBooleanParam('openModule') ?? true; // If false, just scroll to module.
if (!this.modNavOptions) {
// Fallback to old way of passing params. @deprecated since 4.0.
const modParams = CoreNavigator.getRouteParam<Params>('modParams');
@ -157,6 +159,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
if (this.module) {
this.contentsTab.pageParams.moduleId = this.module.id;
if (!this.contentsTab.pageParams.sectionId && !this.contentsTab.pageParams.sectionNumber) {
// No section specified, use module section.
this.contentsTab.pageParams.sectionId = this.module.section;
}
}
this.tabs.push(this.contentsTab);
@ -172,7 +178,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
* A tab was selected.
*/
tabSelected(): void {
if (!this.module || !this.course) {
if (!this.module || !this.course || !this.openModule) {
return;
}
// Now that the first tab has been selected we can load the module.

View File

@ -2022,6 +2022,7 @@ export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completi
isStealth?: boolean;
handlerData?: CoreCourseModuleHandlerData;
completiondata?: CoreCourseModuleCompletionData;
section: number;
};
/**

View File

@ -26,7 +26,9 @@ import { CoreConstants } from '@/core/constants';
import { makeSingleton, Platform, Translate } from '@singletons';
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 { CoreError } from '@classes/errors/error';
import {
@ -49,6 +51,7 @@ import { lazyMap, LazyMap } from '@/core/utils/lazy-map';
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
import { CoreDatabaseTable } from '@classes/database/database-table';
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
import { SQLiteDB } from '@classes/sqlitedb';
const ROOT_CACHE_KEY = 'mmCourse:';
@ -140,6 +143,7 @@ export class CoreCourseProvider {
protected logger: CoreLogger;
protected statusTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseStatusDBRecord>>>;
protected viewedModulesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseViewedModulesDBRecord, 'courseId' | 'cmId'>>>;
constructor() {
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;
}
/**
* 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.
*
@ -425,6 +465,19 @@ export class CoreCourseProvider {
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.
*
@ -548,7 +601,7 @@ export class CoreCourseProvider {
let foundModule: CoreCourseGetContentsWSModule | undefined;
const foundSection = sections.some((section) => {
const foundSection = sections.find((section) => {
if (section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID &&
sectionId !== undefined &&
sectionId != section.id
@ -562,7 +615,7 @@ export class CoreCourseProvider {
});
if (foundSection && foundModule) {
return this.addAdditionalModuleData(foundModule, courseId);
return this.addAdditionalModuleData(foundModule, courseId, foundSection.id);
}
throw new CoreError(Translate.instant('core.course.modulenotfound'));
@ -573,11 +626,13 @@ export class CoreCourseProvider {
*
* @param module Module.
* @param courseId Course ID of the module.
* @param sectionId Section ID of the module.
* @return Module with additional info.
*/
protected addAdditionalModuleData(
module: CoreCourseGetContentsWSModule,
courseId: number,
sectionId: number,
): CoreCourseModuleData {
let completionData: CoreCourseModuleCompletionData | undefined = undefined;
@ -590,13 +645,12 @@ export class CoreCourseProvider {
};
}
const moduleWithCourse: CoreCourseModuleData = {
return {
...module,
course: courseId,
section: sectionId,
completiondata: completionData,
};
return moduleWithCourse;
}
/**
@ -873,7 +927,7 @@ export class CoreCourseProvider {
// Add course to all modules.
return sections.map((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 || []), []);
}
/**
* 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.
*
@ -1348,6 +1419,34 @@ export class CoreCourseProvider {
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.
*
@ -1770,3 +1869,12 @@ type CoreCompletionUpdateActivityCompletionStatusManuallyWSParams = {
export type CoreCourseAnyModuleData = CoreCourseModuleData | CoreCourseModuleBasicInfo & {
contents?: CoreCourseModuleContentFile[]; // If needed, calculated in the app in loadModuleContents.
};
/**
* Options for storeModuleViewed.
*/
export type CoreCourseStoreModuleViewedOptions = {
sectionId?: number;
timeaccess?: number;
siteId?: string;
};

View File

@ -18,9 +18,10 @@ import { CoreSiteSchema } from '@services/sites';
* Database variables for CoreCourse service.
*/
export const COURSE_STATUS_TABLE = 'course_status';
export const COURSE_VIEWED_MODULES_TABLE = 'course_viewed_modules';
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreCourseProvider',
version: 1,
version: 2,
tables: [
{
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',
},
{
name: 'timecompleted',
name: 'timeaccess',
type: 'INTEGER',
},
],
@ -102,6 +126,13 @@ export type CoreCourseStatusDBRecord = {
previousDownloadTime: number;
};
export type CoreCourseViewedModulesDBRecord = {
courseId: number;
cmId: number;
timeaccess: number;
sectionId?: number;
};
export type CoreCourseManualCompletionDBRecord = {
cmid: number;
completed: number;

View File

@ -98,9 +98,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
*
* @param course The course to get the title.
* @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.
@ -299,21 +303,37 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
*
* @param course The course to get the title.
* @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 {
const section = await this.executeFunctionOnEnabled<T>(
const sectionData = await this.executeFunctionOnEnabled<CoreCourseFormatCurrentSectionData<T> | T>(
course.format || '',
'getCurrentSection',
[course, sections],
);
return section || sections[0];
} catch {
// This function should never fail. Just return the first section (usually, "All sections").
return 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 {
// This function should never fail.
}
// 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 type CoreCourseFormatCurrentSectionData<T = CoreCourseSection> = {
section: T; // Current section.
forceSelected: boolean; // If true, the app will force selecting the section when opening the course.
};

View File

@ -18,7 +18,7 @@ import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
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.
@ -74,7 +74,10 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
/**
* @inheritdoc
*/
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
async getCurrentSection(
course: CoreCourseAnyCourseData,
sections: CoreCourseSection[],
): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
let marker: number | undefined;
// 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);
if (section) {
return section;
return {
section,
forceSelected: true,
};
}
}
// Marked section not found or we couldn't retrieve the marker. Return all sections.
return sections[0];
return {
section: sections[0],
forceSelected: false,
};
}
/**

View File

@ -54,6 +54,8 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
totalColumnsSpan?: number;
withinSplitView?: boolean;
protected fetchSuccess = false;
constructor(
protected route: ActivatedRoute,
protected element: ElementRef<HTMLElement>,
@ -93,7 +95,6 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
await this.courses?.start();
await this.fetchInitialGrades();
await CoreGrades.logCourseGradesView(this.courseId, this.userId);
}
/**
@ -198,6 +199,11 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
this.columns = formattedTable.columns;
this.rows = formattedTable.rows;
this.totalColumnsSpan = formattedTable.columns.reduce((total, column) => total + column.colspan, 0);
if (!this.fetchSuccess) {
this.fetchSuccess = true;
await CoreGrades.logCourseGradesView(this.courseId, this.userId);
}
}
}

View File

@ -54,6 +54,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
newsForumModule?: CoreCourseModuleData;
protected updateSiteObserver: CoreEventObserver;
protected fetchSuccess = false;
constructor() {
// Refresh the enabled flags if site is updated.
@ -137,13 +138,15 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
this.hasContent = result.hasContent || this.hasContent;
}
// Add log in Moodle.
if (!this.fetchSuccess) {
this.fetchSuccess = true;
CoreUtils.ignoreErrors(CoreCourse.logView(
this.siteHomeId,
undefined,
undefined,
this.currentSite.getInfo()?.sitename,
));
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
}

View File

@ -48,6 +48,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
protected site!: CoreSite;
protected obsProfileRefreshed: CoreEventObserver;
protected subscription?: Subscription;
protected fetchSuccess = false;
userLoaded = false;
isLoadingHandlers = false;
@ -106,18 +107,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
try {
await this.fetchUser();
if (!this.user) {
return;
}
try {
await CoreUser.logView(this.userId, this.courseId, this.user.fullname);
} catch (error) {
this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted';
this.isSuspended = error?.errorcode === 'wsaccessusersuspended';
this.isEnrolled = error?.errorcode !== 'notenrolledprofile';
}
} finally {
this.userLoaded = true;
}
@ -162,6 +151,18 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
this.isLoadingHandlers = !CoreUserDelegate.areHandlersLoaded(user.id, context, this.courseId);
});
if (!this.fetchSuccess) {
this.fetchSuccess = true;
try {
await CoreUser.logView(this.userId, this.courseId, this.user.fullname);
} catch (error) {
this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted';
this.isSuspended = error?.errorcode === 'wsaccessusersuspended';
this.isEnrolled = error?.errorcode !== 'notenrolledprofile';
}
}
} catch (error) {
// Error is null for deleted users, do not show the modal.
CoreDomUtils.showErrorModal(error);

View File

@ -145,6 +145,10 @@ export const SITE_SCHEMA: CoreSiteSchema = {
name: 'data',
type: 'TEXT',
},
{
name: 'timeaccess',
type: 'INTEGER',
},
],
primaryKeys: ['component', 'id'],
},

View File

@ -60,6 +60,7 @@ export interface CoreEventsData {
[CoreEvents.FILE_SHARED]: CoreEventFileSharedData;
[CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData;
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
[CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
}
/*
@ -110,6 +111,7 @@ export class CoreEvents {
static readonly FORM_ACTION = 'form_action';
static readonly ACTIVITY_DATA_SENT = 'activity_data_sent';
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 observables: { [eventName: string]: Subject<unknown> } = {};
@ -427,3 +429,13 @@ export type CoreEventAppLaunchedData = {
export type CoreEventOrientationData = {
orientation: CoreScreenOrientation;
};
/**
* Data passed to COURSE_MODULE_VIEWED event.
*/
export type CoreEventCourseModuleViewed = {
courseId: number;
cmId: number;
timeaccess: number;
sectionId?: number;
};

View File

@ -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.
- fillContextMenu, expandDescription, gotoBlog, prefetch and removeFiles functions have been removed from CoreCourseModuleMainResourceComponent.
- 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).