MOBILE-3636 assign: Submissions list
parent
82cf017134
commit
a4b356350b
|
@ -17,6 +17,7 @@ import { NgModule } from '@angular/core';
|
|||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AddonModAssignComponentsModule } from './components/components.module';
|
||||
import { AddonModAssignIndexPage } from './pages/index/index.page';
|
||||
import { AddonModAssignSubmissionListPage } from './pages/submission-list/submission-list.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -37,6 +38,7 @@ const routes: Routes = [
|
|||
],
|
||||
declarations: [
|
||||
AddonModAssignIndexPage,
|
||||
AddonModAssignSubmissionListPage,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignLazyModule {}
|
||||
|
|
|
@ -317,7 +317,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
if (typeof status != 'undefined') {
|
||||
params.status = status;
|
||||
}
|
||||
CoreNavigator.instance.navigate('submission-list', {
|
||||
CoreNavigator.instance.navigate('submission', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
AddonModAssignGetSubmissionStatusWSResponse,
|
||||
AddonModAssignSubmittedForGradingEventData,
|
||||
AddonModAssignSavePluginData,
|
||||
AddonModAssignGradedEventData,
|
||||
} from '../../services/assign';
|
||||
import {
|
||||
AddonModAssignAutoSyncData,
|
||||
|
@ -913,7 +914,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
|||
// Invalidate and refresh data.
|
||||
this.invalidateAndRefresh(true);
|
||||
|
||||
CoreEvents.trigger(AddonModAssignProvider.GRADED_EVENT, {
|
||||
CoreEvents.trigger<AddonModAssignGradedEventData>(AddonModAssignProvider.GRADED_EVENT, {
|
||||
assignmentId: this.assign!.id,
|
||||
submissionId: this.submitId,
|
||||
userId: this.currentUserId,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
|
||||
<ion-buttons slot="end"></ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshList($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<core-empty-box *ngIf="!submissions || submissions.length == 0" icon="fas-file-signature"
|
||||
[message]="'addon.mod_assign.submissionstatus_' | translate">
|
||||
</core-empty-box>
|
||||
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap" *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||
<ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.separateGroups">
|
||||
{{ 'core.groupsseparate' | translate }}
|
||||
</ion-label>
|
||||
<ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">
|
||||
{{ 'core.groupsvisible' | translate }}
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="groupId" (ionChange)="setGroup(groupId)" aria-labelledby="addon-assign-groupslabel"
|
||||
interface="action-sheet" slot="end">
|
||||
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
||||
{{groupOpt.name}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<!-- List of submissions. -->
|
||||
<ng-container *ngFor="let submission of submissions">
|
||||
<ion-item class="ion-text-wrap" (click)="loadSubmission(submission)"
|
||||
[class.core-selected-item]="submission.submitid == selectedSubmissionId">
|
||||
<core-user-avatar [user]="submission" [linkProfile]="false" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2>
|
||||
<h2 *ngIf="!submission.userfullname">
|
||||
{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}
|
||||
</h2>
|
||||
<p *ngIf="assign && assign!.teamsubmission">
|
||||
<span *ngIf="submission.groupname">{{submission.groupname}}</span>
|
||||
<span *ngIf="assign!.preventsubmissionnotingroup && !submission.groupname && submission.noGroups
|
||||
&& !submission.blindid" class="text-danger">
|
||||
{{ 'addon.mod_assign.noteam' | translate }}
|
||||
</span>
|
||||
<span *ngIf="assign!.preventsubmissionnotingroup && !submission.groupname && submission.manyGroups
|
||||
&& !submission.blindid" class="text-danger">
|
||||
{{ 'addon.mod_assign.multipleteams' | translate }}
|
||||
</span>
|
||||
<span *ngIf="!assign!.preventsubmissionnotingroup && !submission.groupname">
|
||||
{{ 'addon.mod_assign.defaultteam' | translate }}
|
||||
</span>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor"
|
||||
*ngIf="submission.statusTranslated">
|
||||
{{ submission.statusTranslated }}
|
||||
</ion-badge>
|
||||
<ion-badge class="ion-text-center ion-text-wrap" [color]="submission.gradingColor"
|
||||
*ngIf="submission.gradingStatusTranslationId">
|
||||
{{ submission.gradingStatusTranslationId | translate }}
|
||||
</ion-badge>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ion-card class="ion-text-wrap core-warning-card" *ngIf="!haveAllParticipants">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'addon.mod_assign.notallparticipantsareshown' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</core-split-view>
|
||||
</ion-content>
|
|
@ -0,0 +1,381 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
AddonModAssignAssign,
|
||||
AddonModAssignSubmission,
|
||||
AddonModAssignProvider,
|
||||
AddonModAssign,
|
||||
AddonModAssignGradedEventData,
|
||||
} from '../../services/assign';
|
||||
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper';
|
||||
import { AddonModAssignOffline } from '../../services/assign-offline';
|
||||
import {
|
||||
AddonModAssignSyncProvider,
|
||||
AddonModAssignSync,
|
||||
AddonModAssignManualSyncData,
|
||||
AddonModAssignAutoSyncData,
|
||||
} from '../../services/assign-sync';
|
||||
|
||||
/**
|
||||
* Page that displays a list of submissions of an assignment.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-assign-submission-list',
|
||||
templateUrl: 'submission-list.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
||||
|
||||
// @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||
|
||||
title = ''; // Title to display.
|
||||
assign?: AddonModAssignAssign; // Assignment.
|
||||
submissions: AddonModAssignSubmissionForList[] = []; // List of submissions
|
||||
loaded = false; // Whether data has been loaded.
|
||||
haveAllParticipants = true; // Whether all participants have been loaded.
|
||||
selectedSubmissionId?: number; // Selected submission ID.
|
||||
groupId = 0; // Group ID to show.
|
||||
courseId!: number; // Course ID the assignment belongs to.
|
||||
moduleId!: number; // Module ID the submission belongs to.
|
||||
|
||||
groupInfo: CoreGroupInfo = {
|
||||
groups: [],
|
||||
separateGroups: false,
|
||||
visibleGroups: false,
|
||||
defaultGroupId: 0,
|
||||
};
|
||||
|
||||
protected selectedStatus?: string; // The status to see.
|
||||
protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
|
||||
protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
|
||||
protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = {
|
||||
canviewsubmissions: false,
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
// Update data if some grade changes.
|
||||
this.gradedObserver = CoreEvents.on<AddonModAssignGradedEventData>(
|
||||
AddonModAssignProvider.GRADED_EVENT,
|
||||
(data) => {
|
||||
if (
|
||||
this.loaded &&
|
||||
this.assign &&
|
||||
data.assignmentId == this.assign.id &&
|
||||
data.userId == CoreSites.instance.getCurrentSiteUserId()
|
||||
) {
|
||||
// Grade changed, refresh the data.
|
||||
this.loaded = false;
|
||||
|
||||
this.refreshAllData(true).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
// Refresh data if this assign is synchronized.
|
||||
const events = [AddonModAssignSyncProvider.AUTO_SYNCED, AddonModAssignSyncProvider.MANUAL_SYNCED];
|
||||
this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
|
||||
events,
|
||||
(data) => {
|
||||
if (!this.loaded || ('context' in data && data.context == 'submission-list')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
this.refreshAllData(false).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
},
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
|
||||
|
||||
this.route.queryParams.subscribe((params) => {
|
||||
this.groupId = CoreNavigator.instance.getRouteNumberParam('groupId', params) || 0;
|
||||
this.selectedStatus = CoreNavigator.instance.getRouteParam('status', params);
|
||||
|
||||
if (this.selectedStatus) {
|
||||
if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) {
|
||||
this.title = Translate.instance.instant('addon.mod_assign.numberofsubmissionsneedgrading');
|
||||
} else {
|
||||
this.title = Translate.instance.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus);
|
||||
}
|
||||
} else {
|
||||
this.title = Translate.instance.instant('addon.mod_assign.numberofparticipants');
|
||||
}
|
||||
this.fetchAssignment(true).finally(() => {
|
||||
/* if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) {
|
||||
// Take first and load it.
|
||||
this.loadSubmission(this.submissions[0]);
|
||||
}*/
|
||||
|
||||
this.loaded = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch assignment data.
|
||||
*
|
||||
* @param sync Whether to try to synchronize data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchAssignment(sync?: boolean): Promise<void> {
|
||||
try {
|
||||
// Get assignment data.
|
||||
this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId);
|
||||
|
||||
this.title = this.assign.name || this.title;
|
||||
|
||||
if (sync) {
|
||||
try {
|
||||
// Try to synchronize data.
|
||||
const result = await AddonModAssignSync.instance.syncAssign(this.assign.id);
|
||||
|
||||
if (result && result.updated) {
|
||||
CoreEvents.trigger<AddonModAssignManualSyncData>(
|
||||
AddonModAssignSyncProvider.MANUAL_SYNCED,
|
||||
{
|
||||
assignId: this.assign.id,
|
||||
warnings: result.warnings,
|
||||
gradesBlocked: result.gradesBlocked,
|
||||
context: 'submission-list',
|
||||
},
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors, probably user is offline or sync is blocked.
|
||||
}
|
||||
}
|
||||
|
||||
// Get assignment submissions.
|
||||
this.submissionsData = await AddonModAssign.instance.getSubmissions(this.assign.id, { cmId: this.assign.cmid });
|
||||
|
||||
if (!this.submissionsData.canviewsubmissions) {
|
||||
// User shouldn't be able to reach here.
|
||||
throw new Error('Cannot view submissions.');
|
||||
}
|
||||
|
||||
// Check if groupmode is enabled to avoid showing wrong numbers.
|
||||
this.groupInfo = await CoreGroups.instance.getActivityGroupInfo(this.assign.cmid, false);
|
||||
|
||||
await this.setGroup(CoreGroups.instance.validateGroupId(this.groupId, this.groupInfo));
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group to see the summary.
|
||||
*
|
||||
* @param groupId Group ID.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
async setGroup(groupId: number): Promise<void> {
|
||||
this.groupId = groupId;
|
||||
|
||||
this.haveAllParticipants = true;
|
||||
|
||||
if (!CoreSites.instance.getCurrentSite()?.wsAvailable('mod_assign_list_participants')) {
|
||||
// Submissions are not displayed in Moodle 3.1 without the local plugin, see MOBILE-2968.
|
||||
this.haveAllParticipants = false;
|
||||
this.submissions = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch submissions and grades.
|
||||
const submissions =
|
||||
await AddonModAssignHelper.instance.getSubmissionsUserData(
|
||||
this.assign!,
|
||||
this.submissionsData.submissions,
|
||||
this.groupId,
|
||||
);
|
||||
// Get assignment grades only if workflow is not enabled to check grading date.
|
||||
const grades = !this.assign!.markingworkflow
|
||||
? await AddonModAssign.instance.getAssignmentGrades(this.assign!.id, { cmId: this.assign!.cmid })
|
||||
: [];
|
||||
|
||||
// Filter the submissions to get only the ones with the right status and add some extra data.
|
||||
const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING;
|
||||
const searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
const showSubmissions: AddonModAssignSubmissionForList[] = [];
|
||||
|
||||
submissions.forEach((submission: AddonModAssignSubmissionForList) => {
|
||||
if (!searchStatus || searchStatus == submission.status) {
|
||||
promises.push(
|
||||
CoreUtils.instance.ignoreErrors(
|
||||
AddonModAssignOffline.instance.getSubmissionGrade(this.assign!.id, submission.userid),
|
||||
).then(async (data) => {
|
||||
if (getNeedGrading) {
|
||||
// Only show the submissions that need to be graded.
|
||||
const add = await AddonModAssign.instance.needsSubmissionToBeGraded(submission, this.assign!.id);
|
||||
|
||||
if (!add) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Load offline grades.
|
||||
const notSynced = !!data && submission.timemodified < data.timemodified;
|
||||
|
||||
if (submission.gradingstatus == 'graded' && !this.assign!.markingworkflow) {
|
||||
// Get the last grade of the submission.
|
||||
const grade = grades
|
||||
.filter((grade) => grade.userid == submission.userid)
|
||||
.reduce((a, b) => (a.timemodified > b.timemodified ? a : b));
|
||||
|
||||
if (grade && grade.timemodified < submission.timemodified) {
|
||||
submission.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT;
|
||||
}
|
||||
}
|
||||
submission.statusColor = AddonModAssign.instance.getSubmissionStatusColor(submission.status);
|
||||
submission.gradingColor = AddonModAssign.instance.getSubmissionGradingStatusColor(
|
||||
submission.gradingstatus,
|
||||
);
|
||||
|
||||
// Show submission status if not submitted for grading.
|
||||
if (submission.statusColor != 'success' || !submission.gradingstatus) {
|
||||
submission.statusTranslated = Translate.instance.instant(
|
||||
'addon.mod_assign.submissionstatus_' + submission.status,
|
||||
);
|
||||
} else {
|
||||
submission.statusTranslated = '';
|
||||
}
|
||||
|
||||
if (notSynced) {
|
||||
submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
|
||||
submission.gradingColor = '';
|
||||
} else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') {
|
||||
// Show grading status if one of the statuses is not done.
|
||||
submission.gradingStatusTranslationId = AddonModAssign.instance.getSubmissionGradingStatusTranslationId(
|
||||
submission.gradingstatus,
|
||||
);
|
||||
} else {
|
||||
submission.gradingStatusTranslationId = '';
|
||||
}
|
||||
|
||||
showSubmissions.push(submission);
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.submissions = showSubmissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a certain submission.
|
||||
*
|
||||
* @param submission The submission to load.
|
||||
*/
|
||||
loadSubmission(submission: AddonModAssignSubmissionForList): void {
|
||||
/* if (this.selectedSubmissionId === submission.submitid && this.splitviewCtrl.isOn()) {
|
||||
// Already selected.
|
||||
return;
|
||||
}*/
|
||||
|
||||
this.selectedSubmissionId = submission.submitid;
|
||||
|
||||
/* this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', {
|
||||
courseId: this.courseId,
|
||||
moduleId: this.moduleId,
|
||||
submitId: submission.submitid,
|
||||
blindId: submission.blindid,
|
||||
});*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all the data.
|
||||
*
|
||||
* @param sync Whether to try to synchronize data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async refreshAllData(sync?: boolean): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId));
|
||||
if (this.assign) {
|
||||
promises.push(AddonModAssign.instance.invalidateAllSubmissionData(this.assign.id));
|
||||
promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id));
|
||||
promises.push(AddonModAssign.instance.invalidateAssignmentGradesData(this.assign.id));
|
||||
promises.push(AddonModAssign.instance.invalidateListParticipantsData(this.assign.id));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} finally {
|
||||
this.fetchAssignment(sync);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the list.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshList(refresher?: CustomEvent<IonRefresher>): void {
|
||||
this.refreshAllData(true).finally(() => {
|
||||
refresher?.detail.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.gradedObserver?.off();
|
||||
this.syncObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated data for an assign submission.
|
||||
*/
|
||||
type AddonModAssignSubmissionForList = AddonModAssignSubmissionFormatted & {
|
||||
statusColor?: string; // Calculated in the app. Color of the submission status.
|
||||
gradingColor?: string; // Calculated in the app. Color of the submission grading status.
|
||||
statusTranslated?: string; // Calculated in the app. Translated text of the submission status.
|
||||
gradingStatusTranslationId?: string; // Calculated in the app. Key of the text of the submission grading status.
|
||||
};
|
|
@ -328,7 +328,7 @@ export class AddonModAssignProvider {
|
|||
* @param status Grading status name
|
||||
* @return The color name.
|
||||
*/
|
||||
getSubmissionGradingStatusColor(status: string): string {
|
||||
getSubmissionGradingStatusColor(status?: string): string {
|
||||
if (!status) {
|
||||
return '';
|
||||
}
|
||||
|
@ -1863,3 +1863,12 @@ export type AddonModAssignSubmittedForGradingEventData = {
|
|||
submissionId: number;
|
||||
userId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data sent by GRADED_EVENT event.
|
||||
*/
|
||||
export type AddonModAssignGradedEventData = {
|
||||
assignmentId: number;
|
||||
submissionId: number;
|
||||
userId: number;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue