MOBILE-3636 assign: Submission review and edit pages

main
Pau Ferrer Ocaña 2021-02-19 12:41:30 +01:00
parent a4b356350b
commit 5350f715a3
19 changed files with 912 additions and 174 deletions

View File

@ -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 {}

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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,
);

View File

@ -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>

View 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>

View 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);
}
}
}

View File

@ -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>

View File

@ -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,
},
},
);
}
/**

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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),

View File

@ -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>

View File

@ -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),

View File

@ -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;
}

View File

@ -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;
};