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> | ||||
|  | ||||
| @ -120,7 +120,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|             AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, | ||||
|             (data) => { | ||||
|                 if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { | ||||
|                 // Assignment submitted, check completion.
 | ||||
|                     // Assignment submitted, check completion.
 | ||||
|                     CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); | ||||
| 
 | ||||
|                     // Reload data since it can have offline data now.
 | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
							
								
								
									
										48
									
								
								src/core/features/course/classes/module-completion.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/core/features/course/classes/module-completion.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
|                 const setByData = { | ||||
|                     $a: { | ||||
|                         condition: rule.rulevalue.description, | ||||
|                         setby: fullName, | ||||
|                     }, | ||||
|                 }; | ||||
|                 const overrideStatus = rule.statuscomplete ? 'done' : 'todo'; | ||||
| 
 | ||||
|         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; | ||||
|                 } | ||||
|                 rule.accessibleDescription = Translate.instant('core.course.completion_setby:auto:' + overrideStatus, setByData); | ||||
|             } | ||||
|             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 = { | ||||
|                 $a: { | ||||
|                     overrideuser: profile.fullname, | ||||
|                     modname: result.text, | ||||
|                 }, | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user