MOBILE-3636 assign: Submission review and edit pages
This commit is contained in:
		
							parent
							
								
									a4b356350b
								
							
						
					
					
						commit
						5350f715a3
					
				| @ -12,22 +12,61 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { CanLeaveGuard } from '@guards/can-leave'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { AddonModAssignComponentsModule } from './components/components.module'; | ||||
| import { AddonModAssignEditPage } from './pages/edit/edit'; | ||||
| import { AddonModAssignIndexPage } from './pages/index/index.page'; | ||||
| import { AddonModAssignSubmissionListPage } from './pages/submission-list/submission-list.page'; | ||||
| import { AddonModAssignSubmissionReviewPage } from './pages/submission-review/submission-review'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
| const commonRoutes: Routes = [ | ||||
|     { | ||||
|         path: ':courseId/:cmId', | ||||
|         component: AddonModAssignIndexPage, | ||||
|     }, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission-list', | ||||
|         path: ':courseId/:cmId/edit', | ||||
|         component: AddonModAssignEditPage, | ||||
|         canDeactivate: [CanLeaveGuard], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| const mobileRoutes: Routes = [ | ||||
|     ...commonRoutes, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission', | ||||
|         component: AddonModAssignSubmissionListPage, | ||||
|     }, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission/:submitId', | ||||
|         component: AddonModAssignSubmissionReviewPage, | ||||
|         canDeactivate: [CanLeaveGuard], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| const tabletRoutes: Routes = [ | ||||
|     ...commonRoutes, | ||||
|     { | ||||
|         path: ':courseId/:cmId/submission', | ||||
|         component: AddonModAssignSubmissionListPage, | ||||
|         children: [ | ||||
|             { | ||||
|                 path: ':submitId', | ||||
|                 component: AddonModAssignSubmissionReviewPage, | ||||
|                 canDeactivate: [CanLeaveGuard], | ||||
|             }, | ||||
|         ], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile), | ||||
|     ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet), | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
| @ -39,6 +78,8 @@ const routes: Routes = [ | ||||
|     declarations: [ | ||||
|         AddonModAssignIndexPage, | ||||
|         AddonModAssignSubmissionListPage, | ||||
|         AddonModAssignSubmissionReviewPage, | ||||
|         AddonModAssignEditPage, | ||||
|     ], | ||||
| }) | ||||
| export class AddonModAssignLazyModule {} | ||||
|  | ||||
| @ -35,7 +35,7 @@ | ||||
|         <ion-item class="ion-text-wrap"> | ||||
|             <ion-label> | ||||
|                 <core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120" | ||||
|                     contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" (click)="expandDescription($event)"> | ||||
|                     contextLevel="module" [contextInstanceId]="module!.id" [courseId]="courseId" (click)="expandDescription($event)"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| @ -97,7 +97,7 @@ | ||||
|             <!-- Summary of submissions with draft status. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled" | ||||
|                 [detail]="!showNumbers || summary.submissiondraftscount" | ||||
|                 (click)="goToSubmissionList(submissionStatusDraft, summary.submissiondraftscount)"> | ||||
|                 (click)="goToSubmissionList(submissionStatusDraft, !!summary.submissiondraftscount)"> | ||||
|                 <ion-label><h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2></ion-label> | ||||
|                 <ion-badge slot="end" *ngIf="showNumbers" color="primary"> | ||||
|                     {{ summary.submissiondraftscount }} | ||||
| @ -107,7 +107,7 @@ | ||||
|             <!-- Summary of submissions with submitted status. --> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled" | ||||
|                 [detail]="!showNumbers || summary.submissionssubmittedcount" | ||||
|                 (click)="goToSubmissionList(submissionStatusSubmitted, summary.submissionssubmittedcount)"> | ||||
|                 (click)="goToSubmissionList(submissionStatusSubmitted, !!summary.submissionssubmittedcount)"> | ||||
|                 <ion-label><h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2></ion-label> | ||||
|                 <ion-badge slot="end" *ngIf="showNumbers" color="primary"> | ||||
|                     {{ summary.submissionssubmittedcount }} | ||||
| @ -136,7 +136,7 @@ | ||||
| 
 | ||||
|     <!-- If it's a student, display his submission. --> | ||||
|     <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" | ||||
|         [moduleId]="module.id"> | ||||
|         [moduleId]="module!.id"> | ||||
|     </addon-mod-assign-submission> | ||||
| 
 | ||||
| </core-loading> | ||||
|  | ||||
| @ -34,6 +34,7 @@ import { | ||||
|     AddonModAssignGradedEventData, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssignSubmissionGradingSummary, | ||||
|     AddonModAssignSubmissionSavedEventData, | ||||
|     AddonModAssignSubmittedForGradingEventData, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignOffline } from '../../services/assign-offline'; | ||||
| @ -106,12 +107,16 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|         this.currentSite = CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         // Listen to events.
 | ||||
|         this.savedObserver = CoreEvents.on<any>(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, (data) => { | ||||
|             if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { | ||||
|         this.savedObserver = CoreEvents.on<AddonModAssignSubmissionSavedEventData>( | ||||
|             AddonModAssignProvider.SUBMISSION_SAVED_EVENT, | ||||
|             (data) => { | ||||
|                 if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { | ||||
|                 // Assignment submission saved, refresh data.
 | ||||
|                 this.showLoadingAndRefresh(true, false); | ||||
|             } | ||||
|         }, this.siteId); | ||||
|                     this.showLoadingAndRefresh(true, false); | ||||
|                 } | ||||
|             }, | ||||
|             this.siteId, | ||||
|         ); | ||||
| 
 | ||||
|         this.submittedObserver = CoreEvents.on<AddonModAssignSubmittedForGradingEventData>( | ||||
|             AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, | ||||
| @ -262,7 +267,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|      * @param groupId Group ID. | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     async setGroup(groupId: number): Promise<void> { | ||||
|     async setGroup(groupId = 0): Promise<void> { | ||||
|         this.group = groupId; | ||||
| 
 | ||||
|         const submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign!.id, { | ||||
| @ -303,10 +308,10 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo | ||||
|      * Go to view a list of submissions. | ||||
|      * | ||||
|      * @param status Status to see. | ||||
|      * @param count Number of submissions with the status. | ||||
|      * @param hasSubmissions If the status has any submission. | ||||
|      */ | ||||
|     goToSubmissionList(status: string, count: number): void { | ||||
|         if (typeof status != 'undefined' && !count && this.showNumbers) { | ||||
|     goToSubmissionList(status?: string, hasSubmissions = false): void { | ||||
|         if (typeof status != 'undefined' && !hasSubmissions && this.showNumbers) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
| 
 | ||||
|     <!-- User and status of the submission. --> | ||||
|     <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" [title]="user.fullname"> | ||||
|     <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" [title]="user!.fullname"> | ||||
|         <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|         <ion-label> | ||||
|             <h2>{{ user.fullname }}</h2> | ||||
|             <h2>{{ user!.fullname }}</h2> | ||||
|             <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> | ||||
|         </ion-label> | ||||
|         <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container> | ||||
| @ -39,10 +39,10 @@ | ||||
| 
 | ||||
|                 <!-- Render some data about the submission. --> | ||||
|                 <ion-item class="ion-text-wrap" | ||||
|                     *ngIf="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified"> | ||||
|                     *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2> | ||||
|                         <p>{{ userSubmission.timemodified * 1000 | coreFormatDate }}</p> | ||||
|                         <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
| @ -55,49 +55,49 @@ | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="fromDate && !isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <p *ngIf="assign.intro" | ||||
|                         <p *ngIf="assign!.intro" | ||||
|                             [innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}"> | ||||
|                         </p> | ||||
|                         <p *ngIf="!assign.intro" | ||||
|                         <p *ngIf="!assign!.intro" | ||||
|                             [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate: | ||||
|                                 {'$a': fromDate}"> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign.duedate && !isSubmittedForGrading"> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && !isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.duedate' | translate }}</h2> | ||||
|                         <p *ngIf="assign.duedate" >{{ assign.duedate * 1000 | coreFormatDate }}</p> | ||||
|                         <p *ngIf="!assign.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p> | ||||
|                         <p *ngIf="assign!.duedate" >{{ assign!.duedate * 1000 | coreFormatDate }}</p> | ||||
|                         <p *ngIf="!assign!.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign.duedate && assign.cutoffdate && isSubmittedForGrading"> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && assign!.cutoffdate && isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2> | ||||
|                         <p>{{ assign.cutoffdate * 1000 | coreFormatDate }}</p> | ||||
|                         <p>{{ assign!.cutoffdate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" | ||||
|                     *ngIf="assign.duedate && lastAttempt && lastAttempt.extensionduedate && !isSubmittedForGrading"> | ||||
|                     *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2> | ||||
|                         <p>{{ lastAttempt.extensionduedate * 1000 | coreFormatDate }}</p> | ||||
|                         <p>{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2> | ||||
|                         <p *ngIf="assign.maxattempts == unlimitedAttempts"> | ||||
|                         <p *ngIf="assign!.maxattempts == unlimitedAttempts"> | ||||
|                             {{ 'addon.mod_assign.outof' | translate : | ||||
|                                 {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||
|                         </p> | ||||
|                         <p *ngIf="assign.maxattempts != unlimitedAttempts"> | ||||
|                         <p *ngIf="assign!.maxattempts != unlimitedAttempts"> | ||||
|                             {{ 'addon.mod_assign.outof' | translate : | ||||
|                                 {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }} | ||||
|                                 {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| @ -114,12 +114,12 @@ | ||||
|                             <!-- If no submission or is new, show add submission. --> | ||||
|                             <ion-button expand="block" class="ion-text-wrap" color="primary" | ||||
|                                 *ngIf="!hasOffline && | ||||
|                                     (!userSubmission || !userSubmission.status || userSubmission.status == statusNew)" | ||||
|                                     (!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)" | ||||
|                                 (click)="goToEdit()"> | ||||
|                                 {{ 'addon.mod_assign.addsubmission' | translate }} | ||||
|                             </ion-button> | ||||
|                             <!-- If reopened, show addfromprevious and addnewattempt. --> | ||||
|                             <ng-container *ngIf="!hasOffline && userSubmission && userSubmission.status == statusReopened"> | ||||
|                             <ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened"> | ||||
|                                 <ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" color="primary" | ||||
|                                     (click)="copyPrevious()"> | ||||
|                                     {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} | ||||
| @ -130,9 +130,9 @@ | ||||
|                             </ng-container> | ||||
|                             <!-- Else show editsubmission. --> | ||||
|                             <ion-button expand="block" class="ion-text-wrap" color="primary" | ||||
|                                 *ngIf="!hasOffline && userSubmission && userSubmission.status && | ||||
|                                     userSubmission.status != statusNew && | ||||
|                                     userSubmission.status != statusReopened" (click)="goToEdit()"> | ||||
|                                 *ngIf="!hasOffline && userSubmission && userSubmission!.status && | ||||
|                                     userSubmission!.status != statusNew && | ||||
|                                     userSubmission!.status != statusReopened" (click)="goToEdit()"> | ||||
|                                 {{ 'addon.mod_assign.editsubmission' | translate }} | ||||
|                             </ion-button> | ||||
|                         </div> | ||||
| @ -176,25 +176,25 @@ | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <!-- Team members that need to submit it too. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0"> | ||||
|                 <ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0"> | ||||
|                     <ion-label><h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}</h2></ion-label> | ||||
|                     <ng-container *ngIf="!blindMarking"> | ||||
|                         <ng-container *ngFor="let user of membersToSubmit"> | ||||
|                             <ion-item class="ion-text-wrap" core-user-link [userId]="user.id" | ||||
|                                 [courseId]="courseId" [title]="user.fullname"> | ||||
|                                 <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|                                 <ion-label><h2>{{ user.fullname }}</h2></ion-label> | ||||
|                             </ion-item> | ||||
|                         </ng-container> | ||||
|                 </ion-item-divider> | ||||
|                 <ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && !blindMarking"> | ||||
|                     <ng-container *ngFor="let user of membersToSubmit"> | ||||
|                         <ion-item class="ion-text-wrap" core-user-link [userId]="user.id" | ||||
|                             [courseId]="courseId" [title]="user.fullname"> | ||||
|                             <core-user-avatar [user]="user" slot="start"></core-user-avatar> | ||||
|                             <ion-label><h2>{{ user.fullname }}</h2></ion-label> | ||||
|                         </ion-item> | ||||
|                     </ng-container> | ||||
|                     <ng-container *ngIf="blindMarking"> | ||||
|                         <ng-container *ngFor="let blindId of membersToSubmitBlind"> | ||||
|                             <ion-item class="ion-text-wrap"> | ||||
|                                 <ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label> | ||||
|                             </ion-item> | ||||
|                         </ng-container> | ||||
|                 </ng-container> | ||||
|                 <ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && blindMarking"> | ||||
|                     <ng-container *ngFor="let blindId of membersToSubmitBlind"> | ||||
|                         <ion-item class="ion-text-wrap"> | ||||
|                             <ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label> | ||||
|                         </ion-item> | ||||
|                     </ng-container> | ||||
|                 </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <!-- Submission is locked. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked"> | ||||
| @ -219,12 +219,12 @@ | ||||
|             <ng-template> | ||||
|                 <!-- Current grade if method is advanced. --> | ||||
|                 <ion-item class="ion-text-wrap core-grading-summary" | ||||
|                     *ngIf="feedback.gradefordisplay && (!isGrading || grade.method != 'simple')"> | ||||
|                     *ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> | ||||
|                         <p><core-format-text [text]="feedback.gradefordisplay" [filter]="false"></core-format-text></p> | ||||
|                         <p><core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text></p> | ||||
|                     </ion-label> | ||||
|                     <ion-button slot="end" *ngIf="feedback.advancedgrade" (click)="showAdvancedGrade()"> | ||||
|                     <ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()"> | ||||
|                         <ion-icon name="fas-search" slot="icon-only"></ion-icon> | ||||
|                     </ion-button> | ||||
|                 </ion-item> | ||||
| @ -236,7 +236,7 @@ | ||||
|                         <ion-label position="stacked"> | ||||
|                             <h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2> | ||||
|                         </ion-label> | ||||
|                         <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo.grade" | ||||
|                         <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade" | ||||
|                             [lang]="grade.lang"> | ||||
|                         </ion-input> | ||||
|                         <p item-content *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p> | ||||
| @ -256,7 +256,7 @@ | ||||
|                     <ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes"> | ||||
|                         <ion-label><h2>{{ outcome.name }}</h2></ion-label> | ||||
|                         <ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" | ||||
|                             interface="action-sheet" [disabled]="gradeInfo.disabled"> | ||||
|                             interface="action-sheet" [disabled]="gradeInfo!.disabled"> | ||||
|                             <ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value"> | ||||
|                                 {{grade.label}} | ||||
|                             </ion-select-option> | ||||
| @ -268,18 +268,18 @@ | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> | ||||
|                             <p *ngIf="grade.gradebookGrade !== false && grade.gradebookGrade !== null && !grade.scale"> | ||||
|                             <p *ngIf="grade.gradebookGrade && !grade.scale"> | ||||
|                                 {{ grade.gradebookGrade }} | ||||
|                             </p> | ||||
|                             <p *ngIf="grade.gradebookGrade !== false && grade.gradebookGrade !== null && grade.scale"> | ||||
|                             <p *ngIf="grade.gradebookGrade && grade.scale"> | ||||
|                                 {{ grade.scale[grade.gradebookGrade].label }} | ||||
|                             </p> | ||||
|                             <p *ngIf="grade.gradebookGrade === false || grade.gradebookGrade === null">-</p> | ||||
|                             <p *ngIf="!grade.gradebookGrade">-</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" [assign]="assign" | ||||
|                 <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback!.plugins" [assign]="assign" | ||||
|                     [submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades"> | ||||
|                 </addon-mod-assign-feedback-plugin> | ||||
| 
 | ||||
| @ -292,28 +292,30 @@ | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!--- Apply grade to all team members. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign.teamsubmission && canSaveGrades"> | ||||
|                     <h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2> | ||||
|                     <ion-label>{{ 'addon.mod_assign.applytoteam' | translate }}</ion-label> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2> | ||||
|                         <p>{{ 'addon.mod_assign.applytoteam' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                     <ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Attempt status. --> | ||||
|                 <ng-container *ngIf="isGrading && assign.attemptreopenmethod != attemptReopenMethodNone"> | ||||
|                 <ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone"> | ||||
|                     <ion-item class="ion-text-wrap"> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2> | ||||
|                             <p *ngIf="assign.maxattempts == unlimitedAttempts"> | ||||
|                             <p *ngIf="assign!.maxattempts == unlimitedAttempts"> | ||||
|                                 {{ 'addon.mod_assign.outof' | translate : | ||||
|                                     {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||
|                             </p> | ||||
|                             <p *ngIf="assign.maxattempts != unlimitedAttempts"> | ||||
|                             <p *ngIf="assign!.maxattempts != unlimitedAttempts"> | ||||
|                                 {{ 'addon.mod_assign.outof' | translate : | ||||
|                                     {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }} | ||||
|                                     {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} | ||||
|                             </p> | ||||
|                             <p> | ||||
|                                 {{ 'addon.mod_assign.attemptreopenmethod' | translate }}: | ||||
|                                 {{ 'addon.mod_assign.attemptreopenmethod_' + assign.attemptreopenmethod | translate }} | ||||
|                                 {{ 'addon.mod_assign.attemptreopenmethod_' + assign!.attemptreopenmethod | translate }} | ||||
|                             </p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
| @ -324,33 +326,37 @@ | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <!-- Data about the grader (teacher who graded). --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader.id" [courseId]="courseId" | ||||
|                     [title]="grader.fullname" detail="true"> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId" | ||||
|                     [title]="grader!.fullname" detail="true"> | ||||
|                     <core-user-avatar [user]="grader" slot="start"></core-user-avatar> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2> | ||||
|                         <h2>{{ grader.fullname }}</h2> | ||||
|                         <p *ngIf="feedback.gradeddate">{{ feedback.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                         <h2>{{ grader!.fullname }}</h2> | ||||
|                         <p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Grader is hidden, display only the grade date. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="!grader && feedback.gradeddate"> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="!grader && feedback!.gradeddate"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2> | ||||
|                         <p>{{ feedback.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                         <p>{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <!-- Warning message if cannot save grades. --> | ||||
|                 <div *ngIf="isGrading && !canSaveGrades" class="core-warning-card"> | ||||
|                     <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> | ||||
|                     <p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p> | ||||
|                     <ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link > | ||||
|                         {{ 'core.openinbrowser' | translate }} | ||||
|                         <ion-icon name="fas-external-link-alt" slot="end"></ion-icon> | ||||
|                     </ion-button> | ||||
|                 </div> | ||||
|                 <ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card"> | ||||
|                     <ion-item> | ||||
|                         <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> | ||||
|                         <ion-label> | ||||
|                             <p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p> | ||||
|                             <ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link > | ||||
|                                 {{ 'core.openinbrowser' | translate }} | ||||
|                                 <ion-icon name="fas-external-link-alt" slot="end"></ion-icon> | ||||
|                             </ion-button> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ion-card> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
|     </core-tabs> | ||||
| @ -360,20 +366,20 @@ | ||||
| <ng-template #submissionStatus> | ||||
|     <ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt"> | ||||
|         <p *ngIf="lastAttempt!.submissiongroup && lastAttempt!.submissiongroupname">{{lastAttempt!.submissiongroupname}}</p> | ||||
|         <ng-container *ngIf="assign.preventsubmissionnotingroup && | ||||
|         <ng-container *ngIf="assign!.preventsubmissionnotingroup && | ||||
|             !lastAttempt!.submissiongroup && | ||||
|             (!lastAttempt!.usergroups || lastAttempt!.usergroups.length <= 0)"> | ||||
|             <p class="text-danger"><strong>{{ 'addon.mod_assign.noteam' | translate }}</strong></p> | ||||
|             <p class="text-danger">{{ 'addon.mod_assign.noteam_desc' | translate }}</p> | ||||
|         </ng-container> | ||||
|         <ng-container *ngIf="assign.preventsubmissionnotingroup && | ||||
|         <ng-container *ngIf="assign!.preventsubmissionnotingroup && | ||||
|             !lastAttempt!.submissiongroup && | ||||
|             lastAttempt!.usergroups && | ||||
|             lastAttempt!.usergroups.length > 1"> | ||||
|             <p class="text-danger"><strong>{{ 'addon.mod_assign.multipleteams' | translate }}</strong></p> | ||||
|             <p class="text-danger">{{ 'addon.mod_assign.multipleteams_desc' | translate }}</p> | ||||
|         </ng-container> | ||||
|         <p *ngIf="!assign.preventsubmissionnotingroup && !lastAttempt!.submissiongroup"> | ||||
|         <p *ngIf="!assign!.preventsubmissionnotingroup && !lastAttempt!.submissiongroup"> | ||||
|             {{ 'addon.mod_assign.defaultteam' | translate }} | ||||
|         </p> | ||||
|     </ng-container> | ||||
|  | ||||
| @ -55,6 +55,7 @@ import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreGroups } from '@services/groups'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; | ||||
| import { AddonModAssignModuleHandlerService } from '../../services/handlers/module'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays an assignment submission. | ||||
| @ -73,7 +74,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|     @Input() moduleId!: number; // Module ID the submission belongs to.
 | ||||
|     @Input() submitId!: number; // User that did the submission.
 | ||||
|     @Input() blindId?: number; // Blinded user ID (if it's blinded).
 | ||||
|     @Input() showGrade = false; // Whether to display the grade tab at start.
 | ||||
| 
 | ||||
|     loaded = false; // Whether data has been loaded.
 | ||||
|     selectedTab = 'submission'; // Tab selected on start.
 | ||||
| @ -121,6 +121,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|     canSaveGrades = false; // Whether the user can save the grades.
 | ||||
|     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.
 | ||||
| 
 | ||||
|     // Some constants.
 | ||||
|     statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW; | ||||
| @ -131,7 +132,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|     protected siteId: string; // Current site ID.
 | ||||
|     protected currentUserId: number; // Current user ID.
 | ||||
|     protected previousAttempt?: AddonModAssignSubmissionPreviousAttempt; // The previous attempt.
 | ||||
|     protected isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
 | ||||
|     protected submissionStatusAvailable = false; // Whether we were able to retrieve the submission status.
 | ||||
|     protected originalGrades: AddonModAssignSubmissionOriginalGrades = { | ||||
|         addAttempt: false, | ||||
| @ -180,7 +180,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.selectedTab = this.showGrade ? 'grade' : 'submission'; | ||||
|         this.isSubmittedForGrading = !!this.submitId; | ||||
| 
 | ||||
|         this.loadData(true); | ||||
| @ -343,13 +342,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|      * Go to the page to add or edit submission. | ||||
|      */ | ||||
|     goToEdit(): void { | ||||
|         CoreNavigator.instance.navigate('AddonModAssignEditPage', { | ||||
|             params: { | ||||
|                 moduleId: this.moduleId, | ||||
|                 courseId: this.courseId, | ||||
|                 userId: this.submitId, | ||||
|                 blindId: this.blindId, | ||||
|             } }); | ||||
|         CoreNavigator.instance.navigateToSitePath( | ||||
|             AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit', | ||||
|             { | ||||
|                 params: { | ||||
|                     blindId: this.blindId, | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -393,7 +393,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { | ||||
|         try { | ||||
|             return AddonModAssignHelper.instance.hasFeedbackDataChanged( | ||||
|                 this.assign!, | ||||
|                 this.userSubmission!, // @todo
 | ||||
|                 this.userSubmission, | ||||
|                 this.feedback, | ||||
|                 this.submitId, | ||||
|             ); | ||||
|  | ||||
| @ -24,11 +24,10 @@ | ||||
| 
 | ||||
| <!-- Edit --> | ||||
| <ion-item class="ion-text-wrap" *ngIf="edit && loaded"> | ||||
|     <ion-label> | ||||
|         <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" | ||||
|             name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true" | ||||
|             contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor" | ||||
|             [draftExtraParams]="{userid: userId, action: 'grade'}"> | ||||
|         </core-rich-text-editor> | ||||
|     </ion-label> | ||||
|     <ion-label></ion-label> | ||||
|     <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" | ||||
|         name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true" | ||||
|         contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor" | ||||
|         [draftExtraParams]="{userid: userId, action: 'grade'}"> | ||||
|     </core-rich-text-editor> | ||||
| </ion-item> | ||||
|  | ||||
							
								
								
									
										39
									
								
								src/addons/mod/assign/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/addons/mod/assign/pages/edit/edit.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate"> | ||||
|                 {{ 'core.save' | translate }} | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length"> | ||||
|             <!-- @todo: plagiarism_print_disclosure --> | ||||
|             <form name="addon-mod_assign-edit-form" #editSubmissionForm> | ||||
|                 <!-- Submission statement. --> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> | ||||
|                     <ion-label> | ||||
|                         <core-format-text [text]="submissionStatement" [filter]="false"> | ||||
|                         </core-format-text> | ||||
|                     </ion-label> | ||||
|                     <ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="submissionStatementAccepted"></ion-checkbox> | ||||
|                     <!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. --> | ||||
|                     <input item-content type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement"> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|                 <addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign" | ||||
|                     [submission]="userSubmission" [plugin]="plugin" [edit]="true" [allowOffline]="allowOffline"> | ||||
|                 </addon-mod-assign-submission-plugin> | ||||
|             </form> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										408
									
								
								src/addons/mod/assign/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										408
									
								
								src/addons/mod/assign/pages/edit/edit.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,408 @@ | ||||
| // (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, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreIonLoadingElement } from '@classes/ion-loading'; | ||||
| import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||
| import { CoreSync } from '@services/sync'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreEventActivityDataSentData, CoreEvents } from '@singletons/events'; | ||||
| import { | ||||
|     AddonModAssignAssign, | ||||
|     AddonModAssignSubmission, | ||||
|     AddonModAssignProvider, | ||||
|     AddonModAssign, | ||||
|     AddonModAssignSubmissionStatusOptions, | ||||
|     AddonModAssignGetSubmissionStatusWSResponse, | ||||
|     AddonModAssignSavePluginData, | ||||
|     AddonModAssignSubmissionSavedEventData, | ||||
|     AddonModAssignSubmittedForGradingEventData, | ||||
| } from '../../services/assign'; | ||||
| import { AddonModAssignHelper } from '../../services/assign-helper'; | ||||
| import { AddonModAssignOffline } from '../../services/assign-offline'; | ||||
| import { AddonModAssignSync } from '../../services/assign-sync'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that allows adding or editing an assigment submission. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-assign-edit', | ||||
|     templateUrl: 'edit.html', | ||||
| }) | ||||
| export class AddonModAssignEditPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild('editSubmissionForm') formElement?: ElementRef; | ||||
| 
 | ||||
|     title: string; // Title to display.
 | ||||
|     assign?: AddonModAssignAssign; // Assignment.
 | ||||
|     courseId!: number; // Course ID the assignment belongs to.
 | ||||
|     moduleId!: number; // Module ID the submission belongs to.
 | ||||
|     userSubmission?: AddonModAssignSubmission; // The user submission.
 | ||||
|     allowOffline = false; // Whether offline is allowed.
 | ||||
|     submissionStatement?: string; // The submission statement.
 | ||||
|     submissionStatementAccepted = false; // Whether submission statement is accepted.
 | ||||
|     loaded = false; // Whether data has been loaded.
 | ||||
| 
 | ||||
|     protected userId: number; // User doing the submission.
 | ||||
|     protected isBlind = false; // Whether blind is used.
 | ||||
|     protected editText: string; // "Edit submission" translated text.
 | ||||
|     protected saveOffline = false; // Whether to save data in offline.
 | ||||
|     protected hasOffline = false; // Whether the assignment has offline data.
 | ||||
|     protected isDestroyed = false; // Whether the component has been destroyed.
 | ||||
|     protected forceLeave = false; // To allow leaving the page without checking for changes.
 | ||||
| 
 | ||||
|     constructor( | ||||
|         protected route: ActivatedRoute, | ||||
|     ) { | ||||
|         this.userId = CoreSites.instance.getCurrentSiteUserId(); // Right now we can only edit current user's submissions.
 | ||||
|         this.editText = Translate.instance.instant('addon.mod_assign.editsubmission'); | ||||
|         this.title = this.editText; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!; | ||||
|         this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; | ||||
|         this.isBlind = !!CoreNavigator.instance.getRouteNumberParam('blindId'); | ||||
| 
 | ||||
|         this.fetchAssignment().finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     async ionViewCanLeave(): Promise<void> { | ||||
|         if (this.forceLeave) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Check if data has changed.
 | ||||
|         const changed = await this.hasDataChanged(); | ||||
|         if (changed) { | ||||
|             await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit')); | ||||
|         } | ||||
| 
 | ||||
|         // Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
 | ||||
|         AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, this.getInputData()); | ||||
| 
 | ||||
|         CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch assignment data. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchAssignment(): Promise<void> { | ||||
|         const currentUserId = CoreSites.instance.getCurrentSiteUserId(); | ||||
| 
 | ||||
|         try { | ||||
|             // Get assignment data.
 | ||||
|             this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); | ||||
|             this.title = this.assign.name || this.title; | ||||
| 
 | ||||
|             if (!this.isDestroyed) { | ||||
|                 // Block the assignment.
 | ||||
|                 CoreSync.instance.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); | ||||
|             } | ||||
| 
 | ||||
|             // Wait for sync to be over (if any).
 | ||||
|             await AddonModAssignSync.instance.waitForSync(this.assign.id); | ||||
| 
 | ||||
|             // Get submission status. Ignore cache to get the latest data.
 | ||||
|             const options: AddonModAssignSubmissionStatusOptions = { | ||||
|                 userId: this.userId, | ||||
|                 isBlind: this.isBlind, | ||||
|                 cmId: this.assign.cmid, | ||||
|                 filter: false, | ||||
|                 readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, | ||||
|             }; | ||||
| 
 | ||||
|             let submissionStatus: AddonModAssignGetSubmissionStatusWSResponse; | ||||
|             try { | ||||
|                 submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options); | ||||
|                 this.userSubmission = | ||||
|                     AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt); | ||||
|             } catch (error) { | ||||
|                 // Cannot connect. Get cached data.
 | ||||
|                 options.filter = true; | ||||
|                 options.readingStrategy = CoreSitesReadingStrategy.PreferCache; | ||||
| 
 | ||||
|                 submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options); | ||||
|                 this.userSubmission = | ||||
|                     AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt); | ||||
| 
 | ||||
|                 // Check if the user can edit it in offline.
 | ||||
|                 const canEditOffline = | ||||
|                     await AddonModAssignHelper.instance.canEditSubmissionOffline(this.assign, this.userSubmission); | ||||
|                 if (!canEditOffline) { | ||||
|                     // Submission cannot be edited in offline, reject.
 | ||||
|                     this.allowOffline = false; | ||||
|                     throw error; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!submissionStatus.lastattempt?.canedit) { | ||||
|                 // Can't edit. Reject.
 | ||||
|                 throw new CoreError(Translate.instance.instant('core.nopermissions', { $a: this.editText })); | ||||
|             } | ||||
| 
 | ||||
|             this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
 | ||||
|             // Only show submission statement if we are editing our own submission.
 | ||||
|             if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) { | ||||
|                 this.submissionStatement = this.assign.submissionstatement; | ||||
|             } else { | ||||
|                 this.submissionStatement = undefined; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 // Check if there's any offline data for this submission.
 | ||||
|                 const offlineData = await AddonModAssignOffline.instance.getSubmission(this.assign.id, this.userId); | ||||
| 
 | ||||
|                 this.hasOffline = offlineData?.plugindata && Object.keys(offlineData.plugindata).length > 0; | ||||
|             } catch { | ||||
|                 // No offline data found.
 | ||||
|                 this.hasOffline = false; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.'); | ||||
| 
 | ||||
|             // Leave the player.
 | ||||
|             this.leaveWithoutCheck(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the input data. | ||||
|      * | ||||
|      * @return Input data. | ||||
|      */ | ||||
|     protected getInputData(): Record<string, unknown> { | ||||
|         return CoreDomUtils.instance.getDataFromForm(document.forms['addon-mod_assign-edit-form']); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if data has changed. | ||||
|      * | ||||
|      * @return Promise resolved with boolean: whether data has changed. | ||||
|      */ | ||||
|     protected hasDataChanged(): Promise<boolean> { | ||||
|         // Usually the hasSubmissionDataChanged call will be resolved inmediately, causing the modal to be shown just an instant.
 | ||||
|         // We'll wait a bit before showing it to prevent this "blink".
 | ||||
|         let modal: CoreIonLoadingElement; | ||||
|         let showModal = true; | ||||
| 
 | ||||
|         setTimeout(async () => { | ||||
|             if (showModal) { | ||||
|                 modal = await CoreDomUtils.instance.showModalLoading(); | ||||
|             } | ||||
|         }, 100); | ||||
| 
 | ||||
|         const data = this.getInputData(); | ||||
| 
 | ||||
|         return AddonModAssignHelper.instance.hasSubmissionDataChanged(this.assign!, this.userSubmission, data).finally(() => { | ||||
|             if (modal) { | ||||
|                 modal.dismiss(); | ||||
|             } else { | ||||
|                 showModal = false; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Leave the view without checking for changes. | ||||
|      */ | ||||
|     protected leaveWithoutCheck(): void { | ||||
|         this.forceLeave = true; | ||||
|         CoreNavigator.instance.back(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get data to submit based on the input data. | ||||
|      * | ||||
|      * @param inputData The input data. | ||||
|      * @return Promise resolved with the data to submit. | ||||
|      */ | ||||
|     protected prepareSubmissionData(inputData: Record<string, unknown>): Promise<AddonModAssignSavePluginData> { | ||||
|         // If there's offline data, always save it in offline.
 | ||||
|         this.saveOffline = this.hasOffline; | ||||
| 
 | ||||
|         try { | ||||
|             return AddonModAssignHelper.instance.prepareSubmissionPluginData( | ||||
|                 this.assign!, | ||||
|                 this.userSubmission, | ||||
|                 inputData, | ||||
|                 this.hasOffline, | ||||
|             ); | ||||
|         } catch (error) { | ||||
|             if (this.allowOffline && !this.saveOffline) { | ||||
|                 // Cannot submit in online, prepare for offline usage.
 | ||||
|                 this.saveOffline = true; | ||||
| 
 | ||||
|                 return AddonModAssignHelper.instance.prepareSubmissionPluginData( | ||||
|                     this.assign!, | ||||
|                     this.userSubmission, | ||||
|                     inputData, | ||||
|                     true, | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the submission. | ||||
|      */ | ||||
|     async save(): Promise<void> { | ||||
|         // Check if data has changed.
 | ||||
|         const changed = await this.hasDataChanged(); | ||||
|         if (!changed) { | ||||
|             // Nothing to save, just go back.
 | ||||
|             this.leaveWithoutCheck(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         try { | ||||
|             await this.saveSubmission(); | ||||
|             this.leaveWithoutCheck(); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'Error saving submission.'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the submission. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async saveSubmission(): Promise<void> { | ||||
|         const inputData = this.getInputData(); | ||||
| 
 | ||||
|         if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) { | ||||
|             throw Translate.instance.instant('addon.mod_assign.acceptsubmissionstatement'); | ||||
|         } | ||||
| 
 | ||||
|         let modal = await CoreDomUtils.instance.showModalLoading(); | ||||
|         let size = -1; | ||||
| 
 | ||||
|         // Get size to ask for confirmation.
 | ||||
|         try { | ||||
|             size = await AddonModAssignHelper.instance.getSubmissionSizeForEdit(this.assign!, this.userSubmission!, inputData); | ||||
|         } catch (error) { | ||||
|             // Error calculating size, return -1.
 | ||||
|             size = -1; | ||||
|         } | ||||
| 
 | ||||
|         modal.dismiss(); | ||||
| 
 | ||||
|         try { | ||||
|             // Confirm action.
 | ||||
|             await CoreFileUploaderHelper.instance.confirmUploadFile(size, true, this.allowOffline); | ||||
| 
 | ||||
|             modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); | ||||
| 
 | ||||
|             const pluginData = await this.prepareSubmissionData(inputData); | ||||
|             if (!Object.keys(pluginData).length) { | ||||
|                 // Nothing to save.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             let sent: boolean; | ||||
| 
 | ||||
|             if (this.saveOffline) { | ||||
|                 // Save submission in offline.
 | ||||
|                 sent = false; | ||||
|                 await AddonModAssignOffline.instance.saveSubmission( | ||||
|                     this.assign!.id, | ||||
|                     this.courseId, | ||||
|                     pluginData, | ||||
|                     this.userSubmission!.timemodified, | ||||
|                     !this.assign!.submissiondrafts, | ||||
|                     this.userId, | ||||
|                 ); | ||||
|             } else { | ||||
|                 // Try to send it to server.
 | ||||
|                 sent = await AddonModAssign.instance.saveSubmission( | ||||
|                     this.assign!.id, | ||||
|                     this.courseId, | ||||
|                     pluginData, | ||||
|                     this.allowOffline, | ||||
|                     this.userSubmission!.timemodified, | ||||
|                     !!this.assign!.submissiondrafts, | ||||
|                     this.userId, | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             // Clear temporary data from plugins.
 | ||||
|             AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, inputData); | ||||
| 
 | ||||
|             if (sent) { | ||||
|                 CoreEvents.trigger<CoreEventActivityDataSentData>(CoreEvents.ACTIVITY_DATA_SENT, { module: 'assign' }); | ||||
|             } | ||||
| 
 | ||||
|             // Submission saved, trigger events.
 | ||||
|             CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.instance.getCurrentSiteId()); | ||||
| 
 | ||||
|             CoreEvents.trigger<AddonModAssignSubmissionSavedEventData>( | ||||
|                 AddonModAssignProvider.SUBMISSION_SAVED_EVENT, | ||||
|                 { | ||||
|                     assignmentId: this.assign!.id, | ||||
|                     submissionId: this.userSubmission!.id, | ||||
|                     userId: this.userId, | ||||
|                 }, | ||||
|                 CoreSites.instance.getCurrentSiteId(), | ||||
|             ); | ||||
| 
 | ||||
|             if (!this.assign!.submissiondrafts) { | ||||
|                 // No drafts allowed, so it was submitted. Trigger event.
 | ||||
|                 CoreEvents.trigger<AddonModAssignSubmittedForGradingEventData>( | ||||
|                     AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, | ||||
|                     { | ||||
|                         assignmentId: this.assign!.id, | ||||
|                         submissionId: this.userSubmission!.id, | ||||
|                         userId: this.userId, | ||||
|                     }, | ||||
|                     CoreSites.instance.getCurrentSiteId(), | ||||
|                 ); | ||||
|             } | ||||
|         } finally { | ||||
|             modal.dismiss(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
| 
 | ||||
|         // Unblock the assignment.
 | ||||
|         if (this.assign) { | ||||
|             CoreSync.instance.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -61,15 +61,15 @@ | ||||
|                                     {{ 'addon.mod_assign.defaultteam' | translate }} | ||||
|                                 </span> | ||||
|                             </p> | ||||
|                             <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor" | ||||
|                                 *ngIf="submission.statusTranslated"> | ||||
|                                 {{ submission.statusTranslated }} | ||||
|                             </ion-badge> | ||||
|                             <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.gradingColor" | ||||
|                                 *ngIf="submission.gradingStatusTranslationId"> | ||||
|                                 {{ submission.gradingStatusTranslationId | translate }} | ||||
|                             </ion-badge> | ||||
|                         </ion-label> | ||||
|                         <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor" | ||||
|                             *ngIf="submission.statusTranslated"> | ||||
|                             {{ submission.statusTranslated }} | ||||
|                         </ion-badge> | ||||
|                         <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.gradingColor" | ||||
|                             *ngIf="submission.gradingStatusTranslationId"> | ||||
|                             {{ submission.gradingStatusTranslationId | translate }} | ||||
|                         </ion-badge> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|  | ||||
| @ -17,6 +17,7 @@ import { ActivatedRoute } from '@angular/router'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreGroupInfo, CoreGroups } from '@services/groups'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| @ -37,6 +38,7 @@ import { | ||||
|     AddonModAssignManualSyncData, | ||||
|     AddonModAssignAutoSyncData, | ||||
| } from '../../services/assign-sync'; | ||||
| import { AddonModAssignModuleHandlerService } from '../../services/handlers/module'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a list of submissions of an assignment. | ||||
| @ -137,10 +139,10 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|                 this.title = Translate.instance.instant('addon.mod_assign.numberofparticipants'); | ||||
|             } | ||||
|             this.fetchAssignment(true).finally(() => { | ||||
|                 /* if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { | ||||
|                 if (!this.selectedSubmissionId && CoreScreen.instance.isTablet && this.submissions.length > 0) { | ||||
|                     // Take first and load it.
 | ||||
|                     this.loadSubmission(this.submissions[0]); | ||||
|                 }*/ | ||||
|                 } | ||||
| 
 | ||||
|                 this.loaded = true; | ||||
|             }); | ||||
| @ -153,7 +155,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|      * @param sync Whether to try to synchronize data. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchAssignment(sync?: boolean): Promise<void> { | ||||
|     protected async fetchAssignment(sync = false): Promise<void> { | ||||
|         try { | ||||
|             // Get assignment data.
 | ||||
|             this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); | ||||
| @ -310,19 +312,21 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy { | ||||
|      * @param submission The submission to load. | ||||
|      */ | ||||
|     loadSubmission(submission: AddonModAssignSubmissionForList): void { | ||||
|         /* if (this.selectedSubmissionId === submission.submitid && this.splitviewCtrl.isOn()) { | ||||
|         if (this.selectedSubmissionId === submission.submitid) { | ||||
|             // Already selected.
 | ||||
|             return; | ||||
|         }*/ | ||||
|         } | ||||
| 
 | ||||
|         this.selectedSubmissionId = submission.submitid; | ||||
| 
 | ||||
|         /* this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', { | ||||
|             courseId: this.courseId, | ||||
|             moduleId: this.moduleId, | ||||
|             submitId: submission.submitid, | ||||
|             blindId: submission.blindid, | ||||
|         });*/ | ||||
|         CoreNavigator.instance.navigateToSitePath( | ||||
|             AddonModAssignModuleHandlerService.PAGE_NAME+'/'+this.courseId+'/'+this.moduleId+'/submission/'+submission.submitid, | ||||
|             { | ||||
|                 params: { | ||||
|                     blindId: submission.blindid, | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -0,0 +1,29 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"> | ||||
|             </core-format-text> | ||||
|         </ion-title> | ||||
| 
 | ||||
|         <ion-buttons slot="end"></ion-buttons> | ||||
|     </ion-toolbar> | ||||
| 
 | ||||
|     <core-navbar-buttons slot="end"> | ||||
|         <ion-button [hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()" [attr.aria-label]="'core.done' | translate"> | ||||
|             {{ 'core.done' | translate }} | ||||
|         </ion-button> | ||||
|     </core-navbar-buttons> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
| 
 | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId"> | ||||
|         </addon-mod-assign-submission> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| @ -0,0 +1,184 @@ | ||||
| // (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, OnInit, ViewChild } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { AddonModAssignSubmissionComponent } from '../../components/submission/submission'; | ||||
| import { AddonModAssign, AddonModAssignAssign } from '../../services/assign'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a submission. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-mod-assign-submission-review', | ||||
|     templateUrl: 'submission-review.html', | ||||
| }) | ||||
| export class AddonModAssignSubmissionReviewPage implements OnInit { | ||||
| 
 | ||||
|     @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent; | ||||
| 
 | ||||
|     title = ''; // Title to display.
 | ||||
|     moduleId!: number; // Module ID the submission belongs to.
 | ||||
|     courseId!: number; // Course ID the assignment belongs to.
 | ||||
|     submitId!: number; //  User that did the submission.
 | ||||
|     blindId?: number; // Blinded user ID (if it's blinded).
 | ||||
|     loaded = false; // Whether data has been loaded.
 | ||||
|     canSaveGrades = false; // Whether the user can save grades.
 | ||||
| 
 | ||||
|     protected assign?: AddonModAssignAssign; // The assignment the submission belongs to.
 | ||||
|     protected blindMarking = false; // Whether it uses blind marking.
 | ||||
|     protected forceLeave = false; // To allow leaving the page without checking for changes.
 | ||||
| 
 | ||||
| 
 | ||||
|     constructor( | ||||
|         protected route: ActivatedRoute, | ||||
|     ) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.route.queryParams.subscribe((params) => { | ||||
|             this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!; | ||||
|             this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!; | ||||
|             this.submitId = CoreNavigator.instance.getRouteNumberParam('submitId') || 0; | ||||
|             this.blindId = CoreNavigator.instance.getRouteNumberParam('blindId', params); | ||||
| 
 | ||||
|             this.fetchSubmission().finally(() => { | ||||
|                 this.loaded = true; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we can leave the page or not. | ||||
|      * | ||||
|      * @return Resolved if we can leave it, rejected if not. | ||||
|      */ | ||||
|     ionViewCanLeave(): boolean | Promise<void> { | ||||
|         if (!this.submissionComponent || this.forceLeave) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Check if data has changed.
 | ||||
|         return this.submissionComponent.canLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         this.submissionComponent?.ionViewDidEnter(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         this.submissionComponent?.ionViewDidLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the submission. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchSubmission(): Promise<void> { | ||||
|         this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); | ||||
|         this.title = this.assign.name; | ||||
| 
 | ||||
|         this.blindMarking = !!this.assign.blindmarking && !this.assign.revealidentities; | ||||
| 
 | ||||
|         const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(this.moduleId); | ||||
|         if (!gradeInfo) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Grades can be saved if simple grading.
 | ||||
|         if (gradeInfo.advancedgrading && gradeInfo.advancedgrading[0] && | ||||
|                 typeof gradeInfo.advancedgrading[0].method != 'undefined') { | ||||
| 
 | ||||
|             const method = gradeInfo.advancedgrading[0].method || 'simple'; | ||||
|             this.canSaveGrades = method == 'simple'; | ||||
|         } else { | ||||
|             this.canSaveGrades = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh all the data. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async refreshAllData(): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId)); | ||||
|         if (this.assign) { | ||||
|             promises.push(AddonModAssign.instance.invalidateSubmissionData(this.assign.id)); | ||||
|             promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id)); | ||||
|             promises.push(AddonModAssign.instance.invalidateSubmissionStatusData( | ||||
|                 this.assign.id, | ||||
|                 this.submitId, | ||||
|                 undefined, | ||||
|                 this.blindMarking, | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await Promise.all(promises); | ||||
|         } finally { | ||||
|             this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true); | ||||
| 
 | ||||
|             await this.fetchSubmission(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the data. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      */ | ||||
|     refreshSubmission(refresher?: CustomEvent<IonRefresher>): void { | ||||
|         this.refreshAllData().finally(() => { | ||||
|             refresher?.detail.complete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit a grade and feedback. | ||||
|      */ | ||||
|     async submitGrade(): Promise<void> { | ||||
|         if (!this.submissionComponent) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             await this.submissionComponent.submitGrade(); | ||||
|             // Grade submitted, leave the view if not in tablet.
 | ||||
|             if (!CoreScreen.instance.isTablet) { | ||||
|                 this.forceLeave = true; | ||||
|                 CoreNavigator.instance.back(); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -49,7 +49,7 @@ export class AddonModAssignHelperProvider { | ||||
|      * @param submission Submission. | ||||
|      * @return Whether it can be edited offline. | ||||
|      */ | ||||
|     async canEditSubmissionOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission): Promise<boolean> { | ||||
|     async canEditSubmissionOffline(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): Promise<boolean> { | ||||
|         if (!submission) { | ||||
|             return false; | ||||
|         } | ||||
| @ -85,7 +85,15 @@ export class AddonModAssignHelperProvider { | ||||
|      * @param submission Submission to clear the data for. | ||||
|      * @param inputData Data entered in the submission form. | ||||
|      */ | ||||
|     clearSubmissionPluginTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any): void { | ||||
|     clearSubmissionPluginTmpData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission | undefined, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): void { | ||||
|         if (!submission) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         submission.plugins?.forEach((plugin) => { | ||||
|             AddonModAssignSubmissionDelegate.instance.clearTmpData(assign, submission, plugin, inputData); | ||||
|         }); | ||||
| @ -356,7 +364,7 @@ export class AddonModAssignHelperProvider { | ||||
|     async getSubmissionSizeForEdit( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         inputData: any, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): Promise<number> { | ||||
| 
 | ||||
|         let totalSize = 0; | ||||
| @ -492,27 +500,28 @@ export class AddonModAssignHelperProvider { | ||||
|      */ | ||||
|     async hasFeedbackDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted, | ||||
|         submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted | undefined, | ||||
|         feedback: AddonModAssignSubmissionFeedback, | ||||
|         userId: number, | ||||
|     ): Promise<boolean> { | ||||
|         if (!submission || !feedback.plugins) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         let hasChanged = false; | ||||
| 
 | ||||
|         const promises = feedback.plugins | ||||
|             ? feedback.plugins.map((plugin) => | ||||
|                 this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => { | ||||
|                     const changed = await CoreUtils.instance.ignoreErrors( | ||||
|                         AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId), | ||||
|                         false, | ||||
|                     ); | ||||
|                     if (changed) { | ||||
|                         hasChanged = true; | ||||
|                     } | ||||
|         const promises = feedback.plugins.map((plugin) => | ||||
|             this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => { | ||||
|                 const changed = await CoreUtils.instance.ignoreErrors( | ||||
|                     AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId), | ||||
|                     false, | ||||
|                 ); | ||||
|                 if (changed) { | ||||
|                     hasChanged = true; | ||||
|                 } | ||||
| 
 | ||||
|                     return; | ||||
|                 })) | ||||
|             : []; | ||||
|                 return; | ||||
|             })); | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises(promises); | ||||
| 
 | ||||
| @ -529,9 +538,13 @@ export class AddonModAssignHelperProvider { | ||||
|      */ | ||||
|     async hasSubmissionDataChanged( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         inputData: any, | ||||
|         submission: AddonModAssignSubmission | undefined, | ||||
|         inputData: Record<string, unknown>, | ||||
|     ): Promise<boolean> { | ||||
|         if (!submission) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         let hasChanged = false; | ||||
| 
 | ||||
|         const promises = submission.plugins | ||||
| @ -544,7 +557,7 @@ export class AddonModAssignHelperProvider { | ||||
| 
 | ||||
|                         return; | ||||
|                     }).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                         // Ignore errors.
 | ||||
|                     })) | ||||
|             : []; | ||||
| 
 | ||||
| @ -591,23 +604,25 @@ export class AddonModAssignHelperProvider { | ||||
|      */ | ||||
|     async prepareSubmissionPluginData( | ||||
|         assign: AddonModAssignAssign, | ||||
|         submission: AddonModAssignSubmission, | ||||
|         inputData: any, | ||||
|         submission: AddonModAssignSubmission | undefined, | ||||
|         inputData: Record<string, unknown>, | ||||
|         offline = false, | ||||
|     ): Promise<any> { | ||||
|     ): Promise<AddonModAssignSavePluginData> { | ||||
| 
 | ||||
|         const pluginData = {}; | ||||
|         const promises = submission.plugins | ||||
|             ? submission.plugins.map((plugin) => | ||||
|                 AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData( | ||||
|                     assign, | ||||
|                     submission, | ||||
|                     plugin, | ||||
|                     inputData, | ||||
|                     pluginData, | ||||
|                     offline, | ||||
|                 )) | ||||
|             : []; | ||||
|         if (!submission || !submission.plugins) { | ||||
|             return {}; | ||||
|         } | ||||
| 
 | ||||
|         const pluginData: AddonModAssignSavePluginData = {}; | ||||
|         const promises = submission.plugins.map((plugin) => | ||||
|             AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData( | ||||
|                 assign, | ||||
|                 submission, | ||||
|                 plugin, | ||||
|                 inputData, | ||||
|                 pluginData, | ||||
|                 offline, | ||||
|             )); | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
| 
 | ||||
|  | ||||
| @ -30,6 +30,7 @@ import { CoreUtils } from '@services/utils/utils'; | ||||
| import { AddonModAssignOffline } from './assign-offline'; | ||||
| import { AddonModAssignSubmissionDelegate } from './submission-delegate'; | ||||
| import { CoreComments } from '@features/comments/services/comments'; | ||||
| import { AddonModAssignSubmissionFormatted } from './assign-helper'; | ||||
| 
 | ||||
| const ROOT_CACHE_KEY = 'mmaModAssign:'; | ||||
| 
 | ||||
| @ -1011,7 +1012,7 @@ export class AddonModAssignProvider { | ||||
|      * @param assignId Assignment ID. | ||||
|      * @return Promise resolved with boolean: whether it needs to be graded or not. | ||||
|      */ | ||||
|     async needsSubmissionToBeGraded(submission: any, assignId: number): Promise<boolean> { | ||||
|     async needsSubmissionToBeGraded(submission: AddonModAssignSubmissionFormatted, assignId: number): Promise<boolean> { | ||||
|         if (!submission.gradingstatus) { | ||||
|             // This should not happen, but it's better to show rather than not showing any of the submissions.
 | ||||
|             return true; | ||||
| @ -1864,11 +1865,12 @@ export type AddonModAssignSubmittedForGradingEventData = { | ||||
|     userId: number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data sent by SUBMISSION_SAVED_EVENT event. | ||||
|  */ | ||||
| export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForGradingEventData; | ||||
| 
 | ||||
| /** | ||||
|  * Data sent by GRADED_EVENT event. | ||||
|  */ | ||||
| export type AddonModAssignGradedEventData = { | ||||
|     assignmentId: number; | ||||
|     submissionId: number; | ||||
|     userId: number; | ||||
| }; | ||||
| export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData; | ||||
|  | ||||
| @ -41,7 +41,7 @@ export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmiss | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async nOnInit(): Promise<void> { | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         // Get the offline data.
 | ||||
|         const filesData = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignOffline.instance.getSubmission(this.assign.id), | ||||
|  | ||||
| @ -24,12 +24,11 @@ | ||||
|         </ion-label> | ||||
|     </ion-item> | ||||
|     <ion-item class="ion-text-wrap"> | ||||
|         <ion-label> | ||||
|             <core-rich-text-editor [control]="control" [placeholder]="plugin.name" | ||||
|                 name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" | ||||
|                 [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||
|                 elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}"> | ||||
|             </core-rich-text-editor> | ||||
|         </ion-label> | ||||
|         <ion-label></ion-label> | ||||
|         <core-rich-text-editor [control]="control" [placeholder]="plugin.name" | ||||
|             name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" | ||||
|             [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||
|             elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}"> | ||||
|         </core-rich-text-editor> | ||||
|     </ion-item> | ||||
| </div> | ||||
|  | ||||
| @ -56,7 +56,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async nOnInit(): Promise<void> { | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         // Get the text. Check if we have anything offline.
 | ||||
|         const offlineData = await CoreUtils.instance.ignoreErrors( | ||||
|             AddonModAssignOffline.instance.getSubmission(this.assign.id), | ||||
|  | ||||
| @ -267,7 +267,7 @@ export class CoreCronDelegateService { | ||||
|      * @return True if handler uses network or not defined, false otherwise. | ||||
|      */ | ||||
|     protected handlerUsesNetwork(name: string): boolean { | ||||
|         if (!this.handlers[name] || this.handlers[name].usesNetwork) { | ||||
|         if (!this.handlers[name] || !this.handlers[name].usesNetwork) { | ||||
|             // Invalid, return default.
 | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
| @ -298,3 +298,10 @@ export type CoreEventSectionStatusChangedData = CoreEventSiteData & { | ||||
|     courseId: number; | ||||
|     sectionId?: number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data passed to ACTIVITY_DATA_SENT event. | ||||
|  */ | ||||
| export type CoreEventActivityDataSentData = CoreEventSiteData & { | ||||
|     module: string; | ||||
| }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user