MOBILE-3636 assign: Add assignment submission page
parent
a4eefeb25a
commit
3281196ec0
|
@ -21,13 +21,16 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
import { AddonModAssignIndexComponent } from './index/index';
|
||||
import { AddonModAssignSubmissionComponent } from './submission/submission';
|
||||
import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin';
|
||||
import { AddonModAssignFeedbackPluginComponent } from './feedback-plugin/feedback-plugin';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignIndexComponent,
|
||||
/* AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionPluginComponent,
|
||||
AddonModAssignFeedbackPluginComponent*/
|
||||
AddonModAssignFeedbackPluginComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -39,9 +42,9 @@ import { AddonModAssignIndexComponent } from './index/index';
|
|||
],
|
||||
exports: [
|
||||
AddonModAssignIndexComponent,
|
||||
/* AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionPluginComponent,
|
||||
AddonModAssignFeedbackPluginComponent */
|
||||
AddonModAssignFeedbackPluginComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
<core-dynamic-component [component]="pluginComponent" [data]="data">
|
||||
<!-- This content will be replaced by the component if found. -->
|
||||
<core-loading [hideUntil]="pluginLoaded">
|
||||
<ion-item class="ion-text-wrap" *ngIf="text.length > 0 || files.length > 0">
|
||||
<ion-label>
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<ion-badge *ngIf="notSupported" color="primary">
|
||||
{{ 'addon.mod_assign.feedbacknotsupported' | translate }}
|
||||
</ion-badge>
|
||||
<p *ngIf="text">
|
||||
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true"
|
||||
[fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid"
|
||||
[courseId]="assign.course">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"
|
||||
[alwaysDownload]="true">
|
||||
</core-file>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
|
@ -0,0 +1,117 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit, ViewChild, Type } from '@angular/core';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import {
|
||||
AddonModAssignAssign,
|
||||
AddonModAssignSubmission,
|
||||
AddonModAssignPlugin,
|
||||
AddonModAssignProvider,
|
||||
AddonModAssign,
|
||||
} from '../../services/assign';
|
||||
import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper';
|
||||
import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment feedback plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-feedback-plugin',
|
||||
templateUrl: 'addon-mod-assign-feedback-plugin.html',
|
||||
})
|
||||
export class AddonModAssignFeedbackPluginComponent implements OnInit {
|
||||
|
||||
@ViewChild(CoreDynamicComponent) dynamicComponent!: CoreDynamicComponent;
|
||||
|
||||
@Input() assign!: AddonModAssignAssign; // The assignment.
|
||||
@Input() submission!: AddonModAssignSubmission; // The submission.
|
||||
@Input() plugin!: AddonModAssignPlugin; // The plugin object.
|
||||
@Input() userId!: number; // The user ID of the submission.
|
||||
@Input() canEdit = false; // Whether the user can edit.
|
||||
@Input() edit = false; // Whether the user is editing.
|
||||
|
||||
pluginComponent?: Type<unknown>; // Component to render the plugin.
|
||||
data?: AddonModAssignFeedbackPluginData; // Data to pass to the component.
|
||||
|
||||
// Data to render the plugin if it isn't supported.
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text = '';
|
||||
files: CoreWSExternalFile[] = [];
|
||||
notSupported = false;
|
||||
pluginLoaded = false;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (!this.plugin) {
|
||||
this.pluginLoaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const name = AddonModAssignFeedbackDelegate.instance.getPluginName(this.plugin);
|
||||
|
||||
if (!name) {
|
||||
this.pluginLoaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
this.plugin.name = name;
|
||||
|
||||
// Check if the plugin has defined its own component to render itself.
|
||||
this.pluginComponent = await AddonModAssignFeedbackDelegate.instance.getComponentForPlugin(this.plugin);
|
||||
|
||||
if (this.pluginComponent) {
|
||||
// Prepare the data to pass to the component.
|
||||
this.data = {
|
||||
assign: this.assign,
|
||||
submission: this.submission,
|
||||
plugin: this.plugin,
|
||||
userId: this.userId,
|
||||
configs: AddonModAssignHelper.instance.getPluginConfig(this.assign, 'assignfeedback', this.plugin.type),
|
||||
edit: this.edit,
|
||||
canEdit: this.canEdit,
|
||||
};
|
||||
} else {
|
||||
// Data to render the plugin.
|
||||
this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin);
|
||||
this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin);
|
||||
this.notSupported = AddonModAssignFeedbackDelegate.instance.isPluginSupported(this.plugin.type);
|
||||
this.pluginLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the plugin data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async invalidate(): Promise<void> {
|
||||
await this.dynamicComponent.callComponentFunction('invalidate', []);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type AddonModAssignFeedbackPluginData = {
|
||||
assign: AddonModAssignAssign;
|
||||
submission: AddonModAssignSubmission;
|
||||
plugin: AddonModAssignPlugin;
|
||||
configs: AddonModAssignPluginConfig;
|
||||
edit: boolean;
|
||||
canEdit: boolean;
|
||||
userId: number;
|
||||
};
|
|
@ -135,8 +135,8 @@
|
|||
</ng-container>
|
||||
|
||||
<!-- If it's a student, display his submission. -->
|
||||
<!-- @todo <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
|
||||
<addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
|
||||
[moduleId]="module.id">
|
||||
</addon-mod-assign-submission>-->
|
||||
</addon-mod-assign-submission>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
AddonModAssignGradedEventData,
|
||||
AddonModAssignProvider,
|
||||
AddonModAssignSubmissionGradingSummary,
|
||||
AddonModAssignSubmittedForGradingEventData,
|
||||
} from '../../services/assign';
|
||||
import { AddonModAssignOffline } from '../../services/assign-offline';
|
||||
import {
|
||||
|
@ -42,6 +43,7 @@ import {
|
|||
AddonModAssignSyncProvider,
|
||||
AddonModAssignSyncResult,
|
||||
} from '../../services/assign-sync';
|
||||
import { AddonModAssignSubmissionComponent } from '../submission/submission';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment.
|
||||
|
@ -52,8 +54,7 @@ import {
|
|||
})
|
||||
export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||
|
||||
// @todo @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
|
||||
submissionComponent?: any;
|
||||
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
|
||||
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
moduleName = 'assign';
|
||||
|
@ -112,7 +113,9 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
}, this.siteId);
|
||||
|
||||
this.submittedObserver = CoreEvents.on<any>(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, (data) => {
|
||||
this.submittedObserver = CoreEvents.on<AddonModAssignSubmittedForGradingEventData>(
|
||||
AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT,
|
||||
(data) => {
|
||||
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
|
||||
// Assignment submitted, check completion.
|
||||
CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
|
||||
|
@ -120,7 +123,9 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
// Reload data since it can have offline data now.
|
||||
this.showLoadingAndRefresh(true, false);
|
||||
}
|
||||
}, this.siteId);
|
||||
},
|
||||
this.siteId,
|
||||
);
|
||||
|
||||
this.gradedObserver = CoreEvents.on<AddonModAssignGradedEventData>(AddonModAssignProvider.GRADED_EVENT, (data) => {
|
||||
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
<core-dynamic-component [component]="pluginComponent" [data]="data">
|
||||
<!-- This content will be replaced by the component if found. -->
|
||||
<core-loading [hideUntil]="pluginLoaded">
|
||||
<ion-item class="ion-text-wrap" *ngIf="text.length > 0 || files.length > 0">
|
||||
<ion-label>
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<ion-badge *ngIf="notSupported" color="primary">
|
||||
{{ 'addon.mod_assign.submissionnotsupported' | translate }}
|
||||
</ion-badge>
|
||||
<p *ngIf="text">
|
||||
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true"
|
||||
[fullTitle]="plugin.name" [text]="text" contextLevel="module" [contextInstanceId]="assign.cmid"
|
||||
[courseId]="assign.course">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid"
|
||||
[alwaysDownload]="true">
|
||||
</core-file>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
|
@ -0,0 +1,114 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit, Type, ViewChild } from '@angular/core';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import {
|
||||
AddonModAssignAssign,
|
||||
AddonModAssignSubmission,
|
||||
AddonModAssignPlugin,
|
||||
AddonModAssignProvider,
|
||||
AddonModAssign,
|
||||
} from '../../services/assign';
|
||||
import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper';
|
||||
import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment submission plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-submission-plugin',
|
||||
templateUrl: 'addon-mod-assign-submission-plugin.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionPluginComponent implements OnInit {
|
||||
|
||||
@ViewChild(CoreDynamicComponent) dynamicComponent!: CoreDynamicComponent;
|
||||
|
||||
@Input() assign!: AddonModAssignAssign; // The assignment.
|
||||
@Input() submission!: AddonModAssignSubmission; // The submission.
|
||||
@Input() plugin!: AddonModAssignPlugin; // The plugin object.
|
||||
@Input() edit = false; // Whether the user is editing.
|
||||
@Input() allowOffline = false; // Whether to allow offline.
|
||||
|
||||
pluginComponent?: Type<unknown>; // Component to render the plugin.
|
||||
data?: AddonModAssignSubmissionPluginData; // Data to pass to the component.
|
||||
|
||||
// Data to render the plugin if it isn't supported.
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text = '';
|
||||
files: CoreWSExternalFile[] = [];
|
||||
notSupported = false;
|
||||
pluginLoaded = false;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (!this.plugin) {
|
||||
this.pluginLoaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const name = AddonModAssignSubmissionDelegate.instance.getPluginName(this.plugin);
|
||||
|
||||
if (!name) {
|
||||
this.pluginLoaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
this.plugin.name = name;
|
||||
|
||||
// Check if the plugin has defined its own component to render itself.
|
||||
this.pluginComponent = await AddonModAssignSubmissionDelegate.instance.getComponentForPlugin(this.plugin, this.edit);
|
||||
|
||||
if (this.pluginComponent) {
|
||||
// Prepare the data to pass to the component.
|
||||
this.data = {
|
||||
assign: this.assign,
|
||||
submission: this.submission,
|
||||
plugin: this.plugin,
|
||||
configs: AddonModAssignHelper.instance.getPluginConfig(this.assign, 'assignsubmission', this.plugin.type),
|
||||
edit: this.edit,
|
||||
allowOffline: this.allowOffline,
|
||||
};
|
||||
} else {
|
||||
// Data to render the plugin.
|
||||
this.text = AddonModAssign.instance.getSubmissionPluginText(this.plugin);
|
||||
this.files = AddonModAssign.instance.getSubmissionPluginAttachments(this.plugin);
|
||||
this.notSupported = AddonModAssignSubmissionDelegate.instance.isPluginSupported(this.plugin.type);
|
||||
this.pluginLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the plugin data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async invalidate(): Promise<void> {
|
||||
await this.dynamicComponent.callComponentFunction('invalidate', []);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type AddonModAssignSubmissionPluginData = {
|
||||
assign: AddonModAssignAssign;
|
||||
submission: AddonModAssignSubmission;
|
||||
plugin: AddonModAssignPlugin;
|
||||
configs: AddonModAssignPluginConfig;
|
||||
edit: boolean;
|
||||
allowOffline: boolean;
|
||||
};
|
|
@ -0,0 +1,388 @@
|
|||
<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">
|
||||
<core-user-avatar [user]="user" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>{{ user.fullname }}</h2>
|
||||
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
|
||||
</ion-label>
|
||||
<ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container>
|
||||
</ion-item>
|
||||
|
||||
<!-- Status of the submission if user is blinded. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="blindMarking && !user">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2>
|
||||
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
|
||||
</ion-label>
|
||||
<ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container>
|
||||
</ion-item>
|
||||
|
||||
<!-- Status of the submission in the rest of cases. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2>
|
||||
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
|
||||
</ion-label>
|
||||
<ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container>
|
||||
</ion-item>
|
||||
|
||||
<!-- Tabs: see the submission or grade it. -->
|
||||
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true" (ionChange)="tabSelected($event)">
|
||||
<!-- View the submission tab. -->
|
||||
<core-tab [title]="'addon.mod_assign.submission' | translate" id="submission">
|
||||
<ng-template>
|
||||
<addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins"
|
||||
[assign]="assign" [submission]="userSubmission" [plugin]="plugin">
|
||||
</addon-mod-assign-submission-plugin>
|
||||
|
||||
<!-- Render some data about the submission. -->
|
||||
<ion-item class="ion-text-wrap"
|
||||
*ngIf="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
|
||||
<p>{{ userSubmission.timemodified * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
|
||||
<p [innerHTML]="timeRemaining"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="fromDate && !isSubmittedForGrading">
|
||||
<ion-label>
|
||||
<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-label>
|
||||
</ion-item>
|
||||
|
||||
<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>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<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>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap"
|
||||
*ngIf="assign.duedate && lastAttempt && lastAttempt.extensionduedate && !isSubmittedForGrading">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2>
|
||||
<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">
|
||||
{{ '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-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Add or edit submission. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="canEdit">
|
||||
<ion-label>
|
||||
<div *ngIf="!unsupportedEditPlugins.length && !showErrorStatementEdit">
|
||||
<!-- If has offline data, show edit. -->
|
||||
<ion-button expand="block" class="ion-text-wrap" color="primary" *ngIf="hasOffline"
|
||||
(click)="goToEdit()">
|
||||
{{ 'addon.mod_assign.editsubmission' | translate }}
|
||||
</ion-button>
|
||||
<!-- 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)"
|
||||
(click)="goToEdit()">
|
||||
{{ 'addon.mod_assign.addsubmission' | translate }}
|
||||
</ion-button>
|
||||
<!-- If reopened, show addfromprevious and addnewattempt. -->
|
||||
<ng-container *ngIf="!hasOffline && userSubmission && userSubmission.status == statusReopened">
|
||||
<ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" color="primary"
|
||||
(click)="copyPrevious()">
|
||||
{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}
|
||||
</ion-button>
|
||||
<ion-button expand="block" class="ion-text-wrap" color="primary" (click)="goToEdit()">
|
||||
{{ 'addon.mod_assign.addnewattempt' | translate }}
|
||||
</ion-button>
|
||||
</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()">
|
||||
{{ 'addon.mod_assign.editsubmission' | translate }}
|
||||
</ion-button>
|
||||
</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-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Submit for grading form. -->
|
||||
<ng-container *ngIf="canSubmit">
|
||||
<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)]="acceptStatement">
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
<!-- Submit button. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="!showErrorStatementSubmit">
|
||||
<ion-label>
|
||||
<ion-button expand="block" class="ion-text-wrap"
|
||||
(click)="submitForGrading(acceptStatement)">
|
||||
{{ 'addon.mod_assign.submitassignment' | translate }}
|
||||
</ion-button>
|
||||
<p>{{ 'addon.mod_assign.submitassignment_help' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- Error because we lack submissions statement. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="showErrorStatementSubmit">
|
||||
<ion-label>
|
||||
<p class="core-danger-item">
|
||||
{{ 'addon.mod_assign.cannotsubmitduetostatementsubmission' | translate }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- Team members that need to submit it too. -->
|
||||
<ion-item 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>
|
||||
</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>
|
||||
</ion-item>
|
||||
|
||||
<!-- Submission is locked. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked">
|
||||
<ion-label><h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Editing status. -->
|
||||
<ion-item class="ion-text-wrap"
|
||||
*ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined"
|
||||
[ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}">
|
||||
<ion-label>
|
||||
<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-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
|
||||
<!-- Grade the submission tab. -->
|
||||
<core-tab [title]="'addon.mod_assign.grade' | translate" *ngIf="feedback || isGrading" id="grade">
|
||||
<ng-template>
|
||||
<!-- Current grade if method is advanced. -->
|
||||
<ion-item class="ion-text-wrap core-grading-summary"
|
||||
*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>
|
||||
</ion-label>
|
||||
<ion-button slot="end" *ngIf="feedback.advancedgrade" (click)="showAdvancedGrade()">
|
||||
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="isGrading">
|
||||
<!-- Numeric grade.
|
||||
Use a text input because otherwise we cannot readthe value if it has an invalid character. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && !grade.scale">
|
||||
<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"
|
||||
[lang]="grade.lang">
|
||||
</ion-input>
|
||||
<p item-content *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Grade using a scale. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple' && grade.scale">
|
||||
<ion-label><h2>{{ 'addon.mod_assign.grade' | translate }}</h2></ion-label>
|
||||
<ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled">
|
||||
<ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value">
|
||||
{{grade.label}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<!-- Outcomes. -->
|
||||
<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">
|
||||
<ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value">
|
||||
{{grade.label}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
<p item-content *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Gradebook grade for simple grading. -->
|
||||
<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">
|
||||
{{ grade.gradebookGrade }}
|
||||
</p>
|
||||
<p *ngIf="grade.gradebookGrade !== false && grade.gradebookGrade !== null && grade.scale">
|
||||
{{ grade.scale[grade.gradebookGrade].label }}
|
||||
</p>
|
||||
<p *ngIf="grade.gradebookGrade === false || grade.gradebookGrade === null">-</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Workflow status. -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="workflowStatusTranslationId">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2>
|
||||
<p>{{ workflowStatusTranslationId | translate }}</p>
|
||||
</ion-label>
|
||||
</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-toggle [(ngModel)]="grade.applyToAll"></ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<!-- Attempt status. -->
|
||||
<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">
|
||||
{{ '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>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canSaveGrades && allowAddAttempt" >
|
||||
<ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="grade.addAttempt"></ion-toggle>
|
||||
</ion-item>
|
||||
</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">
|
||||
<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>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Grader is hidden, display only the grade date. -->
|
||||
<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>
|
||||
</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>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
</core-tabs>
|
||||
</core-loading>
|
||||
|
||||
<!-- Template to render some data regarding the submission status. -->
|
||||
<ng-template #submissionStatus>
|
||||
<ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt">
|
||||
<p *ngIf="lastAttempt!.submissiongroup && lastAttempt!.submissiongroupname">{{lastAttempt!.submissiongroupname}}</p>
|
||||
<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 &&
|
||||
!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">
|
||||
{{ 'addon.mod_assign.defaultteam' | translate }}
|
||||
</p>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-template #submissionStatusBadges>
|
||||
<ion-badge slot="end" *ngIf="statusTranslated" [color]="statusColor">
|
||||
{{ statusTranslated }}
|
||||
</ion-badge>
|
||||
<ion-badge slot="end" *ngIf="gradingStatusTranslationId" [color]="gradingColor">
|
||||
{{ gradingStatusTranslationId | translate }}
|
||||
</ion-badge>
|
||||
</ng-template>
|
|
@ -0,0 +1,263 @@
|
|||
:host {
|
||||
div.latesubmission,
|
||||
div.overdue {
|
||||
// @extend .core-danger-item;
|
||||
}
|
||||
|
||||
div.earlysubmission {
|
||||
// @extend .core-success-item;
|
||||
}
|
||||
|
||||
div.submissioneditable p {
|
||||
color: $red;
|
||||
@include darkmode() {
|
||||
color: $red-light;
|
||||
}
|
||||
}
|
||||
|
||||
.core-grading-summary .advancedgrade {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
core-format-text {
|
||||
|
||||
.gradingform_rubric_editform .status {
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
font-size: 60%;
|
||||
padding: 0.25em;
|
||||
border: 1px solid $gray-light;
|
||||
}
|
||||
|
||||
.gradingform_rubric_editform .status.ready {
|
||||
background-color: $green-light;
|
||||
border-color: $green;
|
||||
}
|
||||
|
||||
.gradingform_rubric_editform .status.draft {
|
||||
background-color: $yellow-light;
|
||||
border-color: $yellow;
|
||||
}
|
||||
|
||||
.gradingform_rubric {
|
||||
overflow: auto;
|
||||
padding-bottom: 1.5em;
|
||||
max-width: 720px;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
tbody {
|
||||
background: $white;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not display remark column.
|
||||
.gradingform_rubric .criterion .remark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gradingform_rubric.editor .criterion .controls,
|
||||
.gradingform_rubric .criterion .description,
|
||||
.gradingform_rubric .criterion .levels,
|
||||
.gradingform_rubric.editor .criterion .addlevel,
|
||||
.gradingform_rubric .criterion .remark,
|
||||
.gradingform_rubric .criterion .levels .level {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.gradingform_rubric.editor .criterion .controls,
|
||||
.gradingform_rubric .criterion .description,
|
||||
.gradingform_rubric.editor .criterion .addlevel,
|
||||
.gradingform_rubric .criterion .remark,
|
||||
.gradingform_rubric .criterion .levels .level {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criteria {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion {
|
||||
border: 1px solid $gray;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion.even {
|
||||
background: $gray-lighter;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .description {
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels,
|
||||
.gradingform_rubric .criterion .levels table,
|
||||
.gradingform_rubric .criterion .levels table tbody {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level {
|
||||
border-left: 1px solid $gray;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level .level-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level.last {
|
||||
border-right: 1px solid $gray;
|
||||
}
|
||||
|
||||
.gradingform_rubric .plainvalue.empty {
|
||||
font-style: italic;
|
||||
color: $gray-dark;
|
||||
}
|
||||
|
||||
.gradingform_rubric.editor .criterion .levels .level .delete {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level .score {
|
||||
font-style: italic;
|
||||
color: $green;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level .score .scorevalue {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
/* Make invisible the buttons 'Move up' for the first criterion and
|
||||
'Move down' for the last, because those buttons will make no change */
|
||||
.gradingform_rubric.editor .criterion.first .controls .moveup input,
|
||||
.gradingform_rubric.editor .criterion.last .controls .movedown input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* evaluation */
|
||||
.gradingform_rubric .criterion .levels .level.currentchecked {
|
||||
background: #fff0f0;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level.checked {
|
||||
background: $green-light;
|
||||
border: 1px solid $gray-darker;
|
||||
}
|
||||
|
||||
.gradingform_rubric .options .optionsheading {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.gradingform_rubric .options .option {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.gradingform_rubric .options .option label {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.gradingform_rubric .options .option .value {
|
||||
margin-left: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels.error {
|
||||
border: 1px solid $red;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .description.error,
|
||||
.gradingform_rubric .criterion .levels .level .definition.error,
|
||||
.gradingform_rubric .criterion .levels .level .score.error {
|
||||
background: $gray-lighter;
|
||||
}
|
||||
|
||||
.gradingform_rubric-regrade {
|
||||
padding: 10px;
|
||||
background: $gray-lighter;
|
||||
border: 1px solid $red-light;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.gradingform_rubric-restored {
|
||||
padding: 10px;
|
||||
background: $yellow-light;
|
||||
border: 1px solid $yellow;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.gradingform_rubric-error {
|
||||
color: $red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* special classes for elements created by rubriceditor.js */
|
||||
.gradingform_rubric.editor .hiddenelement {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gradingform_rubric.editor .pseudotablink {
|
||||
background-color: transparent;
|
||||
border: 0 solid;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.gradingform_rubric {
|
||||
padding-bottom: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .description {
|
||||
font-weight: 500;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion,
|
||||
.gradingform_rubric .criterion.even {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.gradingform_rubric.evaluate .criterion .levels .level:hover {
|
||||
background-color: $green-light;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level.checked {
|
||||
background-color: $green-light;
|
||||
border: none;
|
||||
border-left: 1px solid $gray;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .levels .level .score {
|
||||
color: $green;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.gradingform_rubric .criterion .remark textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -24,6 +24,8 @@ import {
|
|||
AddonModAssignParticipant,
|
||||
AddonModAssignSubmissionFeedback,
|
||||
AddonModAssign,
|
||||
AddonModAssignPlugin,
|
||||
AddonModAssignSavePluginData,
|
||||
} from './assign';
|
||||
import { AddonModAssignOffline } from './assign-offline';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
@ -98,7 +100,7 @@ export class AddonModAssignHelperProvider {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
async copyPreviousAttempt(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise<void> {
|
||||
const pluginData: any = {};
|
||||
const pluginData: AddonModAssignSavePluginData = {};
|
||||
const promises = previousSubmission.plugins
|
||||
? previousSubmission.plugins.map((plugin) =>
|
||||
AddonModAssignSubmissionDelegate.instance.copyPluginSubmissionData(assign, plugin, pluginData))
|
||||
|
@ -121,8 +123,8 @@ export class AddonModAssignHelperProvider {
|
|||
createEmptyFeedback(): AddonModAssignSubmissionFeedback {
|
||||
return {
|
||||
grade: undefined,
|
||||
gradefordisplay: undefined,
|
||||
gradeddate: undefined,
|
||||
gradefordisplay: '',
|
||||
gradeddate: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -133,13 +135,13 @@ export class AddonModAssignHelperProvider {
|
|||
*/
|
||||
createEmptySubmission(): AddonModAssignSubmissionFormatted {
|
||||
return {
|
||||
id: undefined,
|
||||
userid: undefined,
|
||||
attemptnumber: undefined,
|
||||
timecreated: undefined,
|
||||
timemodified: undefined,
|
||||
status: undefined,
|
||||
groupid: undefined,
|
||||
id: 0,
|
||||
userid: 0,
|
||||
attemptnumber: 0,
|
||||
timecreated: 0,
|
||||
timemodified: 0,
|
||||
status: '',
|
||||
groupid: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -283,14 +285,15 @@ export class AddonModAssignHelperProvider {
|
|||
* @param subtype Subtype name (assignsubmission or assignfeedback)
|
||||
* @return List of enabled plugins for the assign.
|
||||
*/
|
||||
getPluginsEnabled(assign: AddonModAssignAssign, subtype: string): AddonModAssignPluginsEnabled {
|
||||
const enabled: AddonModAssignPluginsEnabled = [];
|
||||
getPluginsEnabled(assign: AddonModAssignAssign, subtype: string): AddonModAssignPlugin[] {
|
||||
const enabled: AddonModAssignPlugin[] = [];
|
||||
|
||||
assign.configs.forEach((config) => {
|
||||
if (config.subtype == subtype && config.name == 'enabled' && parseInt(config.value, 10) === 1) {
|
||||
// Format the plugin objects.
|
||||
enabled.push({
|
||||
type: config.plugin,
|
||||
name: config.plugin,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -564,7 +567,7 @@ export class AddonModAssignHelperProvider {
|
|||
userId: number,
|
||||
feedback: AddonModAssignSubmissionFeedback,
|
||||
siteId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<AddonModAssignSavePluginData> {
|
||||
|
||||
const pluginData = {};
|
||||
const promises = feedback.plugins
|
||||
|
@ -673,20 +676,22 @@ export class AddonModAssignHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
uploadOrStoreFiles(
|
||||
async uploadOrStoreFiles(
|
||||
assignId: number,
|
||||
folderName: string,
|
||||
files: (CoreWSExternalFile | FileEntry)[],
|
||||
offline = false,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<void> {
|
||||
|
||||
if (offline) {
|
||||
return this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
|
||||
await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return this.uploadFiles(assignId, files, siteId);
|
||||
await this.uploadFiles(assignId, files, siteId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -697,14 +702,8 @@ export const AddonModAssignHelper = makeSingleton(AddonModAssignHelperProvider);
|
|||
* Assign submission with some calculated data.
|
||||
*/
|
||||
export type AddonModAssignSubmissionFormatted =
|
||||
Omit<AddonModAssignSubmission, 'id' | 'userid' | 'attemptnumber' | 'timecreated' | 'timemodified' | 'status' | 'groupid'> & {
|
||||
id?: number; // Submission id.
|
||||
Omit<AddonModAssignSubmission, 'userid'> & {
|
||||
userid?: number; // Student id.
|
||||
attemptnumber?: number; // Attempt number.
|
||||
timecreated?: number; // Submission creation time.
|
||||
timemodified?: number; // Submission last modified time.
|
||||
status?: string; // Submission status.
|
||||
groupid?: number; // Group id.
|
||||
blindid?: number; // Calculated in the app. Blindid of the user that did the submission.
|
||||
submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission.
|
||||
userfullname?: string; // Calculated in the app. Full name of the user that did the submission.
|
||||
|
@ -715,13 +714,6 @@ export type AddonModAssignSubmissionFormatted =
|
|||
};
|
||||
|
||||
/**
|
||||
* Assingment subplugins type enabled.
|
||||
*/
|
||||
export type AddonModAssignPluginsEnabled = {
|
||||
type: string; // Plugin type.
|
||||
}[];
|
||||
|
||||
/**
|
||||
* Assingment plugin config.
|
||||
* Assignment plugin config.
|
||||
*/
|
||||
export type AddonModAssignPluginConfig = {[name: string]: string};
|
||||
|
|
|
@ -114,7 +114,7 @@ export class AddonModAssignOfflineProvider {
|
|||
* @return Promise resolved with submissions.
|
||||
*/
|
||||
async getAssignSubmissions(assignId: number, siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
|
||||
return this.getAssignSubmissionsFormatted({ assingid: assignId }, siteId);
|
||||
return this.getAssignSubmissionsFormatted({ assignid: assignId }, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,7 +167,7 @@ export class AddonModAssignOfflineProvider {
|
|||
assignId: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
|
||||
return this.getAssignSubmissionsGradeFormatted({ assingid: assignId }, siteId);
|
||||
return this.getAssignSubmissionsGradeFormatted({ assignid: assignId }, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -453,7 +453,7 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid
|
|||
|
||||
const status = await AddonModAssign.instance.getSubmissionStatus(assign.id, options);
|
||||
|
||||
const timemodified = (status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified)) || 0;
|
||||
const timemodified = (status.feedback && (status.feedback.gradeddate || status.feedback.grade?.timemodified)) || 0;
|
||||
|
||||
if (timemodified > offlineData.timemodified) {
|
||||
// The submission grade was modified in Moodle, discard it.
|
||||
|
@ -480,7 +480,7 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid
|
|||
if (gradeInfo && gradeInfo.scale) {
|
||||
offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.grade || '');
|
||||
} else {
|
||||
offlineData.grade = parseFloat(grade.grade || '') || undefined;
|
||||
offlineData.grade = parseFloat(grade.grade || '');
|
||||
}
|
||||
} else if (gradeInfo && grade.outcomeid && AddonModAssign.instance.isOutcomesEditEnabled() && gradeInfo.outcomes) {
|
||||
gradeInfo.outcomes.forEach((outcome, index) => {
|
||||
|
|
|
@ -1326,7 +1326,7 @@ export class AddonModAssignProvider {
|
|||
async submitGradingFormOnline(
|
||||
assignId: number,
|
||||
userId: number,
|
||||
grade: number | undefined,
|
||||
grade: number,
|
||||
attemptNumber: number,
|
||||
addAttempt: boolean,
|
||||
workflowState: string,
|
||||
|
@ -1553,7 +1553,7 @@ export type AddonModAssignSubmissionPreviousAttempt = {
|
|||
* Feedback of an assign submission.
|
||||
*/
|
||||
export type AddonModAssignSubmissionFeedback = {
|
||||
grade: AddonModAssignGrade; // Grade information.
|
||||
grade?: AddonModAssignGrade; // Grade information.
|
||||
gradefordisplay: string; // Grade rendered into a format suitable for display.
|
||||
gradeddate: number; // The date the user was graded.
|
||||
plugins?: AddonModAssignPlugin[]; // Plugins info.
|
||||
|
@ -1853,3 +1853,12 @@ type AddonModAssignSaveGradeWSParams = {
|
|||
* Assignment grade outcomes.
|
||||
*/
|
||||
export type AddonModAssignOutcomes = { [itemNumber: number]: number };
|
||||
|
||||
/**
|
||||
* Data sent by SUBMITTED_FOR_GRADING_EVENT event.
|
||||
*/
|
||||
export type AddonModAssignSubmittedForGradingEventData = {
|
||||
assignmentId: number;
|
||||
submissionId: number;
|
||||
userId: number;
|
||||
};
|
||||
|
|
|
@ -139,7 +139,7 @@ export type AddonModAssignSubmissionsGradingDBRecord = {
|
|||
assignid: number; // Primary key.
|
||||
userid: number; // Primary key.
|
||||
courseid: number;
|
||||
grade?: number; // Real.
|
||||
grade: number; // Real.
|
||||
attemptnumber: number;
|
||||
addattempt: number;
|
||||
workflowstate: string;
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedback';
|
||||
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign';
|
||||
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
|
||||
|
@ -37,7 +37,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
discardDraft?(assignId: number, userId: number, siteId?: string): void | Promise<any>;
|
||||
discardDraft?(assignId: number, userId: number, siteId?: string): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the plugin data.
|
||||
|
@ -46,7 +46,8 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
|
|||
* @param plugin The plugin object.
|
||||
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent?(plugin: AddonModAssignPlugin): any | Promise<any>;
|
||||
getComponent?(plugin: AddonModAssignPlugin): Type<unknown> | undefined | Promise<Type<unknown> | undefined>;
|
||||
|
||||
|
||||
/**
|
||||
* Return the draft saved data of the feedback plugin.
|
||||
|
@ -126,7 +127,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
|
|||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
siteId?: string,
|
||||
): Promise<any>;
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to send to the server based on the draft data saved.
|
||||
|
@ -142,9 +143,9 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
|
|||
assignId: number,
|
||||
userId: number,
|
||||
plugin: AddonModAssignPlugin,
|
||||
pluginData: any,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
siteId?: string,
|
||||
): void | Promise<any>;
|
||||
): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Save draft data of the feedback plugin.
|
||||
|
@ -197,7 +198,7 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonMod
|
|||
* @param plugin The plugin object.
|
||||
* @return Promise resolved with the component to use, undefined if not found.
|
||||
*/
|
||||
async getComponentForPlugin(plugin: AddonModAssignPlugin): Promise<any | undefined> {
|
||||
async getComponentForPlugin(plugin: AddonModAssignPlugin): Promise<Type<unknown> | undefined> {
|
||||
return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin]);
|
||||
}
|
||||
|
||||
|
@ -317,7 +318,7 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonMod
|
|||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
siteId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<void> {
|
||||
return await this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]);
|
||||
}
|
||||
|
||||
|
@ -335,9 +336,9 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonMod
|
|||
assignId: number,
|
||||
userId: number,
|
||||
plugin: AddonModAssignPlugin,
|
||||
pluginData: any,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
siteId?: string,
|
||||
): Promise<any> {
|
||||
): Promise<void> {
|
||||
|
||||
return await this.executeFunctionOnEnabled(
|
||||
plugin.type,
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { Translate } from '@singletons';
|
||||
import { AddonModAssignPlugin } from '../assign';
|
||||
import { AddonModAssignFeedbackHandler } from '../feedback-delegate';
|
||||
|
@ -35,16 +36,6 @@ export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedb
|
|||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the plugin data.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(): void {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the draft saved data of the feedback plugin.
|
||||
*
|
||||
|
@ -60,7 +51,7 @@ export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedb
|
|||
*
|
||||
* @return The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(): any[] {
|
||||
getPluginFiles(): CoreWSExternalFile[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -121,7 +112,7 @@ export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedb
|
|||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetch(): Promise<any> {
|
||||
async prefetch(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { Translate } from '@singletons';
|
||||
import { AddonModAssignPlugin } from '../assign';
|
||||
import { AddonModAssignSubmissionHandler } from '../submission-delegate';
|
||||
|
@ -72,23 +73,13 @@ export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSub
|
|||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the plugin data, either in read or in edit mode.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(): void {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @return The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(): any[] {
|
||||
getPluginFiles(): CoreWSExternalFile[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -176,7 +167,7 @@ export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSub
|
|||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetch(): Promise<any> {
|
||||
async prefetch(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submission';
|
||||
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign';
|
||||
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
|
||||
|
@ -86,7 +86,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
copySubmissionData?(
|
||||
assign: AddonModAssignAssign,
|
||||
plugin: AddonModAssignPlugin,
|
||||
pluginData: any,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): void | Promise<void>;
|
||||
|
@ -120,7 +120,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
getComponent?(
|
||||
plugin: AddonModAssignPlugin,
|
||||
edit?: boolean,
|
||||
): any | Promise<any>;
|
||||
): Type<unknown> | undefined | Promise<Type<unknown> | undefined>;
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
|
@ -233,7 +233,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
|
|||
submission: AddonModAssignSubmission,
|
||||
plugin: AddonModAssignPlugin,
|
||||
inputData: any,
|
||||
pluginData: any,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
offline?: boolean,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
|
@ -321,7 +321,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
async copyPluginSubmissionData(
|
||||
assign: AddonModAssignAssign,
|
||||
plugin: AddonModAssignPlugin,
|
||||
pluginData: any,
|
||||
pluginData: AddonModAssignSavePluginData,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<void | undefined> {
|
||||
|
@ -363,7 +363,7 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonM
|
|||
* @param edit Whether the user is editing.
|
||||
* @return Promise resolved with the component to use, undefined if not found.
|
||||
*/
|
||||
async getComponentForPlugin(plugin: AddonModAssignPlugin, edit?: boolean): Promise<any | undefined> {
|
||||
async getComponentForPlugin(plugin: AddonModAssignPlugin, edit?: boolean): Promise<Type<unknown> | undefined> {
|
||||
return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin, edit]);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue