MOBILE-2334 assign: Implement submission component

main
Dani Palou 2018-04-13 08:16:14 +02:00
parent 0bb96f0e80
commit f3ae04600f
11 changed files with 1280 additions and 12 deletions

View File

@ -18,12 +18,15 @@ import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModAssignIndexComponent } from './index/index';
import { AddonModAssignSubmissionComponent } from './submission/submission';
@NgModule({
declarations: [
AddonModAssignIndexComponent
AddonModAssignIndexComponent,
AddonModAssignSubmissionComponent
],
imports: [
CommonModule,
@ -31,12 +34,14 @@ import { AddonModAssignIndexComponent } from './index/index';
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonModAssignIndexComponent
AddonModAssignIndexComponent,
AddonModAssignSubmissionComponent
],
entryComponents: [
AddonModAssignIndexComponent

View File

@ -82,6 +82,7 @@
</div>
</ion-card>
<!-- @todo <addon-mod-assign-submission *ngIf="!canViewSubmissions" [courseId]="courseId" [moduleId]="module.id"></addon-mod-assign-submission> -->
<!-- If it's a student, display his submission. -->
<addon-mod-assign-submission *ngIf="loaded && !canViewSubmissions" [courseId]="courseId" [moduleId]="module.id"></addon-mod-assign-submission>
</core-loading>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Optional, Injector } from '@angular/core';
import { Component, Optional, Injector, ViewChild } from '@angular/core';
import { Content, NavController } from 'ionic-angular';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
@ -22,6 +22,7 @@ import { AddonModAssignHelperProvider } from '../../providers/helper';
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
import { AddonModAssignSyncProvider } from '../../providers/assign-sync';
import * as moment from 'moment';
import { AddonModAssignSubmissionComponent } from '../submission/submission';
/**
* Component that displays an assignment.
@ -31,6 +32,8 @@ import * as moment from 'moment';
templateUrl: 'index.html',
})
export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent {
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent: AddonModAssignSubmissionComponent;
component = AddonModAssignProvider.COMPONENT;
moduleName = 'assign';
@ -238,11 +241,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
*/
protected hasSyncSucceed(result: any): boolean {
if (result.updated) {
// Sync done, trigger event.
this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, {
assignId: this.assign.id,
warnings: result.warnings
}, this.siteId);
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
}
return result.updated;
@ -267,7 +266,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
}
return Promise.all(promises).finally(() => {
// @todo $scope.$broadcast(mmaModAssignSubmissionInvalidatedEvent);
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
});
}

View File

@ -0,0 +1,247 @@
<core-loading [hideUntil]="loaded">
<!-- User and status of the submission. -->
<a ion-item text-wrap *ngIf="!blindMarking && user" (click)="openUserProfile(submitId)" [title]="user.fullname">
<ion-avatar item-start>
<img [src]="user.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: user.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2>{{ user.fullname }}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</a>
<!-- Status of the submission if user is blinded. -->
<ion-item text-wrap *ngIf="blindMarking && !user">
<h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-item>
<!-- Status of the submission in the rest of cases. -->
<ion-item text-wrap *ngIf="(blindMarking && user) || (!blindMarking && !user)">
<h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-item>
<!-- Tabs: see the submission or grade it. -->
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded">
<!-- View the submission tab. -->
<core-tab [title]="'addon.mod_assign.submission' | translate">
<ng-template>
<ion-content>
<!-- @todo <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" assign="assign" submission="userSubmission" plugin="plugin" scroll-handle="{{scrollHandle}}"></addon-mod-assign-submission-plugin> -->
<!-- Render some data about the submission. -->
<ion-item text-wrap *ngIf="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified">
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
<p>{{ userSubmission.timemodified * 1000 | coreFormatDate:"dfmediumdate" }}</p>
</ion-item>
<ion-item text-wrap *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]">
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p><core-format-text [text]="timeRemaining"></core-format-text></p>
</ion-item>
<ion-item text-wrap *ngIf="fromDate && !isSubmittedForGrading">
<p *ngIf="assign.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}"></p>
<p *ngIf="!assign.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate: {'$a': fromDate}"></p>
</ion-item>
<ion-item text-wrap *ngIf="assign.duedate && !isSubmittedForGrading">
<h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
<p *ngIf="assign.duedate" >{{ assign.duedate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
<p *ngIf="!assign.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p>
</ion-item>
<ion-item text-wrap *ngIf="assign.duedate && assign.cutoffdate && isSubmittedForGrading">
<h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2>
<p>{{ assign.cutoffdate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
</ion-item>
<ion-item text-wrap *ngIf="assign.duedate && lastAttempt && lastAttempt.extensionduedate && !isSubmittedForGrading">
<h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2>
<p>{{ lastAttempt.extensionduedate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
</ion-item>
<ion-item text-wrap *ngIf="currentAttempt && !isGrading">
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
<p *ngIf="assign.maxattempts == unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}</p>
<p *ngIf="assign.maxattempts != unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }}</p>
</ion-item>
<!-- Add or edit submission. -->
<ion-item text-wrap *ngIf="canEdit">
<div *ngIf="!unsupportedEditPlugins.length && !showErrorStatementEdit">
<!-- If has offline data, show edit. -->
<a ion-button block color="primary" *ngIf="hasOffline" (click)="goToEdit()">{{ 'addon.mod_assign.editsubmission' | translate }}</a>
<!-- If no submission or is new, show add submission. -->
<a ion-button block color="primary" *ngIf="!hasOffline && (!userSubmission || !userSubmission.status || userSubmission.status == statusNew)" (click)="goToEdit()">{{ 'addon.mod_assign.addsubmission' | translate }}</a>
<!-- If reopened, show addfromprevious and addnewattempt. -->
<ng-container *ngIf="!hasOffline && userSubmission && userSubmission.status == statusReopened">
<a ion-button block color="primary" (click)="copyPrevious()">{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}</a>
<a ion-button block color="primary" (click)="goToEdit()">{{ 'addon.mod_assign.addnewattempt' | translate }}</a>
</ng-container>
<!-- Else show editsubmission. -->
<a ion-button block color="primary" *ngIf="!hasOffline && userSubmission && userSubmission.status && userSubmission.status != statusNew && userSubmission.status != statusReopened" (click)="goToEdit()">{{ 'addon.mod_assign.editsubmission' | translate }}</a>
</div>
<div *ngIf="unsupportedEditPlugins && unsupportedEditPlugins.length && !showErrorStatementEdit">
<p class="core-danger-item">{{ 'addon.mod_assign.erroreditpluginsnotsupported' | translate }}</p>
<p class="core-danger-item" *ngFor="let name of unsupportedEditPlugins">{{ name }}</p>
</div>
<div *ngIf="showErrorStatementEdit">
<p class="core-danger-item">{{ 'addon.mod_assign.cannoteditduetostatementsubmission' | translate }}</p>
</div>
</ion-item>
<!-- Submit for grading form. -->
<div *ngIf="canSubmit">
<ion-item text-wrap *ngIf="submissionStatement">
<ion-label><core-format-text [text]="submissionStatement"></core-format-text></ion-label>
<ion-checkbox item-end name="submissionstatement" [(ngModel)]="submitModel.submissionStatement">
</ion-checkbox>
</ion-item>
<!-- Submit button. -->
<ion-item text-wrap *ngIf="!showErrorStatementSubmit">
<a ion-button block (click)="submitForGrading(submitModel.submissionStatement)">{{ 'addon.mod_assign.submitassignment' | translate }}</a>
<p>{{ 'addon.mod_assign.submitassignment_help' | translate }}</p>
</ion-item>
<!-- Error because we lack submissions statement. -->
<ion-item text-wrap *ngIf="showErrorStatementSubmit">
<p class="core-danger-item">{{ 'addon.mod_assign.cannotsubmitduetostatementsubmission' | translate }}</p>
</ion-item>
</div>
<!-- Team members that need to submit it too. -->
<ion-item text-wrap *ngIf="membersToSubmit && membersToSubmit.length > 0">
<h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate }}</h2>
<div *ngFor="let user of membersToSubmit">
<a *ngIf="user.fullname" (click)="openUserProfile(user.id)" [title]="user.fullname">
<ion-avatar item-start>
<img [src]="user.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: user.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2>{{ user.fullname }}</h2>
</a>
<p *ngIf="!user.fullname">
{{ 'addon.mod_assign.hiddenuser' | translate }} <core-format-text [text]="user"></core-format-text>
</p>
</div>
</ion-item>
<!-- Submission is locked. -->
<ion-item text-wrap *ngIf="lastAttempt && lastAttempt.locked">
<h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2>
</ion-item>
<!-- Editing status. -->
<ion-item text-wrap *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt.caneditowner !== undefined" [ngClass]="{submissioneditable: lastAttempt.caneditowner, submissionnoteditable: !lastAttempt.caneditowner}">
<h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2>
<p *ngIf="lastAttempt.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p>
<p *ngIf="!lastAttempt.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p>
</ion-item>
</ion-content>
</ng-template>
</core-tab>
<!-- Grade the submission tab. -->
<core-tab [title]="'addon.mod_assign.grade' | translate" *ngIf="feedback || isGrading">
<ng-template>
<ion-content>
<!-- Current grade if method is advanced. -->
<ion-item text-wrap *ngIf="feedback.gradefordisplay && (!isGrading || grade.method != 'simple')" class="core-grading-summary">
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
<p><core-format-text [text]="feedback.gradefordisplay"></core-format-text></p>
<a ion-button item-end icon-only *ngIf="feedback.advancedgrade" (click)="showAdvancedGrade()">
<ion-icon name="search"></ion-icon>
</a>
</ion-item>
<!-- Numeric grade. -->
<ion-item text-wrap *ngIf="grade.method == 'simple' && !grade.scale">
<ion-label stacked>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo.grade} }}</ion-label>
<ion-input type="number" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo.grade" [lang]="grade.lang" core-input-errors></ion-input>
</ion-item>
<!-- Grade using a scale. -->
<ion-item text-wrap *ngIf="grade.method == 'simple' && grade.scale">
<ion-label>{{ 'addon.mod_assign.grade' | translate }}</ion-label>
<ion-select [(ngModel)]="grade.grade" interface="popover">
<ion-option *ngFor="let grade of grade.scale" [value]="grade.value">{{grade.label}}</ion-option>
</ion-select>
</ion-item>
<!-- Outcomes. -->
<ion-item text-wrap *ngFor="let outcome of gradeInfo.outcomes">
<ion-label>{{ outcome.name }}</ion-label>
<ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" interface="popover">
<ion-option *ngFor="let grade of outcome.options" [value]="grade.value">{{grade.label}}</ion-option>
</ion-select>
<p item-content *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
</ion-item>
<!-- @todo <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" assign="assign" submission="userSubmission" userid="submitId" plugin="plugin" can-edit="canSaveGrades"></addon-mod-assign-feedback-plugin> -->
<!-- Workflow status. -->
<ion-item text-wrap *ngIf="workflowStatusTranslationId">
<h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2>
<p>{{ workflowStatusTranslationId | translate }}</p>
</ion-item>
<!--- Apply grade to all team members. -->
<ion-item text-wrap *ngIf="assign.teamsubmission && canSaveGrades">
<h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2>
<ion-label>{{ 'addon.mod_assign.applytoteam' | translate }}</ion-label>
<ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle>
</ion-item>
<!-- Attempt status. -->
<ion-item text-wrap *ngIf="isGrading && assign.attemptreopenmethod != attemptReopenMethodNone">
<h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2>
<p *ngIf="assign.maxattempts == unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}</p>
<p *ngIf="assign.maxattempts != unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }}</p>
<p>{{ 'addon.mod_assign.attemptreopenmethod' | translate }}: {{ 'addon.mod_assign.attemptreopenmethod_' + assign.attemptreopenmethod | translate }}</p>
<ng-container *ngIf="canSaveGrades && allowAddAttempt" >
<ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label>
<ion-toggle [(ngModel)]="grade.addAttempt"></ion-toggle>
</ng-container>
</ion-item>
<!-- Data about the grader (teacher who graded). -->
<ion-item text-wrap *ngIf="grader">
<h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2>
<a (click)="openUserProfile(grader.id)" [title]="grader.fullname">
<ion-avatar item-start>
<img [src]="grader.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: grader.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2>{{ grader.fullname }}</h2>
<p *ngIf="feedback.gradeddate">{{ feedback.gradeddate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
</a>
</ion-item>
<!-- Warning message if cannot save grades. -->
<div *ngIf="isGrading && !canSaveGrades" class="core-warning-card" icon-start>
<ion-icon name="warning"></ion-icon>
<p>{{ 'addon.mod_assign.cannotgradefromapp' | translate:{$a: moduleName} }}</p>
<a ion-button block *ngIf="gradeUrl" [href]="gradeUrl" core-link icon-end>
{{ 'core.openinbrowser' | translate }}
<ion-icon name="open"></ion-icon>
</a>
</div>
</ion-content>
</ng-template>
</core-tab>
</core-tabs>
</core-loading>
<!-- Template to render some data regarding the submission status. -->
<ng-template #submissionStatus>
<p *ngIf="assign && assign.teamsubmission && lastAttempt">
<span *ngIf="lastAttempt.submissiongroup && lastAttempt.submissiongroupname">{{lastAttempt.submissiongroupname}}</span>
<span *ngIf="assign.preventsubmissionnotingroup && !lastAttempt.submissiongroup && !lastAttempt.usergroups">{{ 'addon.mod_assign.noteam' | translate }}</span>
<span *ngIf="assign.preventsubmissionnotingroup && !lastAttempt.submissiongroup && lastAttempt.usergroups">{{ 'addon.mod_assign.multipleteams' | translate }}</span>
<span *ngIf="!assign.preventsubmissionnotingroup && !lastAttempt.submissiongroup">{{ 'addon.mod_assign.defaultteam' | translate }}</span>
</p>
<ion-badge item-end *ngIf="statusTranslated" [color]="statusColor">
{{ statusTranslated }}
</ion-badge>
<ion-badge item-end *ngIf="gradingStatusTranslationId" [color]="gradingColor">
{{ gradingStatusTranslationId | translate }}
</ion-badge>
</ng-template>

View File

@ -0,0 +1,18 @@
addon-mod-assign-submission {
div.latesubmission,
div.overdue {
// @extend .core-danger-item;
}
div.earlysubmission {
// @extend .core-success-item;
}
div.submissioneditable p {
color: $red;
}
.core-grading-summary .advancedgrade {
display: none;
}
}

View File

@ -0,0 +1,924 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit, OnDestroy, ViewChild, Optional } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreLangProvider } from '@providers/lang';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSyncProvider } from '@providers/sync';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper';
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModAssignProvider } from '../../providers/assign';
import { AddonModAssignHelperProvider } from '../../providers/helper';
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
import * as moment from 'moment';
import { CoreTabsComponent } from '@components/tabs/tabs';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
/**
* Component that displays an assignment submission.
*/
@Component({
selector: 'addon-mod-assign-submission',
templateUrl: 'submission.html',
})
export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
@ViewChild(CoreTabsComponent) tabs: CoreTabsComponent;
@Input() courseId: number; // Course ID the submission belongs to.
@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: boolean | string; // Whether to display the grade tab at start.
loaded: boolean; // Whether data has been loaded.
selectedTab: number; // Tab selected on start.
assign: any; // The assignment the submission belongs to.
userSubmission: any; // The submission object.
isSubmittedForGrading: boolean; // Whether the submission has been submitted for grading.
submitModel: any = {}; // Model where to store the data to submit (for grading).
feedback: any; // The feedback.
hasOffline: boolean; // Whether there is offline data.
submittedOffline: boolean; // Whether it was submitted in offline.
fromDate: string; // Readable date when the assign started accepting submissions.
currentAttempt: number; // The current attempt number.
maxAttemptsText: string; // The text for maximum attempts.
blindMarking: boolean; // Whether blind marking is enabled.
user: any; // The user.
lastAttempt: any; // The last attempt.
membersToSubmit: any[]; // Team members that need to submit the assignment.
canSubmit: boolean; // Whether the user can submit for grading.
canEdit: boolean; // Whether the user can edit the submission.
submissionStatement: string; // The submission statement.
showErrorStatementEdit: boolean; // Whether to show an error in edit due to submission statement.
showErrorStatementSubmit: boolean; // Whether to show an error in submit due to submission statement.
gradingStatusTranslationId: string; // Key of the text to display for the grading status.
gradingColor: string; // Color to apply to the grading status.
workflowStatusTranslationId: string; // Key of the text to display for the workflow status.
submissionPlugins: string[]; // List of submission plugins names.
timeRemaining: string; // Message about time remaining.
timeRemainingClass: string; // Class to apply to time remaining message.
statusTranslated: string; // Status.
statusColor: string; // Color to apply to the status.
unsupportedEditPlugins: string[]; // List of submission plugins that don't support edit.
grade: any; // Data about the grade.
grader: any; // Profile of the teacher that graded the submission.
gradeInfo: any; // Grade data for the assignment, retrieved from the server.
isGrading: boolean; // Whether the user is grading.
canSaveGrades: boolean; // Whether the user can save the grades.
allowAddAttempt: boolean; // Allow adding a new attempt when grading.
gradeUrl: string; // URL to grade in browser.
// Some constants.
statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW;
statusReopened = AddonModAssignProvider.SUBMISSION_STATUS_REOPENED;
attemptReopenMethodNone = AddonModAssignProvider.ATTEMPT_REOPEN_METHOD_NONE;
unlimitedAttempts = AddonModAssignProvider.UNLIMITED_ATTEMPTS;
protected siteId: string; // Current site ID.
protected currentUserId: number; // Current user ID.
protected previousAttempt: any; // The previous attempt.
protected submissionStatusAvailable: boolean; // Whether we were able to retrieve the submission status.
protected originalGrades: any = {}; // Object with the original grade data, to check for changes.
protected isDestroyed: boolean; // Whether the component has been destroyed.
constructor(protected navCtrl: NavController, protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider,
sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected timeUtils: CoreTimeUtilsProvider,
protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, protected utils: CoreUtilsProvider,
protected eventsProvider: CoreEventsProvider, protected courseProvider: CoreCourseProvider,
protected fileUploaderHelper: CoreFileUploaderHelperProvider, protected gradesHelper: CoreGradesHelperProvider,
protected userProvider: CoreUserProvider, protected groupsProvider: CoreGroupsProvider,
protected langProvider: CoreLangProvider, protected assignProvider: AddonModAssignProvider,
protected assignHelper: AddonModAssignHelperProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
@Optional() protected splitviewCtrl: CoreSplitViewComponent) {
this.siteId = sitesProvider.getCurrentSiteId();
this.currentUserId = sitesProvider.getCurrentSiteUserId();
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.selectedTab = this.showGrade && this.showGrade !== 'false' ? 1 : 0;
this.isSubmittedForGrading = !!this.submitId;
this.loadData();
}
/**
* Calculate the time remaining message and class.
*
* @param {any} response Response of get submission status.
*/
protected calculateTimeRemaining(response: any): void {
if (this.assign.duedate > 0) {
const time = this.timeUtils.timestamp(),
dueDate = response.lastattempt && response.lastattempt.extensionduedate ?
response.lastattempt.extensionduedate : this.assign.duedate,
timeRemaining = dueDate - time;
if (timeRemaining <= 0) {
if (!this.userSubmission || this.userSubmission.status != AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED) {
if ((response.lastattempt && response.lastattempt.submissionsenabled) ||
(response.gradingsummary && response.gradingsummary.submissionsenabled)) {
this.timeRemaining = this.translate.instant('addon.mod_assign.overdue',
{$a: this.timeUtils.formatDuration(-timeRemaining, 3) });
this.timeRemainingClass = 'overdue';
} else {
this.timeRemaining = this.translate.instant('addon.mod_assign.duedatereached');
this.timeRemainingClass = '';
}
} else {
const timeSubmittedDiff = this.userSubmission.timemodified - dueDate;
if (timeSubmittedDiff > 0) {
this.timeRemaining = this.translate.instant('addon.mod_assign.submittedlate',
{$a: this.timeUtils.formatDuration(timeSubmittedDiff, 2) });
this.timeRemainingClass = 'latesubmission';
} else {
this.timeRemaining = this.translate.instant('addon.mod_assign.submittedearly',
{$a: this.timeUtils.formatDuration(-timeSubmittedDiff, 2) });
this.timeRemainingClass = 'earlysubmission';
}
}
} else {
this.timeRemaining = this.timeUtils.formatDuration(timeRemaining, 3);
this.timeRemainingClass = '';
}
} else {
this.timeRemaining = '';
this.timeRemainingClass = '';
}
}
/**
* Check if the user can leave the view. If there are changes to be saved, it will ask for confirm.
*
* @return {Promise<void>} Promise resolved if can leave the view, rejected otherwise.
*/
canLeave(): Promise<void> {
// Check if there is data to save.
return this.hasDataToSave().then((modified) => {
if (modified) {
// Modified, confirm user wants to go back.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')).then(() => {
return this.discardDrafts().catch(() => {
// Ignore errors.
});
});
}
});
}
/**
* Copy a previous attempt and then go to edit.
*/
copyPrevious(): void {
if (!this.appProvider.isOnline()) {
this.domUtils.showErrorModal('mm.core.networkerrormsg', true);
return;
}
if (!this.previousAttempt) {
// Cannot access previous attempts, just go to edit.
return this.goToEdit();
}
const previousSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, this.previousAttempt);
let modal = this.domUtils.showModalLoading();
this.assignHelper.getSubmissionSizeForCopy(this.assign, previousSubmission).catch(() => {
// Error calculating size, return -1.
return -1;
}).then((size) => {
modal.dismiss();
// Confirm action.
return this.fileUploaderHelper.confirmUploadFile(size, true);
}).then(() => {
// User confirmed, copy the attempt.
modal = this.domUtils.showModalLoading('core.sending', true);
this.assignHelper.copyPreviousAttempt(this.assign, previousSubmission).then(() => {
// Now go to edit.
this.goToEdit();
// Invalidate and refresh data to update this view.
this.invalidateAndRefresh();
if (!this.assign.submissiondrafts) {
// No drafts allowed, so it was submitted. Trigger event.
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, {
assignmentId: this.assign.id,
submissionId: this.userSubmission.id,
userId: this.currentUserId
}, this.siteId);
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
}).finally(() => {
modal.dismiss();
});
});
}
/**
* Discard feedback drafts.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected discardDrafts(): Promise<any> {
if (this.feedback && this.feedback.plugins) {
return this.assignHelper.discardFeedbackPluginData(this.assign.id, this.submitId, this.feedback);
}
return Promise.resolve();
}
/**
* Go to the page to add or edit submission.
*/
goToEdit(): void {
this.navCtrl.push('AddonModAssignEditPage', {
moduleId: this.moduleId,
courseId: this.courseId,
userId: this.submitId,
blindId: this.blindId
});
}
/**
* Check if there's data to save (grade).
*
* @return {Promise<boolean>} Promise resolved with boolean: whether there's data to save.
*/
protected hasDataToSave(): Promise<boolean> {
if (!this.canSaveGrades || !this.loaded) {
return Promise.resolve(false);
}
// Check if numeric grade and toggles changed.
if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt ||
this.originalGrades.applyToAll != this.grade.applyToAll) {
return Promise.resolve(true);
}
// Check if outcomes changed.
if (this.gradeInfo && this.gradeInfo.outcomes) {
for (const x in this.gradeInfo.outcomes) {
const outcome = this.gradeInfo.outcomes[x];
if (this.originalGrades.outcomes[outcome.id] == 'undefined' ||
this.originalGrades.outcomes[outcome.id] != outcome.selectedId) {
return Promise.resolve(true);
}
}
}
if (this.feedback && this.feedback.plugins) {
return this.assignHelper.hasFeedbackDataChanged(this.assign, this.submitId, this.feedback).catch(() => {
// Error ocurred, consider there are no changes.
return false;
});
}
return Promise.resolve(false);
}
/**
* Invalidate and refresh data.
*
* @return {Promise<any>} Promise resolved when done.
*/
invalidateAndRefresh(): Promise<any> {
this.loaded = false;
const promises = [];
promises.push(this.assignProvider.invalidateAssignmentData(this.courseId));
if (this.assign) {
promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, !!this.blindId));
promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id));
promises.push(this.assignProvider.invalidateListParticipantsData(this.assign.id));
}
promises.push(this.gradesHelper.invalidateGradeModuleItems(this.courseId, this.submitId));
promises.push(this.courseProvider.invalidateModule(this.moduleId));
return Promise.all(promises).catch(() => {
// Ignore errors.
}).then(() => {
return this.loadData();
});
}
/**
* Load the data to render the submission.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected loadData(): Promise<any> {
let isBlind = !!this.blindId;
this.previousAttempt = undefined;
if (!this.submitId) {
this.submitId = this.currentUserId;
isBlind = false;
}
// Get the assignment.
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => {
const time = this.timeUtils.timestamp(),
promises = [];
this.assign = assign;
if (assign.allowsubmissionsfromdate && assign.allowsubmissionsfromdate >= time) {
this.fromDate = moment(assign.allowsubmissionsfromdate * 1000).format(this.translate.instant('core.dfmediumdate'));
}
this.currentAttempt = 0;
this.maxAttemptsText = this.translate.instant('addon.mod_assign.unlimitedattempts');
this.blindMarking = this.isSubmittedForGrading && assign.blindmarking && !assign.revealidentities;
if (!this.blindMarking && this.submitId != this.currentUserId) {
promises.push(this.userProvider.getProfile(this.submitId, this.courseId).then((profile) => {
this.user = profile;
}));
}
// Check if there's any offline data for this submission.
promises.push(this.assignOfflineProvider.getSubmission(assign.id, this.submitId).then((data) => {
this.hasOffline = data && data.plugindata && Object.keys(data.plugindata).length > 0;
this.submittedOffline = data && data.submitted;
}).catch(() => {
// No offline data found.
this.hasOffline = false;
this.submittedOffline = false;
}));
return Promise.all(promises);
}).then(() => {
// Get submission status.
return this.assignProvider.getSubmissionStatus(this.assign.id, this.submitId, isBlind);
}).then((response) => {
const promises = [];
this.submissionStatusAvailable = true;
this.lastAttempt = response.lastattempt;
this.membersToSubmit = [];
// Search the previous attempt.
if (response.previousattempts && response.previousattempts.length > 0) {
const previousAttempts = response.previousattempts.sort((a, b) => {
return a.attemptnumber - b.attemptnumber;
});
this.previousAttempt = previousAttempts[previousAttempts.length - 1];
}
// Treat last attempt.
this.treatLastAttempt(response, promises);
// Calculate the time remaining.
this.calculateTimeRemaining(response);
// Load the feedback.
promises.push(this.loadFeedback(response.feedback));
// Check if there's any unsupported plugin for editing.
if (!this.userSubmission || !this.userSubmission.plugins) {
// Submission not created yet, we have to use assign configs to detect the plugins used.
this.userSubmission = {};
this.userSubmission.plugins = this.assignHelper.getPluginsEnabled(this.assign, 'assignsubmission');
}
// Get the submission plugins that don't support editing.
promises.push(this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins).then((list) => {
this.unsupportedEditPlugins = list;
}));
return Promise.all(promises);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.');
}).finally(() => {
this.loaded = true;
});
}
/**
* Load the data to render the feedback and grade.
*
* @param {any} feedback The feedback data from the submission status.
* @return {Promise<any>} Promise resolved when done.
*/
protected loadFeedback(feedback: any): Promise<any> {
this.grade = {
method: false,
grade: false,
modified: 0,
gradingStatus: false,
addAttempt : false,
applyToAll: false,
scale: false,
lang: false
};
this.originalGrades = {
grade: false,
addAttempt: false,
applyToAll: false,
outcomes: {}
};
if (feedback) {
this.feedback = feedback;
// If we have data about the grader, get its profile.
if (feedback.grade && feedback.grade.grader) {
this.userProvider.getProfile(feedback.grade.grader, this.courseId).then((profile) => {
this.grader = profile;
}).catch(() => {
// Ignore errors.
});
}
// Check if the grade uses advanced grading.
if (feedback.gradefordisplay) {
const position = feedback.gradefordisplay.indexOf('class="advancedgrade"');
if (position > -1) {
this.feedback.advancedgrade = true;
}
}
// Do not override already loaded grade.
if (feedback.grade && feedback.grade.grade && !this.grade.grade) {
const parsedGrade = parseFloat(feedback.grade.grade);
this.grade.grade = parsedGrade || parsedGrade == 0 ? parsedGrade : null;
}
} else {
// If no feedback, always show Submission.
this.selectedTab = 0;
this.tabs.selectTab(0);
}
this.grade.gradingStatus = this.lastAttempt && this.lastAttempt.gradingstatus;
// Get the grade for the assign.
return this.courseProvider.getModuleBasicGradeInfo(this.moduleId).then((gradeInfo) => {
this.gradeInfo = gradeInfo;
if (!gradeInfo) {
return;
}
if (!this.isDestroyed) {
// Block the assignment.
this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
}
// Treat the grade info.
return this.treatGradeInfo();
}).then(() => {
if (!this.isGrading) {
return;
}
const isManual = this.assign.attemptreopenmethod == AddonModAssignProvider.ATTEMPT_REOPEN_METHOD_MANUAL,
isUnlimited = this.assign.maxattempts == AddonModAssignProvider.UNLIMITED_ATTEMPTS,
isLessThanMaxAttempts = this.userSubmission && (this.userSubmission.attemptnumber < (this.assign.maxattempts - 1));
this.allowAddAttempt = isManual && (!this.userSubmission || isUnlimited || isLessThanMaxAttempts);
if (this.assign.teamsubmission) {
this.grade.applyToAll = true;
this.originalGrades.applyToAll = true;
}
if (this.assign.markingworkflow && this.grade.gradingStatus) {
this.workflowStatusTranslationId =
this.assignProvider.getSubmissionGradingStatusTranslationId(this.grade.gradingStatus);
}
if (!this.feedback || !this.feedback.plugins) {
// Feedback plugins not present, we have to use assign configs to detect the plugins used.
this.feedback = {};
this.feedback.plugins = this.assignHelper.getPluginsEnabled(this.assign, 'assignfeedback');
}
// Check if there's any offline data for this submission.
if (this.canSaveGrades) {
// Submission grades aren't identified by attempt number so it can retrieve the feedback for a previous attempt.
// The app will not treat that as an special case.
return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.submitId).catch(() => {
// Grade not found.
}).then((data) => {
// Load offline grades.
if (data && (!feedback || !feedback.gradeddate || feedback.gradeddate < data.timemodified)) {
// If grade has been modified from gradebook, do not use offline.
if (this.grade.modified < data.timemodified) {
this.grade.grade = data.grade;
this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
this.gradingColor = '';
this.originalGrades.grade = this.grade.grade;
}
this.grade.applyToAll = data.applytoall;
this.grade.addAttempt = data.addattempt;
this.originalGrades.applyToAll = this.grade.applyToAll;
this.originalGrades.addAttempt = this.grade.addAttempt;
if (data.outcomes && Object.keys(data.outcomes).length) {
this.gradeInfo.outcomes.forEach((outcome) => {
if (typeof data.outcomes[outcome.itemNumber] != 'undefined') {
// If outcome has been modified from gradebook, do not use offline.
if (outcome.modified < data.timemodified) {
outcome.selectedId = data.outcomes[outcome.itemNumber];
this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
}
}
});
}
}
});
} else {
// User cannot save grades in the app. Load the URL to grade it in browser.
return this.courseProvider.getModule(this.moduleId, this.courseId, undefined, true).then((mod) => {
this.gradeUrl = mod.url + '&action=grader&userid=' + this.submitId;
});
}
});
}
/**
* Open a user profile.
*
* @param {number} userId User to open.
*/
openUserProfile(userId: number): void {
// Open a user profile. If this component is inside a split view, use the master nav to open it.
const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl;
navCtrl.push('CoreUserProfilePage', { userId: userId, courseId: this.courseId });
}
/**
* Set the submission status name and class.
*
* @param {any} status Submission status.
*/
protected setStatusNameAndClass(status: any): void {
if (this.hasOffline) {
// Offline data.
this.statusTranslated = this.translate.instant('core.notsent');
this.statusColor = 'warning';
} else if (!this.assign.teamsubmission) {
// Single submission.
if (this.userSubmission && this.userSubmission.status != this.statusNew) {
this.statusTranslated = this.translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
this.statusColor = this.assignProvider.getSubmissionStatusColor(this.userSubmission.status);
} else {
if (!status.lastattempt.submissionsenabled) {
this.statusTranslated = this.translate.instant('addon.mod_assign.noonlinesubmissions');
this.statusColor = this.assignProvider.getSubmissionStatusColor('noonlinesubmissions');
} else {
this.statusTranslated = this.translate.instant('addon.mod_assign.noattempt');
this.statusColor = this.assignProvider.getSubmissionStatusColor('noattempt');
}
}
} else {
// Team submission.
if (!status.lastattempt.submissiongroup && this.assign.preventsubmissionnotingroup) {
this.statusTranslated = this.translate.instant('addon.mod_assign.nosubmission');
this.statusColor = this.assignProvider.getSubmissionStatusColor('nosubmission');
} else if (this.userSubmission && this.userSubmission.status != this.statusNew) {
this.statusTranslated = this.translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
this.statusColor = this.assignProvider.getSubmissionStatusColor(this.userSubmission.status);
} else {
if (!status.lastattempt.submissionsenabled) {
this.statusTranslated = this.translate.instant('addon.mod_assign.noonlinesubmissions');
this.statusColor = this.assignProvider.getSubmissionStatusColor('noonlinesubmissions');
} else {
this.statusTranslated = this.translate.instant('addon.mod_assign.nosubmission');
this.statusColor = this.assignProvider.getSubmissionStatusColor('nosubmission');
}
}
}
}
/**
* Show advanced grade.
*/
showAdvancedGrade(): void {
if (this.feedback && this.feedback.advancedgrade) {
this.textUtils.expandText(this.translate.instant('core.grades.grade'), this.feedback.gradefordisplay,
AddonModAssignProvider.COMPONENT, this.moduleId);
}
}
/**
* Submit for grading.
*
* @param {boolean} acceptStatement Whether the statement has been accepted.
*/
submitForGrading(acceptStatement: boolean): void {
if (this.assign.requiresubmissionstatement && !acceptStatement) {
this.domUtils.showErrorModal('addon.mod_assign.acceptsubmissionstatement', true);
return;
}
// Ask for confirmation. @todo plugin precheck_submission
this.domUtils.showConfirm(this.translate.instant('addon.mod_assign.confirmsubmission')).then(() => {
const modal = this.domUtils.showModalLoading('core.sending', true);
this.assignProvider.submitForGrading(this.assign.id, this.courseId, acceptStatement, this.userSubmission.timemodified,
this.hasOffline).then(() => {
// Invalidate and refresh data.
this.invalidateAndRefresh();
// Submitted, trigger event.
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, {
assignmentId: this.assign.id,
submissionId: this.userSubmission.id,
userId: this.currentUserId
}, this.siteId);
}).finally(() => {
modal.dismiss();
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
});
}
/**
* Submit a grade and feedback.
*
* @return {Promise<any>} Promise resolved when done.
*/
submitGrade(): Promise<any> {
// Check if there's something to be saved.
return this.hasDataToSave().then((modified) => {
if (!modified) {
return;
}
const attemptNumber = this.userSubmission ? this.userSubmission.attemptnumber : -1,
outcomes = {},
// Scale "no grade" uses -1 instead of 0.
grade = this.grade.scale && this.grade.grade == 0 ? -1 : this.utils.unformatFloat(this.grade.grade);
if (grade === false) {
// Grade is invalid.
return Promise.reject(this.translate.instant('core.grades.badgrade'));
}
const modal = this.domUtils.showModalLoading('core.sending', true);
let pluginPromise;
this.gradeInfo.outcomes.forEach((outcome) => {
if (outcome.itemNumber) {
outcomes[outcome.itemNumber] = outcome.selectedId;
}
});
if (this.feedback && this.feedback.plugins) {
pluginPromise = this.assignHelper.prepareFeedbackPluginData(this.assign.id, this.submitId, this.feedback);
} else {
pluginPromise = Promise.resolve({});
}
return pluginPromise.then((pluginData) => {
// We have all the data, now send it.
return this.assignProvider.submitGradingForm(this.assign.id, this.submitId, this.courseId, grade, attemptNumber,
this.grade.addAttempt, this.grade.gradingStatus, this.grade.applyToAll, outcomes, pluginData).then(() => {
// Data sent, discard draft.
return this.discardDrafts();
}).finally(() => {
// Invalidate and refresh data.
this.invalidateAndRefresh();
this.eventsProvider.trigger(AddonModAssignProvider.GRADED_EVENT, {
assignmentId: this.assign.id,
submissionId: this.submitId,
userId: this.currentUserId
}, this.siteId);
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.error', true);
}).finally(() => {
// Select submission view.
this.tabs.selectTab(0);
modal.dismiss();
});
});
}
/**
* Treat the grade info.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected treatGradeInfo(): Promise<any> {
// Check if grading method is simple or not.
if (this.gradeInfo.advancedgrading && this.gradeInfo.advancedgrading[0] &&
typeof this.gradeInfo.advancedgrading[0].method != 'undefined') {
this.grade.method = this.gradeInfo.advancedgrading[0].method || 'simple';
} else {
this.grade.method = 'simple';
}
this.isGrading = true;
this.canSaveGrades = this.grade.method == 'simple'; // Grades can be saved if simple grading.
if (this.gradeInfo.scale) {
this.grade.scale = this.utils.makeMenuFromList(this.gradeInfo.scale, this.translate.instant('core.nograde'));
} else {
// Get current language to format grade input field.
this.langProvider.getCurrentLanguage().then((lang) => {
this.grade.lang = lang;
});
}
// Treat outcomes.
if (this.assignProvider.isOutcomesEditEnabled()) {
this.gradeInfo.outcomes.forEach((outcome) => {
if (outcome.scale) {
outcome.options =
this.utils.makeMenuFromList(outcome.scale, this.translate.instant('core.grades.nooutcome'));
}
outcome.selectedId = 0;
this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
});
}
// Get grade items.
return this.gradesHelper.getGradeModuleItems(this.courseId, this.moduleId, this.submitId).then((grades) => {
const outcomes = [];
grades.forEach((grade) => {
if (!grade.outcomeid && !grade.scaleid) {
// Not using outcomes or scale, get the numeric grade.
if (this.grade.scale) {
this.grade.grade = this.gradesHelper.getGradeValueFromLabel(this.grade.scale, grade.gradeformatted);
} else {
const parsedGrade = parseFloat(grade.gradeformatted);
this.grade.grade = parsedGrade || parsedGrade == 0 ? parsedGrade : null;
}
this.grade.modified = grade.gradedategraded;
this.originalGrades.grade = this.grade.grade;
} else if (grade.outcomeid) {
// Only show outcomes with info on it, outcomeid could be null if outcomes are disabled on site.
this.gradeInfo.outcomes.forEach((outcome) => {
if (outcome.id == grade.outcomeid) {
outcome.selected = grade.gradeformatted;
outcome.modified = grade.gradedategraded;
if (outcome.options) {
outcome.selectedId = this.gradesHelper.getGradeValueFromLabel(outcome.options, outcome.selected);
this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
outcome.itemNumber = grade.itemnumber;
}
outcomes.push(outcome);
}
});
}
});
this.gradeInfo.outcomes = outcomes;
});
}
/**
* Treat the last attempt.
*
* @param {any} response Response of get submission status.
* @param {any[]} promises List where to add the promises.
*/
protected treatLastAttempt(response: any, promises: any[]): void {
if (!response.lastattempt) {
return;
}
const submissionStatementMissing = this.assign.requiresubmissionstatement &&
typeof this.assign.submissionstatement == 'undefined';
this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (response.lastattempt.cansubmit ||
(this.hasOffline && this.assignProvider.canSubmitOffline(this.assign, response)));
this.canEdit = !this.isSubmittedForGrading && response.lastattempt.canedit &&
(!this.submittedOffline || !this.assign.submissiondrafts);
// Get submission statement if needed.
if (this.assign.requiresubmissionstatement && this.assign.submissiondrafts && this.submitId == this.currentUserId) {
this.submissionStatement = this.assign.submissionstatement;
this.submitModel.submissionStatement = false;
} else {
this.submissionStatement = undefined;
this.submitModel.submissionStatement = true; // No submission statement, so it's accepted.
}
// Show error if submission statement should be shown but it couldn't be retrieved.
this.showErrorStatementEdit = submissionStatementMissing && !this.assign.submissiondrafts &&
this.submitId == this.currentUserId;
this.showErrorStatementSubmit = submissionStatementMissing && this.assign.submissiondrafts;
this.userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt);
if (this.assign.attemptreopenmethod != this.attemptReopenMethodNone && this.userSubmission) {
this.currentAttempt = this.userSubmission.attemptnumber + 1;
}
this.setStatusNameAndClass(response);
if (this.assign.teamsubmission) {
if (response.lastattempt.submissiongroup) {
// Get the name of the group.
promises.push(this.groupsProvider.getActivityAllowedGroups(this.assign.cmid).then((groups) => {
groups.forEach((group) => {
if (group.id == response.lastattempt.submissiongroup) {
this.lastAttempt.submissiongroupname = group.name;
}
});
}));
}
// Get the members that need to submit.
if (this.userSubmission && this.userSubmission.status != this.statusNew) {
response.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => {
if (this.blindMarking) {
// Users not blinded! (Moodle < 3.1.1, 3.2).
promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member).then((blindId) => {
this.membersToSubmit.push(blindId);
}));
} else {
promises.push(this.userProvider.getProfile(member, this.courseId).then((profile) => {
this.membersToSubmit.push(profile);
}));
}
});
response.lastattempt.submissiongroupmemberswhoneedtosubmitblind.forEach((member) => {
this.membersToSubmit.push(member);
});
}
}
// Get grading text and color.
this.gradingStatusTranslationId = this.assignProvider.getSubmissionGradingStatusTranslationId(
response.lastattempt.gradingstatus);
this.gradingColor = this.assignProvider.getSubmissionGradingStatusColor(response.lastattempt.gradingstatus);
// Get the submission plugins.
if (this.userSubmission) {
if (!this.assign.teamsubmission || !response.lastattempt.submissiongroup || !this.assign.preventsubmissionnotingroup) {
if (this.previousAttempt && this.previousAttempt.submission.plugins &&
this.userSubmission.status == this.statusReopened) {
// Get latest attempt if avalaible.
this.submissionPlugins = this.previousAttempt.submission.plugins;
} else {
this.submissionPlugins = this.userSubmission.plugins;
}
}
}
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
if (this.assign && this.isGrading) {
this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
}
}
}

View File

@ -52,7 +52,6 @@ export interface AddonModAssignSyncResult {
export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_assign_autom_synced';
static MANUAL_SYNCED = 'addon_mod_assign_manual_synced';
static SYNC_TIME = 300000;
protected componentTranslate: string;

View File

@ -1,5 +1,5 @@
<core-loading [hideUntil]="hideUntil" class="core-loading-center">
<div class="core-tabs-bar" #topTabs>
<div class="core-tabs-bar" #topTabs [hidden]="!tabs || tabs.length < 2">
<ng-container *ngFor="let tab of tabs; let idx = index">
<a *ngIf="tab.show" [attr.aria-selected]="selected == idx" (click)="selectTab(idx)">
<ion-icon *ngIf="tab.icon" [name]="tab.icon"></ion-icon>

View File

@ -1,5 +1,6 @@
{
"average": "Average",
"badgrade": "Supplied grade is invalid",
"contributiontocoursetotal": "Contribution to course total",
"feedback": "Feedback",
"grade": "Grade",
@ -7,6 +8,7 @@
"grades": "Grades",
"lettergrade": "Letter grade",
"nogradesreturned": "No grades returned",
"nooutcome": "No outcome",
"percentage": "Percentage",
"range": "Range",
"rank": "Rank",

View File

@ -234,6 +234,29 @@ export class CoreGradesHelperProvider {
});
}
/**
* Returns the label of the selected grade.
*
* @param {any[]} grades Array with objects with value and label.
* @param {number} selectedGrade Selected grade value.
* @return {string} Selected grade label.
*/
getGradeLabelFromValue(grades: any[], selectedGrade: number): string {
selectedGrade = Number(selectedGrade);
if (!grades || !selectedGrade || selectedGrade <= 0) {
return '';
}
for (const x in grades) {
if (grades[x].value == selectedGrade) {
return grades[x].label;
}
}
return '';
}
/**
* Get the grade items for a certain module. Keep in mind that may have more than one item to include outcomes and scales.
*
@ -266,6 +289,27 @@ export class CoreGradesHelperProvider {
});
}
/**
* Returns the value of the selected grade.
*
* @param {any[]} grades Array with objects with value and label.
* @param {string} selectedGrade Selected grade label.
* @return {number} Selected grade value.
*/
getGradeValueFromLabel(grades: any[], selectedGrade: string): number {
if (!grades || !selectedGrade) {
return 0;
}
for (const x in grades) {
if (grades[x].label == selectedGrade) {
return grades[x].value < 0 ? 0 : grades[x].value;
}
}
return 0;
}
/**
* Gets the link to the module for the selected grade.
*

View File

@ -629,6 +629,35 @@ export class CoreUtilsProvider {
return typeof error.errorcode == 'undefined' && typeof error.warningcode == 'undefined';
}
/**
* Given a list (e.g. a,b,c,d,e) this function returns an array of 1->a, 2->b, 3->c etc.
* Taken from make_menu_from_list on moodlelib.php (not the same but similar).
*
* @param {string} list The string to explode into array bits
* @param {string} [defaultLabel] Element that will become default option, if not defined, it won't be added.
* @param {string} [separator] The separator used within the list string. Default ','.
* @param {any} [defaultValue] Element that will become default option value. Default 0.
* @return {any[]} The now assembled array
*/
makeMenuFromList(list: string, defaultLabel?: string, separator: string = ',', defaultValue?: any): any[] {
// Split and format the list.
const split = list.split(separator).map((label, index) => {
return {
label: label.trim(),
value: index + 1
};
});
if (defaultLabel) {
split.unshift({
label: defaultLabel,
value: defaultValue || 0
});
}
return split;
}
/**
* Merge two arrays, removing duplicate values.
*