commit
09b962cf04
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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) });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
await AddonModData.logView(this.database.id, this.database.name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
|
||||
try {
|
||||
await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
|
||||
|
||||
CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
await this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,13 +84,31 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchData(): Promise<void> {
|
||||
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
|
||||
try {
|
||||
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
|
||||
|
||||
this.attempt = await AddonModH5PActivity.getAttemptResults(this.h5pActivity.id, this.attemptId, {
|
||||
cmId: this.cmId,
|
||||
});
|
||||
this.attempt = await AddonModH5PActivity.getAttemptResults(this.h5pActivity.id, this.attemptId, {
|
||||
cmId: this.cmId,
|
||||
});
|
||||
|
||||
await this.fetchUserProfile();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
await this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,12 +87,30 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchData(): Promise<void> {
|
||||
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
|
||||
try {
|
||||
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
|
||||
|
||||
await Promise.all([
|
||||
this.fetchAttempts(),
|
||||
this.fetchUserProfile(),
|
||||
]);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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> {
|
||||
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
|
||||
try {
|
||||
this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
|
||||
|
||||
await Promise.all([
|
||||
this.fetchUsers(refresh),
|
||||
]);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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) });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
}];
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
}];
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -271,6 +271,10 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
|
|||
{
|
||||
replace: true,
|
||||
animationDirection: 'back',
|
||||
params: {
|
||||
module: this.module,
|
||||
openModule: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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. -->
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -2022,6 +2022,7 @@ export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completi
|
|||
isStealth?: boolean;
|
||||
handlerData?: CoreCourseModuleHandlerData;
|
||||
completiondata?: CoreCourseModuleCompletionData;
|
||||
section: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
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. Just return the first section (usually, "All sections").
|
||||
return sections[0];
|
||||
// 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.
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
CoreUtils.ignoreErrors(CoreCourse.logView(
|
||||
this.siteHomeId,
|
||||
undefined,
|
||||
undefined,
|
||||
this.currentSite.getInfo()?.sitename,
|
||||
));
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -145,6 +145,10 @@ export const SITE_SCHEMA: CoreSiteSchema = {
|
|||
name: 'data',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'timeaccess',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['component', 'id'],
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue