commit
7353bfac79
|
@ -408,7 +408,6 @@
|
|||
"addon.mod_assign.ungroupedusersoptional": "assign",
|
||||
"addon.mod_assign.unlimitedattempts": "assign",
|
||||
"addon.mod_assign.userswhoneedtosubmit": "assign",
|
||||
"addon.mod_assign.userwithid": "local_moodlemobileapp",
|
||||
"addon.mod_assign.viewsubmission": "assign",
|
||||
"addon.mod_assign.warningsubmissiongrademodified": "local_moodlemobileapp",
|
||||
"addon.mod_assign.warningsubmissionmodified": "local_moodlemobileapp",
|
||||
|
@ -1041,7 +1040,6 @@
|
|||
"addon.notes.personalnotes": "notes",
|
||||
"addon.notes.publishstate": "notes",
|
||||
"addon.notes.sitenotes": "notes",
|
||||
"addon.notes.userwithid": "local_moodlemobileapp",
|
||||
"addon.notes.warningnotenotsent": "local_moodlemobileapp",
|
||||
"addon.notifications.errorgetnotifications": "local_moodlemobileapp",
|
||||
"addon.notifications.markallread": "moodle",
|
||||
|
@ -1447,6 +1445,17 @@
|
|||
"core.course.askadmintosupport": "local_moodlemobileapp",
|
||||
"core.course.availablespace": "local_moodlemobileapp",
|
||||
"core.course.cannotdeletewhiledownloading": "local_moodlemobileapp",
|
||||
"core.course.completion_automatic:done": "course",
|
||||
"core.course.completion_automatic:failed": "course",
|
||||
"core.course.completion_automatic:todo": "course",
|
||||
"core.course.completion_manual:aria:done": "course",
|
||||
"core.course.completion_manual:aria:markdone": "course",
|
||||
"core.course.completion_manual:markdone": "course",
|
||||
"core.course.completion_setby:auto:done": "course",
|
||||
"core.course.completion_setby:auto:todo": "course",
|
||||
"core.course.completion_setby:manual:done": "course",
|
||||
"core.course.completion_setby:manual:markdone": "course",
|
||||
"core.course.completionrequirements": "course",
|
||||
"core.course.confirmdeletemodulefiles": "local_moodlemobileapp",
|
||||
"core.course.confirmdeletestoreddata": "local_moodlemobileapp",
|
||||
"core.course.confirmdownload": "local_moodlemobileapp",
|
||||
|
@ -1470,6 +1479,7 @@
|
|||
"core.course.nocontentavailable": "local_moodlemobileapp",
|
||||
"core.course.overriddennotice": "grades",
|
||||
"core.course.refreshcourse": "local_moodlemobileapp",
|
||||
"core.course.section": "moodle",
|
||||
"core.course.sections": "moodle",
|
||||
"core.course.useactivityonbrowser": "local_moodlemobileapp",
|
||||
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
|
||||
|
@ -2198,6 +2208,7 @@
|
|||
"core.user.sendemail": "local_moodlemobileapp",
|
||||
"core.user.student": "moodle/defaultcoursestudent",
|
||||
"core.user.teacher": "moodle/noneditingteacher",
|
||||
"core.user.userwithid": "local_moodlemobileapp",
|
||||
"core.user.webpage": "moodle",
|
||||
"core.userdeleted": "moodle",
|
||||
"core.userdetails": "moodle",
|
||||
|
|
|
@ -30,6 +30,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<!-- Description and intro attachments. -->
|
||||
<ion-card *ngIf="description">
|
||||
<ion-item class="ion-text-wrap">
|
||||
|
@ -141,5 +146,4 @@
|
|||
<addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
|
||||
[moduleId]="module.id">
|
||||
</addon-mod-assign-submission>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="fromDate && !isSubmittedForGrading">
|
||||
<ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading">
|
||||
<ion-label>
|
||||
<p *ngIf="assign!.intro"
|
||||
[innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
|
||||
|
@ -66,7 +66,7 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="assign!.duedate && !isSubmittedForGrading">
|
||||
<ion-item class="ion-text-wrap" *ngIf="showDates && assign!.duedate && !isSubmittedForGrading">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
|
||||
<p *ngIf="assign!.duedate" >{{ assign!.duedate * 1000 | coreFormatDate }}</p>
|
||||
|
|
|
@ -122,6 +122,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
|||
allowAddAttempt = false; // Allow adding a new attempt when grading.
|
||||
gradeUrl?: string; // URL to grade in browser.
|
||||
isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
|
||||
showDates = false; // Whether to show some dates.
|
||||
|
||||
// Some constants.
|
||||
statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW;
|
||||
|
@ -181,6 +182,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
|||
*/
|
||||
ngOnInit(): void {
|
||||
this.isSubmittedForGrading = !!this.submitId;
|
||||
this.showDates = !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.11');
|
||||
|
||||
this.loadData(true);
|
||||
}
|
||||
|
|
|
@ -95,7 +95,6 @@
|
|||
"ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.",
|
||||
"ungroupedusersoptional": "The setting 'Students submit in groups' is enabled and some users are either not a member of any group, or are a member of more than one group. Please be aware that these students will submit as members of the 'Default group'.",
|
||||
"unlimitedattempts": "Unlimited",
|
||||
"userwithid": "User with ID {{id}}",
|
||||
"userswhoneedtosubmit": "Users who need to submit: {{$a}}",
|
||||
"viewsubmission": "View submission",
|
||||
"warningsubmissionmodified": "The user submission was modified on the site.",
|
||||
|
|
|
@ -23,6 +23,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>
|
||||
|
||||
|
|
|
@ -26,6 +26,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -28,6 +28,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -39,6 +39,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -28,6 +28,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="discussions.loaded" class="core-loading-center">
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description *ngIf="forum && forum.type != 'single'"
|
||||
[description]="description" [component]="component" [componentId]="componentId" [note]="descriptionNote"
|
||||
contextLevel="module" [contextInstanceId]="module && module.id" [courseId]="courseId">
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
</core-search-box>
|
||||
|
||||
<core-loading [hideUntil]="entries.loaded" class="core-loading-center">
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -32,6 +32,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -28,6 +28,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<ion-card class="core-warning-card" *ngIf="warning">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||
|
|
|
@ -71,5 +71,12 @@ export class AddonModLabelModuleHandlerService implements CoreCourseModuleHandle
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
manualCompletionAlwaysShown(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModLabelModuleHandler = makeSingleton(AddonModLabelModuleHandlerService);
|
||||
|
|
|
@ -31,6 +31,12 @@
|
|||
<!-- Index/Preview tab. -->
|
||||
<core-tab [title]="'addon.mod_lesson.preview' | translate" (ionSelect)="indexSelected()">
|
||||
<ng-template>
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -18,6 +18,12 @@
|
|||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description *ngIf="lti && lti.showdescriptionlaunch" [description]="description" [component]="component"
|
||||
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component"
|
||||
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -27,6 +27,12 @@
|
|||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -20,6 +20,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description *ngIf="mode != 'iframe' && (mode != 'embedded' || displayDescription)"
|
||||
[description]="description" [component]="component" [componentId]="componentId" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId">
|
||||
|
|
|
@ -28,6 +28,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description *ngIf="survey && !survey.surveydone && !hasOffline" [description]="description"
|
||||
[component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
|
|
|
@ -15,6 +15,11 @@
|
|||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component"
|
||||
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
|
|
@ -47,6 +47,12 @@
|
|||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<div *ngIf="description || pageIsOffline || hasOffline || pageWarning">
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
|
|
|
@ -27,6 +27,12 @@
|
|||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="showCompletion" [module]="module" [showManualCompletion]="true"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<ion-card class="with-borders" *ngIf="phases">
|
||||
<ion-item button (click)="viewPhaseInfo()" detail="true">
|
||||
<ion-label>
|
||||
|
|
|
@ -10,6 +10,5 @@
|
|||
"personalnotes": "Personal notes",
|
||||
"publishstate": "Context",
|
||||
"sitenotes": "Site notes",
|
||||
"userwithid": "User with ID {{id}}",
|
||||
"warningnotenotsent": "Couldn't add note(s) to course {{course}}. {{error}}"
|
||||
}
|
|
@ -370,7 +370,7 @@ export class AddonNotesProvider {
|
|||
|
||||
return;
|
||||
}).catch(() => {
|
||||
note.userfullname = Translate.instant('addon.notes.userwithid', { id: note.userid });
|
||||
note.userfullname = Translate.instant('core.user.userwithid', { id: note.userid });
|
||||
}));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
|
|
@ -1826,15 +1826,15 @@ export class CoreSite {
|
|||
* @return Object with major and minor. Returns false if invalid version.
|
||||
*/
|
||||
protected getMajorAndMinor(version: string): {major: string; minor: number} | false {
|
||||
const match = version.match(/(\d)+(?:\.(\d)+)?(?:\.(\d)+)?/);
|
||||
const match = version.match(/^(\d+)(\.(\d+)(\.\d+)?)?/);
|
||||
if (!match || !match[1]) {
|
||||
// Invalid version.
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
major: match[1] + '.' + (match[2] || '0'),
|
||||
minor: parseInt(match[3], 10) || 0,
|
||||
major: match[1] + '.' + (match[3] || '0'),
|
||||
minor: parseInt(match[5], 10) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -122,7 +122,14 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
this.syncIcon = CoreConstants.ICON_LOADING;
|
||||
|
||||
try {
|
||||
await CoreUtils.ignoreErrors(this.invalidateContent());
|
||||
await CoreUtils.ignoreErrors(Promise.all([
|
||||
this.invalidateContent(),
|
||||
this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined,
|
||||
]));
|
||||
|
||||
if (this.showCompletion) {
|
||||
this.module = await CoreCourse.getModule(this.module.id, this.courseId);
|
||||
}
|
||||
|
||||
await this.loadContent(true, sync, showErrors);
|
||||
} finally {
|
||||
|
|
|
@ -70,13 +70,16 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
|||
isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu.
|
||||
contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
|
||||
contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
|
||||
showCompletion = false; // Whether to show completion inside the activity.
|
||||
|
||||
protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
|
||||
protected isCurrentView = false; // Whether the component is in the current view.
|
||||
protected siteId?: string; // Current Site ID.
|
||||
protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called.
|
||||
protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
|
||||
protected completionObserver?: CoreEventObserver;
|
||||
protected logger: CoreLogger;
|
||||
protected debouncedUpdateModule?: () => void; // Update the module after a certain time.
|
||||
|
||||
constructor(
|
||||
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
|
||||
|
@ -94,6 +97,22 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
|||
this.componentId = this.module.id;
|
||||
this.externalUrl = this.module.url;
|
||||
this.courseId = this.courseId || this.module.course!;
|
||||
this.showCompletion = !!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.11');
|
||||
|
||||
if (this.showCompletion) {
|
||||
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_MODULE_VIEWED, async (data) => {
|
||||
if (data && data.cmId == this.module.id) {
|
||||
await CoreCourse.invalidateModule(this.module.id);
|
||||
|
||||
this.fetchModule();
|
||||
}
|
||||
});
|
||||
|
||||
this.debouncedUpdateModule = CoreUtils.debounce(() => {
|
||||
this.fetchModule();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
this.blog = await AddonBlog.isPluginEnabled();
|
||||
}
|
||||
|
||||
|
@ -140,7 +159,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
|||
this.refreshIcon = CoreConstants.ICON_LOADING;
|
||||
|
||||
try {
|
||||
await CoreUtils.ignoreErrors(this.invalidateContent());
|
||||
await CoreUtils.ignoreErrors(Promise.all([
|
||||
this.invalidateContent(),
|
||||
this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined,
|
||||
]));
|
||||
|
||||
if (this.showCompletion) {
|
||||
this.fetchModule();
|
||||
}
|
||||
|
||||
await this.loadContent(true);
|
||||
} finally {
|
||||
|
@ -376,6 +402,31 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The completion of the modules has changed.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async onCompletionChange(): Promise<void> {
|
||||
// Update the module data after a while.
|
||||
this.debouncedUpdateModule?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch module.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchModule(): Promise<void> {
|
||||
const module = await CoreCourse.getModule(this.module.id, this.courseId);
|
||||
|
||||
CoreCourseHelper.calculateModuleCompletionData(module, this.courseId);
|
||||
|
||||
await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, module);
|
||||
|
||||
this.module = module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
|
@ -384,6 +435,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
|||
this.contextMenuStatusObserver?.off();
|
||||
this.contextFileStatusObserver?.off();
|
||||
this.statusObserver?.off();
|
||||
this.completionObserver?.off();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
|
||||
/**
|
||||
* Base class for completion components.
|
||||
*/
|
||||
@Component({
|
||||
template: '',
|
||||
})
|
||||
export class CoreCourseModuleCompletionBaseComponent implements OnChanges {
|
||||
|
||||
@Input() completion?: CoreCourseModuleCompletionData; // The completion status.
|
||||
@Input() moduleId?: number; // The name of the module this completion affects.
|
||||
@Input() moduleName?: string; // The name of the module this completion affects.
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes.
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.completion && this.completion) {
|
||||
this.calculateData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate data to render the completion.
|
||||
*/
|
||||
protected calculateData(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
|
@ -23,13 +23,19 @@ import { CoreCourseModuleDescriptionComponent } from './module-description/modul
|
|||
import { CoreCourseSectionSelectorComponent } from './section-selector/section-selector';
|
||||
import { CoreCourseTagAreaComponent } from './tag-area/tag-area';
|
||||
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
|
||||
import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy';
|
||||
import { CoreCourseModuleInfoComponent } from './module-info/module-info';
|
||||
import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCourseFormatComponent,
|
||||
CoreCourseModuleComponent,
|
||||
CoreCourseModuleCompletionComponent,
|
||||
CoreCourseModuleCompletionLegacyComponent,
|
||||
CoreCourseModuleDescriptionComponent,
|
||||
CoreCourseModuleInfoComponent,
|
||||
CoreCourseModuleManualCompletionComponent,
|
||||
CoreCourseSectionSelectorComponent,
|
||||
CoreCourseTagAreaComponent,
|
||||
CoreCourseUnsupportedModuleComponent,
|
||||
|
@ -42,7 +48,10 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup
|
|||
CoreCourseFormatComponent,
|
||||
CoreCourseModuleComponent,
|
||||
CoreCourseModuleCompletionComponent,
|
||||
CoreCourseModuleCompletionLegacyComponent,
|
||||
CoreCourseModuleDescriptionComponent,
|
||||
CoreCourseModuleInfoComponent,
|
||||
CoreCourseModuleManualCompletionComponent,
|
||||
CoreCourseSectionSelectorComponent,
|
||||
CoreCourseTagAreaComponent,
|
||||
CoreCourseUnsupportedModuleComponent,
|
||||
|
|
|
@ -146,7 +146,8 @@
|
|||
<ng-container *ngFor="let module of section.modules">
|
||||
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [courseId]="course?.id"
|
||||
[downloadEnabled]="downloadEnabled" [section]="section" (completionChanged)="onCompletionChange($event)"
|
||||
(statusChanged)="onModuleStatusChange()">
|
||||
(statusChanged)="onModuleStatusChange()" [showActivityDates]="course?.showactivitydates"
|
||||
[showCompletionConditions]="course?.showcompletionconditions">
|
||||
</core-course-module>
|
||||
</ng-container>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<img *ngIf="completion && completion.tracking !== 1" [src]="completionImage" [alt]="completionDescription">
|
||||
|
||||
<ion-button
|
||||
fill="clear"
|
||||
*ngIf="completion && completion.tracking === 1"
|
||||
(click)="completionClicked($event)"
|
||||
[title]="completionDescription">
|
||||
<img [src]="completionImage" role="presentation" alt="">
|
||||
</ion-button>
|
|
@ -0,0 +1,20 @@
|
|||
:host {
|
||||
min-width: var(--a11y-min-target-size);
|
||||
min-height: var(--a11y-min-target-size);
|
||||
--size: 30px;
|
||||
|
||||
img {
|
||||
padding: 5px;
|
||||
width: var(--size);
|
||||
vertical-align: middle;
|
||||
max-width: none;
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--padding-top: 0;
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
--padding-bottom: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
|
||||
/**
|
||||
* Component to handle activity completion in sites previous to 3.11.
|
||||
* It shows a checkbox with the current status, and allows manually changing the completion if it's allowed.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-course-module-completion-legacy [completion]="module.completiondata" [moduleName]="module.name"
|
||||
* (completionChanged)="completionChanged()"></core-course-module-completion-legacy>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-module-completion-legacy',
|
||||
templateUrl: 'core-course-module-completion-legacy.html',
|
||||
styleUrls: ['module-completion-legacy.scss'],
|
||||
})
|
||||
export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent {
|
||||
|
||||
completionImage?: string;
|
||||
completionDescription?: string;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async calculateData(): Promise<void> {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = this.moduleName || '';
|
||||
let langKey: string | undefined;
|
||||
let image: string | undefined;
|
||||
|
||||
if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-manual-n';
|
||||
langKey = 'core.completion-alt-manual-n';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||
image = 'completion-manual-y';
|
||||
langKey = 'core.completion-alt-manual-y';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-auto-n';
|
||||
langKey = 'core.completion-alt-auto-n';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||
image = 'completion-auto-y';
|
||||
langKey = 'core.completion-alt-auto-y';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_PASS) {
|
||||
image = 'completion-auto-pass';
|
||||
langKey = 'core.completion-alt-auto-pass';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_FAIL) {
|
||||
image = 'completion-auto-fail';
|
||||
langKey = 'core.completion-alt-auto-fail';
|
||||
}
|
||||
|
||||
if (image) {
|
||||
if (this.completion.overrideby && this.completion.overrideby > 0) {
|
||||
image += '-override';
|
||||
}
|
||||
this.completionImage = 'assets/img/completion/' + image + '.svg';
|
||||
}
|
||||
|
||||
if (!moduleName || !this.moduleId || !langKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await CoreFilterHelper.getFiltersAndFormatText(
|
||||
moduleName,
|
||||
'module',
|
||||
this.moduleId,
|
||||
{ clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId },
|
||||
);
|
||||
|
||||
let translateParams: Record<string, unknown> = {
|
||||
$a: result.text,
|
||||
};
|
||||
|
||||
if (this.completion.overrideby && this.completion.overrideby > 0) {
|
||||
langKey += '-override';
|
||||
|
||||
const profile = await CoreUser.getProfile(this.completion.overrideby, this.completion.courseId, true);
|
||||
|
||||
translateParams = {
|
||||
$a: {
|
||||
overrideuser: profile.fullname,
|
||||
modname: result.text,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
this.completionDescription = Translate.instant(langKey, translateParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion clicked.
|
||||
*
|
||||
* @param event The click event.
|
||||
*/
|
||||
async completionClicked(event: Event): Promise<void> {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
||||
|
||||
this.calculateData();
|
||||
|
||||
this.completionChanged.emit(this.completion);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,32 @@
|
|||
<img *ngIf="completion && completion.tracking !== 1" [src]="completionImage" [alt]="completionDescription">
|
||||
<div *ngIf="showCompletionConditions && completion && completion.isautomatic" class="core-module-automatic-completion-conditions"
|
||||
role="list" [attr.aria-label]="'core.course.completionrequirements' | translate:{ $a: moduleName }">
|
||||
|
||||
<ion-button
|
||||
fill="clear"
|
||||
*ngIf="completion && completion.tracking === 1"
|
||||
(click)="completionClicked($event)"
|
||||
[title]="completionDescription">
|
||||
<img [src]="completionImage" role="presentation" alt="">
|
||||
</ion-button>
|
||||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ng-container *ngFor="let rule of details">
|
||||
<ion-badge *ngIf="rule.statuscomplete" color="success" role="listitem"
|
||||
[attr.aria-label]="rule.accessibleDescription">
|
||||
<strong>{{ 'core.course.completion_automatic:done' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
|
||||
<ion-badge *ngIf="rule.statuscompletefail" color="danger" role="listitem"
|
||||
[attr.aria-label]="rule.accessibleDescription">
|
||||
<strong>{{ 'core.course.completion_automatic:failed' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
|
||||
<ion-badge *ngIf="rule.statusincomplete" color="medium" role="listitem"
|
||||
[attr.aria-label]="rule.accessibleDescription">
|
||||
<strong>{{ 'core.course.completion_automatic:todo' | translate }}</strong> {{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!completion.istrackeduser">
|
||||
<ion-badge *ngFor="let rule of details" color="light" role="listitem">
|
||||
{{ rule.rulevalue.description }}
|
||||
</ion-badge>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<core-course-module-manual-completion *ngIf="showManualCompletion" [completion]="completion" [moduleName]="moduleName"
|
||||
(completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-manual-completion>
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
:host {
|
||||
min-width: var(--a11y-min-target-size);
|
||||
min-height: var(--a11y-min-target-size);
|
||||
--size: 30px;
|
||||
.core-module-automatic-completion-conditions {
|
||||
ion-badge {
|
||||
font-weight: normal;
|
||||
margin-right: 5px;
|
||||
|
||||
img {
|
||||
padding: 5px;
|
||||
width: var(--size);
|
||||
vertical-align: middle;
|
||||
max-width: none;
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--padding-top: 0;
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
--padding-bottom: 0;
|
||||
&[color="medium"] {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
||||
import { CoreCourseModuleWSRuleDetails, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
import { Translate } from '@singletons';
|
||||
|
||||
/**
|
||||
|
@ -35,142 +33,53 @@ import { Translate } from '@singletons';
|
|||
templateUrl: 'core-course-module-completion.html',
|
||||
styleUrls: ['module-completion.scss'],
|
||||
})
|
||||
export class CoreCourseModuleCompletionComponent implements OnChanges {
|
||||
export class CoreCourseModuleCompletionComponent extends CoreCourseModuleCompletionBaseComponent {
|
||||
|
||||
@Input() completion?: CoreCourseModuleCompletionData; // The completion status.
|
||||
@Input() moduleId?: number; // The name of the module this completion affects.
|
||||
@Input() moduleName?: string; // The name of the module this completion affects.
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes.
|
||||
@Input() showCompletionConditions = false; // Whether to show activity completion conditions.
|
||||
@Input() showManualCompletion = false; // Whether to show manual completion.
|
||||
|
||||
completionImage?: string;
|
||||
completionDescription?: string;
|
||||
details?: CompletionRule[];
|
||||
accessibleDescription: string | null = null;
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.completion && this.completion) {
|
||||
this.showStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion clicked.
|
||||
*
|
||||
* @param e The click event.
|
||||
*/
|
||||
async completionClicked(e: Event): Promise<void> {
|
||||
if (!this.completion) {
|
||||
protected async calculateData(): Promise<void> {
|
||||
if (!this.completion?.details) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.completion.cmid == 'undefined' || this.completion.tracking !== 1) {
|
||||
return;
|
||||
}
|
||||
// Format rules.
|
||||
this.details = await Promise.all(this.completion.details.map(async (rule: CompletionRule) => {
|
||||
rule.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE ||
|
||||
rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_PASS;
|
||||
rule.statuscompletefail = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_FAIL;
|
||||
rule.statusincomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_INCOMPLETE;
|
||||
rule.accessibleDescription = null;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.completion!.overrideby) {
|
||||
const fullName = await CoreUser.getUserFullNameWithDefault(this.completion!.overrideby, this.completion!.courseId);
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
this.completion.state = this.completion.state === 1 ? 0 : 1;
|
||||
|
||||
try {
|
||||
const response = await CoreCourse.markCompletedManually(
|
||||
this.completion.cmid,
|
||||
this.completion.state === 1,
|
||||
this.completion.courseId!,
|
||||
this.completion.courseName,
|
||||
);
|
||||
|
||||
if (this.completion.valueused === false) {
|
||||
this.showStatus();
|
||||
if (response.offline) {
|
||||
this.completion.offline = true;
|
||||
}
|
||||
}
|
||||
this.completionChanged.emit(this.completion);
|
||||
} catch (error) {
|
||||
this.completion.state = this.completion.state === 1 ? 0 : 1;
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.errorchangecompletion', true);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set image and description to show as completion icon.
|
||||
*/
|
||||
protected async showStatus(): Promise<void> {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = this.moduleName || '';
|
||||
let langKey: string | undefined;
|
||||
let image: string | undefined;
|
||||
|
||||
if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-manual-n';
|
||||
langKey = 'core.completion-alt-manual-n';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||
image = 'completion-manual-y';
|
||||
langKey = 'core.completion-alt-manual-y';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||
image = 'completion-auto-n';
|
||||
langKey = 'core.completion-alt-auto-n';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||
image = 'completion-auto-y';
|
||||
langKey = 'core.completion-alt-auto-y';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_PASS) {
|
||||
image = 'completion-auto-pass';
|
||||
langKey = 'core.completion-alt-auto-pass';
|
||||
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_FAIL) {
|
||||
image = 'completion-auto-fail';
|
||||
langKey = 'core.completion-alt-auto-fail';
|
||||
}
|
||||
|
||||
if (image) {
|
||||
if (this.completion.overrideby > 0) {
|
||||
image += '-override';
|
||||
}
|
||||
this.completionImage = 'assets/img/completion/' + image + '.svg';
|
||||
}
|
||||
|
||||
if (!moduleName || !this.moduleId || !langKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await CoreFilterHelper.getFiltersAndFormatText(
|
||||
moduleName,
|
||||
'module',
|
||||
this.moduleId,
|
||||
{ clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId },
|
||||
);
|
||||
|
||||
let translateParams: Record<string, unknown> = {
|
||||
$a: result.text,
|
||||
};
|
||||
|
||||
if (this.completion.overrideby > 0) {
|
||||
langKey += '-override';
|
||||
|
||||
const profile = await CoreUser.getProfile(this.completion.overrideby, this.completion.courseId, true);
|
||||
|
||||
translateParams = {
|
||||
const setByData = {
|
||||
$a: {
|
||||
overrideuser: profile.fullname,
|
||||
modname: result.text,
|
||||
condition: rule.rulevalue.description,
|
||||
setby: fullName,
|
||||
},
|
||||
};
|
||||
const overrideStatus = rule.statuscomplete ? 'done' : 'todo';
|
||||
|
||||
rule.accessibleDescription = Translate.instant('core.course.completion_setby:auto:' + overrideStatus, setByData);
|
||||
}
|
||||
|
||||
this.completionDescription = Translate.instant(langKey, translateParams);
|
||||
return rule;
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CompletionRule = CoreCourseModuleWSRuleDetails & {
|
||||
statuscomplete?: boolean;
|
||||
statuscompletefail?: boolean;
|
||||
statusincomplete?: boolean;
|
||||
accessibleDescription?: string | null;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<ion-card *ngIf="module.dates?.length || (module.completiondata && (module.completiondata.isautomatic || showManualCompletion))">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<!-- Activity dates. -->
|
||||
<div *ngIf="module.dates && module.dates.length" class="core-module-dates">
|
||||
<p *ngFor="let date of module.dates">
|
||||
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata"
|
||||
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="true"
|
||||
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
|
@ -0,0 +1,31 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { CoreCourseModule, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
|
||||
/**
|
||||
* Display info about a module: dates and completion.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-module-info',
|
||||
templateUrl: 'core-course-module-info.html',
|
||||
})
|
||||
export class CoreCourseModuleInfoComponent {
|
||||
|
||||
@Input() module!: CoreCourseModule; // The module to render.
|
||||
@Input() showManualCompletion = false; // Whether to show manual completion.
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes.
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<div *ngIf="completion && !completion.isautomatic" class="core-module-manual-completion">
|
||||
|
||||
<ng-container *ngIf="completion.istrackeduser">
|
||||
<ng-container *ngIf="completion.state">
|
||||
<ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription"
|
||||
(click)="completionClicked($event)">
|
||||
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.course.completion_manual:done' | translate }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!completion.state">
|
||||
<ion-button color="light" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!completion.istrackeduser">
|
||||
<ion-button disabled="true" color="light">
|
||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
:host {
|
||||
ion-button {
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
|
||||
/**
|
||||
* Component to display a button for manual completion.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-course-module-manual-completion',
|
||||
templateUrl: 'core-course-module-manual-completion.html',
|
||||
styleUrls: ['module-manual-completion.scss'],
|
||||
})
|
||||
export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input() completion?: CoreCourseModuleCompletionData; // The completion status.
|
||||
@Input() moduleName?: string; // The name of the module this completion affects.
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes.
|
||||
|
||||
accessibleDescription: string | null = null;
|
||||
|
||||
protected manualChangedObserver?: CoreEventObserver;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => {
|
||||
if (!this.completion || this.completion.cmid != data.completion.cmid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completion = data.completion;
|
||||
this.calculateData();
|
||||
this.completionChanged.emit(this.completion);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.completion && this.completion) {
|
||||
this.calculateData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async calculateData(): Promise<void> {
|
||||
if (!this.completion || this.completion.isautomatic) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set an accessible description for manual completions with overridden completion state.
|
||||
if (this.completion.overrideby) {
|
||||
const fullName = await CoreUser.getUserFullNameWithDefault(this.completion.overrideby, this.completion.courseId);
|
||||
|
||||
const setByData = {
|
||||
$a: {
|
||||
activityname: this.moduleName,
|
||||
setby: fullName,
|
||||
},
|
||||
};
|
||||
const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
|
||||
this.accessibleDescription = Translate.instant('core.course.' + setByLangKey, setByData);
|
||||
} else {
|
||||
const langKey = this.completion.state ? 'completion_manual:aria:done' : 'completion_manual:aria:markdone';
|
||||
this.accessibleDescription = Translate.instant('core.course.' + langKey, { $a: this.moduleName });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion clicked.
|
||||
*
|
||||
* @param event The click event.
|
||||
*/
|
||||
async completionClicked(event: Event): Promise<void> {
|
||||
if (!this.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
||||
|
||||
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.manualChangedObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
(click)="moduleClicked($event)"
|
||||
[attr.aria-label]="module.handlerData.a11yTitle"
|
||||
[ngClass]="{
|
||||
'has-module-description': module.description,
|
||||
'has-module-info': hasInfo,
|
||||
'item-media': module.handlerData.icon,
|
||||
'item-dimmed': module.visible === 0 || module.uservisible === false
|
||||
}"
|
||||
|
@ -50,12 +50,13 @@
|
|||
slot="end"
|
||||
*ngIf="module.uservisible !== false"
|
||||
class="buttons core-module-buttons"
|
||||
[ngClass]="{'core-button-completion': module.completiondata}"
|
||||
[ngClass]="{'core-button-completion': module.completiondata && showLegacyCompletion}"
|
||||
>
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata"
|
||||
[moduleName]="module.name" [moduleId]="module.id" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
<!-- Module completion (legacy). -->
|
||||
<core-course-module-completion-legacy *ngIf="module.completiondata && showLegacyCompletion"
|
||||
[completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id"
|
||||
(completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion-legacy>
|
||||
|
||||
<div class="core-module-buttons-more">
|
||||
<core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled"
|
||||
|
@ -74,9 +75,9 @@
|
|||
</div>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
*ngIf="module.description"
|
||||
*ngIf="hasInfo"
|
||||
id="core-course-module-{{module.id}}-info"
|
||||
class="ion-text-wrap core-course-module-handler core-module-module-description {{module.handlerData.class}}"
|
||||
class="ion-text-wrap core-course-module-handler core-module-module-info {{module.handlerData.class}}"
|
||||
[ngClass]="{
|
||||
'item-media': module.handlerData.icon,
|
||||
'item-dimmed': module.visible === 0 || module.uservisible === false
|
||||
|
@ -84,6 +85,19 @@
|
|||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<!-- Activity dates. -->
|
||||
<div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates">
|
||||
<p *ngFor="let date of module.dates">
|
||||
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Module completion. -->
|
||||
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata"
|
||||
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
|
||||
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
||||
</core-course-module-completion>
|
||||
|
||||
<core-format-text class="core-module-description" *ngIf="module.description" maxHeight="80" [text]="module.description"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.core-module-module-description {
|
||||
.core-module-module-info {
|
||||
ion-badge {
|
||||
text-align: start;
|
||||
}
|
||||
|
@ -51,15 +51,15 @@
|
|||
clear: both;
|
||||
}
|
||||
|
||||
.core-module-main-item + .core-module-module-description ion-label {
|
||||
.core-module-main-item + .core-module-module-info ion-label {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.core-module-main-item.has-module-description {
|
||||
.core-module-main-item.has-module-info {
|
||||
--inner-border-width: 0;
|
||||
}
|
||||
|
||||
.core-module-module-description ion-label {
|
||||
.core-module-module-info ion-label {
|
||||
margin-inline-start: 50px;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
CoreCourseSection,
|
||||
} from '@features/course/services/course-helper';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
|
||||
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
|
||||
import {
|
||||
CoreCourseModulePrefetchDelegate,
|
||||
CoreCourseModulePrefetchHandler,
|
||||
|
@ -47,6 +47,8 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
@Input() module!: CoreCourseModule; // The module to render.
|
||||
@Input() courseId?: number; // The course the module belongs to.
|
||||
@Input() section?: CoreCourseSection; // The section the module belongs to.
|
||||
@Input() showActivityDates = false; // Whether to show activity dates.
|
||||
@Input() showCompletionConditions = false; // Whether to show activity completion conditions.
|
||||
// eslint-disable-next-line @angular-eslint/no-input-rename
|
||||
@Input('downloadEnabled') set enabled(value: boolean) {
|
||||
this.downloadEnabled = value;
|
||||
|
@ -61,7 +63,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
|
||||
// Get current status to decide which icon should be shown.
|
||||
this.calculateAndShowStatus();
|
||||
};
|
||||
}
|
||||
|
||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
|
||||
@Output() statusChanged = new EventEmitter<CoreCourseModuleStatusChangedData>(); // Notify when the download status changes.
|
||||
|
@ -71,6 +73,9 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
spinner?: boolean; // Whether to display a loading spinner.
|
||||
downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||
modNameTranslated = '';
|
||||
hasInfo = false;
|
||||
showLegacyCompletion = false; // Whether to show module completion in the old format.
|
||||
showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
|
||||
|
||||
protected prefetchHandler?: CoreCourseModulePrefetchHandler;
|
||||
protected statusObserver?: CoreEventObserver;
|
||||
|
@ -83,12 +88,20 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
ngOnInit(): void {
|
||||
this.courseId = this.courseId || this.module.course;
|
||||
this.modNameTranslated = CoreCourse.translateModuleName(this.module.modname) || '';
|
||||
this.showLegacyCompletion = !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.11');
|
||||
this.showManualCompletion =
|
||||
this.showCompletionConditions || CoreCourseModuleDelegate.manualCompletionAlwaysShown(this.module);
|
||||
|
||||
if (!this.module.handlerData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
|
||||
this.hasInfo = !!(
|
||||
this.module.description ||
|
||||
(this.showActivityDates && this.module.dates && this.module.dates.length) ||
|
||||
this.module.completiondata
|
||||
);
|
||||
|
||||
if (this.module.handlerData.showDownloadButton) {
|
||||
// Listen for changes on this module status, even if download isn't enabled.
|
||||
|
|
|
@ -6,6 +6,18 @@
|
|||
"askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.",
|
||||
"availablespace": " You currently have about {{available}} free space.",
|
||||
"cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.",
|
||||
"completion_automatic:done": "Done:",
|
||||
"completion_automatic:failed": "Failed:",
|
||||
"completion_automatic:todo": "To do:",
|
||||
"completion_manual:aria:done": "{{$a}} is marked as done. Press to undo.",
|
||||
"completion_manual:aria:markdone": "Mark {{$a}} as done",
|
||||
"completion_manual:done": "Done",
|
||||
"completion_manual:markdone": "Mark as done",
|
||||
"completion_setby:auto:done": "Done: {{$a.condition}} (set by {{$a.setby}})",
|
||||
"completion_setby:auto:todo": "To do: {{$a.condition}} (set by {{$a.setby}})",
|
||||
"completion_setby:manual:done": "{{$a.activityname}} is marked by {{$a.setby}} as done. Press to undo.",
|
||||
"completion_setby:manual:markdone": "{{$a.activityname}} is marked by {{$a.setby}} as not done. Press to mark as done.",
|
||||
"completionrequirements": "Completion requirements for {{$a}}",
|
||||
"confirmdeletemodulefiles": "Are you sure you want to delete these files?",
|
||||
"confirmdeletestoreddata": "Are you sure you want to delete the stored data?",
|
||||
"confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?",
|
||||
|
|
|
@ -81,6 +81,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
protected courseStatusObserver?: CoreEventObserver;
|
||||
protected syncObserver?: CoreEventObserver;
|
||||
protected isDestroyed = false;
|
||||
protected modulesHaveCompletion = false;
|
||||
protected debouncedUpdateCachedCompletion?: () => void; // Update the cached completion after a certain time.
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
|
@ -104,6 +106,21 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
CoreCourseFormatDelegate.displayEnableDownload(this.course);
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
|
||||
this.debouncedUpdateCachedCompletion = CoreUtils.debounce(() => {
|
||||
if (this.modulesHaveCompletion) {
|
||||
CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true));
|
||||
} else {
|
||||
CoreUtils.ignoreErrors(CoreCourse.getActivitiesCompletionStatus(
|
||||
this.course.id,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
this.initListeners();
|
||||
|
||||
await this.loadData(false, true);
|
||||
|
@ -254,6 +271,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
|
||||
if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') {
|
||||
// The module already has completion (3.6 onwards). Load the offline completion.
|
||||
this.modulesHaveCompletion = true;
|
||||
|
||||
await CoreUtils.ignoreErrors(CoreCourseHelper.loadOfflineCompletion(this.course.id, sections));
|
||||
} else {
|
||||
const fetchedData = await CoreUtils.ignoreErrors(
|
||||
|
@ -353,6 +372,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
|||
const shouldReload = typeof completionData.valueused == 'undefined' || completionData.valueused;
|
||||
|
||||
if (!shouldReload) {
|
||||
// Invalidate the completion.
|
||||
await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.course.id));
|
||||
|
||||
this.debouncedUpdateCachedCompletion?.();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ import { CoreNetworkError } from '@classes/errors/network-error';
|
|||
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home';
|
||||
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
|
||||
|
||||
/**
|
||||
* Prefetch info of a module.
|
||||
|
@ -194,11 +195,8 @@ export class CoreCourseHelperProvider {
|
|||
forCoursePage,
|
||||
);
|
||||
|
||||
if (module.completiondata && module.completion && module.completion > 0) {
|
||||
module.completiondata.courseId = courseId;
|
||||
module.completiondata.courseName = courseName;
|
||||
module.completiondata.tracking = module.completion;
|
||||
module.completiondata.cmid = module.id;
|
||||
if (module.completiondata) {
|
||||
this.calculateModuleCompletionData(module, courseId, courseName);
|
||||
} else if (completionStatus && typeof completionStatus[module.id] != 'undefined') {
|
||||
// Should not happen on > 3.6. Check if activity has completions and if it's marked.
|
||||
const activityStatus = completionStatus[module.id];
|
||||
|
@ -223,6 +221,24 @@ export class CoreCourseHelperProvider {
|
|||
return { hasContent, sections: formattedSections };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate completion data of a module.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID of the module.
|
||||
* @param courseName Course name.
|
||||
*/
|
||||
calculateModuleCompletionData(module: CoreCourseModule, courseId: number, courseName?: string): void {
|
||||
if (!module.completiondata || !module.completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
module.completiondata.courseId = courseId;
|
||||
module.completiondata.courseName = courseName;
|
||||
module.completiondata.tracking = module.completion;
|
||||
module.completiondata.cmid = module.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the status of a section.
|
||||
*
|
||||
|
@ -1176,6 +1192,31 @@ export class CoreCourseHelperProvider {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load offline completion for a certain module.
|
||||
* This should be used in 3.6 sites or higher, where the course contents already include the completion.
|
||||
*
|
||||
* @param courseId The course to get the completion.
|
||||
* @param mmodule The module.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadModuleOfflineCompletion(courseId: number, module: CoreCourseModule, siteId?: string): Promise<void> {
|
||||
if (!module.completiondata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offlineCompletions = await CoreCourseOffline.getCourseManualCompletions(courseId, siteId);
|
||||
|
||||
const offlineCompletion = offlineCompletions.find(completion => completion.cmid == module.id);
|
||||
|
||||
if (offlineCompletion && offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) {
|
||||
// The module has offline completion. Load it.
|
||||
module.completiondata.state = offlineCompletion.completed;
|
||||
module.completiondata.offline = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the courses in the array.
|
||||
*
|
||||
|
@ -1890,6 +1931,52 @@ export class CoreCourseHelperProvider {
|
|||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completion clicked.
|
||||
*
|
||||
* @param completion The completion.
|
||||
* @param event The click event.
|
||||
* @return Promise resolved with the result.
|
||||
*/
|
||||
async changeManualCompletion(
|
||||
completion: CoreCourseModuleCompletionData,
|
||||
event?: Event,
|
||||
): Promise<CoreStatusWithWarningsWSResponse | void> {
|
||||
if (!completion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof completion.cmid == 'undefined' || completion.tracking !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
completion.state = completion.state === 1 ? 0 : 1;
|
||||
|
||||
try {
|
||||
const response = await CoreCourse.markCompletedManually(
|
||||
completion.cmid,
|
||||
completion.state === 1,
|
||||
completion.courseId!,
|
||||
completion.courseName,
|
||||
);
|
||||
|
||||
if (response.offline) {
|
||||
completion.offline = true;
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
completion.state = completion.state === 1 ? 0 : 1;
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.errorchangecompletion', true);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);
|
||||
|
|
|
@ -150,9 +150,12 @@ export class CoreCourseProvider {
|
|||
* @param completion Completion status of the module.
|
||||
*/
|
||||
checkModuleCompletion(courseId: number, completion?: CoreCourseModuleCompletionData): void {
|
||||
if (completion && completion.tracking === 2 && completion.state === 0) {
|
||||
if (completion && completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC && completion.state === 0) {
|
||||
this.invalidateSections(courseId).finally(() => {
|
||||
CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, { courseId: courseId });
|
||||
CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, {
|
||||
courseId: courseId,
|
||||
cmId: completion.cmid,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -969,6 +972,9 @@ export class CoreCourseProvider {
|
|||
// Ignore errors, shouldn't happen.
|
||||
}
|
||||
|
||||
// Invalidate module now, completion has changed.
|
||||
await this.invalidateModule(cmId, siteId);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (CoreUtils.isWebServiceError(error) || !courseId) {
|
||||
|
@ -1262,6 +1268,8 @@ export type CoreCourseSummary = {
|
|||
timeaccess?: number; // @since 3.6. Timeaccess.
|
||||
showshortname: boolean; // @since 3.6. Showshortname.
|
||||
coursecategory: string; // @since 3.7. Coursecategory.
|
||||
showactivitydates: boolean | null; // @since 3.11. Whether the activity dates are shown or not.
|
||||
showcompletionconditions: boolean | null; // @since 3.11. Whether the activity completion conditions are shown or not.
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1294,14 +1302,25 @@ export type CoreCourseCompletionActivityStatusWSResponse = {
|
|||
* Activity status.
|
||||
*/
|
||||
export type CoreCourseCompletionActivityStatus = {
|
||||
cmid: number; // Comment ID.
|
||||
cmid: number; // Course module ID.
|
||||
modname: string; // Activity module name.
|
||||
instance: number; // Instance ID.
|
||||
state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail.
|
||||
timecompleted: number; // Timestamp for completed activity.
|
||||
tracking: number; // Type of tracking: 0 means none, 1 manual, 2 automatic.
|
||||
overrideby?: number; // The user id who has overriden the status, or null.
|
||||
overrideby?: number | null; // The user id who has overriden the status, or null.
|
||||
valueused?: boolean; // Whether the completion status affects the availability of another activity.
|
||||
hascompletion?: boolean; // @since 3.11. Whether this activity module has completion enabled.
|
||||
isautomatic?: boolean; // @since 3.11. Whether this activity module instance tracks completion automatically.
|
||||
istrackeduser?: boolean; // @since 3.11. Whether completion is being tracked for this user.
|
||||
uservisible?: boolean; // @since 3.11. Whether this activity is visible to the user.
|
||||
details?: { // @since 3.11. An array of completion details containing the description and status.
|
||||
rulename: string; // Rule name.
|
||||
rulevalue: {
|
||||
status: number; // Completion status.
|
||||
description: string; // Completion description.
|
||||
};
|
||||
}[];
|
||||
offline?: boolean; // Whether the completions is offline and not yet synced.
|
||||
};
|
||||
|
||||
|
@ -1442,6 +1461,10 @@ export type CoreCourseWSModule = {
|
|||
completion?: number; // Type of completion tracking: 0 means none, 1 manual, 2 automatic.
|
||||
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
|
||||
contents: CoreCourseModuleContentFile[];
|
||||
dates?: {
|
||||
label: string;
|
||||
timestamp: number;
|
||||
}[]; // @since 3.11. Activity dates.
|
||||
contentsinfo?: { // Contents summary information.
|
||||
filescount: number; // Total number of files.
|
||||
filessize: number; // Total files size.
|
||||
|
@ -1457,8 +1480,24 @@ export type CoreCourseWSModule = {
|
|||
export type CoreCourseModuleWSCompletionData = {
|
||||
state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail.
|
||||
timecompleted: number; // Timestamp for completion status.
|
||||
overrideby: number; // The user id who has overriden the status.
|
||||
overrideby: number | null; // The user id who has overriden the status.
|
||||
valueused?: boolean; // Whether the completion status affects the availability of another activity.
|
||||
hascompletion?: boolean; // @since 3.11. Whether this activity module has completion enabled.
|
||||
isautomatic?: boolean; // @since 3.11. Whether this activity module instance tracks completion automatically.
|
||||
istrackeduser?: boolean; // @since 3.11. Whether completion is being tracked for this user.
|
||||
uservisible?: boolean; // @since 3.11. Whether this activity is visible to the user.
|
||||
details?: CoreCourseModuleWSRuleDetails[]; // @since 3.11. An array of completion details.
|
||||
};
|
||||
|
||||
/**
|
||||
* Module completion rule details.
|
||||
*/
|
||||
export type CoreCourseModuleWSRuleDetails = {
|
||||
rulename: string; // Rule name.
|
||||
rulevalue: {
|
||||
status: number; // Completion status.
|
||||
description: string; // Completion description.
|
||||
};
|
||||
};
|
||||
|
||||
export type CoreCourseModuleContentFile = {
|
||||
|
|
|
@ -92,6 +92,15 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
|
|||
* @return The result of the supports check.
|
||||
*/
|
||||
supportsFeature?(feature: string): unknown;
|
||||
|
||||
/**
|
||||
* Return true to show the manual completion regardless of the course's showcompletionconditions setting.
|
||||
* Returns false by default.
|
||||
*
|
||||
* @param module Module.
|
||||
* @return Whether the manual completion should always be displayed.
|
||||
*/
|
||||
manualCompletionAlwaysShown?(module: CoreCourseModule): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -366,6 +375,17 @@ export class CoreCourseModuleDelegateService extends CoreDelegate<CoreCourseModu
|
|||
return result ?? defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true to show the manual completion regardless of the course's showcompletionconditions setting.
|
||||
* Returns false by default.
|
||||
*
|
||||
* @param module Module.
|
||||
* @return Whether the manual completion should always be displayed.
|
||||
*/
|
||||
manualCompletionAlwaysShown(module: CoreCourseModule): boolean {
|
||||
return !!this.executeFunctionOnEnabled<boolean>(module.modname, 'manualCompletionAlwaysShown', [module]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreCourseModuleDelegate = makeSingleton(CoreCourseModuleDelegateService);
|
||||
|
|
|
@ -1264,6 +1264,8 @@ export type CoreEnrolledCourseData = CoreEnrolledCourseBasicData & {
|
|||
isfavourite?: boolean; // If the user marked this course a favourite.
|
||||
hidden?: boolean; // If the user hide the course from the dashboard.
|
||||
overviewfiles?: CoreWSExternalFile[];
|
||||
showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not.
|
||||
showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not.
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1281,6 +1283,8 @@ export type CoreCourseBasicSearchedData = CoreCourseBasicData & {
|
|||
}[];
|
||||
enrollmentmethods: string[]; // Enrollment methods list.
|
||||
customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
|
||||
showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not.
|
||||
showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not.
|
||||
};
|
||||
|
||||
export type CoreCourseSearchedData = CoreCourseBasicSearchedData & {
|
||||
|
@ -1342,6 +1346,8 @@ export type CoreCourseGetCoursesData = CoreEnrolledCourseBasicData & {
|
|||
forcetheme?: string; // Name of the force theme.
|
||||
courseformatoptions?: CoreCourseFormatOption[]; // Additional options for particular course format.
|
||||
customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
|
||||
showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not.
|
||||
showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -189,4 +189,20 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
|
|||
return CoreSitePluginsModuleIndexComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
manualCompletionAlwaysShown(module: CoreCourseModule): boolean {
|
||||
if (this.handlerSchema.manualcompletionalwaysshown !== undefined) {
|
||||
return this.handlerSchema.manualcompletionalwaysshown;
|
||||
}
|
||||
|
||||
if (this.initResult?.jsResult && this.initResult.jsResult.manualCompletionAlwaysShown) {
|
||||
// The init result defines a function to check if a feature is supported, use it.
|
||||
return this.initResult.jsResult.manualCompletionAlwaysShown(module);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -848,6 +848,7 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
|
|||
coursepagemethod?: string;
|
||||
ptrenabled?: boolean;
|
||||
supportedfeatures?: Record<string, unknown>;
|
||||
manualcompletionalwaysshown?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,5 +23,6 @@
|
|||
"sendemail": "Email",
|
||||
"student": "Student",
|
||||
"teacher": "Non-editing teacher",
|
||||
"userwithid": "User with ID {{id}}",
|
||||
"webpage": "Web page"
|
||||
}
|
|
@ -21,7 +21,7 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
import { CoreUserOffline } from './user-offline';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
@ -305,6 +305,25 @@ export class CoreUserProvider {
|
|||
return site.getDb().getRecord(USERS_TABLE_NAME, { id: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user fullname, using a default text if user not found.
|
||||
*
|
||||
* @param userId User ID.
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved with user name.
|
||||
*/
|
||||
async getUserFullNameWithDefault(userId: number, courseId?: number, siteId?: string): Promise<string> {
|
||||
try {
|
||||
const user = await CoreUser.getProfile(userId, courseId, true, siteId);
|
||||
|
||||
return user.fullname;
|
||||
|
||||
} catch {
|
||||
return Translate.instant('core.user.userwithid', { id: userId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile from WS.
|
||||
*
|
||||
|
|
|
@ -18,6 +18,7 @@ import { CoreLogger } from '@singletons/logger';
|
|||
import { CoreSite, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/site';
|
||||
import { CoreFilepoolComponentFileEventData } from '@services/filepool';
|
||||
import { CoreNavigationOptions } from '@services/navigator';
|
||||
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||
|
||||
/**
|
||||
* Observer instance to stop listening to an event.
|
||||
|
@ -45,6 +46,7 @@ export interface CoreEventsData {
|
|||
[CoreEvents.NOTIFICATION_SOUND_CHANGED]: CoreEventNotificationSoundChangedData;
|
||||
[CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData;
|
||||
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
|
||||
[CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData;
|
||||
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
|
||||
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
|
||||
[CoreEvents.IAB_LOAD_START]: InAppBrowserEvent;
|
||||
|
@ -53,7 +55,7 @@ export interface CoreEventsData {
|
|||
[CoreEvents.COMPONENT_FILE_ACTION]: CoreFilepoolComponentFileEventData;
|
||||
[CoreEvents.FILE_SHARED]: CoreEventFileSharedData;
|
||||
[CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData;
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Service to send and listen to events.
|
||||
|
@ -72,6 +74,7 @@ export class CoreEvents {
|
|||
static readonly SITE_UPDATED = 'site_updated';
|
||||
static readonly SITE_DELETED = 'site_deleted';
|
||||
static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
|
||||
static readonly MANUAL_COMPLETION_CHANGED = 'manual_completion_changed';
|
||||
static readonly USER_DELETED = 'user_deleted';
|
||||
static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed';
|
||||
static readonly COURSE_STATUS_CHANGED = 'course_status_changed';
|
||||
|
@ -330,7 +333,15 @@ export type CoreEventSelectCourseTabData = {
|
|||
* Data passed to COMPLETION_MODULE_VIEWED event.
|
||||
*/
|
||||
export type CoreEventCompletionModuleViewedData = {
|
||||
courseId?: number;
|
||||
courseId: number;
|
||||
cmId?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to MANUAL_COMPLETION_CHANGED event.
|
||||
*/
|
||||
export type CoreEventManualCompletionChangedData = {
|
||||
completion: CoreCourseModuleCompletionData;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue