commit
c2065ef83a
|
@ -22,6 +22,6 @@ RUN npm install && rm -rf /root/.npm
|
|||
RUN npx gulp
|
||||
|
||||
# Provide a Healthcheck command for easier use in CI.
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s CMD curl -f http://localhost:8100 || exit 1
|
||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=60s CMD curl -f http://localhost:8100 || exit 1
|
||||
|
||||
CMD ["npm", "run", "ionic:serve"]
|
||||
|
|
|
@ -68,7 +68,8 @@
|
|||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "app:build"
|
||||
"browserTarget": "app:build",
|
||||
"port": 8100
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"test:coverage": "NODE_ENV=testing gulp && jest --coverage",
|
||||
"lint": "NODE_OPTIONS=--max-old-space-size=4096 ng lint",
|
||||
"ionic:serve:before": "gulp",
|
||||
"ionic:serve": "gulp watch & ng serve",
|
||||
"ionic:serve": "gulp watch & NODE_OPTIONS=--max-old-space-size=4096 ng serve",
|
||||
"ionic:build:before": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -1013,6 +1013,7 @@
|
|||
"addon.mod_workshop.yourassessmentfor": "workshop",
|
||||
"addon.mod_workshop.yourgrades": "workshop",
|
||||
"addon.mod_workshop.yoursubmission": "workshop",
|
||||
"addon.mod_workshop.yoursubmissionwithassessments": "workshop",
|
||||
"addon.mod_workshop_assessment_accumulative.dimensioncommentfor": "workshopform_accumulative",
|
||||
"addon.mod_workshop_assessment_accumulative.dimensiongradefor": "workshopform_accumulative",
|
||||
"addon.mod_workshop_assessment_accumulative.dimensionnumber": "workshopform_accumulative",
|
||||
|
@ -1468,6 +1469,9 @@
|
|||
"core.coursenogroups": "local_moodlemobileapp",
|
||||
"core.courses.addtofavourites": "block_myoverview",
|
||||
"core.courses.allowguests": "enrol_guest",
|
||||
"core.courses.aria:coursename": "course",
|
||||
"core.courses.aria:courseprogress": "block_myoverview",
|
||||
"core.courses.aria:favourite": "course",
|
||||
"core.courses.availablecourses": "moodle",
|
||||
"core.courses.cannotretrievemorecategories": "local_moodlemobileapp",
|
||||
"core.courses.categories": "moodle",
|
||||
|
@ -1480,6 +1484,7 @@
|
|||
"core.courses.errorloadplugins": "local_moodlemobileapp",
|
||||
"core.courses.errorsearching": "local_moodlemobileapp",
|
||||
"core.courses.errorselfenrol": "local_moodlemobileapp",
|
||||
"core.courses.favourite": "course",
|
||||
"core.courses.filtermycourses": "local_moodlemobileapp",
|
||||
"core.courses.frontpage": "admin",
|
||||
"core.courses.hidecourse": "block_myoverview",
|
||||
|
@ -1859,8 +1864,10 @@
|
|||
"core.lostconnection": "local_moodlemobileapp",
|
||||
"core.mainmenu.changesite": "local_moodlemobileapp",
|
||||
"core.mainmenu.help": "moodle",
|
||||
"core.mainmenu.home": "moodle",
|
||||
"core.mainmenu.logout": "moodle",
|
||||
"core.mainmenu.website": "local_moodlemobileapp",
|
||||
"core.maxfilesize": "moodle",
|
||||
"core.maxsizeandattachments": "moodle",
|
||||
"core.min": "moodle",
|
||||
"core.mins": "moodle",
|
||||
|
@ -1944,8 +1951,8 @@
|
|||
"core.question.certainty": "qbehaviour_deferredcbm",
|
||||
"core.question.complete": "question",
|
||||
"core.question.correct": "question",
|
||||
"core.question.errorattachmentsnotsupported": "local_moodlemobileapp",
|
||||
"core.question.errorinlinefilesnotsupported": "local_moodlemobileapp",
|
||||
"core.question.errorattachmentsnotsupportedinsite": "local_moodlemobileapp",
|
||||
"core.question.errorembeddedfilesnotsupportedinsite": "local_moodlemobileapp",
|
||||
"core.question.errorquestionnotsupported": "local_moodlemobileapp",
|
||||
"core.question.feedback": "question",
|
||||
"core.question.howtodraganddrop": "local_moodlemobileapp",
|
||||
|
@ -2000,9 +2007,10 @@
|
|||
"core.settings.cannotsyncoffline": "local_moodlemobileapp",
|
||||
"core.settings.cannotsyncwithoutwifi": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme-auto": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme-dark": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme-light": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme-system": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme-system-notice": "local_moodlemobileapp",
|
||||
"core.settings.compilationinfo": "local_moodlemobileapp",
|
||||
"core.settings.copyinfo": "local_moodlemobileapp",
|
||||
"core.settings.cordovadevicemodel": "local_moodlemobileapp",
|
||||
|
|
|
@ -14,45 +14,45 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AddonBlockModule } from './block/block.module';
|
||||
import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module';
|
||||
import { AddonFilterModule } from './filter/filter.module';
|
||||
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
|
||||
import { AddonBadgesModule } from './badges/badges.module';
|
||||
import { AddonBlockModule } from './block/block.module';
|
||||
import { AddonBlogModule } from './blog/blog.module';
|
||||
import { AddonCalendarModule } from './calendar/calendar.module';
|
||||
import { AddonCompetencyModule } from './competency/competency.module';
|
||||
import { AddonCourseCompletionModule } from './coursecompletion/coursecompletion.module';
|
||||
import { AddonNotificationsModule } from './notifications/notifications.module';
|
||||
import { AddonFilterModule } from './filter/filter.module';
|
||||
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
|
||||
import { AddonMessagesModule } from './messages/messages.module';
|
||||
import { AddonModModule } from './mod/mod.module';
|
||||
import { AddonNotesModule } from './notes/notes.module';
|
||||
import { AddonNotificationsModule } from './notifications/notifications.module';
|
||||
import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module';
|
||||
import { AddonQbehaviourModule } from './qbehaviour/qbehaviour.module';
|
||||
import { AddonQtypeModule } from './qtype/qtype.module';
|
||||
import { AddonBlogModule } from './blog/blog.module';
|
||||
import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module';
|
||||
import { AddonNotesModule } from './notes/notes.module';
|
||||
import { AddonCompetencyModule } from './competency/competency.module';
|
||||
import { AddonStorageManagerModule } from './storagemanager/storagemanager.module';
|
||||
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AddonBlockModule,
|
||||
AddonBadgesModule,
|
||||
AddonBlockModule,
|
||||
AddonBlogModule,
|
||||
AddonCalendarModule,
|
||||
AddonCompetencyModule,
|
||||
AddonCourseCompletionModule,
|
||||
AddonMessagesModule,
|
||||
AddonPrivateFilesModule,
|
||||
AddonFilterModule,
|
||||
AddonUserProfileFieldModule,
|
||||
AddonNotificationsModule,
|
||||
AddonMessageOutputModule,
|
||||
AddonMessagesModule,
|
||||
AddonModModule,
|
||||
AddonNotesModule,
|
||||
AddonNotificationsModule,
|
||||
AddonPrivateFilesModule,
|
||||
AddonQbehaviourModule,
|
||||
AddonQtypeModule,
|
||||
AddonRemoteThemesModule,
|
||||
AddonStorageManagerModule,
|
||||
AddonUserProfileFieldModule,
|
||||
],
|
||||
})
|
||||
export class AddonsModule {}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AddonBlockActivityModulesModule } from './activitymodules/activitymodules.module';
|
||||
import { AddonBlockActivityResultsModule } from './activityresults/activityresults.module';
|
||||
import { AddonBlockBadgesModule } from './badges/badges.module';
|
||||
import { AddonBlockBlogMenuModule } from './blogmenu/blogmenu.module';
|
||||
|
@ -30,19 +31,19 @@ import { AddonBlockMyOverviewModule } from './myoverview/myoverview.module';
|
|||
import { AddonBlockNewsItemsModule } from './newsitems/newsitems.module';
|
||||
import { AddonBlockOnlineUsersModule } from './onlineusers/onlineusers.module';
|
||||
import { AddonBlockPrivateFilesModule } from './privatefiles/privatefiles.module';
|
||||
import { AddonBlockRecentActivityModule } from './recentactivity/recentactivity.module';
|
||||
import { AddonBlockRecentlyAccessedCoursesModule } from './recentlyaccessedcourses/recentlyaccessedcourses.module';
|
||||
import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/recentlyaccesseditems.module';
|
||||
import { AddonBlockRssClientModule } from './rssclient/rssclient.module';
|
||||
import { AddonBlockSelfCompletionModule } from './selfcompletion/selfcompletion.module';
|
||||
import { AddonBlockSiteMainMenuModule } from './sitemainmenu/sitemainmenu.module';
|
||||
import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses.module';
|
||||
import { AddonBlockTagsModule } from './tags/tags.module';
|
||||
import { AddonBlockActivityModulesModule } from './activitymodules/activitymodules.module';
|
||||
import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/recentlyaccesseditems.module';
|
||||
import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonBlockActivityModulesModule,
|
||||
AddonBlockActivityResultsModule,
|
||||
AddonBlockBadgesModule,
|
||||
AddonBlockBlogMenuModule,
|
||||
|
@ -54,22 +55,20 @@ import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
|||
AddonBlockCompletionStatusModule,
|
||||
AddonBlockGlossaryRandomModule,
|
||||
AddonBlockHtmlModule,
|
||||
AddonBlockMyOverviewModule,
|
||||
AddonBlockLearningPlansModule,
|
||||
AddonBlockMyOverviewModule,
|
||||
AddonBlockNewsItemsModule,
|
||||
AddonBlockOnlineUsersModule,
|
||||
AddonBlockPrivateFilesModule,
|
||||
AddonBlockRecentActivityModule,
|
||||
AddonBlockRecentlyAccessedCoursesModule,
|
||||
AddonBlockRecentlyAccessedItemsModule,
|
||||
AddonBlockRssClientModule,
|
||||
AddonBlockSelfCompletionModule,
|
||||
AddonBlockSiteMainMenuModule,
|
||||
AddonBlockStarredCoursesModule,
|
||||
AddonBlockTagsModule,
|
||||
AddonBlockActivityModulesModule,
|
||||
AddonBlockRecentlyAccessedItemsModule,
|
||||
AddonBlockTimelineModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class AddonBlockModule { }
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</ion-label>
|
||||
<ion-input type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate" formControlName="name">
|
||||
</ion-input>
|
||||
<core-input-errors item-content [control]="form.controls.name" [errorMessages]="errors"></core-input-errors>
|
||||
<core-input-errors [control]="form.controls.name" [errorMessages]="errors"></core-input-errors>
|
||||
</ion-item>
|
||||
|
||||
<!-- Date. -->
|
||||
|
@ -34,7 +34,7 @@
|
|||
</ion-label>
|
||||
<ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat">
|
||||
</ion-datetime>
|
||||
<core-input-errors item-content [control]="form.controls.timestart" [errorMessages]="errors"></core-input-errors>
|
||||
<core-input-errors [control]="form.controls.timestart" [errorMessages]="errors"></core-input-errors>
|
||||
</ion-item>
|
||||
|
||||
<!-- Type. -->
|
||||
|
@ -135,7 +135,7 @@
|
|||
<ion-label position="stacked">
|
||||
<h2>{{ 'core.description' | translate }}</h2>
|
||||
</ion-label>
|
||||
<core-rich-text-editor item-content [control]="descriptionControl"
|
||||
<core-rich-text-editor [control]="descriptionControl"
|
||||
[placeholder]="'core.description' | translate" name="description" [component]="component"
|
||||
[componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
|
||||
</ion-item>
|
||||
|
|
|
@ -30,7 +30,6 @@ import { AddonFilterTidyModule } from './tidy/tidy.module';
|
|||
import { AddonFilterUrlToLinkModule } from './urltolink/urltolink.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonFilterActivityNamesModule,
|
||||
AddonFilterAlgebraModule,
|
||||
|
@ -47,7 +46,5 @@ import { AddonFilterUrlToLinkModule } from './urltolink/urltolink.module';
|
|||
AddonFilterTidyModule,
|
||||
AddonFilterUrlToLinkModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class AddonFilterModule { }
|
||||
|
|
|
@ -22,12 +22,8 @@ export const ADDON_MESSAGEOUTPUT_SERVICES: Type<unknown>[] = [
|
|||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
AddonMessageOutputAirnotifierModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
})
|
||||
export class AddonMessageOutputModule {}
|
||||
|
|
|
@ -240,7 +240,7 @@
|
|||
<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>
|
||||
<p *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Grade using a scale. -->
|
||||
|
@ -262,7 +262,7 @@
|
|||
{{grade.label}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
<p item-content *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
|
||||
<p *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Gradebook grade for simple grading. -->
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<!-- Edit -->
|
||||
<ion-item class="ion-text-wrap" *ngIf="edit && loaded">
|
||||
<ion-label></ion-label>
|
||||
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name"
|
||||
<core-rich-text-editor [control]="control" [placeholder]="plugin.name"
|
||||
name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true"
|
||||
contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor"
|
||||
[draftExtraParams]="{userid: userId, action: 'grade'}">
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
</ion-label>
|
||||
<ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="submissionStatementAccepted"></ion-checkbox>
|
||||
<!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. -->
|
||||
<input item-content type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement">
|
||||
<input type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement">
|
||||
</ion-item>
|
||||
|
||||
<addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign"
|
||||
|
|
|
@ -694,16 +694,40 @@ export class AddonModAssignHelperProvider {
|
|||
assignId: number,
|
||||
folderName: string,
|
||||
files: CoreFileEntry[],
|
||||
offline = false,
|
||||
offline: true,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult>;
|
||||
async uploadOrStoreFiles(
|
||||
assignId: number,
|
||||
folderName: string,
|
||||
files: CoreFileEntry[],
|
||||
offline: false,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<number>;
|
||||
async uploadOrStoreFiles(
|
||||
assignId: number,
|
||||
folderName: string,
|
||||
files: CoreFileEntry[],
|
||||
offline: boolean,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<number | CoreFileUploaderStoreFilesResult>;
|
||||
async uploadOrStoreFiles(
|
||||
assignId: number,
|
||||
folderName: string,
|
||||
files: CoreFileEntry[],
|
||||
offline: boolean,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<number | CoreFileUploaderStoreFilesResult> {
|
||||
|
||||
if (offline) {
|
||||
return await this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
|
||||
return this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
|
||||
}
|
||||
|
||||
return await this.uploadFiles(assignId, files, siteId);
|
||||
return this.uploadFiles(assignId, files, siteId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import { makeSingleton } from '@singletons';
|
|||
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreGrades } from '@features/grades/services/grades';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
@ -34,6 +33,7 @@ import { AddonModAssignSubmissionFormatted } from './assign-helper';
|
|||
import { CoreWSError } from '@classes/errors/wserror';
|
||||
import { AddonModAssignAutoSyncData, AddonModAssignManualSyncData, AddonModAssignSyncProvider } from './assign-sync';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { CoreFileHelper } from '@services/file-helper';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmaModAssign:';
|
||||
|
||||
|
@ -421,7 +421,7 @@ export class AddonModAssignProvider {
|
|||
filearea.files.forEach((file) => {
|
||||
if (!file.filename) {
|
||||
// We don't have filename, extract it from the path.
|
||||
file.filename = file.filepath?.charAt(0) == '/' ? file.filepath.substr(1) : file.filepath;
|
||||
file.filename = CoreFileHelper.getFilenameFromPath(file);
|
||||
}
|
||||
|
||||
files.push(file);
|
||||
|
@ -759,7 +759,6 @@ export class AddonModAssignProvider {
|
|||
|
||||
/**
|
||||
* Invalidate the prefetched content except files.
|
||||
* To invalidate files, use AddonModAssignProvider.invalidateFiles.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @param courseId Course ID.
|
||||
|
@ -783,20 +782,6 @@ export class AddonModAssignProvider {
|
|||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched files.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @return Promise resolved when the files are invalidated.
|
||||
*/
|
||||
async invalidateFiles(moduleId: number): Promise<void> {
|
||||
await CoreFilepool.invalidateFilesByComponent(
|
||||
CoreSites.getCurrentSiteId(),
|
||||
AddonModAssignProvider.COMPONENT,
|
||||
moduleId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates assignment submissions data WS calls.
|
||||
*
|
||||
|
|
|
@ -278,7 +278,7 @@ export class AddonModAssignSubmissionFileHandlerService implements AddonModAssig
|
|||
plugin: AddonModAssignPlugin,
|
||||
inputData: AddonModAssignSubmissionFileData,
|
||||
pluginData: AddonModAssignSubmissionFilePluginData,
|
||||
offline?: boolean,
|
||||
offline = false,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<ion-input *ngIf="searchMode" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input>
|
||||
|
||||
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
|
||||
<core-rich-text-editor *ngIf="editMode" item-content [control]="form.controls['f_'+field.id]" [placeholder]="field.name"
|
||||
[formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true"
|
||||
<core-rich-text-editor *ngIf="editMode" [control]="form.controls['f_'+field.id]" [placeholder]="field.name"
|
||||
[name]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true"
|
||||
contextLevel="module" [contextInstanceId]="componentId" [elementId]="'field_'+field.id" ngDefaultControl>
|
||||
</core-rich-text-editor>
|
||||
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
|
||||
|
|
|
@ -542,7 +542,7 @@ export class AddonModDataHelperProvider {
|
|||
dataId: number,
|
||||
entryId: number,
|
||||
entryContents: AddonModDataEntryFields,
|
||||
offline: boolean = false,
|
||||
offline = false,
|
||||
siteId?: string,
|
||||
): Promise<AddonModDataEntryWSField[]> {
|
||||
if (!inputData) {
|
||||
|
@ -766,6 +766,33 @@ export class AddonModDataHelperProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the itemId for the uploaded file/s.
|
||||
*/
|
||||
async uploadOrStoreFiles(
|
||||
dataId: number,
|
||||
itemId: number,
|
||||
entryId: number,
|
||||
fieldId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: true,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult>;
|
||||
async uploadOrStoreFiles(
|
||||
dataId: number,
|
||||
itemId: number,
|
||||
entryId: number,
|
||||
fieldId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: false,
|
||||
siteId?: string,
|
||||
): Promise<number>;
|
||||
async uploadOrStoreFiles(
|
||||
dataId: number,
|
||||
itemId: number,
|
||||
entryId: number,
|
||||
fieldId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: boolean,
|
||||
siteId?: string,
|
||||
): Promise<number | CoreFileUploaderStoreFilesResult>;
|
||||
async uploadOrStoreFiles(
|
||||
dataId: number,
|
||||
itemId: number = 0,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
|
||||
<core-rich-text-editor item-content elementId="message"
|
||||
<core-rich-text-editor elementId="message"
|
||||
[name]="'mod_forum_reply_' + replyData.id" [control]="messageControl"
|
||||
[placeholder]="'addon.mod_forum.replyplaceholder' | translate" [autoSave]="true"
|
||||
[component]="component" [componentId]="componentId" [draftExtraParams]="{edit: replyData.id}"
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
|
||||
<core-rich-text-editor item-content elementId="message" contextLevel="module"
|
||||
<core-rich-text-editor elementId="message" contextLevel="module"
|
||||
[control]="messageControl" [placeholder]="'addon.mod_forum.replyplaceholder' | translate"
|
||||
[name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId" [autoSave]="true"
|
||||
[contextInstanceId]="forum && forum.cmid" [draftExtraParams]="{reply: post.id}"
|
||||
|
|
|
@ -287,7 +287,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
const attachment = await AddonModForumHelper.uploadOrStoreReplyFiles(
|
||||
this.forum.id,
|
||||
this.post.id,
|
||||
files,
|
||||
files as CoreFileEntry[],
|
||||
false,
|
||||
);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
|
||||
<core-rich-text-editor item-content name="addon_mod_forum_new_discussion" contextLevel="module" elementId="message"
|
||||
<core-rich-text-editor name="addon_mod_forum_new_discussion" contextLevel="module" elementId="message"
|
||||
[control]="messageControl" [placeholder]="'addon.mod_forum.message' | translate" [component]="component"
|
||||
[componentId]="forum.cmid" [autoSave]="true" [contextInstanceId]="forum.cmid"
|
||||
(contentChanged)="onMessageChange($event)">
|
||||
|
|
|
@ -445,11 +445,17 @@ export class AddonModForumHelperProvider {
|
|||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
async storeReplyFiles(forumId: number, postId: number, files: any[], siteId?: string, userId?: number): Promise<void> {
|
||||
async storeReplyFiles(
|
||||
forumId: number,
|
||||
postId: number,
|
||||
files: CoreFileEntry[],
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<CoreFileUploaderStoreFilesResult> {
|
||||
// Get the folder where to store the files.
|
||||
const folderPath = await AddonModForumOffline.getReplyFolder(forumId, postId, siteId, userId);
|
||||
|
||||
await CoreFileUploader.storeFilesToUpload(folderPath, files);
|
||||
return CoreFileUploader.storeFilesToUpload(folderPath, files);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -485,9 +491,9 @@ export class AddonModForumHelperProvider {
|
|||
): Promise<CoreFileUploaderStoreFilesResult | number> {
|
||||
if (offline) {
|
||||
return this.storeNewDiscussionFiles(forumId, timecreated, files, siteId);
|
||||
} else {
|
||||
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
|
||||
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -501,19 +507,35 @@ export class AddonModForumHelperProvider {
|
|||
* @param userId User the reply belongs to. If not defined, current user in site.
|
||||
* @return Promise resolved if success.
|
||||
*/
|
||||
uploadOrStoreReplyFiles(
|
||||
async uploadOrStoreReplyFiles(
|
||||
forumId: number,
|
||||
postId: number,
|
||||
files: any[],
|
||||
files: CoreFileEntry[],
|
||||
offline: true,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<CoreFileUploaderStoreFilesResult>;
|
||||
async uploadOrStoreReplyFiles(
|
||||
forumId: number,
|
||||
postId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: false,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<number>;
|
||||
async uploadOrStoreReplyFiles(
|
||||
forumId: number,
|
||||
postId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: boolean,
|
||||
siteId?: string,
|
||||
userId?: number,
|
||||
): Promise<any> {
|
||||
): Promise<CoreFileUploaderStoreFilesResult | number> {
|
||||
if (offline) {
|
||||
return this.storeReplyFiles(forumId, postId, files, siteId, userId);
|
||||
} else {
|
||||
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
|
||||
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModForumProvider.COMPONENT, forumId, siteId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import { CoreTagItem } from '@features/tag/services/tag';
|
|||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreFileEntry } from '@services/file-helper';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreGroups } from '@services/groups';
|
||||
import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
|
@ -870,7 +869,6 @@ export class AddonModForumProvider {
|
|||
|
||||
/**
|
||||
* Invalidate the prefetched content except files.
|
||||
* To invalidate files, use AddonModForum#invalidateFiles.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @param courseId Course ID.
|
||||
|
@ -963,18 +961,6 @@ export class AddonModForumProvider {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched files.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @return Promise resolved when the files are invalidated.
|
||||
*/
|
||||
async invalidateFiles(moduleId: number): Promise<void> {
|
||||
const siteId = CoreSites.getCurrentSiteId();
|
||||
|
||||
await CoreFilepool.invalidateFilesByComponent(siteId, AddonModForumProvider.COMPONENT, moduleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates forum data.
|
||||
*
|
||||
|
|
|
@ -792,7 +792,7 @@ export type AddonModH5PActivityWSResultAnswer = {
|
|||
/**
|
||||
* User attempts data with some calculated data.
|
||||
*/
|
||||
export type AddonModH5PActivityUserAttempts = Omit<AddonModH5PActivityWSUserAttempts, 'attempts|scored'> & {
|
||||
export type AddonModH5PActivityUserAttempts = Omit<AddonModH5PActivityWSUserAttempts, 'attempts'|'scored'> & {
|
||||
attempts: AddonModH5PActivityAttempt[]; // The complete attempts list.
|
||||
scored?: { // Attempts used to grade the activity.
|
||||
title: string; // Scored attempts title.
|
||||
|
|
|
@ -16,49 +16,51 @@ import { NgModule } from '@angular/core';
|
|||
|
||||
import { AddonModAssignModule } from './assign/assign.module';
|
||||
import { AddonModBookModule } from './book/book.module';
|
||||
import { AddonModChatModule } from './chat/chat.module';
|
||||
import { AddonModChoiceModule } from './choice/choice.module';
|
||||
import { AddonModDataModule } from './data/data.module';
|
||||
import { AddonModFeedbackModule } from './feedback/feedback.module';
|
||||
import { AddonModFolderModule } from './folder/folder.module';
|
||||
import { AddonModForumModule } from './forum/forum.module';
|
||||
import { AddonModLabelModule } from './label/label.module';
|
||||
import { AddonModGlossaryModule } from './glossary/glossary.module';
|
||||
import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module';
|
||||
import { AddonModImscpModule } from './imscp/imscp.module';
|
||||
import { AddonModLabelModule } from './label/label.module';
|
||||
import { AddonModLessonModule } from './lesson/lesson.module';
|
||||
import { AddonModLtiModule } from './lti/lti.module';
|
||||
import { AddonModPageModule } from './page/page.module';
|
||||
import { AddonModQuizModule } from './quiz/quiz.module';
|
||||
import { AddonModResourceModule } from './resource/resource.module';
|
||||
import { AddonModUrlModule } from './url/url.module';
|
||||
import { AddonModLtiModule } from './lti/lti.module';
|
||||
import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module';
|
||||
import { AddonModSurveyModule } from './survey/survey.module';
|
||||
import { AddonModScormModule } from './scorm/scorm.module';
|
||||
import { AddonModChoiceModule } from './choice/choice.module';
|
||||
import { AddonModSurveyModule } from './survey/survey.module';
|
||||
import { AddonModUrlModule } from './url/url.module';
|
||||
import { AddonModWikiModule } from './wiki/wiki.module';
|
||||
import { AddonModGlossaryModule } from './glossary/glossary.module';
|
||||
import { AddonModChatModule } from './chat/chat.module';
|
||||
import { AddonModFeedbackModule } from './feedback/feedback.module';
|
||||
import { AddonModWorkshopModule } from './workshop/workshop.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AddonModAssignModule,
|
||||
AddonModBookModule,
|
||||
AddonModChatModule,
|
||||
AddonModChoiceModule,
|
||||
AddonModDataModule,
|
||||
AddonModFeedbackModule,
|
||||
AddonModFolderModule,
|
||||
AddonModForumModule,
|
||||
AddonModGlossaryModule,
|
||||
AddonModH5PActivityModule,
|
||||
AddonModImscpModule,
|
||||
AddonModLabelModule,
|
||||
AddonModLessonModule,
|
||||
AddonModLtiModule,
|
||||
AddonModPageModule,
|
||||
AddonModQuizModule,
|
||||
AddonModUrlModule,
|
||||
AddonModLabelModule,
|
||||
AddonModResourceModule,
|
||||
AddonModFolderModule,
|
||||
AddonModImscpModule,
|
||||
AddonModLtiModule,
|
||||
AddonModH5PActivityModule,
|
||||
AddonModSurveyModule,
|
||||
AddonModScormModule,
|
||||
AddonModChoiceModule,
|
||||
AddonModSurveyModule,
|
||||
AddonModUrlModule,
|
||||
AddonModWikiModule,
|
||||
AddonModGlossaryModule,
|
||||
AddonModChatModule,
|
||||
AddonModFeedbackModule,
|
||||
AddonModWorkshopModule,
|
||||
],
|
||||
})
|
||||
export class AddonModModule { }
|
||||
|
|
|
@ -25,7 +25,6 @@ import { AddonModQuizAccessSecureWindowModule } from './securewindow/securewindo
|
|||
import { AddonModQuizAccessTimeLimitModule } from './timelimit/timelimit.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonModQuizAccessDelayBetweenAttemptsModule,
|
||||
AddonModQuizAccessIpAddressModule,
|
||||
|
@ -37,7 +36,5 @@ import { AddonModQuizAccessTimeLimitModule } from './timelimit/timelimit.module'
|
|||
AddonModQuizAccessSecureWindowModule,
|
||||
AddonModQuizAccessTimeLimitModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class AddonModQuizAccessRulesModule { }
|
||||
|
|
|
@ -28,7 +28,6 @@ import {
|
|||
CoreQuestionsAnswers,
|
||||
} from '@features/question/services/question';
|
||||
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
|
@ -1255,7 +1254,6 @@ export class AddonModQuizProvider {
|
|||
|
||||
/**
|
||||
* Invalidate the prefetched content except files.
|
||||
* To invalidate files, use AddonModQuizProvider.invalidateFiles.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @param courseId Course ID.
|
||||
|
@ -1306,20 +1304,6 @@ export class AddonModQuizProvider {
|
|||
await site.invalidateWsCacheForKey(this.getFeedbackForGradeCacheKey(quizId, grade));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched files.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @return Promise resolved when the files are invalidated.
|
||||
*/
|
||||
async invalidateFiles(moduleId: number): Promise<void> {
|
||||
await CoreFilepool.invalidateFilesByComponent(
|
||||
CoreSites.getCurrentSiteId(),
|
||||
AddonModQuizProvider.COMPONENT,
|
||||
moduleId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates grade from gradebook for a certain user.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from './component/accumulative';
|
||||
import { AddonModWorkshopAssessmentStrategyAccumulativeHandler } from './services/handler';
|
||||
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModWorkshopAssessmentStrategyAccumulativeComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => () => {
|
||||
AddonWorkshopAssessmentStrategyDelegate.registerHandler(
|
||||
AddonModWorkshopAssessmentStrategyAccumulativeHandler.instance,
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
AddonModWorkshopAssessmentStrategyAccumulativeComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyAccumulativeModule {}
|
|
@ -0,0 +1,25 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component';
|
||||
|
||||
/**
|
||||
* Component for accumulative assessment strategy.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-assessment-strategy-accumulative',
|
||||
templateUrl: 'addon-mod-workshop-assessment-strategy-accumulative.html',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyAccumulativeComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { }
|
|
@ -0,0 +1,50 @@
|
|||
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
|
||||
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ field.dimtitle }}</h2>
|
||||
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="edit && field.grades">
|
||||
<ion-label position="stacked">
|
||||
<span [core-mark-required]="true">
|
||||
{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="selectedValues[n].grade" interface="action-sheet">
|
||||
<ion-select-option *ngFor="let grade of field.grades" [value]="grade.value">{{grade.label}}</ion-select-option>
|
||||
</ion-select>
|
||||
<core-input-errors *ngIf="fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]">
|
||||
</core-input-errors>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!edit && field.grades" class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}</h2>
|
||||
<ng-container *ngFor="let grade of field.grades">
|
||||
<p *ngIf="grade.value === selectedValues[n].grade">{{grade.label}}</p>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="edit">
|
||||
<ion-label position="stacked">
|
||||
{{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
|
||||
</ion-label>
|
||||
<ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" core-auto-rows></ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!edit" class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>
|
||||
{{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
|
||||
</h2>
|
||||
<p>
|
||||
<core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</ng-container>
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dimensioncommentfor": "Comment for {{$a}}",
|
||||
"dimensiongradefor": "Grade for {{$a}}",
|
||||
"dimensionnumber": "Aspect {{$a}}",
|
||||
"mustchoosegrade": "You have to select a grade for this aspect"
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
// (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 {
|
||||
AddonModWorkshopAssessmentStrategyFieldErrors,
|
||||
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
|
||||
import {
|
||||
AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
AddonModWorkshopGetAssessmentFormFieldsParsedData,
|
||||
} from '@addons/mod/workshop/services/workshop';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreGradesHelper } from '@features/grades/services/grades-helper';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { AddonWorkshopAssessmentStrategyHandler } from '../../../services/assessment-strategy-delegate';
|
||||
import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from '../component/accumulative';
|
||||
|
||||
/**
|
||||
* Handler for accumulative assessment strategy plugin.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopAssessmentStrategyAccumulativeHandlerService implements AddonWorkshopAssessmentStrategyHandler {
|
||||
|
||||
name = 'AddonModWorkshopAssessmentStrategyAccumulative';
|
||||
strategyName = 'accumulative';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getComponent(): Type<unknown> {
|
||||
return AddonModWorkshopAssessmentStrategyAccumulativeComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getOriginalValues(
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
|
||||
const defaultGrade = Translate.instant('core.choosedots');
|
||||
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
form.fields.forEach((field, n) => {
|
||||
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_accumulative.dimensionnumber', { $a: field.number });
|
||||
|
||||
if (!form.current[n]) {
|
||||
form.current[n] = {};
|
||||
}
|
||||
|
||||
originalValues[n] = {};
|
||||
originalValues[n].peercomment = form.current[n].peercomment || '';
|
||||
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
|
||||
|
||||
form.current[n].grade = form.current[n].grade ? parseInt(String(form.current[n].grade), 10) : -1;
|
||||
|
||||
const gradingType = parseInt(String(field.grade), 10);
|
||||
const dimension = form.dimensionsinfo.find((dimension) => dimension.id == parseInt(field.dimensionid, 10));
|
||||
const scale = dimension && gradingType < 0 ? dimension.scale : undefined;
|
||||
|
||||
promises.push(CoreGradesHelper.makeGradesMenu(gradingType, undefined, defaultGrade, -1, scale).then((grades) => {
|
||||
field.grades = grades;
|
||||
originalValues[n].grade = form.current[n].grade;
|
||||
|
||||
return;
|
||||
}));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return originalValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
hasDataChanged(
|
||||
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
): boolean {
|
||||
for (const x in originalValues) {
|
||||
if (originalValues[x].grade != currentValues[x].grade) {
|
||||
return true;
|
||||
}
|
||||
if (originalValues[x].peercomment != currentValues[x].peercomment) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async prepareAssessmentData(
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<CoreFormFields> {
|
||||
const data: CoreFormFields = {};
|
||||
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
|
||||
let hasErrors = false;
|
||||
|
||||
form.fields.forEach((field, idx) => {
|
||||
if (idx < form.dimenssionscount) {
|
||||
const grade = parseInt(String(currentValues[idx].grade), 10);
|
||||
if (!isNaN(grade) && grade >= 0) {
|
||||
data['grade__idx_' + idx] = grade;
|
||||
} else {
|
||||
errors['grade_' + idx] = Translate.instant('addon.mod_workshop_assessment_accumulative.mustchoosegrade');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (currentValues[idx].peercomment) {
|
||||
data['peercomment__idx_' + idx] = currentValues[idx].peercomment;
|
||||
}
|
||||
|
||||
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
|
||||
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
|
||||
data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopAssessmentStrategyAccumulativeHandler =
|
||||
makeSingleton(AddonModWorkshopAssessmentStrategyAccumulativeHandlerService);
|
|
@ -0,0 +1,29 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { AddonModWorkshopAssessmentStrategyAccumulativeModule } from './accumulative/accumulative.module';
|
||||
import { AddonModWorkshopAssessmentStrategyCommentsModule } from './comments/comments.module';
|
||||
import { AddonModWorkshopAssessmentStrategyNumErrorsModule } from './numerrors/numerrors.module';
|
||||
import { AddonModWorkshopAssessmentStrategyRubricModule } from './rubric/rubric.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AddonModWorkshopAssessmentStrategyAccumulativeModule,
|
||||
AddonModWorkshopAssessmentStrategyCommentsModule,
|
||||
AddonModWorkshopAssessmentStrategyNumErrorsModule,
|
||||
AddonModWorkshopAssessmentStrategyRubricModule,
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyModule {}
|
|
@ -0,0 +1,44 @@
|
|||
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
|
||||
import { AddonModWorkshopAssessmentStrategyCommentsComponent } from './component/comments';
|
||||
import { AddonModWorkshopAssessmentStrategyCommentsHandler } from './services/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModWorkshopAssessmentStrategyCommentsComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => () => {
|
||||
AddonWorkshopAssessmentStrategyDelegate.registerHandler(
|
||||
AddonModWorkshopAssessmentStrategyCommentsHandler.instance,
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
AddonModWorkshopAssessmentStrategyCommentsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyCommentsModule {}
|
|
@ -0,0 +1,30 @@
|
|||
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
|
||||
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ field.dimtitle }}</h2>
|
||||
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="edit">
|
||||
<ion-label position="stacked">
|
||||
<span [core-mark-required]="true">
|
||||
{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" core-auto-rows></ion-textarea>
|
||||
<core-input-errors *ngIf="fieldErrors['peercomment_' + n]" [errorText]="fieldErrors['peercomment_' + n]">
|
||||
</core-input-errors>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!edit" class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2>
|
||||
<p><core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</ng-container>
|
|
@ -0,0 +1,25 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component';
|
||||
|
||||
/**
|
||||
* Component for comments assessment strategy.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-assessment-strategy-comments',
|
||||
templateUrl: 'addon-mod-workshop-assessment-strategy-comments.html',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyCommentsComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { }
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"dimensioncommentfor": "Comment for {{$a}}",
|
||||
"dimensionnumber": "Aspect {{$a}}"
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
// (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 {
|
||||
AddonModWorkshopAssessmentStrategyFieldErrors,
|
||||
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
|
||||
import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate';
|
||||
import {
|
||||
AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
AddonModWorkshopGetAssessmentFormFieldsParsedData,
|
||||
} from '@addons/mod/workshop/services/workshop';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { AddonModWorkshopAssessmentStrategyCommentsComponent } from '../component/comments';
|
||||
|
||||
/**
|
||||
* Handler for comments assessment strategy plugin.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopAssessmentStrategyCommentsHandlerService implements AddonWorkshopAssessmentStrategyHandler {
|
||||
|
||||
name = 'AddonModWorkshopAssessmentStrategyComments';
|
||||
strategyName = 'comments';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getComponent(): Type<unknown> {
|
||||
return AddonModWorkshopAssessmentStrategyCommentsComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getOriginalValues(
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
|
||||
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
|
||||
|
||||
form.fields.forEach((field, n) => {
|
||||
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_comments.dimensionnumber', { $a: field.number });
|
||||
|
||||
if (!form.current[n]) {
|
||||
form.current[n] = {};
|
||||
}
|
||||
|
||||
originalValues[n] = {};
|
||||
originalValues[n].peercomment = form.current[n].peercomment || '';
|
||||
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
|
||||
});
|
||||
|
||||
return originalValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
hasDataChanged(
|
||||
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
): boolean {
|
||||
for (const x in originalValues) {
|
||||
if (originalValues[x].peercomment != currentValues[x].peercomment) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async prepareAssessmentData(
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<CoreFormFields> {
|
||||
const data: CoreFormFields = {};
|
||||
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
|
||||
let hasErrors = false;
|
||||
|
||||
form.fields.forEach((field, idx) => {
|
||||
if (idx < form.dimenssionscount) {
|
||||
if (currentValues[idx].peercomment) {
|
||||
data['peercomment__idx_' + idx] = currentValues[idx].peercomment;
|
||||
} else {
|
||||
errors['peercomment_' + idx] = Translate.instant('core.err_required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
|
||||
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopAssessmentStrategyCommentsHandler =
|
||||
makeSingleton(AddonModWorkshopAssessmentStrategyCommentsHandlerService);
|
|
@ -0,0 +1,53 @@
|
|||
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
|
||||
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ field.dimtitle }}</h2>
|
||||
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-list>
|
||||
<ion-radio-group [(ngModel)]="selectedValues[n].grade" [name]="'grade_' + n">
|
||||
<ion-item>
|
||||
<ion-label position="stacked">
|
||||
<span [core-mark-required]="edit">
|
||||
{{ 'addon.mod_workshop.yourassessmentfor' | translate : {'$a': field.dimtitle } }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<core-input-errors *ngIf="edit && fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]">
|
||||
</core-input-errors>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<core-format-text [text]="field.grade0" [filter]="false"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-radio slot="start" [value]="-1" [disabled]="!edit"></ion-radio>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label><core-format-text [text]="field.grade1" [filter]="false"></core-format-text></ion-label>
|
||||
<ion-radio slot="start" [value]="1" [disabled]="!edit"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
<ion-item *ngIf="edit">
|
||||
<ion-label position="stacked">
|
||||
{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}
|
||||
</ion-label>
|
||||
<ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" [name]="'peercomment_' + n"
|
||||
core-auto-rows>
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!edit" class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2>
|
||||
<p>
|
||||
<core-format-text [text]="selectedValues[n].peercomment" contextLevel="module" [contextInstanceId]="moduleId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</ng-container>
|
|
@ -0,0 +1,25 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component';
|
||||
|
||||
/**
|
||||
* Component for numerrors assessment strategy.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-assessment-strategy-numerrors',
|
||||
templateUrl: 'addon-mod-workshop-assessment-strategy-numerrors.html',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyNumErrorsComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { }
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dimensioncommentfor": "Comment for {{$a}}",
|
||||
"dimensiongradefor": "Grade for {{$a}}",
|
||||
"dimensionnumber": "Assertion {{$a}}"
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
|
||||
import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from './component/numerrors';
|
||||
import { AddonModWorkshopAssessmentStrategyNumErrorsHandler } from './services/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModWorkshopAssessmentStrategyNumErrorsComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => () => {
|
||||
AddonWorkshopAssessmentStrategyDelegate.registerHandler(
|
||||
AddonModWorkshopAssessmentStrategyNumErrorsHandler.instance,
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
AddonModWorkshopAssessmentStrategyNumErrorsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyNumErrorsModule {}
|
|
@ -0,0 +1,134 @@
|
|||
// (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 {
|
||||
AddonModWorkshopAssessmentStrategyFieldErrors,
|
||||
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
|
||||
import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate';
|
||||
import {
|
||||
AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
AddonModWorkshopGetAssessmentFormFieldsParsedData,
|
||||
} from '@addons/mod/workshop/services/workshop';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { Translate, makeSingleton } from '@singletons';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from '../component/numerrors';
|
||||
|
||||
/**
|
||||
* Handler for numerrors assessment strategy plugin.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopAssessmentStrategyNumErrorsHandlerService implements AddonWorkshopAssessmentStrategyHandler {
|
||||
|
||||
name = 'AddonModWorkshopAssessmentStrategyNumErrors';
|
||||
strategyName = 'numerrors';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getComponent(): Type<unknown> {
|
||||
return AddonModWorkshopAssessmentStrategyNumErrorsComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getOriginalValues(
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
|
||||
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
|
||||
|
||||
form.fields.forEach((field, n) => {
|
||||
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_numerrors.dimensionnumber', { $a: field.number });
|
||||
|
||||
if (!form.current[n]) {
|
||||
form.current[n] = {};
|
||||
}
|
||||
|
||||
originalValues[n] = {};
|
||||
originalValues[n].peercomment = form.current[n].peercomment || '';
|
||||
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
|
||||
originalValues[n].grade = form.current[n].grade || '';
|
||||
});
|
||||
|
||||
return originalValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
hasDataChanged(
|
||||
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
): boolean {
|
||||
for (const x in originalValues) {
|
||||
if (originalValues[x].grade != currentValues[x].grade) {
|
||||
return true;
|
||||
}
|
||||
if (originalValues[x].peercomment != currentValues[x].peercomment) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async prepareAssessmentData(
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<CoreFormFields> {
|
||||
const data: CoreFormFields = {};
|
||||
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
|
||||
let hasErrors = false;
|
||||
|
||||
form.fields.forEach((field, idx) => {
|
||||
if (idx < form.dimenssionscount) {
|
||||
const grade = parseInt(String(currentValues[idx].grade), 10);
|
||||
if (!isNaN(grade) && (grade == 1 || grade == -1)) {
|
||||
data['grade__idx_' + idx] = grade;
|
||||
} else {
|
||||
errors['grade_' + idx] = Translate.instant('core.required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (currentValues[idx].peercomment) {
|
||||
data['peercomment__idx_' + idx] = currentValues[idx].peercomment;
|
||||
}
|
||||
|
||||
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
|
||||
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
|
||||
data['weight__idx_' + idx] = parseInt(field.weight, 10) || 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopAssessmentStrategyNumErrorsHandler =
|
||||
makeSingleton(AddonModWorkshopAssessmentStrategyNumErrorsHandlerService);
|
|
@ -0,0 +1,26 @@
|
|||
<ng-container *ngFor="let field of assessment.form?.fields; let n = index">
|
||||
<ion-card *ngIf="n < assessment.form?.dimenssionscount">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2 [core-mark-required]="edit">{{ field.dimtitle }}</h2>
|
||||
<core-format-text [text]="field.description" contextLevel="module" [contextInstanceId]="moduleId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
<core-input-errors *ngIf="edit && fieldErrors['chosenlevelid_' + n]" [errorText]="fieldErrors['chosenlevelid_' + n]">
|
||||
</core-input-errors>
|
||||
</ion-item>
|
||||
<ion-list>
|
||||
<ion-radio-group [(ngModel)]="selectedValues[n].chosenlevelid" [name]="'chosenlevelid_' + n">
|
||||
<ion-item *ngFor="let subfield of field.fields">
|
||||
<ion-label>
|
||||
<p><core-format-text [text]="subfield.definition" contextLevel="module"
|
||||
[contextInstanceId]="moduleId" [courseId]="courseId">
|
||||
</core-format-text></p>
|
||||
</ion-label>
|
||||
<ion-radio slot="start" [value]="subfield.levelid" [disabled]="!edit"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
</ng-container>
|
|
@ -0,0 +1,25 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { AddonModWorkshopAssessmentStrategyBaseComponent } from '../../../classes/assessment-strategy-component';
|
||||
|
||||
/**
|
||||
* Component for rubric assessment strategy.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-assessment-strategy-rubric',
|
||||
templateUrl: 'addon-mod-workshop-assessment-strategy-rubric.html',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyRubricComponent extends AddonModWorkshopAssessmentStrategyBaseComponent { }
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"dimensionnumber": "Criterion {{$a}}",
|
||||
"mustchooseone": "You have to select one of these items"
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
|
||||
import { AddonModWorkshopAssessmentStrategyRubricComponent } from './component/rubric';
|
||||
import { AddonModWorkshopAssessmentStrategyRubricHandler } from './services/handler';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModWorkshopAssessmentStrategyRubricComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => () => {
|
||||
AddonWorkshopAssessmentStrategyDelegate.registerHandler(
|
||||
AddonModWorkshopAssessmentStrategyRubricHandler.instance,
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
AddonModWorkshopAssessmentStrategyRubricComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyRubricModule {}
|
|
@ -0,0 +1,125 @@
|
|||
// (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 {
|
||||
AddonModWorkshopAssessmentStrategyFieldErrors,
|
||||
} from '@addons/mod/workshop/components/assessment-strategy/assessment-strategy';
|
||||
import { AddonWorkshopAssessmentStrategyHandler } from '@addons/mod/workshop/services/assessment-strategy-delegate';
|
||||
import {
|
||||
AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
AddonModWorkshopGetAssessmentFormFieldsParsedData,
|
||||
} from '@addons/mod/workshop/services/workshop';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { Translate, makeSingleton } from '@singletons';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { AddonModWorkshopAssessmentStrategyRubricComponent } from '../component/rubric';
|
||||
|
||||
/**
|
||||
* Handler for rubric assessment strategy plugin.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopAssessmentStrategyRubricHandlerService implements AddonWorkshopAssessmentStrategyHandler {
|
||||
|
||||
name = 'AddonModWorkshopAssessmentStrategyRubric';
|
||||
strategyName = 'rubric';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getComponent(): Type<unknown> {
|
||||
return AddonModWorkshopAssessmentStrategyRubricComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getOriginalValues(
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
|
||||
const originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[] = [];
|
||||
|
||||
form.fields.forEach((field, n) => {
|
||||
field.dimtitle = Translate.instant('addon.mod_workshop_assessment_rubric.dimensionnumber', { $a: field.number });
|
||||
|
||||
if (!form.current[n]) {
|
||||
form.current[n] = {};
|
||||
}
|
||||
|
||||
originalValues[n] = {};
|
||||
originalValues[n].chosenlevelid = form.current[n].chosenlevelid || '';
|
||||
originalValues[n].number = field.number; // eslint-disable-line id-blacklist
|
||||
});
|
||||
|
||||
return originalValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
hasDataChanged(
|
||||
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
): boolean {
|
||||
for (const x in originalValues) {
|
||||
if (originalValues[x].chosenlevelid != (currentValues[x].chosenlevelid || '')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async prepareAssessmentData(
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<CoreFormFields> {
|
||||
const data: CoreFormFields = {};
|
||||
const errors: AddonModWorkshopAssessmentStrategyFieldErrors = {};
|
||||
let hasErrors = false;
|
||||
|
||||
form.fields.forEach((field, idx) => {
|
||||
if (idx < form.dimenssionscount) {
|
||||
const id = parseInt(currentValues[idx].chosenlevelid, 10);
|
||||
if (!isNaN(id) && id >= 0) {
|
||||
data['chosenlevelid__idx_' + idx] = id;
|
||||
} else {
|
||||
errors['chosenlevelid_' + idx] = Translate.instant('addon.mod_workshop_assessment_rubric.mustchooseone');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
data['gradeid__idx_' + idx] = parseInt(form.current[idx].gradeid, 10) || 0;
|
||||
data['dimensionid__idx_' + idx] = parseInt(field.dimensionid, 10);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopAssessmentStrategyRubricHandler =
|
||||
makeSingleton(AddonModWorkshopAssessmentStrategyRubricHandlerService);
|
|
@ -0,0 +1,36 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { AddonModWorkshopGetAssessmentFormFieldsParsedData } from '../services/workshop';
|
||||
import { AddonModWorkshopSubmissionAssessmentWithFormData } from '../services/workshop-helper';
|
||||
|
||||
/**
|
||||
* Base class for component to render an assessment strategy.
|
||||
*/
|
||||
@Component({
|
||||
template: '',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyBaseComponent {
|
||||
|
||||
@Input() workshopId!: number;
|
||||
@Input() assessment!: AddonModWorkshopSubmissionAssessmentWithFormData;
|
||||
@Input() edit!: boolean;
|
||||
@Input() selectedValues!: AddonModWorkshopGetAssessmentFormFieldsParsedData[];
|
||||
@Input() fieldErrors!: Record<string, string>;
|
||||
@Input() strategy!: string;
|
||||
@Input() moduleId!: number;
|
||||
@Input() courseId?: number;
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<h3 class="ion-padding">{{ 'addon.mod_workshop.assessmentform' | translate }}</h3>
|
||||
|
||||
<form name="mma-mod_workshop-assessment-form" #assessmentForm>
|
||||
<core-loading [hideUntil]="assessmentStrategyLoaded">
|
||||
<ng-container *ngIf="componentClass && assessmentStrategyLoaded">
|
||||
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
||||
</ng-container>
|
||||
|
||||
<ion-card class="core-info-card" *ngIf="notSupported">
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<p>{{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card *ngIf="assessmentStrategyLoaded && overallFeedkback &&
|
||||
(edit || data.assessment?.feedbackauthor || data.assessment?.feedbackattachmentfiles?.length) ">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label><h2>{{ 'addon.mod_workshop.overallfeedback' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item position="stacked" *ngIf="edit">
|
||||
<ion-label position="stacked">
|
||||
<span [core-mark-required]="overallFeedkbackRequired">
|
||||
{{ 'addon.mod_workshop.feedbackauthor' | translate }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<core-rich-text-editor [control]="feedbackControl" [component]="component"
|
||||
[componentId]="workshop.coursemodule" [autoSave]="true" contextLevel="module"
|
||||
[contextInstanceId]="workshop.coursemodule" elementId="feedbackauthor_editor"
|
||||
[draftExtraParams]="{asid: assessmentId}" (contentChanged)="onFeedbackChange($event)">
|
||||
</core-rich-text-editor>
|
||||
<core-input-errors
|
||||
*ngIf="overallFeedkbackRequired && data.fieldErrors && data.fieldErrors['feedbackauthor']"
|
||||
[errorText]="data.fieldErrors['feedbackauthor']">
|
||||
</core-input-errors>
|
||||
</ion-item>
|
||||
<core-attachments *ngIf="edit && workshop.overallfeedbackfiles" [files]="data.assessment?.feedbackattachmentfiles"
|
||||
[maxSize]="workshop.overallfeedbackmaxbytes" [maxSubmissions]="workshop.overallfeedbackfiles"
|
||||
[component]="component" [componentId]="componentId" [allowOffline]="true">
|
||||
</core-attachments>
|
||||
<ion-item *ngIf="edit && access && access.canallocate">
|
||||
<ion-label position="stacked">
|
||||
<span [core-mark-required]="true">
|
||||
{{ 'addon.mod_workshop.assessmentweight' | translate }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="weight" interface="action-sheet" name="weight">
|
||||
<ion-select-option *ngFor="let w of weights" [value]="w">{{w}}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!edit && data.assessment?.feedbackauthor">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="data.assessment?.feedbackauthor"
|
||||
contextLevel="module" [contextInstanceId]="workshop.coursemodule" [courseId]="workshop.course">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!edit && workshop.overallfeedbackfiles && data.assessment?.feedbackattachmentfiles?.length"
|
||||
lines="none">
|
||||
<ion-label>
|
||||
<core-files [files]="data.assessment?.feedbackattachmentfiles" [component]="component"
|
||||
[componentId]="componentId"></core-files>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
</form>
|
|
@ -0,0 +1,426 @@
|
|||
// (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, ElementRef, Type, OnDestroy } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
|
||||
import { CoreFileSession } from '@services/file-session';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreFormFields, CoreForms } from '@singletons/form';
|
||||
import { AddonWorkshopAssessmentStrategyDelegate } from '../../services/assessment-strategy-delegate';
|
||||
import {
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshopOverallFeedbackMode,
|
||||
AddonModWorkshop,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
AddonModWorkshopGetAssessmentFormFieldsParsedData,
|
||||
} from '../../services/workshop';
|
||||
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper';
|
||||
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
|
||||
|
||||
/**
|
||||
* Component that displays workshop assessment strategy form.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-assessment-strategy',
|
||||
templateUrl: 'addon-mod-workshop-assessment-strategy.html',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() workshop!: AddonModWorkshopData;
|
||||
@Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
|
||||
@Input() assessmentId!: number;
|
||||
@Input() userId!: number;
|
||||
@Input() strategy!: string;
|
||||
@Input() edit = false;
|
||||
|
||||
@ViewChild('assessmentForm') formElement!: ElementRef;
|
||||
|
||||
componentClass?: Type<unknown>;
|
||||
data: AddonModWorkshopAssessmentStrategyData = {
|
||||
workshopId: 0,
|
||||
assessment: undefined,
|
||||
edit: false,
|
||||
selectedValues: [],
|
||||
fieldErrors: {},
|
||||
strategy: '',
|
||||
moduleId: 0,
|
||||
courseId: undefined,
|
||||
};
|
||||
|
||||
assessmentStrategyLoaded = false;
|
||||
notSupported = false;
|
||||
feedbackText = '';
|
||||
feedbackControl = new FormControl();
|
||||
overallFeedkback = false;
|
||||
overallFeedkbackRequired = false;
|
||||
component = AddonModWorkshopProvider.COMPONENT;
|
||||
componentId?: number;
|
||||
weights: number[] = [];
|
||||
weight?: number;
|
||||
|
||||
protected obsInvalidated?: CoreEventObserver;
|
||||
protected hasOffline = false;
|
||||
protected originalData: {
|
||||
text: string;
|
||||
files: CoreFileEntry[];
|
||||
weight: number;
|
||||
selectedValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[];
|
||||
} = {
|
||||
text: '',
|
||||
files: [],
|
||||
weight: 1,
|
||||
selectedValues: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (!this.assessmentId || !this.strategy) {
|
||||
this.assessmentStrategyLoaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.workshopId = this.workshop.id;
|
||||
this.data.edit = this.edit;
|
||||
this.data.strategy = this.strategy;
|
||||
this.data.moduleId = this.workshop.coursemodule;
|
||||
this.data.courseId = this.workshop.course;
|
||||
|
||||
this.componentClass = AddonWorkshopAssessmentStrategyDelegate.getComponentForPlugin(this.strategy);
|
||||
if (this.componentClass) {
|
||||
this.overallFeedkback = this.workshop.overallfeedbackmode != AddonModWorkshopOverallFeedbackMode.DISABLED;
|
||||
this.overallFeedkbackRequired =
|
||||
this.workshop.overallfeedbackmode == AddonModWorkshopOverallFeedbackMode.ENABLED_REQUIRED;
|
||||
this.componentId = this.workshop.coursemodule;
|
||||
|
||||
// Load Weights selector.
|
||||
if (this.edit && this.access.canallocate) {
|
||||
this.weights;
|
||||
for (let i = 16; i >= 0; i--) {
|
||||
this.weights[i] = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if rich text editor is enabled.
|
||||
if (this.edit) {
|
||||
// Block the workshop.
|
||||
CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshop.id);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.load();
|
||||
this.obsInvalidated = CoreEvents.on(
|
||||
AddonModWorkshopProvider.ASSESSMENT_INVALIDATED,
|
||||
this.load.bind(this),
|
||||
|
||||
CoreSites.getCurrentSiteId(),
|
||||
);
|
||||
} catch (error) {
|
||||
this.componentClass = undefined;
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error loading assessment.');
|
||||
} finally {
|
||||
this.assessmentStrategyLoaded = true;
|
||||
}
|
||||
} else {
|
||||
// Helper data and fallback.
|
||||
this.notSupported = !AddonWorkshopAssessmentStrategyDelegate.isPluginSupported(this.strategy);
|
||||
this.assessmentStrategyLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to load the assessment data.
|
||||
*
|
||||
* @return Promised resvoled when data is loaded.
|
||||
*/
|
||||
protected async load(): Promise<void> {
|
||||
this.data.assessment = await AddonModWorkshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, {
|
||||
userId: this.userId,
|
||||
cmId: this.workshop.coursemodule,
|
||||
});
|
||||
|
||||
if (this.edit) {
|
||||
try {
|
||||
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId);
|
||||
const offlineData = offlineAssessment.inputdata;
|
||||
|
||||
this.hasOffline = true;
|
||||
|
||||
this.data.assessment.feedbackauthor = <string>offlineData.feedbackauthor;
|
||||
|
||||
if (this.access.canallocate) {
|
||||
this.data.assessment.weight = <number>offlineData.weight;
|
||||
}
|
||||
|
||||
// Override assessment plugins values.
|
||||
this.data.assessment.form!.current = AddonModWorkshop.parseFields(
|
||||
CoreUtils.objectToArrayOfObjects(offlineData, 'name', 'value'),
|
||||
);
|
||||
|
||||
// Override offline files.
|
||||
if (offlineData) {
|
||||
this.data.assessment.feedbackattachmentfiles =
|
||||
await AddonModWorkshopHelper.getAssessmentFilesFromOfflineFilesObject(
|
||||
<CoreFileUploaderStoreFilesResult>offlineData.feedbackauthorattachmentsid,
|
||||
this.workshop.id,
|
||||
this.assessmentId,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
this.hasOffline = false;
|
||||
// Ignore errors.
|
||||
} finally {
|
||||
this.feedbackText = this.data.assessment.feedbackauthor;
|
||||
this.feedbackControl.setValue(this.feedbackText);
|
||||
|
||||
this.originalData.text = this.data.assessment.feedbackauthor;
|
||||
|
||||
if (this.access.canallocate) {
|
||||
this.originalData.weight = this.data.assessment.weight;
|
||||
}
|
||||
|
||||
this.originalData.files = [];
|
||||
this.data.assessment.feedbackattachmentfiles.forEach((file) => {
|
||||
let filename = CoreFile.getFileName(file);
|
||||
if (!filename) {
|
||||
// We don't have filename, extract it from the path.
|
||||
filename = CoreFileHelper.getFilenameFromPath(file) || '';
|
||||
}
|
||||
|
||||
this.originalData.files.push({
|
||||
filename,
|
||||
fileurl: '', // No needed to compare.
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.data.selectedValues = await AddonWorkshopAssessmentStrategyDelegate.getOriginalValues(
|
||||
this.strategy,
|
||||
this.data.assessment.form!,
|
||||
this.workshop.id,
|
||||
);
|
||||
} finally {
|
||||
this.originalData.selectedValues = CoreUtils.clone(this.data.selectedValues);
|
||||
if (this.edit) {
|
||||
CoreFileSession.setFiles(
|
||||
AddonModWorkshopProvider.COMPONENT,
|
||||
this.workshop.id + '_' + this.assessmentId,
|
||||
this.data.assessment.feedbackattachmentfiles,
|
||||
);
|
||||
if (this.access.canallocate) {
|
||||
this.weight = this.data.assessment.weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data has changed.
|
||||
*
|
||||
* @return True if data has changed.
|
||||
*/
|
||||
hasDataChanged(): boolean {
|
||||
if (!this.assessmentStrategyLoaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare feedback text.
|
||||
const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []);
|
||||
if (this.originalData.text != text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.access.canallocate && this.originalData.weight != this.weight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare feedback files.
|
||||
const files = CoreFileSession.getFiles(
|
||||
AddonModWorkshopProvider.COMPONENT,
|
||||
this.workshop.id + '_' + this.assessmentId,
|
||||
) || [];
|
||||
if (CoreFileUploader.areFileListDifferent(files, this.originalData.files)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return AddonWorkshopAssessmentStrategyDelegate.hasDataChanged(
|
||||
this.workshop.strategy!,
|
||||
this.originalData.selectedValues,
|
||||
this.data.selectedValues,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the assessment.
|
||||
*
|
||||
* @return Promise resolved when done, rejected if assessment could not be saved.
|
||||
*/
|
||||
async saveAssessment(): Promise<void> {
|
||||
const files = CoreFileSession.getFiles(
|
||||
AddonModWorkshopProvider.COMPONENT,
|
||||
this.workshop.id + '_' + this.assessmentId,
|
||||
) || [];
|
||||
|
||||
let saveOffline = false;
|
||||
let allowOffline = !files.length;
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
|
||||
this.data.fieldErrors = {};
|
||||
|
||||
try {
|
||||
let attachmentsId: CoreFileUploaderStoreFilesResult | number;
|
||||
try {
|
||||
// Upload attachments first if any.
|
||||
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreAssessmentFiles(
|
||||
this.workshop.id,
|
||||
this.assessmentId,
|
||||
files,
|
||||
saveOffline,
|
||||
);
|
||||
} catch {
|
||||
// Cannot upload them in online, save them in offline.
|
||||
saveOffline = true;
|
||||
allowOffline = true;
|
||||
|
||||
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreAssessmentFiles(
|
||||
this.workshop.id,
|
||||
this.assessmentId,
|
||||
files,
|
||||
saveOffline,
|
||||
);
|
||||
}
|
||||
|
||||
const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []);
|
||||
|
||||
let assessmentData: CoreFormFields<unknown>;
|
||||
try {
|
||||
assessmentData = await AddonModWorkshopHelper.prepareAssessmentData(
|
||||
this.workshop,
|
||||
this.data.selectedValues,
|
||||
text,
|
||||
this.data.assessment!.form!,
|
||||
attachmentsId,
|
||||
);
|
||||
} catch (errors) {
|
||||
this.data.fieldErrors = errors;
|
||||
|
||||
throw new CoreError(Translate.instant('core.errorinvalidform'));
|
||||
}
|
||||
|
||||
let gradeUpdated = false;
|
||||
if (saveOffline) {
|
||||
// Save assessment in offline.
|
||||
await AddonModWorkshopOffline.saveAssessment(
|
||||
this.workshop.id,
|
||||
this.assessmentId,
|
||||
this.workshop.course,
|
||||
assessmentData,
|
||||
);
|
||||
|
||||
gradeUpdated = false;
|
||||
} else {
|
||||
|
||||
// Try to send it to server.
|
||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||
gradeUpdated = await AddonModWorkshop.updateAssessment(
|
||||
this.workshop.id,
|
||||
this.assessmentId,
|
||||
this.workshop.course,
|
||||
assessmentData,
|
||||
undefined,
|
||||
allowOffline,
|
||||
);
|
||||
}
|
||||
|
||||
CoreForms.triggerFormSubmittedEvent(this.formElement, !!gradeUpdated, CoreSites.getCurrentSiteId());
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// If sent to the server, invalidate and clean.
|
||||
if (gradeUpdated) {
|
||||
promises.push(AddonModWorkshopHelper.deleteAssessmentStoredFiles(this.workshop.id, this.assessmentId));
|
||||
promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshop.id, this.assessmentId));
|
||||
promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshop.id, this.assessmentId));
|
||||
}
|
||||
|
||||
await CoreUtils.ignoreErrors(Promise.all(promises));
|
||||
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, {
|
||||
workshopId: this.workshop.id,
|
||||
assessmentId: this.assessmentId,
|
||||
userId: CoreSites.getCurrentSiteUserId(),
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
||||
if (files) {
|
||||
// Delete the local files from the tmp folder.
|
||||
CoreFileUploader.clearTmpFiles(files);
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error saving assessment.');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feedback text changed.
|
||||
*
|
||||
* @param text The new text.
|
||||
*/
|
||||
onFeedbackChange(text: string): void {
|
||||
this.feedbackText = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.obsInvalidated?.off();
|
||||
|
||||
if (this.data.assessment?.feedbackattachmentfiles) {
|
||||
// Delete the local files from the tmp folder.
|
||||
CoreFileUploader.clearTmpFiles(this.data.assessment.feedbackattachmentfiles);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonModWorkshopAssessmentStrategyData = {
|
||||
workshopId: number;
|
||||
assessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
|
||||
edit: boolean;
|
||||
selectedValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[];
|
||||
fieldErrors: AddonModWorkshopAssessmentStrategyFieldErrors;
|
||||
strategy: string;
|
||||
moduleId: number;
|
||||
courseId?: number;
|
||||
};
|
||||
|
||||
export type AddonModWorkshopAssessmentStrategyFieldErrors = Record<string, string>;
|
|
@ -0,0 +1,27 @@
|
|||
<core-loading [hideUntil]="loaded">
|
||||
<ion-item class="ion-text-wrap" [detail]="canViewAssessment && !canSelfAssess" (click)="gotoAssessment($event)">
|
||||
<core-user-avatar [user]="profile" slot="start" [courseId]="courseId" [userId]="profile?.id"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2 *ngIf="profile && profile.fullname">{{profile.fullname}}</h2>
|
||||
<p *ngIf="showGrade(assessment.grade)">
|
||||
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}}
|
||||
</p>
|
||||
<p *ngIf="access.canviewallsubmissions && !showGrade(assessment.gradinggradeover) && showGrade(assessment.gradinggrade)">
|
||||
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}}
|
||||
</p>
|
||||
<p *ngIf="access.canviewallsubmissions && showGrade(assessment.gradinggradeover)" class="core-overriden-grade">
|
||||
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggradeover}}
|
||||
</p>
|
||||
<p *ngIf="assessment.weight && assessment.weight != 1">
|
||||
{{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }}
|
||||
</p>
|
||||
<ion-badge *ngIf="!assessment.grade" color="danger">{{ 'addon.mod_workshop.notassessed' | translate }}</ion-badge>
|
||||
<ion-button expand="block" *ngIf="canSelfAssess && !showGrade(assessment.grade)" (click)="gotoOwnAssessment($event)">
|
||||
{{ 'addon.mod_workshop.assess' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
<ion-note slot="end" *ngIf="offline">
|
||||
<ion-icon name="fas-clock"></ion-icon>{{ 'core.notsent' | translate }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</core-loading>
|
|
@ -0,0 +1,170 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { AddonModWorkshopData, AddonModWorkshopGetWorkshopAccessInformationWSResponse } from '../../services/workshop';
|
||||
import {
|
||||
AddonModWorkshopHelper,
|
||||
AddonModWorkshopSubmissionAssessmentWithFormData,
|
||||
AddonModWorkshopSubmissionDataWithOfflineData,
|
||||
} from '../../services/workshop-helper';
|
||||
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
|
||||
|
||||
/**
|
||||
* Component that displays workshop assessment.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-assessment',
|
||||
templateUrl: 'addon-mod-workshop-assessment.html',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentComponent implements OnInit {
|
||||
|
||||
@Input() assessment!: AddonModWorkshopSubmissionAssessmentWithFormData;
|
||||
@Input() courseId!: number;
|
||||
@Input() workshop!: AddonModWorkshopData;
|
||||
@Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
|
||||
@Input() protected submission!: AddonModWorkshopSubmissionDataWithOfflineData;
|
||||
@Input() protected module!: CoreCourseModule;
|
||||
|
||||
canViewAssessment = false;
|
||||
canSelfAssess = false;
|
||||
profile?: CoreUserProfile;
|
||||
showGrade: (grade?: string | number) => boolean;
|
||||
offline = false;
|
||||
loaded = false;
|
||||
|
||||
protected currentUserId: number;
|
||||
protected assessmentId?: number;
|
||||
|
||||
constructor() {
|
||||
this.currentUserId = CoreSites.getCurrentSiteUserId();
|
||||
this.showGrade = AddonModWorkshopHelper.showGrade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const canAssess = this.access && this.access.assessingallowed;
|
||||
const userId = this.assessment.reviewerid;
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
this.assessmentId = this.assessment.id;
|
||||
this.canViewAssessment = !!this.assessment.grade;
|
||||
this.canSelfAssess = canAssess && userId == this.currentUserId;
|
||||
|
||||
if (userId) {
|
||||
promises.push(CoreUser.getProfile(userId, this.courseId, true).then((profile) => {
|
||||
this.profile = profile;
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
let assessOffline: Promise<void>;
|
||||
if (userId == this.currentUserId) {
|
||||
assessOffline = AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId) .then((offlineAssess) => {
|
||||
this.offline = true;
|
||||
this.assessment.weight = <number>offlineAssess.inputdata.weight;
|
||||
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
assessOffline = AddonModWorkshopOffline.getEvaluateAssessment(this.workshop.id, this.assessmentId)
|
||||
.then((offlineAssess) => {
|
||||
this.offline = true;
|
||||
this.assessment.gradinggradeover = offlineAssess.gradinggradeover;
|
||||
this.assessment.weight = <number>offlineAssess.weight;
|
||||
|
||||
return;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
promises.push(assessOffline.catch(() => {
|
||||
this.offline = false;
|
||||
// Ignore errors.
|
||||
}));
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the assessment.
|
||||
*/
|
||||
async gotoAssessment(event: Event): Promise<void> {
|
||||
if (!this.canSelfAssess && this.canViewAssessment) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const params: Params = {
|
||||
assessment: this.assessment,
|
||||
submission: this.submission,
|
||||
profile: this.profile,
|
||||
};
|
||||
|
||||
if (!this.submission) {
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
params.submission = await AddonModWorkshopHelper.getSubmissionById(
|
||||
this.workshop.id,
|
||||
this.assessment.submissionid,
|
||||
{ cmId: this.workshop.coursemodule },
|
||||
);
|
||||
|
||||
CoreNavigator.navigate(String(this.assessmentId), { params });
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Cannot load submission');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
} else {
|
||||
CoreNavigator.navigate(String(this.assessmentId), { params });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to my own assessment.
|
||||
*/
|
||||
gotoOwnAssessment(event: Event): void {
|
||||
if (!this.canSelfAssess) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const params: Params = {
|
||||
module: this.module,
|
||||
workshop: this.workshop,
|
||||
access: this.access,
|
||||
profile: this.profile,
|
||||
submission: this.submission,
|
||||
assessment: this.assessment,
|
||||
};
|
||||
|
||||
CoreNavigator.navigate(String(this.submission.id), params);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { AddonModWorkshopIndexComponent } from './index/index';
|
||||
import { AddonModWorkshopSubmissionComponent } from './submission/submission';
|
||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonModWorkshopPhaseInfoComponent } from './phase/phase';
|
||||
import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
|
||||
import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModWorkshopIndexComponent,
|
||||
AddonModWorkshopSubmissionComponent,
|
||||
AddonModWorkshopPhaseInfoComponent,
|
||||
AddonModWorkshopAssessmentComponent,
|
||||
AddonModWorkshopAssessmentStrategyComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreCourseComponentsModule,
|
||||
CoreEditorComponentsModule,
|
||||
],
|
||||
exports: [
|
||||
AddonModWorkshopIndexComponent,
|
||||
AddonModWorkshopSubmissionComponent,
|
||||
AddonModWorkshopPhaseInfoComponent,
|
||||
AddonModWorkshopAssessmentComponent,
|
||||
AddonModWorkshopAssessmentStrategyComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopComponentsModule {}
|
|
@ -0,0 +1,250 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
|
||||
[href]="externalUrl" iconAction="fas-external-link-alt">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
|
||||
(action)="expandDescription()" iconAction="fas-arrow-right">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
|
||||
iconAction="far-newspaper" (action)="gotoBlog()">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
|
||||
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600"
|
||||
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon"
|
||||
[closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
|
||||
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="200" [content]="'core.clearstoreddata' | translate:{$a: size}"
|
||||
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
|
||||
</core-context-menu-item> </core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
<ion-card class="with-borders" *ngIf="phases">
|
||||
<ion-item (click)="viewPhaseInfo()" detail="true">
|
||||
<ion-label>
|
||||
<h2 class="ion-text-wrap">{{ phases[workshop!.phase].title }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="phases && phases[workshop!.phase] && phases[workshop!.phase].tasks &&
|
||||
phases[workshop!.phase].tasks.length">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let task of phases[workshop!.phase].tasks"
|
||||
[class.item-dimmed]="task.code == 'submit' && !showSubmit" (click)="runTask(task)" detail="false">
|
||||
<ion-icon slot="start" name="far-circle" *ngIf="task.completed == null"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-times-circle" color="danger" *ngIf="task.completed == ''"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-info-circle" color="info" *ngIf="task.completed == 'info'"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-check-circle" color="success" *ngIf="task.completed == '1'"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{task.title}}</h2>
|
||||
<p *ngIf="task.details" [innerHTML]="task.details"></p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" *ngIf="task.link && task.code != 'submit'" name="fas-external-link-alt"></ion-icon>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
|
||||
<!-- Has something offline. -->
|
||||
<ion-card class="core-warning-card" *ngIf="hasOffline">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Description (setup phase only) -->
|
||||
<ion-card *ngIf="description && workshop && workshop!.phase == PHASE_SETUP">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.description' | translate }}</h2>
|
||||
<core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<div *ngIf="access && workshop && workshop!.phase >= PHASE_SUBMISSION">
|
||||
<!-- CLOSED PHASE -->
|
||||
<ng-container *ngIf="workshop!.phase >= PHASE_CLOSED">
|
||||
<ion-card *ngIf="workshop!.conclusion">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.conclusion' | translate }}</h2>
|
||||
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id"
|
||||
[text]="workshop!.conclusion" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card class="with-borders" *ngIf="userGrades">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label><h2>{{ 'addon.mod_workshop.yourgrades' | translate }}</h2></ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="userGrades.submissionlongstrgrade">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.submissiongrade' | translate }}</h2>
|
||||
<p>{{ userGrades.submissionlongstrgrade }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="userGrades.assessmentlongstrgrade">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.gradinggrade' | translate }}</h2>
|
||||
<p>{{ userGrades.assessmentlongstrgrade }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
|
||||
<!-- SUBMISSION PHASE -->
|
||||
<ion-card *ngIf="workshop!.phase == PHASE_SUBMISSION && workshop!.instructauthors">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.areainstructauthors' | translate }}</h2>
|
||||
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id"
|
||||
[text]="workshop!.instructauthors" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card *ngIf="canSubmit">
|
||||
<ion-item class="ion-text-wrap" *ngIf="!submission">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.yoursubmission' | translate }}</h2>
|
||||
<p>{{ 'addon.mod_workshop.noyoursubmission' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="submission">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2 *ngIf="workshop!.phase != PHASE_CLOSED">{{ 'addon.mod_workshop.yoursubmission' | translate }}</h2>
|
||||
<h2 *ngIf="workshop!.phase == PHASE_CLOSED">
|
||||
{{ 'addon.mod_workshop.yoursubmissionwithassessments' | translate }}
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<addon-mod-workshop-submission [submission]="submission" [courseId]="workshop!.course" [module]="module"
|
||||
[workshop]="workshop" [access]="access">
|
||||
</addon-mod-workshop-submission>
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
|
||||
<!-- Show only on current phase -->
|
||||
<ng-container *ngIf="workshop!.phase == PHASE_SUBMISSION">
|
||||
<ion-item class="ion-text-wrap" *ngIf="showSubmit">
|
||||
<ion-label>
|
||||
<ion-button expand="block" *ngIf="access.creatingsubmissionallowed && !submission" (click)="gotoSubmit()">
|
||||
<ion-icon slot="start" name="fas-plus"></ion-icon>
|
||||
{{ 'addon.mod_workshop.createsubmission' | translate }}
|
||||
</ion-button>
|
||||
<ion-button expand="block" *ngIf="access.modifyingsubmissionallowed && submission" (click)="gotoSubmit()">
|
||||
<ion-icon slot="start" name="fas-edit"></ion-icon>
|
||||
{{ 'addon.mod_workshop.editsubmission' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="workshop!.phase >= PHASE_CLOSED">
|
||||
<ion-card class="with-borders" *ngIf="publishedSubmissions && publishedSubmissions.length">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label><h2>{{ 'addon.mod_workshop.publishedsubmissions' | translate }}</h2></ion-label>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngFor="let submission of publishedSubmissions">
|
||||
<addon-mod-workshop-submission [submission]="submission" [courseId]="workshop!.course" [module]="module"
|
||||
[workshop]="workshop" [access]="access" summary="true" class="core-as-item">
|
||||
</addon-mod-workshop-submission>
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
|
||||
<!-- ASSESSMENT PHASE -->
|
||||
<ng-container *ngIf="workshop!.phase >= PHASE_ASSESSMENT">
|
||||
<ion-card *ngIf="workshop!.phase == PHASE_ASSESSMENT && workshop!.instructreviewers">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.areainstructreviewers' | translate }}</h2>
|
||||
<core-format-text fullOnClick="true" [component]="component" [componentId]="module.id"
|
||||
[text]="workshop!.instructreviewers" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card class="with-borders" *ngIf="canAssess">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label><h2>{{ 'addon.mod_workshop.assignedassessments' | translate }}</h2></ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!assessments || !assessments.length">
|
||||
<ion-label><p>{{ 'addon.mod_workshop.assignedassessmentsnone' | translate }}</p></ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let assessment of (assessments || [])">
|
||||
<addon-mod-workshop-submission [submission]="assessment.submission" [assessment]="assessment"
|
||||
[courseId]="workshop!.course" [module]="module" [workshop]="workshop" [access]="access" summary="true"
|
||||
class="core-as-item">
|
||||
</addon-mod-workshop-submission>
|
||||
</ng-container>
|
||||
</ion-card >
|
||||
</ng-container>
|
||||
|
||||
<!-- MULTIPLE PHASES SUBMISSION OR GREATER only teachers -->
|
||||
<ion-card class="with-borders" *ngIf="access.canviewallsubmissions && workshop!.phase >= PHASE_SUBMISSION &&
|
||||
((grades && grades.length) || (groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)))">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2 *ngIf="workshop!.phase == PHASE_SUBMISSION">{{ 'addon.mod_workshop.submissionsreport' | translate }}</h2>
|
||||
<h2 *ngIf="workshop!.phase > PHASE_SUBMISSION">{{ 'addon.mod_workshop.gradesreport' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||
<ion-label id="addon-workshop-groupslabel" *ngIf="groupInfo.separateGroups">
|
||||
{{ 'core.groupsseparate' | translate }}
|
||||
</ion-label>
|
||||
<ion-label id="addon-workshop-groupslabel" *ngIf="groupInfo.visibleGroups">
|
||||
{{ 'core.groupsvisible' | translate }}
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-workshop-groupslabel"
|
||||
interface="action-sheet">
|
||||
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
||||
{{groupOpt.name}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngFor="let submission of grades">
|
||||
<addon-mod-workshop-submission [submission]="submission" [courseId]="workshop!.course" [module]="module"
|
||||
[workshop]="workshop" [access]="access" summary="true" class="core-as-item">
|
||||
</addon-mod-workshop-submission>
|
||||
</ng-container>
|
||||
|
||||
<ion-grid *ngIf="page > 0 || hasNextPage">
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col *ngIf="page > 0">
|
||||
<ion-button expand="block" fill="outline" (click)="gotoSubmissionsPage(page! -1)">
|
||||
<ion-icon name="fas-chevron-left" slot="start"></ion-icon>
|
||||
{{ 'core.previous' | translate }}
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="hasNextPage">
|
||||
<ion-button expand="block" (click)="gotoSubmissionsPage(page! + 1)">
|
||||
{{ 'core.next' | translate }}
|
||||
<ion-icon name="fas-chevron-right" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-card>
|
||||
</div>
|
||||
</core-loading>
|
|
@ -0,0 +1,555 @@
|
|||
// (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, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController, Platform } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AddonModWorkshopModuleHandlerService } from '../../services/handlers/module';
|
||||
import {
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshopPhase,
|
||||
AddonModWorkshop,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
AddonModWorkshopPhaseData,
|
||||
AddonModWorkshopGetGradesWSResponse,
|
||||
AddonModWorkshopAssessmentSavedChangedEventData,
|
||||
AddonModWorkshopSubmissionChangedEventData,
|
||||
AddonModWorkshopGradesData,
|
||||
AddonModWorkshopPhaseTaskData,
|
||||
AddonModWorkshopReviewer,
|
||||
} from '../../services/workshop';
|
||||
import {
|
||||
AddonModWorkshopHelper,
|
||||
AddonModWorkshopSubmissionAssessmentWithFormData,
|
||||
AddonModWorkshopSubmissionDataWithOfflineData,
|
||||
} from '../../services/workshop-helper';
|
||||
import { AddonModWorkshopOffline, AddonModWorkshopOfflineSubmission } from '../../services/workshop-offline';
|
||||
import {
|
||||
AddonModWorkshopSyncProvider,
|
||||
AddonModWorkshopSync,
|
||||
AddonModWorkshopAutoSyncData,
|
||||
AddonModWorkshopSyncResult,
|
||||
} from '../../services/workshop-sync';
|
||||
import { AddonModWorkshopPhaseInfoComponent } from '../phase/phase';
|
||||
|
||||
/**
|
||||
* Component that displays a workshop index page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-index',
|
||||
templateUrl: 'addon-mod-workshop-index.html',
|
||||
})
|
||||
export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() group = 0;
|
||||
|
||||
component = AddonModWorkshopProvider.COMPONENT;
|
||||
moduleName = 'workshop';
|
||||
|
||||
workshop?: AddonModWorkshopData;
|
||||
page = 0;
|
||||
access?: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
|
||||
phases?: Record<string, AddonModWorkshopPhaseData>;
|
||||
grades: AddonModWorkshopSubmissionDataWithOfflineData[] = [];
|
||||
assessments: AddonModWorkshopSubmissionAssessmentWithFormData[] = [];
|
||||
userGrades?: AddonModWorkshopGetGradesWSResponse;
|
||||
publishedSubmissions: AddonModWorkshopSubmissionDataWithOfflineData[] = [];
|
||||
submission?: AddonModWorkshopSubmissionDataWithOfflineData;
|
||||
groupInfo: CoreGroupInfo = {
|
||||
groups: [],
|
||||
separateGroups: false,
|
||||
visibleGroups: false,
|
||||
defaultGroupId: 0,
|
||||
};
|
||||
|
||||
canSubmit = false;
|
||||
showSubmit = false;
|
||||
canAssess = false;
|
||||
hasNextPage = false;
|
||||
|
||||
readonly PHASE_SETUP = AddonModWorkshopPhase.PHASE_SETUP;
|
||||
readonly PHASE_SUBMISSION = AddonModWorkshopPhase.PHASE_SUBMISSION;
|
||||
readonly PHASE_ASSESSMENT = AddonModWorkshopPhase.PHASE_ASSESSMENT;
|
||||
readonly PHASE_EVALUATION = AddonModWorkshopPhase.PHASE_EVALUATION;
|
||||
readonly PHASE_CLOSED = AddonModWorkshopPhase.PHASE_CLOSED;
|
||||
|
||||
protected offlineSubmissions: AddonModWorkshopOfflineSubmission[] = [];
|
||||
protected obsSubmissionChanged: CoreEventObserver;
|
||||
protected obsAssessmentSaved: CoreEventObserver;
|
||||
protected appResumeSubscription: Subscription;
|
||||
protected syncObserver?: CoreEventObserver;
|
||||
protected syncEventName = AddonModWorkshopSyncProvider.AUTO_SYNCED;
|
||||
|
||||
constructor (
|
||||
@Optional() content: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModWorkshopIndexComponent', content, courseContentsPage);
|
||||
|
||||
// Listen to submission and assessment changes.
|
||||
this.obsSubmissionChanged = CoreEvents.on(AddonModWorkshopProvider.SUBMISSION_CHANGED, (data) => {
|
||||
this.eventReceived(data);
|
||||
}, this.siteId);
|
||||
|
||||
// Listen to submission and assessment changes.
|
||||
this.obsAssessmentSaved = CoreEvents.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => {
|
||||
this.eventReceived(data);
|
||||
}, this.siteId);
|
||||
|
||||
// Since most actions will take the user out of the app, we should refresh the view when the app is resumed.
|
||||
this.appResumeSubscription = Platform.resume.subscribe(() => {
|
||||
this.showLoadingAndRefresh(true);
|
||||
});
|
||||
|
||||
// Refresh workshop on sync.
|
||||
this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
|
||||
// Update just when all database is synced.
|
||||
this.eventReceived(data);
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
super.ngOnInit();
|
||||
|
||||
await this.loadContent(false, true);
|
||||
if (!this.workshop) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AddonModWorkshop.logView(this.workshop.id, this.workshop.name);
|
||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when we receive an event of submission changes.
|
||||
*
|
||||
* @param data Data received by the event.
|
||||
*/
|
||||
protected eventReceived(
|
||||
data: AddonModWorkshopAutoSyncData |
|
||||
AddonModWorkshopSubmissionChangedEventData |
|
||||
AddonModWorkshopAssessmentSavedChangedEventData,
|
||||
): void {
|
||||
if (this.workshop?.id === data.workshopId) {
|
||||
this.showLoadingAndRefresh(true);
|
||||
|
||||
// Check completion since it could be configured to complete once the user adds a new discussion or replies.
|
||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModWorkshop.invalidateWorkshopData(this.courseId));
|
||||
if (this.workshop) {
|
||||
promises.push(AddonModWorkshop.invalidateWorkshopAccessInformationData(this.workshop.id));
|
||||
promises.push(AddonModWorkshop.invalidateUserPlanPhasesData(this.workshop.id));
|
||||
if (this.canSubmit) {
|
||||
promises.push(AddonModWorkshop.invalidateSubmissionsData(this.workshop.id));
|
||||
}
|
||||
if (this.access?.canviewallsubmissions) {
|
||||
promises.push(AddonModWorkshop.invalidateGradeReportData(this.workshop.id));
|
||||
promises.push(CoreGroups.invalidateActivityAllowedGroups(this.workshop.coursemodule));
|
||||
promises.push(CoreGroups.invalidateActivityGroupMode(this.workshop.coursemodule));
|
||||
}
|
||||
if (this.canAssess) {
|
||||
promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshop.id));
|
||||
}
|
||||
promises.push(AddonModWorkshop.invalidateGradesData(this.workshop.id));
|
||||
promises.push(AddonModWorkshop.invalidateWorkshopWSData(this.workshop.id));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares sync event data with current data to check if refresh content is needed.
|
||||
*
|
||||
* @param syncEventData Data receiven on sync observer.
|
||||
* @return True if refresh is needed, false otherwise.
|
||||
*/
|
||||
protected isRefreshSyncNeeded(syncEventData: AddonModWorkshopAutoSyncData): boolean {
|
||||
if (this.workshop && syncEventData.workshopId == this.workshop.id) {
|
||||
// Refresh the data.
|
||||
this.content?.scrollToTop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download feedback contents.
|
||||
*
|
||||
* @param refresh If it's refreshing content.
|
||||
* @param sync If it should try to sync.
|
||||
* @param showErrors If show errors to the user of hide them.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> {
|
||||
try {
|
||||
this.workshop = await AddonModWorkshop.getWorkshop(this.courseId, this.module.id);
|
||||
|
||||
this.description = this.workshop.intro;
|
||||
this.dataRetrieved.emit(this.workshop);
|
||||
|
||||
if (sync) {
|
||||
// Try to synchronize the feedback.
|
||||
await this.syncActivity(showErrors);
|
||||
}
|
||||
|
||||
// Check if there are answers stored in offline.
|
||||
this.access = await AddonModWorkshop.getWorkshopAccessInformation(this.workshop.id, { cmId: this.module.id });
|
||||
|
||||
if (this.access.canviewallsubmissions) {
|
||||
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.workshop.coursemodule);
|
||||
this.group = CoreGroups.validateGroupId(this.group, this.groupInfo);
|
||||
}
|
||||
|
||||
this.phases = await AddonModWorkshop.getUserPlanPhases(this.workshop.id, { cmId: this.module.id });
|
||||
|
||||
this.phases[this.workshop.phase].tasks.forEach((task) => {
|
||||
if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) {
|
||||
// Add links to manage examples.
|
||||
task.link = this.externalUrl!;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if there are info stored in offline.
|
||||
this.hasOffline = await AddonModWorkshopOffline.hasWorkshopOfflineData(this.workshop.id);
|
||||
if (this.hasOffline) {
|
||||
this.offlineSubmissions = await AddonModWorkshopOffline.getSubmissions(this.workshop.id);
|
||||
} else {
|
||||
this.offlineSubmissions = [];
|
||||
}
|
||||
|
||||
await this.setPhaseInfo();
|
||||
|
||||
} finally {
|
||||
this.fillContextMenu(refresh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and shows submissions grade page.
|
||||
*
|
||||
* @param page Page number to be retrieved.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
async gotoSubmissionsPage(page: number): Promise<void> {
|
||||
const report = await AddonModWorkshop.getGradesReport(this.workshop!.id, {
|
||||
groupId: this.group,
|
||||
page,
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
||||
const numEntries = (report && report.grades && report.grades.length) || 0;
|
||||
|
||||
this.page = page;
|
||||
|
||||
this.hasNextPage = numEntries >= AddonModWorkshopProvider.PER_PAGE && ((this.page + 1) *
|
||||
AddonModWorkshopProvider.PER_PAGE) < report.totalcount;
|
||||
|
||||
const grades: AddonModWorkshopGradesData[] = report.grades || [];
|
||||
|
||||
this.grades = [];
|
||||
|
||||
await Promise.all(grades.map(async (grade) => {
|
||||
const submission: AddonModWorkshopSubmissionDataWithOfflineData = {
|
||||
id: grade.submissionid,
|
||||
workshopid: this.workshop!.id,
|
||||
example: false,
|
||||
authorid: grade.userid,
|
||||
timecreated: grade.submissionmodified,
|
||||
timemodified: grade.submissionmodified,
|
||||
title: grade.submissiontitle,
|
||||
content: '',
|
||||
contenttrust: 0,
|
||||
attachment: 0,
|
||||
grade: grade.submissiongrade,
|
||||
gradeover: grade.submissiongradeover,
|
||||
gradeoverby: grade.submissiongradeoverby,
|
||||
published: !!grade.submissionpublished,
|
||||
gradinggrade: grade.gradinggrade,
|
||||
late: 0,
|
||||
reviewedby: this.parseReviewer(grade.reviewedby),
|
||||
reviewerof: this.parseReviewer(grade.reviewerof),
|
||||
};
|
||||
|
||||
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
|
||||
submission.reviewedbydone = grade.reviewedby?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
|
||||
submission.reviewerofdone = grade.reviewerof?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
|
||||
submission.reviewedbycount = grade.reviewedby?.length || 0;
|
||||
submission.reviewerofcount = grade.reviewerof?.length || 0;
|
||||
}
|
||||
|
||||
const offlineData = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
|
||||
|
||||
if (typeof offlineData != 'undefined') {
|
||||
this.grades!.push(offlineData);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected parseReviewer(reviewers: AddonModWorkshopReviewer[] = []): AddonModWorkshopSubmissionAssessmentWithFormData[] {
|
||||
return reviewers.map((reviewer: AddonModWorkshopReviewer) => {
|
||||
const parsed: AddonModWorkshopSubmissionAssessmentWithFormData = {
|
||||
grade: reviewer.grade,
|
||||
gradinggrade: reviewer.gradinggrade,
|
||||
gradinggradeover: reviewer.gradinggradeover,
|
||||
id: reviewer.assessmentid,
|
||||
reviewerid: reviewer.userid,
|
||||
submissionid: reviewer.submissionid,
|
||||
weight: reviewer.weight,
|
||||
timecreated: 0,
|
||||
timemodified: 0,
|
||||
feedbackauthor: '',
|
||||
gradinggradeoverby: 0,
|
||||
feedbackattachmentfiles: [],
|
||||
feedbackcontentfiles: [],
|
||||
feedbackauthorattachment: 0,
|
||||
};
|
||||
|
||||
return parsed;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open task.
|
||||
*
|
||||
* @param task Task to be done.
|
||||
*/
|
||||
runTask(task: AddonModWorkshopPhaseTaskData): void {
|
||||
if (task.code == 'submit') {
|
||||
this.gotoSubmit();
|
||||
} else if (task.link) {
|
||||
CoreUtils.openInBrowser(task.link);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to submit page.
|
||||
*/
|
||||
gotoSubmit(): void {
|
||||
if (this.canSubmit && ((this.access!.creatingsubmissionallowed && !this.submission) ||
|
||||
(this.access!.modifyingsubmissionallowed && this.submission))) {
|
||||
const params: Params = {
|
||||
module: this.module,
|
||||
access: this.access,
|
||||
};
|
||||
|
||||
const submissionId = this.submission?.id || 0;
|
||||
CoreNavigator.navigateToSitePath(
|
||||
AddonModWorkshopModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/${submissionId}/edit`,
|
||||
{ params },
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View Phase info.
|
||||
*/
|
||||
async viewPhaseInfo(): Promise<void> {
|
||||
if (this.phases) {
|
||||
const modal = await ModalController.create({
|
||||
component: AddonModWorkshopPhaseInfoComponent,
|
||||
componentProps: {
|
||||
phases: CoreUtils.objectToArray(this.phases),
|
||||
workshopPhase: this.workshop!.phase,
|
||||
externalUrl: this.externalUrl,
|
||||
showSubmit: this.showSubmit,
|
||||
},
|
||||
});
|
||||
await modal.present();
|
||||
|
||||
const result = await modal.onDidDismiss();
|
||||
if (result.data === true) {
|
||||
this.gotoSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group to see the workshop.
|
||||
*
|
||||
* @param groupId Group Id.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async setGroup(groupId: number): Promise<void> {
|
||||
this.group = groupId;
|
||||
|
||||
await this.gotoSubmissionsPage(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to set current phase information.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async setPhaseInfo(): Promise<void> {
|
||||
this.submission = undefined;
|
||||
this.canAssess = false;
|
||||
this.assessments = [];
|
||||
this.userGrades = undefined;
|
||||
this.publishedSubmissions = [];
|
||||
|
||||
this.canSubmit = AddonModWorkshopHelper.canSubmit(
|
||||
this.workshop!,
|
||||
this.access!,
|
||||
this.phases![AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
|
||||
);
|
||||
|
||||
this.showSubmit = this.workshop!.phase == AddonModWorkshopPhase.PHASE_SUBMISSION && this.canSubmit &&
|
||||
((this.access!.creatingsubmissionallowed && !this.submission) ||
|
||||
(this.access!.modifyingsubmissionallowed && !!this.submission));
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (this.canSubmit) {
|
||||
promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop!.id, { cmId: this.module.id })
|
||||
.then(async (submission) => {
|
||||
this.submission = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.access!.canviewallsubmissions && this.workshop!.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
|
||||
promises.push(this.gotoSubmissionsPage(this.page));
|
||||
}
|
||||
|
||||
let assessPromise = Promise.resolve();
|
||||
|
||||
if (this.workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
|
||||
this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop!, this.access!);
|
||||
|
||||
if (this.canAssess) {
|
||||
assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop!.id, {
|
||||
cmId: this.module.id,
|
||||
}).then(async (assessments) => {
|
||||
await Promise.all(assessments.map(async (assessment) => {
|
||||
assessment.strategy = this.workshop!.strategy;
|
||||
if (!this.hasOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop!.id, assessment.id);
|
||||
|
||||
assessment.offline = true;
|
||||
assessment.timemodified = Math.floor(offlineAssessment.timemodified / 1000);
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}));
|
||||
|
||||
this.assessments = assessments;
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
promises.push(AddonModWorkshop.getGrades(this.workshop!.id, { cmId: this.module.id }).then((grades) => {
|
||||
this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : undefined;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
if (this.access!.canviewpublishedsubmissions) {
|
||||
promises.push(assessPromise.then(async () => {
|
||||
const submissions: AddonModWorkshopSubmissionDataWithOfflineData[] =
|
||||
await AddonModWorkshop.getSubmissions(this.workshop!.id, { cmId: this.module.id });
|
||||
|
||||
this.publishedSubmissions = submissions.filter((submission) => {
|
||||
if (submission.published) {
|
||||
submission.reviewedby = [];
|
||||
|
||||
this.assessments.forEach((assessment) => {
|
||||
if (assessment.submissionid == submission.id) {
|
||||
submission.reviewedby!.push(AddonModWorkshopHelper.realGradeValue(this.workshop!, assessment));
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected sync(): Promise<AddonModWorkshopSyncResult> {
|
||||
return AddonModWorkshopSync.syncWorkshop(this.workshop!.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param result Data returned on the sync function.
|
||||
* @return If suceed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModWorkshopSyncResult): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
this.obsSubmissionChanged?.off();
|
||||
this.obsAssessmentSaved?.off();
|
||||
this.appResumeSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_workshop.userplan' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="fas-times" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ng-container *ngFor="let phase of phases">
|
||||
<ion-item-divider [class.core-selected-item]="workshopPhase == phase.code">
|
||||
<ion-label>
|
||||
<h2>{{ phase.title }}</h2>
|
||||
<p class="ion-text-wrap" *ngIf="workshopPhase == phase.code">
|
||||
{{ 'addon.mod_workshop.userplancurrentphase' | translate }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="phase.switchUrl" [href]="phase.switchUrl" detail="false">
|
||||
<ion-icon slot="start" name="fas-exchange-alt"></ion-icon>
|
||||
<ion-label>
|
||||
<p>{{ 'addon.mod_workshop.switchphase' + phase.code | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="fas-external-link-alt"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let task of phase.tasks"
|
||||
[class.item-dimmed]="phase.code != workshopPhase || (task.code == 'submit' && !showSubmit)"
|
||||
(click)="runTask(task)" detail="false">
|
||||
<ion-icon slot="start" name="far-circle" *ngIf="task.completed == null"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-times-circle" color="danger" *ngIf="task.completed == ''"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-info-circle" color="info" *ngIf="task.completed == 'info'"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-check-circle" color="success" *ngIf="task.completed == '1'"></ion-icon>
|
||||
|
||||
<ion-label>
|
||||
<h2 class="ion-text-wrap">{{task.title}}</h2>
|
||||
<p *ngIf="task.details" [innerHTML]="task.details"></p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" *ngIf="task.link && task.code != 'submit'" name="fas-external-link-alt"></ion-icon>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -0,0 +1,73 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController } from '@singletons';
|
||||
import { AddonModWorkshopPhaseData, AddonModWorkshopPhase, AddonModWorkshopPhaseTaskData } from '../../services/workshop';
|
||||
|
||||
/**
|
||||
* Page that displays the phase info modal.
|
||||
*/
|
||||
@Component({
|
||||
templateUrl: 'phase.html',
|
||||
})
|
||||
export class AddonModWorkshopPhaseInfoComponent implements OnInit {
|
||||
|
||||
@Input() phases!: AddonModWorkshopPhaseDataWithSwitch[];
|
||||
@Input() workshopPhase!: AddonModWorkshopPhase;
|
||||
@Input() showSubmit = false;
|
||||
@Input() protected externalUrl!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
// Treat phases.
|
||||
for (const x in this.phases) {
|
||||
this.phases[x].tasks.forEach((task) => {
|
||||
if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) {
|
||||
// Add links to manage examples.
|
||||
task.link = this.externalUrl;
|
||||
}
|
||||
});
|
||||
const action = this.phases[x].actions.find((action) => action.url && action.type == 'switchphase');
|
||||
this.phases[x].switchUrl = action ? action.url : '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open task.
|
||||
*
|
||||
* @param task Task to be done.
|
||||
*/
|
||||
runTask(task: AddonModWorkshopPhaseTaskData): void {
|
||||
if (task.code == 'submit') {
|
||||
// This will close the modal and go to the submit.
|
||||
ModalController.dismiss(true);
|
||||
} else if (task.link) {
|
||||
CoreUtils.openInBrowser(task.link);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonModWorkshopPhaseDataWithSwitch = AddonModWorkshopPhaseData & {
|
||||
switchUrl?: string;
|
||||
};
|
|
@ -0,0 +1,108 @@
|
|||
<core-loading [hideUntil]="loaded">
|
||||
<div *ngIf="!summary">
|
||||
<ion-item class="ion-text-wrap addon-workshop-submission-title">
|
||||
<core-user-avatar [user]="profile" [courseId]="courseId" [userId]="profile?.id" slot="start">
|
||||
</core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="submission.title" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="profile && profile?.fullname">{{profile.fullname}}</p>
|
||||
<p *ngIf="showGrade(submission.grade)"
|
||||
[class.addon-has-overriden-grade]="showGrade(submission.gradeover)">
|
||||
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.grade}}
|
||||
</p>
|
||||
<p *ngIf="showGrade(submission.gradeover)" class="addon-overriden-grade">
|
||||
{{ 'addon.mod_workshop.gradeover' | translate }}: {{submission.gradeover}}
|
||||
</p>
|
||||
<p *ngIf="access.canviewallsubmissions && showGrade(submission.gradinggrade)">
|
||||
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{submission.gradinggrade}}
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-note slot="end" *ngIf="!submission.timemodified">
|
||||
<ion-icon name="fas-clock"></ion-icon> {{ 'core.notsent' | translate }}
|
||||
</ion-note>
|
||||
<ion-note slot="end" *ngIf="submission.timemodified">
|
||||
{{submission.timemodified | coreDateDayOrTime}}
|
||||
<ng-container *ngIf="submission.offline">
|
||||
<ion-icon name="fas-clock"></ion-icon> {{ 'core.notsent' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="submission.deleted">
|
||||
<ion-icon name="fas-trash"></ion-icon> {{ 'core.deletedoffline' | translate }}
|
||||
</ng-container>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="submission.content">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="submission.content"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-files [files]="submission.attachmentfiles" [component]="component" [componentId]="componentId"></core-files>
|
||||
<ion-item class="ion-text-wrap" *ngIf="viewDetails && submission.feedbackauthor">
|
||||
<core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start" [courseId]="courseId"
|
||||
[userId]="evaluateByProfile.id"></core-user-avatar>
|
||||
|
||||
<ion-label>
|
||||
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">
|
||||
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}
|
||||
</h2>
|
||||
<core-format-text [text]="submission.feedbackauthor" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="viewDetails">
|
||||
<ion-label>
|
||||
<ion-button expand="block" (click)="gotoSubmission()">
|
||||
{{ 'core.showmore' | translate }}
|
||||
<ion-icon name="fas-chevron-right" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="summary" [detail]="submission.timemodified" (click)="gotoSubmission()">
|
||||
<core-user-avatar [user]="profile" slot="start" [courseId]="courseId" [userId]="profile?.id">
|
||||
</core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="submission.title" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="profile && profile.fullname">{{profile.fullname}}</p>
|
||||
<p *ngIf="submission.reviewedbydone">
|
||||
{{ 'addon.mod_workshop.receivedgrades' | translate }}: {{submission.reviewedbydone}} / {{submission.reviewedbycount}}
|
||||
</p>
|
||||
<p *ngIf="submission.reviewerofdone">
|
||||
{{ 'addon.mod_workshop.givengrades' | translate }}: {{submission.reviewerofdone}} / {{submission.reviewerofcount}}
|
||||
</p>
|
||||
<p *ngIf="!showGrade(submission.gradeover) && showGrade(submission.grade)">
|
||||
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.grade}}
|
||||
</p>
|
||||
<p *ngIf="showGrade(submission.gradeover)" class="addon-overriden-grade">
|
||||
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.gradeover}}
|
||||
</p>
|
||||
<p *ngIf="access.canviewallsubmissions && showGrade(submission.gradinggrade)">
|
||||
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{submission.gradinggrade}}
|
||||
</p>
|
||||
|
||||
<ion-badge *ngIf="assessment && (showGrade(assessment.grade) || assessment.offline)" color="success">
|
||||
{{ 'addon.mod_workshop.assessedsubmission' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge *ngIf="assessment && !showGrade(assessment.grade) && !assessment.offline" color="danger">
|
||||
{{ 'addon.mod_workshop.notassessed' | translate }}
|
||||
</ion-badge>
|
||||
|
||||
</ion-label>
|
||||
<ion-note slot="end" *ngIf="submission.timemodified">
|
||||
{{submission.timemodified | coreDateDayOrTime}}
|
||||
<div *ngIf="offline"><ion-icon name="fas-clock"></ion-icon> {{ 'core.notsent' | translate }}</div>
|
||||
<div *ngIf="submission.deleted"><ion-icon name="fas-trash"></ion-icon> {{ 'core.deletedoffline' | translate }}</div>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</core-loading>
|
|
@ -0,0 +1,10 @@
|
|||
:host {
|
||||
p.addon-overriden-grade {
|
||||
color: var(--ion-color-success);
|
||||
}
|
||||
|
||||
p.addon-has-overriden-grade {
|
||||
color: var(--ion-color-danger);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { AddonModWorkshopSubmissionPage } from '../../pages/submission/submission';
|
||||
import { AddonModWorkshopModuleHandlerService } from '../../services/handlers/module';
|
||||
import {
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshopPhase,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
} from '../../services/workshop';
|
||||
import {
|
||||
AddonModWorkshopHelper,
|
||||
AddonModWorkshopSubmissionAssessmentWithFormData,
|
||||
AddonModWorkshopSubmissionDataWithOfflineData,
|
||||
} from '../../services/workshop-helper';
|
||||
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
|
||||
|
||||
/**
|
||||
* Component that displays workshop submission.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-workshop-submission',
|
||||
templateUrl: 'addon-mod-workshop-submission.html',
|
||||
styleUrls: ['submission.scss'],
|
||||
})
|
||||
export class AddonModWorkshopSubmissionComponent implements OnInit {
|
||||
|
||||
@Input() submission!: AddonModWorkshopSubmissionDataWithOfflineData;
|
||||
@Input() module!: CoreCourseModule;
|
||||
@Input() workshop!: AddonModWorkshopData;
|
||||
@Input() access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
|
||||
@Input() courseId!: number;
|
||||
@Input() assessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
|
||||
@Input() summary = false;
|
||||
|
||||
component = AddonModWorkshopProvider.COMPONENT;
|
||||
componentId?: number;
|
||||
userId: number;
|
||||
loaded = false;
|
||||
offline = false;
|
||||
viewDetails = false;
|
||||
profile?: CoreUserProfile;
|
||||
showGrade: (grade?: number|string) => boolean;
|
||||
evaluateByProfile?: CoreUserProfile;
|
||||
|
||||
constructor() {
|
||||
this.userId = CoreSites.getCurrentSiteUserId();
|
||||
this.showGrade = AddonModWorkshopHelper.showGrade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.componentId = this.module.instance;
|
||||
this.userId = this.submission.authorid || this.userId;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
this.offline = !!this.submission?.offline || !!this.assessment?.offline;
|
||||
|
||||
if (this.submission.id) {
|
||||
promises.push(AddonModWorkshopOffline.getEvaluateSubmission(this.workshop.id, this.submission.id)
|
||||
.then((offlineSubmission) => {
|
||||
this.submission.gradeover = parseInt(offlineSubmission.gradeover, 10);
|
||||
this.offline = true;
|
||||
|
||||
return;
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.userId) {
|
||||
promises.push(CoreUser.getProfile(this.userId, this.courseId, true).then((profile) => {
|
||||
this.profile = profile;
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
this.viewDetails = !this.summary && this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED &&
|
||||
CoreNavigator.getCurrentRoute().component != AddonModWorkshopSubmissionPage;
|
||||
|
||||
if (this.viewDetails && this.submission.gradeoverby) {
|
||||
promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
|
||||
this.evaluateByProfile = profile;
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the submission.
|
||||
*/
|
||||
gotoSubmission(): void {
|
||||
if (this.submission.timemodified) {
|
||||
const params: Params = {
|
||||
module: this.module,
|
||||
workshop: this.workshop,
|
||||
access: this.access,
|
||||
profile: this.profile,
|
||||
submission: this.submission,
|
||||
assessment: this.assessment,
|
||||
};
|
||||
|
||||
CoreNavigator.navigateToSitePath(
|
||||
AddonModWorkshopModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/${this.submission.id}`,
|
||||
{ params },
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"alreadygraded": "Already graded",
|
||||
"areainstructauthors": "Instructions for submission",
|
||||
"areainstructreviewers": "Instructions for assessment",
|
||||
"assess": "Assess",
|
||||
"assessedsubmission": "Assessed submission",
|
||||
"assessmentform": "Assessment form",
|
||||
"assessmentsettings": "Assessment settings",
|
||||
"assessmentstrategynotsupported": "Assessment strategy {{$a}} not supported",
|
||||
"assessmentweight": "Assessment weight",
|
||||
"assignedassessments": "Assigned submissions to assess",
|
||||
"assignedassessmentsnone": "You have no assigned submission to assess",
|
||||
"conclusion": "Conclusion",
|
||||
"createsubmission": "Add submission",
|
||||
"deletesubmission": "Delete submission",
|
||||
"editsubmission": "Edit submission",
|
||||
"feedbackauthor": "Feedback for the author",
|
||||
"feedbackby": "Feedback by {{$a}}",
|
||||
"feedbackreviewer": "Feedback for the reviewer",
|
||||
"givengrades": "Grades given",
|
||||
"gradecalculated": "Calculated grade for submission",
|
||||
"gradeinfo": "Grade: {{$a.received}} of {{$a.max}}",
|
||||
"gradeover": "Override grade for submission",
|
||||
"gradesreport": "Workshop grades report",
|
||||
"gradinggrade": "Grade for assessment",
|
||||
"gradinggradecalculated": "Calculated grade for assessment",
|
||||
"gradinggradeof": "Grade for assessment (of {{$a}})",
|
||||
"gradinggradeover": "Override grade for assessment",
|
||||
"modulenameplural": "Workshops",
|
||||
"nogradeyet": "No grade yet",
|
||||
"notassessed": "Not assessed yet",
|
||||
"notoverridden": "Not overridden",
|
||||
"noyoursubmission": "You have not submitted your work yet",
|
||||
"overallfeedback": "Overall feedback",
|
||||
"publishedsubmissions": "Published submissions",
|
||||
"publishsubmission": "Publish submission",
|
||||
"publishsubmission_help": "Published submissions are available to the others when the workshop is closed.",
|
||||
"reassess": "Re-assess",
|
||||
"receivedgrades": "Grades received",
|
||||
"submissionattachment": "Attachment",
|
||||
"submissioncontent": "Submission content",
|
||||
"submissiondeleteconfirm": "Are you sure you want to delete the following submission?",
|
||||
"submissiongrade": "Grade for submission",
|
||||
"submissiongradeof": "Grade for submission (of {{$a}})",
|
||||
"submissionrequiredcontent": "You need to enter some text or add a file.",
|
||||
"submissionrequiredtitle": "You need to enter a title.",
|
||||
"submissionsreport": "Workshop submissions report",
|
||||
"submissiontitle": "Title",
|
||||
"switchphase10": "Switch to the setup phase",
|
||||
"switchphase20": "Switch to the submission phase",
|
||||
"switchphase30": "Switch to the assessment phase",
|
||||
"switchphase40": "Switch to the evaluation phase",
|
||||
"switchphase50": "Close workshop",
|
||||
"userplan": "Workshop planner",
|
||||
"userplancurrentphase": "Current phase",
|
||||
"warningassessmentmodified": "The submission was modified on the site.",
|
||||
"warningsubmissionmodified": "The assessment was modified on the site.",
|
||||
"weightinfo": "Weight: {{$a}}",
|
||||
"yourassessment": "Your assessment",
|
||||
"yourassessmentfor": "Your assessment for {{$a}}",
|
||||
"yourgrades": "Your grades",
|
||||
"yoursubmission": "Your submission",
|
||||
"yoursubmissionwithassessments": "Your submission with assessments"
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
<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]="workshop && workshop.coursemodule"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end" [hidden]="!evaluating">
|
||||
<ion-button fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
|
||||
{{ 'core.save' | translate }}
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshAssessment($event.target)" *ngIf="!evaluating">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
|
||||
<ion-item class="ion-text-wrap">
|
||||
<core-user-avatar *ngIf="profile" [user]="profile" slot="start" [courseId]="courseId" [userId]="profile.id">
|
||||
</core-user-avatar>
|
||||
|
||||
<ion-label>
|
||||
<h2 *ngIf="profile && profile.fullname">{{profile.fullname}}</h2>
|
||||
|
||||
<p *ngIf="workshop && assessment && showGrade(assessment.grade)">
|
||||
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{assessment.grade}}
|
||||
</p>
|
||||
<p *ngIf="workshop && access && access.canviewallsubmissions && assessment && showGrade(assessment.gradinggrade)"
|
||||
[class.core-has-overriden-grade]=" showGrade(assessment.gradinggrade)">
|
||||
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggrade}}
|
||||
</p>
|
||||
<p *ngIf="access && access.canviewallsubmissions && assessment && showGrade(assessment.gradinggradeover)"
|
||||
class="core-overriden-grade">
|
||||
{{ 'addon.mod_workshop.gradinggradeover' | translate }}: {{assessment.gradinggradeover}}
|
||||
</p>
|
||||
<p *ngIf="assessment && assessment.weight && assessment.weight != 1">
|
||||
{{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }}
|
||||
</p>
|
||||
<ion-badge *ngIf="!assessment || !showGrade(assessment.grade)" color="danger">
|
||||
{{ 'addon.mod_workshop.notassessed' | translate }}
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<addon-mod-workshop-assessment-strategy
|
||||
*ngIf="assessment && assessmentId && showGrade(assessment.grade) && workshop && access" [workshop]="workshop"
|
||||
[access]="access" [assessmentId]="assessmentId" [userId]="profile && profile.id" [strategy]="strategy">
|
||||
</addon-mod-workshop-assessment-strategy>
|
||||
|
||||
<form ion-list [formGroup]="evaluateForm" *ngIf="evaluating" #evaluateFormEl>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label><h2>{{ 'addon.mod_workshop.assessmentsettings' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="access?.canallocate">
|
||||
<ion-label position="stacked">
|
||||
<span core-mark-required="true">
|
||||
{{ 'addon.mod_workshop.assessmentweight' | translate }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<ion-select formControlName="weight" required="true" interface="action-sheet">
|
||||
<ion-select-option *ngFor="let w of weights" [value]="w">{{ w }}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.gradinggradecalculated' | translate }}</h2>
|
||||
<p>{{ assessment.gradinggrade }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="access?.canoverridegrades">
|
||||
<ion-label position="stacked">{{ 'addon.mod_workshop.gradinggradeover' | translate }}</ion-label>
|
||||
<ion-select formControlName="grade" interface="action-sheet">
|
||||
<ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value">
|
||||
{{grade.label}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="access?.canoverridegrades">
|
||||
<ion-label position="stacked">{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label>
|
||||
<core-rich-text-editor [control]="evaluateForm.controls['text']" name="text"
|
||||
[autoSave]="true" contextLevel="module" [contextInstanceId]="workshop?.coursemodule"
|
||||
elementId="feedbackreviewer_editor" [draftExtraParams]="{asid: assessmentId}">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
</form>
|
||||
<ion-list *ngIf="!evaluating && evaluate && evaluate.text">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start"
|
||||
[courseId]="courseId" [userId]="evaluateByProfile.id"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">
|
||||
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}
|
||||
</h2>
|
||||
<core-format-text [text]="evaluate.text" contextLevel="module" [contextInstanceId]="workshop?.coursemodule"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,398 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreGradesHelper, CoreGradesMenuItem } from '@features/grades/services/grades-helper';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
import {
|
||||
AddonModWorkshop,
|
||||
AddonModWorkshopAssessmentSavedChangedEventData,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
AddonModWorkshopPhase,
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshopSubmissionData,
|
||||
} from '../../services/workshop';
|
||||
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper';
|
||||
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
|
||||
import { AddonModWorkshopSyncProvider } from '../../services/workshop-sync';
|
||||
|
||||
/**
|
||||
* Page that displays a workshop assessment.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-workshop-assessment-page',
|
||||
templateUrl: 'assessment.html',
|
||||
})
|
||||
export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLeave {
|
||||
|
||||
@ViewChild('evaluateFormEl') formElement!: ElementRef;
|
||||
|
||||
assessment!: AddonModWorkshopSubmissionAssessmentWithFormData;
|
||||
submission!: AddonModWorkshopSubmissionData;
|
||||
profile!: CoreUserProfile;
|
||||
courseId!: number;
|
||||
access?: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
|
||||
assessmentId!: number;
|
||||
evaluating = false;
|
||||
loaded = false;
|
||||
showGrade: (grade?: string | number) => boolean;
|
||||
evaluateForm: FormGroup;
|
||||
maxGrade?: number;
|
||||
workshop?: AddonModWorkshopData;
|
||||
strategy?: string;
|
||||
title = '';
|
||||
evaluate: AddonModWorkshopAssessmentEvaluation = {
|
||||
text: '',
|
||||
grade: -1,
|
||||
weight: 1,
|
||||
};
|
||||
|
||||
weights: number[] = [];
|
||||
evaluateByProfile?: CoreUserProfile;
|
||||
evaluationGrades: CoreGradesMenuItem[] =[];
|
||||
|
||||
protected workshopId!: number;
|
||||
protected originalEvaluation: AddonModWorkshopAssessmentEvaluation = {
|
||||
text: '',
|
||||
grade: -1,
|
||||
weight: 1,
|
||||
};
|
||||
|
||||
protected hasOffline = false;
|
||||
protected syncObserver: CoreEventObserver;
|
||||
protected isDestroyed = false;
|
||||
protected siteId: string;
|
||||
protected currentUserId: number;
|
||||
protected forceLeave = false;
|
||||
|
||||
constructor(
|
||||
protected fb: FormBuilder,
|
||||
) {
|
||||
this.siteId = CoreSites.getCurrentSiteId();
|
||||
this.currentUserId = CoreSites.getCurrentSiteUserId();
|
||||
|
||||
this.showGrade = AddonModWorkshopHelper.showGrade;
|
||||
|
||||
this.evaluateForm = new FormGroup({});
|
||||
this.evaluateForm.addControl('weight', this.fb.control('', Validators.required));
|
||||
this.evaluateForm.addControl('grade', this.fb.control(''));
|
||||
this.evaluateForm.addControl('text', this.fb.control(''));
|
||||
|
||||
// Refresh workshop on sync.
|
||||
this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
|
||||
// Update just when all database is synced.
|
||||
if (this.workshopId === data.workshopId) {
|
||||
this.loaded = false;
|
||||
this.refreshAllData();
|
||||
}
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.assessment = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionAssessmentWithFormData>('assessment')!;
|
||||
this.submission = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionData>('submission')!;
|
||||
this.profile = CoreNavigator.getRouteParam<CoreUserProfile>('profile')!;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
|
||||
|
||||
this.assessmentId = this.assessment.id;
|
||||
this.workshopId = this.submission.workshopid;
|
||||
|
||||
this.fetchAssessmentData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
async canLeave(): Promise<boolean> {
|
||||
if (this.forceLeave || !this.evaluating) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.hasEvaluationChanged()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show confirmation if some data has been modified.
|
||||
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
|
||||
|
||||
CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the assessment data.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async fetchAssessmentData(): Promise<void> {
|
||||
try {
|
||||
this.workshop = await AddonModWorkshop.getWorkshopById(this.courseId, this.workshopId);
|
||||
this.title = this.workshop.name;
|
||||
this.strategy = this.workshop.strategy;
|
||||
|
||||
const gradeInfo = await CoreCourse.getModuleBasicGradeInfo(this.workshop.coursemodule);
|
||||
this.maxGrade = gradeInfo?.grade;
|
||||
|
||||
this.access = await AddonModWorkshop.getWorkshopAccessInformation(
|
||||
this.workshopId,
|
||||
{ cmId: this.workshop.coursemodule },
|
||||
);
|
||||
|
||||
// Load Weights selector.
|
||||
if (this.assessmentId && (this.access.canallocate || this.access.canoverridegrades)) {
|
||||
if (!this.isDestroyed) {
|
||||
// Block the workshop.
|
||||
CoreSync.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId);
|
||||
}
|
||||
|
||||
this.evaluating = true;
|
||||
} else {
|
||||
this.evaluating = false;
|
||||
}
|
||||
|
||||
if (!this.evaluating && this.workshop.phase != AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all info of the assessment.
|
||||
const assessment = await AddonModWorkshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, {
|
||||
userId: this.profile && this.profile.id,
|
||||
cmId: this.workshop.coursemodule,
|
||||
});
|
||||
|
||||
this.assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment);
|
||||
this.evaluate.text = this.assessment.feedbackreviewer || '';
|
||||
this.evaluate.weight = this.assessment.weight;
|
||||
|
||||
if (this.evaluating) {
|
||||
if (this.access.canallocate) {
|
||||
this.weights = [];
|
||||
for (let i = 16; i >= 0; i--) {
|
||||
this.weights[i] = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.access.canoverridegrades) {
|
||||
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
|
||||
this.evaluationGrades =
|
||||
await CoreGradesHelper.makeGradesMenu(this.workshop.gradinggrade, undefined, defaultGrade, -1);
|
||||
}
|
||||
|
||||
try {
|
||||
const offlineAssess = await AddonModWorkshopOffline.getEvaluateAssessment(this.workshopId, this.assessmentId);
|
||||
this.hasOffline = true;
|
||||
this.evaluate.weight = offlineAssess.weight;
|
||||
if (this.access.canoverridegrades) {
|
||||
this.evaluate.text = offlineAssess.feedbacktext || '';
|
||||
this.evaluate.grade = parseInt(offlineAssess.gradinggradeover, 10) || -1;
|
||||
}
|
||||
} catch {
|
||||
this.hasOffline = false;
|
||||
// No offline, load online.
|
||||
if (this.access.canoverridegrades) {
|
||||
this.evaluate.text = this.assessment.feedbackreviewer || '';
|
||||
this.evaluate.grade = parseInt(String(this.assessment.gradinggradeover), 10) || -1;
|
||||
}
|
||||
} finally {
|
||||
this.originalEvaluation.weight = this.evaluate.weight;
|
||||
if (this.access.canoverridegrades) {
|
||||
this.originalEvaluation.text = this.evaluate.text;
|
||||
this.originalEvaluation.grade = this.evaluate.grade;
|
||||
}
|
||||
|
||||
this.evaluateForm.controls['weight'].setValue(this.evaluate.weight);
|
||||
if (this.access.canoverridegrades) {
|
||||
this.evaluateForm.controls['grade'].setValue(this.evaluate.grade);
|
||||
this.evaluateForm.controls['text'].setValue(this.evaluate.text);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.assessment.gradinggradeoverby) {
|
||||
this.evaluateByProfile = await CoreUser.getProfile(this.assessment.gradinggradeoverby, this.courseId, true);
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'mm.course.errorgetmodule', true);
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force leaving the page, without checking for changes.
|
||||
*/
|
||||
protected forceLeavePage(): void {
|
||||
this.forceLeave = true;
|
||||
CoreNavigator.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data has changed.
|
||||
*
|
||||
* @return True if changed, false otherwise.
|
||||
*/
|
||||
protected hasEvaluationChanged(): boolean {
|
||||
if (!this.loaded || !this.evaluating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputData = this.evaluateForm.value;
|
||||
|
||||
if (this.originalEvaluation.weight != inputData.weight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.access && this.access.canoverridegrades) {
|
||||
if (this.originalEvaluation.text != inputData.text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.originalEvaluation.grade != inputData.grade) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to refresh all the data.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async refreshAllData(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModWorkshop.invalidateWorkshopData(this.courseId));
|
||||
promises.push(AddonModWorkshop.invalidateWorkshopAccessInformationData(this.workshopId));
|
||||
promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshopId));
|
||||
|
||||
if (this.assessmentId) {
|
||||
promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshopId, this.assessmentId));
|
||||
promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} finally {
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId);
|
||||
|
||||
await this.fetchAssessmentData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull to refresh.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshAssessment(refresher: IonRefresher): void {
|
||||
if (this.loaded) {
|
||||
this.refreshAllData().finally(() => {
|
||||
refresher?.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the assessment evaluation.
|
||||
*/
|
||||
async saveEvaluation(): Promise<void> {
|
||||
// Check if data has changed.
|
||||
if (this.hasEvaluationChanged()) {
|
||||
await this.sendEvaluation();
|
||||
}
|
||||
|
||||
// Go back.
|
||||
this.forceLeavePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the evaluation to be saved on the server.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async sendEvaluation(): Promise<void> {
|
||||
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
const inputData: AddonModWorkshopAssessmentEvaluation = this.evaluateForm.value;
|
||||
|
||||
const grade = inputData.grade >= 0 ? String(inputData.grade) : '';
|
||||
// Add some HTML to the message if needed.
|
||||
const text = CoreTextUtils.formatHtmlLines(inputData.text);
|
||||
|
||||
try {
|
||||
// Try to send it to server.
|
||||
const result = await AddonModWorkshop.evaluateAssessment(
|
||||
this.workshopId,
|
||||
this.assessmentId,
|
||||
this.courseId,
|
||||
text,
|
||||
inputData.weight,
|
||||
grade,
|
||||
);
|
||||
|
||||
CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId);
|
||||
|
||||
const data: AddonModWorkshopAssessmentSavedChangedEventData = {
|
||||
workshopId: this.workshopId,
|
||||
assessmentId: this.assessmentId,
|
||||
userId: this.currentUserId,
|
||||
};
|
||||
|
||||
return AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId).finally(() => {
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, data, this.siteId);
|
||||
});
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Cannot save assessment evaluation');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
|
||||
this.syncObserver?.off();
|
||||
// Restore original back functions.
|
||||
CoreSync.unblockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonModWorkshopAssessmentEvaluation = {
|
||||
text: string;
|
||||
grade: number;
|
||||
weight: number;
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_workshop.editsubmission' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate">
|
||||
{{ 'core.save' | translate }}
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<form ion-list [formGroup]="editForm" *ngIf="workshop" #editFormEl>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label position="stacked">
|
||||
<span core-mark-required="true">
|
||||
{{ 'addon.mod_workshop.submissiontitle' | translate }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<ion-input name="title" type="text" [placeholder]="'addon.mod_workshop.submissiontitle' | translate"
|
||||
formControlName="title">
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="textAvailable">
|
||||
<ion-label position="stacked">
|
||||
<span [core-mark-required]="textRequired">
|
||||
{{ 'addon.mod_workshop.submissioncontent' | translate }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<core-rich-text-editor [control]="editForm.controls['content']" name="content"
|
||||
[placeholder]="'addon.mod_workshop.submissioncontent' | translate" name="content" [component]="component"
|
||||
[componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id"
|
||||
elementId="content_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor>
|
||||
</ion-item>
|
||||
|
||||
<core-attachments *ngIf="fileAvailable" [files]="submission?.attachmentfiles || []" [maxSize]="workshop.maxbytes"
|
||||
[maxSubmissions]="workshop.nattachments" [component]="component" [componentId]="workshop.coursemodule"
|
||||
allowOffline="true" [acceptedTypes]="workshop.submissionfiletypes" [required]="fileRequired">
|
||||
</core-attachments>
|
||||
</form>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,476 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
|
||||
import { CoreFileSession } from '@services/file-session';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
import {
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshop,
|
||||
AddonModWorkshopSubmissionType,
|
||||
AddonModWorkshopSubmissionChangedEventData,
|
||||
AddonModWorkshopAction,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
AddonModWorkshopData,
|
||||
} from '../../services/workshop';
|
||||
import { AddonModWorkshopHelper, AddonModWorkshopSubmissionDataWithOfflineData } from '../../services/workshop-helper';
|
||||
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
|
||||
|
||||
/**
|
||||
* Page that displays the workshop edit submission.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-workshop-edit-submission',
|
||||
templateUrl: 'edit-submission.html',
|
||||
})
|
||||
export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, CanLeave {
|
||||
|
||||
@ViewChild('editFormEl') formElement!: ElementRef;
|
||||
|
||||
module!: CoreCourseModule;
|
||||
courseId!: number;
|
||||
access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
|
||||
submission?: AddonModWorkshopSubmissionDataWithOfflineData;
|
||||
|
||||
loaded = false;
|
||||
component = AddonModWorkshopProvider.COMPONENT;
|
||||
componentId!: number;
|
||||
editForm: FormGroup; // The form group.
|
||||
editorExtraParams: Record<string, unknown> = {}; // Extra params to identify the draft.
|
||||
workshop?: AddonModWorkshopData;
|
||||
textAvailable = false;
|
||||
textRequired = false;
|
||||
fileAvailable = false;
|
||||
fileRequired = false;
|
||||
|
||||
protected workshopId!: number;
|
||||
protected submissionId = 0;
|
||||
protected userId: number;
|
||||
protected originalData: AddonModWorkshopEditSubmissionInputData = {
|
||||
title: '',
|
||||
content: '',
|
||||
attachmentfiles: [],
|
||||
};
|
||||
|
||||
protected hasOffline = false;
|
||||
protected editing = false;
|
||||
protected forceLeave = false;
|
||||
protected siteId: string;
|
||||
protected isDestroyed = false;
|
||||
|
||||
constructor(
|
||||
protected fb: FormBuilder,
|
||||
) {
|
||||
|
||||
this.userId = CoreSites.getCurrentSiteUserId();
|
||||
this.siteId = CoreSites.getCurrentSiteId();
|
||||
|
||||
this.editForm = new FormGroup({});
|
||||
this.editForm.addControl('title', this.fb.control('', Validators.required));
|
||||
this.editForm.addControl('content', this.fb.control(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
|
||||
this.access = CoreNavigator.getRouteParam<AddonModWorkshopGetWorkshopAccessInformationWSResponse>('access')!;
|
||||
this.submissionId = CoreNavigator.getRouteNumberParam('submissionId') || 0;
|
||||
|
||||
if (this.submissionId > 0) {
|
||||
this.editorExtraParams.id = this.submissionId;
|
||||
}
|
||||
|
||||
this.workshopId = this.module.instance!;
|
||||
this.componentId = this.module.id;
|
||||
|
||||
if (!this.isDestroyed) {
|
||||
// Block the workshop.
|
||||
CoreSync.blockOperation(this.component, this.workshopId);
|
||||
}
|
||||
|
||||
this.fetchSubmissionData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
async canLeave(): Promise<boolean> {
|
||||
if (this.forceLeave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if data has changed.
|
||||
if (this.hasDataChanged()) {
|
||||
// Show confirmation if some data has been modified.
|
||||
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
|
||||
}
|
||||
|
||||
if (this.submission?.attachmentfiles) {
|
||||
// Delete the local files from the tmp folder.
|
||||
CoreFileUploader.clearTmpFiles(this.submission.attachmentfiles);
|
||||
}
|
||||
|
||||
CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the submission data.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async fetchSubmissionData(): Promise<void> {
|
||||
try {
|
||||
this.workshop = await AddonModWorkshop.getWorkshop(this.courseId, this.module.id);
|
||||
this.textAvailable = (this.workshop.submissiontypetext != AddonModWorkshopSubmissionType.SUBMISSION_TYPE_DISABLED);
|
||||
this.textRequired = (this.workshop.submissiontypetext == AddonModWorkshopSubmissionType.SUBMISSION_TYPE_REQUIRED);
|
||||
this.fileAvailable = (this.workshop.submissiontypefile != AddonModWorkshopSubmissionType.SUBMISSION_TYPE_DISABLED);
|
||||
this.fileRequired = (this.workshop.submissiontypefile == AddonModWorkshopSubmissionType.SUBMISSION_TYPE_REQUIRED);
|
||||
|
||||
this.editForm.controls.content.setValidators(this.textRequired ? Validators.required : null);
|
||||
|
||||
if (this.submissionId > 0) {
|
||||
this.editing = true;
|
||||
|
||||
this.submission =
|
||||
await AddonModWorkshopHelper.getSubmissionById(this.workshopId, this.submissionId, { cmId: this.module.id });
|
||||
|
||||
const canEdit = this.userId == this.submission.authorid &&
|
||||
this.access.cansubmit &&
|
||||
this.access.modifyingsubmissionallowed;
|
||||
|
||||
if (!canEdit) {
|
||||
// Should not happen, but go back if does.
|
||||
this.forceLeavePage();
|
||||
|
||||
return;
|
||||
}
|
||||
} else if (!this.access.cansubmit || !this.access.creatingsubmissionallowed) {
|
||||
// Should not happen, but go back if does.
|
||||
this.forceLeavePage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const submissionsActions = await AddonModWorkshopOffline.getSubmissions(this.workshopId);
|
||||
if (submissionsActions && submissionsActions.length) {
|
||||
this.hasOffline = true;
|
||||
this.submission = await AddonModWorkshopHelper.applyOfflineData(this.submission, submissionsActions);
|
||||
} else {
|
||||
this.hasOffline = false;
|
||||
}
|
||||
|
||||
if (this.submission) {
|
||||
this.originalData.title = this.submission.title || '';
|
||||
this.originalData.content = this.submission.content || '';
|
||||
this.originalData.attachmentfiles = [];
|
||||
|
||||
(this.submission.attachmentfiles || []).forEach((file) => {
|
||||
let filename = CoreFile.getFileName(file);
|
||||
if (!filename) {
|
||||
// We don't have filename, extract it from the path.
|
||||
filename = CoreFileHelper.getFilenameFromPath(file) || '';
|
||||
}
|
||||
|
||||
this.originalData.attachmentfiles.push({
|
||||
filename,
|
||||
fileurl: 'fileurl' in file ? file.fileurl : '',
|
||||
});
|
||||
});
|
||||
|
||||
this.editForm.controls['title'].setValue(this.submission.title);
|
||||
this.editForm.controls['content'].setValue(this.submission.content);
|
||||
}
|
||||
|
||||
CoreFileSession.setFiles(
|
||||
this.component,
|
||||
this.getFilesComponentId(),
|
||||
this.submission?.attachmentfiles || [],
|
||||
);
|
||||
|
||||
this.loaded = true;
|
||||
} catch (error) {
|
||||
this.loaded = false;
|
||||
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
|
||||
this.forceLeavePage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force leaving the page, without checking for changes.
|
||||
*/
|
||||
protected forceLeavePage(): void {
|
||||
this.forceLeave = true;
|
||||
CoreNavigator.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form input data.
|
||||
*
|
||||
* @return Object with all the info.
|
||||
*/
|
||||
protected getInputData(): AddonModWorkshopEditSubmissionInputData {
|
||||
const values: AddonModWorkshopEditSubmissionInputData = {
|
||||
title: this.editForm.value.title,
|
||||
content: '',
|
||||
attachmentfiles: [],
|
||||
};
|
||||
|
||||
if (this.textAvailable) {
|
||||
values.content = this.editForm.value.content || '';
|
||||
}
|
||||
|
||||
if (this.fileAvailable) {
|
||||
values.attachmentfiles = CoreFileSession.getFiles(this.component, this.getFilesComponentId()) || [];
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data has changed.
|
||||
*
|
||||
* @return True if changed or false if not.
|
||||
*/
|
||||
protected hasDataChanged(): boolean {
|
||||
if (!this.loaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputData = this.getInputData();
|
||||
if (this.originalData.title != inputData.title || this.textAvailable && this.originalData.content != inputData.content) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.fileAvailable) {
|
||||
return CoreFileUploader.areFileListDifferent(inputData.attachmentfiles, this.originalData.attachmentfiles);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the submission.
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
// Check if data has changed.
|
||||
if (this.hasDataChanged()) {
|
||||
try {
|
||||
await this.saveSubmission();
|
||||
// Go back to entry list.
|
||||
this.forceLeavePage();
|
||||
} catch{
|
||||
// Nothing to do.
|
||||
}
|
||||
} else {
|
||||
// Nothing to save, just go back.
|
||||
this.forceLeavePage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send submission and save.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async saveSubmission(): Promise<void> {
|
||||
const inputData = this.getInputData();
|
||||
|
||||
if (!inputData.title) {
|
||||
CoreDomUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredtitle');
|
||||
|
||||
throw new CoreError(Translate.instant('addon.mod_workshop.submissionrequiredtitle'));
|
||||
}
|
||||
|
||||
const noText = CoreTextUtils.htmlIsBlank(inputData.content);
|
||||
const noFiles = !inputData.attachmentfiles.length;
|
||||
|
||||
if ((this.textRequired && noText) || (this.fileRequired && noFiles) || (noText && noFiles)) {
|
||||
CoreDomUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredcontent');
|
||||
|
||||
throw new CoreError(Translate.instant('addon.mod_workshop.submissionrequiredcontent'));
|
||||
}
|
||||
|
||||
let saveOffline = false;
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
const submissionId = this.submission?.id;
|
||||
|
||||
// Add some HTML to the message if needed.
|
||||
if (this.textAvailable) {
|
||||
inputData.content = CoreTextUtils.formatHtmlLines(inputData.content);
|
||||
}
|
||||
|
||||
// Upload attachments first if any.
|
||||
let allowOffline = !inputData.attachmentfiles.length;
|
||||
try {
|
||||
let attachmentsId: CoreFileUploaderStoreFilesResult | number | undefined;
|
||||
try {
|
||||
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles(
|
||||
this.workshopId,
|
||||
inputData.attachmentfiles,
|
||||
false,
|
||||
);
|
||||
} catch {
|
||||
// Cannot upload them in online, save them in offline.
|
||||
saveOffline = true;
|
||||
allowOffline = true;
|
||||
|
||||
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles(
|
||||
this.workshopId,
|
||||
inputData.attachmentfiles,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (!saveOffline && !this.fileAvailable) {
|
||||
attachmentsId = undefined;
|
||||
}
|
||||
|
||||
let newSubmissionId: number | false;
|
||||
if (this.editing) {
|
||||
if (saveOffline) {
|
||||
// Save submission in offline.
|
||||
await AddonModWorkshopOffline.saveSubmission(
|
||||
this.workshopId,
|
||||
this.courseId,
|
||||
inputData.title,
|
||||
inputData.content,
|
||||
attachmentsId as CoreFileUploaderStoreFilesResult,
|
||||
submissionId,
|
||||
AddonModWorkshopAction.UPDATE,
|
||||
);
|
||||
newSubmissionId = false;
|
||||
} else {
|
||||
// Try to send it to server.
|
||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||
newSubmissionId = await AddonModWorkshop.updateSubmission(
|
||||
this.workshopId,
|
||||
submissionId!,
|
||||
this.courseId,
|
||||
inputData.title,
|
||||
inputData.content,
|
||||
attachmentsId,
|
||||
undefined,
|
||||
allowOffline,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (saveOffline) {
|
||||
// Save submission in offline.
|
||||
await AddonModWorkshopOffline.saveSubmission(
|
||||
this.workshopId,
|
||||
this.courseId,
|
||||
inputData.title,
|
||||
inputData.content,
|
||||
attachmentsId as CoreFileUploaderStoreFilesResult,
|
||||
undefined,
|
||||
AddonModWorkshopAction.ADD,
|
||||
);
|
||||
newSubmissionId = false;
|
||||
} else {
|
||||
// Try to send it to server.
|
||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||
newSubmissionId = await AddonModWorkshop.addSubmission(
|
||||
this.workshopId,
|
||||
this.courseId,
|
||||
inputData.title,
|
||||
inputData.content,
|
||||
attachmentsId,
|
||||
undefined,
|
||||
allowOffline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CoreForms.triggerFormSubmittedEvent(this.formElement, !!newSubmissionId, this.siteId);
|
||||
|
||||
const data: AddonModWorkshopSubmissionChangedEventData = {
|
||||
workshopId: this.workshopId,
|
||||
};
|
||||
|
||||
if (newSubmissionId) {
|
||||
// Data sent to server, delete stored files (if any).
|
||||
AddonModWorkshopOffline.deleteSubmissionAction(
|
||||
this.workshopId,
|
||||
this.editing ? AddonModWorkshopAction.UPDATE : AddonModWorkshopAction.ADD,
|
||||
);
|
||||
|
||||
AddonModWorkshopHelper.deleteSubmissionStoredFiles(this.workshopId, this.siteId);
|
||||
data.submissionId = newSubmissionId;
|
||||
}
|
||||
|
||||
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'workshop' });
|
||||
|
||||
const promise = newSubmissionId ? AddonModWorkshop.invalidateSubmissionData(this.workshopId, newSubmissionId) :
|
||||
Promise.resolve();
|
||||
|
||||
await promise.finally(() => {
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||
|
||||
// Delete the local files from the tmp folder.
|
||||
CoreFileUploader.clearTmpFiles(inputData.attachmentfiles);
|
||||
});
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Cannot save submission');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
protected getFilesComponentId(): string {
|
||||
const id = this.submissionId > 0
|
||||
? this.submissionId
|
||||
: 'newsub';
|
||||
|
||||
return this.workshopId + '_' + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
CoreSync.unblockOperation(this.component, this.workshopId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonModWorkshopEditSubmissionInputData = {
|
||||
title: string;
|
||||
content: string;
|
||||
attachmentfiles: CoreFileEntry[];
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
<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]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<addon-mod-workshop-index [module]="module" [courseId]="courseId" [group]="selectedGroup" (dataRetrieved)="updateData($event)">
|
||||
</addon-mod-workshop-index>
|
||||
</ion-content>
|
|
@ -0,0 +1,41 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { AddonModWorkshopIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays a workshop.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-workshop-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModWorkshopIndexPage extends CoreCourseModuleMainActivityPage<AddonModWorkshopIndexComponent> implements OnInit {
|
||||
|
||||
@ViewChild(AddonModWorkshopIndexComponent) activityComponent?: AddonModWorkshopIndexComponent;
|
||||
|
||||
selectedGroup = 0;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
<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 *ngIf="title" [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end" [hidden]="!loaded">
|
||||
<ion-button *ngIf="assessmentId && access.assessingallowed" fill="clear" (click)="saveAssessment()"
|
||||
[attr.aria-label]="'core.save' | translate">
|
||||
{{ 'core.save' | translate }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="canAddFeedback" fill="clear" (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
|
||||
{{ 'core.save' | translate }}
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)"
|
||||
*ngIf="!((assessmentId && access.assessingallowed) || canAddFeedback)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list *ngIf="submission">
|
||||
<addon-mod-workshop-submission [submission]="submission" [courseId]="courseId" [module]="module" [workshop]="workshop"
|
||||
[access]="access">
|
||||
</addon-mod-workshop-submission>
|
||||
<ion-item class="ion-text-wrap" *ngIf="canEdit || canDelete">
|
||||
<ion-label>
|
||||
<ion-button expand="block" *ngIf="canEdit" (click)="editSubmission()">
|
||||
<ion-icon name="fas-edit" slot="start"></ion-icon>
|
||||
{{ 'addon.mod_workshop.editsubmission' | translate }}
|
||||
</ion-button>
|
||||
<ion-button expand="block" *ngIf="!submission.deleted && canDelete" color="danger" (click)="deleteSubmission()">
|
||||
<ion-icon name="fas-trash" slot="start"></ion-icon>
|
||||
{{ 'addon.mod_workshop.deletesubmission' | translate }}
|
||||
</ion-button>
|
||||
<ion-button expand="block" fill="outline" *ngIf="submission.deleted && canDelete" color="danger"
|
||||
(click)="undoDeleteSubmission()">
|
||||
<ion-icon name="fas-undo-alt" slot="start"></ion-icon>
|
||||
{{ 'core.restore' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list *ngIf="!canAddFeedback && evaluate?.text">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<core-user-avatar *ngIf="evaluateByProfile" [user]="evaluateByProfile" slot="start" [courseId]="courseId"
|
||||
[userId]="evaluateByProfile.id"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">
|
||||
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}
|
||||
</h2>
|
||||
<core-format-text [text]="evaluate?.text" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list *ngIf="ownAssessment && !assessment">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.yourassessment' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<addon-mod-workshop-assessment [submission]="submission" [assessment]="ownAssessment" [courseId]="courseId"
|
||||
[access]="access" [module]="module" [workshop]="workshop">
|
||||
</addon-mod-workshop-assessment>
|
||||
</ion-list>
|
||||
|
||||
<ion-list *ngIf="submissionInfo && submissionInfo.reviewedby && submissionInfo.reviewedby.length && !assessment">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.receivedgrades' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let reviewer of submissionInfo.reviewedby">
|
||||
<addon-mod-workshop-assessment *ngIf="!reviewer.ownAssessment" [submission]="submission" [assessment]="reviewer"
|
||||
[courseId]="courseId" [access]="access" [module]="module" [workshop]="workshop">
|
||||
</addon-mod-workshop-assessment>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
<ion-list *ngIf="submissionInfo && submissionInfo.reviewerof && submissionInfo.reviewerof.length && !assessment">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.givengrades' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<addon-mod-workshop-assessment *ngFor="let reviewer of submissionInfo.reviewerof" [assessment]="reviewer"
|
||||
[courseId]="courseId" [module]="module" [workshop]="workshop" [access]="access">
|
||||
</addon-mod-workshop-assessment>
|
||||
</ion-list>
|
||||
|
||||
<form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback && submission" #feedbackFormEl>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="access.canpublishsubmissions">
|
||||
<ion-label>{{ 'addon.mod_workshop.publishsubmission' | translate }}</ion-label>
|
||||
<ion-toggle formControlName="published"></ion-toggle>
|
||||
<p class="item-help">{{ 'addon.mod_workshop.publishsubmission_help' | translate }}</p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_workshop.gradecalculated' | translate }}</h2>
|
||||
<p>{{ submission.grade }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label position="stacked">{{ 'addon.mod_workshop.gradeover' | translate }}</ion-label>
|
||||
<ion-select formControlName="grade" interface="action-sheet">
|
||||
<ion-select-option *ngFor="let grade of evaluationGrades" [value]="grade.value">
|
||||
{{grade.label}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
|
||||
<core-rich-text-editor [control]="feedbackForm.controls['text']" name="text"
|
||||
[autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="feedbackauthor_editor"
|
||||
[draftExtraParams]="{id: submissionId}">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
</form>
|
||||
|
||||
<addon-mod-workshop-assessment-strategy *ngIf="assessmentId" [workshop]="workshop" [access]="access"
|
||||
[assessmentId]="assessmentId" [userId]="assessmentUserId" [strategy]="strategy" [edit]="access.assessingallowed">
|
||||
</addon-mod-workshop-assessment-strategy>
|
||||
|
||||
<ion-list *ngIf="assessmentId && !access.assessingallowed && assessment?.feedbackreviewer">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<core-user-avatar *ngIf="evaluateGradingByProfile" [user]="evaluateGradingByProfile" slot="start"
|
||||
[courseId]="courseId" [userId]="evaluateGradingByProfile.id"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2 *ngIf="evaluateGradingByProfile && evaluateGradingByProfile.fullname">
|
||||
{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }}
|
||||
</h2>
|
||||
<core-format-text [text]="assessment!.feedbackreviewer" contextLevel="module" [contextInstanceId]="module.id"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,610 @@
|
|||
// (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, Optional, ViewChild, ElementRef } from '@angular/core';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreGradesHelper, CoreGradesMenuItem } from '@features/grades/services/grades-helper';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
import { IonContent, IonRefresher } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
import { AddonModWorkshopAssessmentStrategyComponent } from '../../components/assessment-strategy/assessment-strategy';
|
||||
import {
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshop,
|
||||
AddonModWorkshopPhase,
|
||||
AddonModWorkshopSubmissionChangedEventData,
|
||||
AddonModWorkshopAction,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
AddonModWorkshopAssessmentSavedChangedEventData,
|
||||
} from '../../services/workshop';
|
||||
import {
|
||||
AddonModWorkshopHelper,
|
||||
AddonModWorkshopSubmissionAssessmentWithFormData,
|
||||
AddonModWorkshopSubmissionDataWithOfflineData,
|
||||
} from '../../services/workshop-helper';
|
||||
import { AddonModWorkshopOffline } from '../../services/workshop-offline';
|
||||
import { AddonModWorkshopSyncProvider, AddonModWorkshopAutoSyncData } from '../../services/workshop-sync';
|
||||
|
||||
/**
|
||||
* Page that displays a workshop submission.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-workshop-submission-page',
|
||||
templateUrl: 'submission.html',
|
||||
})
|
||||
export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLeave {
|
||||
|
||||
@ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy?: AddonModWorkshopAssessmentStrategyComponent;
|
||||
|
||||
@ViewChild('feedbackFormEl') formElement?: ElementRef;
|
||||
|
||||
module!: CoreCourseModule;
|
||||
workshop!: AddonModWorkshopData;
|
||||
access!: AddonModWorkshopGetWorkshopAccessInformationWSResponse;
|
||||
assessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
|
||||
submissionInfo!: AddonModWorkshopSubmissionDataWithOfflineData;
|
||||
profile?: CoreUserProfile;
|
||||
courseId!: number;
|
||||
|
||||
submission?: AddonModWorkshopSubmissionDataWithOfflineData;
|
||||
title?: string;
|
||||
loaded = false;
|
||||
ownAssessment?: AddonModWorkshopSubmissionAssessmentWithFormData;
|
||||
strategy?: string;
|
||||
assessmentId?: number;
|
||||
assessmentUserId?: number;
|
||||
evaluate?: AddonWorkshopSubmissionEvaluateData;
|
||||
canAddFeedback = false;
|
||||
canEdit = false;
|
||||
canDelete = false;
|
||||
evaluationGrades: CoreGradesMenuItem[] = [];
|
||||
evaluateGradingByProfile?: CoreUserProfile;
|
||||
evaluateByProfile?: CoreUserProfile;
|
||||
feedbackForm: FormGroup; // The form group.
|
||||
submissionId!: number;
|
||||
|
||||
protected workshopId!: number;
|
||||
protected currentUserId: number;
|
||||
protected userId?: number;
|
||||
protected siteId: string;
|
||||
protected originalEvaluation: Omit<AddonWorkshopSubmissionEvaluateData, 'grade'> & { grade: number | string} = {
|
||||
published: false,
|
||||
text: '',
|
||||
grade: '',
|
||||
};
|
||||
|
||||
protected hasOffline = false;
|
||||
protected component = AddonModWorkshopProvider.COMPONENT;
|
||||
protected forceLeave = false;
|
||||
protected obsAssessmentSaved: CoreEventObserver;
|
||||
protected syncObserver: CoreEventObserver;
|
||||
protected isDestroyed = false;
|
||||
|
||||
constructor(
|
||||
protected fb: FormBuilder,
|
||||
@Optional() protected content: IonContent,
|
||||
) {
|
||||
this.currentUserId = CoreSites.getCurrentSiteUserId();
|
||||
this.siteId = CoreSites.getCurrentSiteId();
|
||||
|
||||
this.feedbackForm = new FormGroup({});
|
||||
this.feedbackForm.addControl('published', this.fb.control(''));
|
||||
this.feedbackForm.addControl('grade', this.fb.control(''));
|
||||
this.feedbackForm.addControl('text', this.fb.control(''));
|
||||
|
||||
this.obsAssessmentSaved = CoreEvents.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => {
|
||||
this.eventReceived(data);
|
||||
}, this.siteId);
|
||||
|
||||
// Refresh workshop on sync.
|
||||
this.syncObserver = CoreEvents.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
|
||||
// Update just when all database is synced.
|
||||
this.eventReceived(data);
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
|
||||
this.submissionId = CoreNavigator.getRouteNumberParam('submissionId')!;
|
||||
this.module = CoreNavigator.getRouteParam<CoreCourseModule>('module')!;
|
||||
this.workshop = CoreNavigator.getRouteParam<AddonModWorkshopData>('workshop')!;
|
||||
this.access = CoreNavigator.getRouteParam<AddonModWorkshopGetWorkshopAccessInformationWSResponse>('access')!;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
|
||||
this.profile = CoreNavigator.getRouteParam<CoreUserProfile>('profile');
|
||||
this.submissionInfo = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionDataWithOfflineData>('submission')!;
|
||||
this.assessment = CoreNavigator.getRouteParam<AddonModWorkshopSubmissionAssessmentWithFormData>('assessment');
|
||||
|
||||
this.title = this.module.name;
|
||||
this.workshopId = this.module.instance || this.workshop.id;
|
||||
|
||||
this.userId = this.submissionInfo?.authorid;
|
||||
this.strategy = (this.assessment && this.assessment.strategy) || (this.workshop && this.workshop.strategy);
|
||||
this.assessmentId = this.assessment?.id;
|
||||
this.assessmentUserId = this.assessment?.reviewerid;
|
||||
|
||||
await this.fetchSubmissionData();
|
||||
|
||||
try {
|
||||
await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name);
|
||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
async canLeave(): Promise<boolean> {
|
||||
const assessmentHasChanged = this.assessmentStrategy?.hasDataChanged();
|
||||
if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show confirmation if some data has been modified.
|
||||
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
|
||||
|
||||
CoreForms.triggerFormCancelledEvent(this.formElement, this.siteId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goto edit submission page.
|
||||
*/
|
||||
editSubmission(): void {
|
||||
const params: Params = {
|
||||
module: module,
|
||||
access: this.access,
|
||||
};
|
||||
|
||||
CoreNavigator.navigate(String(this.submissionId) + '/edit', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when we receive an event of submission changes.
|
||||
*
|
||||
* @param data Event data received.
|
||||
*/
|
||||
protected eventReceived(data: AddonModWorkshopAutoSyncData |
|
||||
AddonModWorkshopAssessmentSavedChangedEventData): void {
|
||||
if (this.workshopId === data.workshopId) {
|
||||
this.content?.scrollToTop();
|
||||
|
||||
this.loaded = false;
|
||||
this.refreshAllData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the submission data.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async fetchSubmissionData(): Promise<void> {
|
||||
try {
|
||||
this.submission = await AddonModWorkshopHelper.getSubmissionById(this.workshopId, this.submissionId, {
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
this.submission.grade = this.submissionInfo?.grade;
|
||||
this.submission.gradinggrade = this.submissionInfo?.gradinggrade;
|
||||
this.submission.gradeover = this.submissionInfo?.gradeover;
|
||||
this.userId = this.submission.authorid || this.userId;
|
||||
this.canEdit = this.currentUserId == this.userId && this.access.cansubmit && this.access.modifyingsubmissionallowed;
|
||||
this.canDelete = this.access.candeletesubmissions;
|
||||
|
||||
this.canAddFeedback = !this.assessmentId && this.workshop.phase > AddonModWorkshopPhase.PHASE_ASSESSMENT &&
|
||||
this.workshop.phase < AddonModWorkshopPhase.PHASE_CLOSED && this.access.canoverridegrades;
|
||||
this.ownAssessment = undefined;
|
||||
|
||||
if (this.access.canviewallassessments) {
|
||||
// Get new data, different that came from stateParams.
|
||||
promises.push(AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, {
|
||||
cmId: this.module.id,
|
||||
}).then((subAssessments) => {
|
||||
// Only allow the student to delete their own submission if it's still editable and hasn't been assessed.
|
||||
if (this.canDelete) {
|
||||
this.canDelete = !subAssessments.length;
|
||||
}
|
||||
|
||||
this.submissionInfo.reviewedby = subAssessments;
|
||||
|
||||
this.submissionInfo.reviewedby.forEach((assessment) => {
|
||||
assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment);
|
||||
|
||||
if (this.currentUserId == assessment.reviewerid) {
|
||||
this.ownAssessment = assessment;
|
||||
assessment.ownAssessment = true;
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}));
|
||||
} else if (this.currentUserId == this.userId && this.assessmentId) {
|
||||
// Get new data, different that came from stateParams.
|
||||
promises.push(AddonModWorkshop.getAssessment(this.workshopId, this.assessmentId, {
|
||||
cmId: this.module.id,
|
||||
}).then((assessment) => {
|
||||
// Only allow the student to delete their own submission if it's still editable and hasn't been assessed.
|
||||
if (this.canDelete) {
|
||||
this.canDelete = !assessment;
|
||||
}
|
||||
|
||||
this.submissionInfo.reviewedby = [this.parseAssessment(assessment)];
|
||||
|
||||
return;
|
||||
}));
|
||||
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.userId == this.currentUserId) {
|
||||
const assessments = await AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, {
|
||||
cmId: this.module.id,
|
||||
});
|
||||
|
||||
this.submissionInfo.reviewedby = assessments.map((assessment) => this.parseAssessment(assessment));
|
||||
}
|
||||
|
||||
if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
this.evaluate = {
|
||||
published: this.submission.published,
|
||||
text: this.submission.feedbackauthor || '',
|
||||
};
|
||||
}
|
||||
|
||||
if (this.canAddFeedback) {
|
||||
|
||||
if (!this.isDestroyed) {
|
||||
// Block the workshop.
|
||||
CoreSync.blockOperation(this.component, this.workshopId);
|
||||
}
|
||||
|
||||
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
|
||||
|
||||
promises.push(CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1)
|
||||
.then(async (grades) => {
|
||||
this.evaluationGrades = grades;
|
||||
|
||||
this.evaluate!.grade = {
|
||||
label: CoreGradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.gradeover) ||
|
||||
defaultGrade,
|
||||
value: this.submissionInfo.gradeover || -1,
|
||||
};
|
||||
|
||||
try {
|
||||
const offlineSubmission =
|
||||
await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
|
||||
|
||||
this.hasOffline = true;
|
||||
this.evaluate!.published = offlineSubmission.published;
|
||||
this.evaluate!.text = offlineSubmission.feedbacktext;
|
||||
this.evaluate!.grade = {
|
||||
label: CoreGradesHelper.getGradeLabelFromValue(
|
||||
grades,
|
||||
parseInt(offlineSubmission.gradeover, 10),
|
||||
) || defaultGrade,
|
||||
value: offlineSubmission.gradeover || -1,
|
||||
};
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
this.hasOffline = false;
|
||||
} finally {
|
||||
this.originalEvaluation.published = this.evaluate!.published;
|
||||
this.originalEvaluation.text = this.evaluate!.text;
|
||||
this.originalEvaluation.grade = this.evaluate!.grade.value;
|
||||
|
||||
this.feedbackForm.controls['published'].setValue(this.evaluate!.published);
|
||||
this.feedbackForm.controls['grade'].setValue(this.evaluate!.grade.value);
|
||||
this.feedbackForm.controls['text'].setValue(this.evaluate!.text);
|
||||
}
|
||||
|
||||
return;
|
||||
}));
|
||||
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
|
||||
this.evaluate && this.evaluate.text) {
|
||||
promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
|
||||
this.evaluateByProfile = profile;
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.assessment && !this.access.assessingallowed && this.assessment.feedbackreviewer &&
|
||||
this.assessment.gradinggradeoverby) {
|
||||
promises.push(CoreUser.getProfile(this.assessment.gradinggradeoverby, this.courseId, true)
|
||||
.then((profile) => {
|
||||
this.evaluateGradingByProfile = profile;
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const submissionsActions = await AddonModWorkshopOffline.getSubmissions(this.workshopId);
|
||||
|
||||
this.submission = await AddonModWorkshopHelper.applyOfflineData(this.submission, submissionsActions);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse assessment to be shown.
|
||||
*
|
||||
* @param assessment Original assessment.
|
||||
* @return Parsed assessment.
|
||||
*/
|
||||
protected parseAssessment(
|
||||
assessment: AddonModWorkshopSubmissionAssessmentWithFormData,
|
||||
): AddonModWorkshopSubmissionAssessmentWithFormData {
|
||||
assessment = AddonModWorkshopHelper.realGradeValue(this.workshop, assessment);
|
||||
|
||||
if (this.currentUserId == assessment.reviewerid) {
|
||||
this.ownAssessment = assessment;
|
||||
assessment.ownAssessment = true;
|
||||
}
|
||||
|
||||
return assessment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force leaving the page, without checking for changes.
|
||||
*/
|
||||
protected forceLeavePage(): void {
|
||||
this.forceLeave = true;
|
||||
CoreNavigator.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data has changed.
|
||||
*
|
||||
* @return True if changed, false otherwise.
|
||||
*/
|
||||
protected hasEvaluationChanged(): boolean {
|
||||
if (!this.loaded || !this.access.canoverridegrades) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputData = this.feedbackForm.value;
|
||||
|
||||
if (this.originalEvaluation.published != inputData.published) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.originalEvaluation.text != inputData.text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.originalEvaluation.grade != inputData.grade) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to refresh all the data.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async refreshAllData(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId));
|
||||
promises.push(AddonModWorkshop.invalidateSubmissionsData(this.workshopId));
|
||||
promises.push(AddonModWorkshop.invalidateSubmissionAssesmentsData(this.workshopId, this.submissionId));
|
||||
|
||||
if (this.assessmentId) {
|
||||
promises.push(AddonModWorkshop.invalidateAssessmentFormData(this.workshopId, this.assessmentId));
|
||||
promises.push(AddonModWorkshop.invalidateAssessmentData(this.workshopId, this.assessmentId));
|
||||
}
|
||||
|
||||
if (this.assessmentUserId) {
|
||||
promises.push(AddonModWorkshop.invalidateReviewerAssesmentsData(this.workshopId, this.assessmentId));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} finally {
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId);
|
||||
|
||||
await this.fetchSubmissionData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull to refresh.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshSubmission(refresher: IonRefresher): void {
|
||||
if (this.loaded) {
|
||||
this.refreshAllData().finally(() => {
|
||||
refresher?.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the assessment.
|
||||
*/
|
||||
async saveAssessment(): Promise<void> {
|
||||
if (this.assessmentStrategy?.hasDataChanged()) {
|
||||
try {
|
||||
await this.assessmentStrategy.saveAssessment();
|
||||
this.forceLeavePage();
|
||||
} catch {
|
||||
// Error, stay on the page.
|
||||
}
|
||||
} else {
|
||||
// Nothing to save, just go back.
|
||||
this.forceLeavePage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the submission evaluation.
|
||||
*/
|
||||
async saveEvaluation(): Promise<void> {
|
||||
// Check if data has changed.
|
||||
if (this.hasEvaluationChanged()) {
|
||||
await this.sendEvaluation();
|
||||
this.forceLeavePage();
|
||||
} else {
|
||||
// Nothing to save, just go back.
|
||||
this.forceLeavePage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the evaluation to be saved on the server.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async sendEvaluation(): Promise<void> {
|
||||
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
|
||||
const inputData: {
|
||||
grade: number | string;
|
||||
text: string;
|
||||
published: boolean;
|
||||
} = this.feedbackForm.value;
|
||||
|
||||
inputData.grade = inputData.grade >= 0 ? inputData.grade : '';
|
||||
// Add some HTML to the message if needed.
|
||||
inputData.text = CoreTextUtils.formatHtmlLines(inputData.text);
|
||||
|
||||
// Try to send it to server.
|
||||
try {
|
||||
const result = await AddonModWorkshop.evaluateSubmission(
|
||||
this.workshopId,
|
||||
this.submissionId,
|
||||
this.courseId,
|
||||
inputData.text,
|
||||
inputData.published,
|
||||
String(inputData.grade),
|
||||
);
|
||||
CoreForms.triggerFormSubmittedEvent(this.formElement, !!result, this.siteId);
|
||||
|
||||
await AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId).finally(() => {
|
||||
const data: AddonModWorkshopSubmissionChangedEventData = {
|
||||
workshopId: this.workshopId,
|
||||
submissionId: this.submissionId,
|
||||
};
|
||||
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||
});
|
||||
} catch (message) {
|
||||
CoreDomUtils.showErrorModalDefault(message, 'Cannot save submission evaluation');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the submission delete action.
|
||||
*/
|
||||
async deleteSubmission(): Promise<void> {
|
||||
try {
|
||||
await CoreDomUtils.showDeleteConfirm('addon.mod_workshop.submissiondeleteconfirm');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading('core.deleting', true);
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await AddonModWorkshop.deleteSubmission(this.workshopId, this.submissionId, this.courseId);
|
||||
success = true;
|
||||
|
||||
await AddonModWorkshop.invalidateSubmissionData(this.workshopId, this.submissionId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Cannot delete submission');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
if (success) {
|
||||
const data: AddonModWorkshopSubmissionChangedEventData = {
|
||||
workshopId: this.workshopId,
|
||||
submissionId: this.submissionId,
|
||||
};
|
||||
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||
|
||||
this.forceLeavePage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the submission delete action.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
async undoDeleteSubmission(): Promise<void> {
|
||||
await AddonModWorkshopOffline.deleteSubmissionAction(
|
||||
this.workshopId,
|
||||
AddonModWorkshopAction.DELETE,
|
||||
).finally(async () => {
|
||||
|
||||
const data: AddonModWorkshopSubmissionChangedEventData = {
|
||||
workshopId: this.workshopId,
|
||||
submissionId: this.submissionId,
|
||||
};
|
||||
|
||||
CoreEvents.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||
|
||||
await this.refreshAllData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
|
||||
this.syncObserver?.off();
|
||||
this.obsAssessmentSaved?.off();
|
||||
// Restore original back functions.
|
||||
CoreSync.unblockOperation(this.component, this.workshopId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonWorkshopSubmissionEvaluateData = {
|
||||
published: boolean;
|
||||
text: string;
|
||||
grade?: CoreGradesMenuItem;
|
||||
};
|
|
@ -0,0 +1,159 @@
|
|||
// (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 { Injectable, Type } from '@angular/core';
|
||||
import { CoreDelegateHandler, CoreDelegate } from '@classes/delegate';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { AddonModWorkshopGetAssessmentFormDefinitionData, AddonModWorkshopGetAssessmentFormFieldsParsedData } from './workshop';
|
||||
|
||||
/**
|
||||
* Interface that all assessment strategy handlers must implement.
|
||||
*/
|
||||
export interface AddonWorkshopAssessmentStrategyHandler extends CoreDelegateHandler {
|
||||
/**
|
||||
* The name of the assessment strategy. E.g. 'accumulative'.
|
||||
*/
|
||||
strategyName: string;
|
||||
|
||||
/**
|
||||
* Return the Component to render the plugin.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param injector Injector.
|
||||
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent?(): Type<unknown>;
|
||||
|
||||
/**
|
||||
* Prepare original values to be shown and compared.
|
||||
*
|
||||
* @param form Original data of the form.
|
||||
* @param workshopId WorkShop Id
|
||||
* @return Promise resolved with original values sorted.
|
||||
*/
|
||||
getOriginalValues?(
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
workshopId: number,
|
||||
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]>;
|
||||
|
||||
/**
|
||||
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||
*
|
||||
* @param originalValues Original values of the form.
|
||||
* @param currentValues Current values of the form.
|
||||
* @return True if data has changed, false otherwise.
|
||||
*/
|
||||
hasDataChanged?(
|
||||
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||
*
|
||||
* @param currentValues Current values of the form.
|
||||
* @param form Assessment form data.
|
||||
* @return Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||
*/
|
||||
prepareAssessmentData(
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<CoreFormFields<unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to register workshop assessment strategy handlers.
|
||||
* You can use this service to register your own assessment strategy handlers to be used in a workshop.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonWorkshopAssessmentStrategyDelegateService extends CoreDelegate<AddonWorkshopAssessmentStrategyHandler> {
|
||||
|
||||
protected handlerNameProperty = 'strategyName';
|
||||
|
||||
constructor() {
|
||||
super('AddonWorkshopAssessmentStrategyDelegate', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an assessment strategy plugin is supported.
|
||||
*
|
||||
* @param workshopStrategy Assessment strategy name.
|
||||
* @return True if supported, false otherwise.
|
||||
*/
|
||||
isPluginSupported(workshopStrategy: string): boolean {
|
||||
return this.hasHandler(workshopStrategy, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directive to use for a certain assessment strategy plugin.
|
||||
*
|
||||
* @param injector Injector.
|
||||
* @param workshopStrategy Assessment strategy name.
|
||||
* @return The component, undefined if not found.
|
||||
*/
|
||||
getComponentForPlugin(workshopStrategy: string): Type<unknown> | undefined {
|
||||
return this.executeFunctionOnEnabled(workshopStrategy, 'getComponent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare original values to be shown and compared depending on the strategy selected.
|
||||
*
|
||||
* @param workshopStrategy Workshop strategy.
|
||||
* @param form Original data of the form.
|
||||
* @param workshopId Workshop ID.
|
||||
* @return Resolved with original values sorted.
|
||||
*/
|
||||
getOriginalValues(
|
||||
workshopStrategy: string,
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
workshopId: number,
|
||||
): Promise<AddonModWorkshopGetAssessmentFormFieldsParsedData[]> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'getOriginalValues', [form, workshopId]) || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||
*
|
||||
* @param workshopStrategy Workshop strategy.
|
||||
* @param originalValues Original values of the form.
|
||||
* @param currentValues Current values of the form.
|
||||
* @return True if data has changed, false otherwise.
|
||||
*/
|
||||
hasDataChanged(
|
||||
workshopStrategy: string,
|
||||
originalValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
): boolean {
|
||||
return this.executeFunctionOnEnabled(workshopStrategy, 'hasDataChanged', [originalValues, currentValues]) || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||
*
|
||||
* @param workshopStrategy Workshop strategy to follow.
|
||||
* @param currentValues Current values of the form.
|
||||
* @param form Assessment form data.
|
||||
* @return Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||
*/
|
||||
prepareAssessmentData(
|
||||
workshopStrategy: string,
|
||||
currentValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
): Promise<CoreFormFields<unknown> | undefined> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'prepareAssessmentData', [currentValues, form]));
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonWorkshopAssessmentStrategyDelegate = makeSingleton(AddonWorkshopAssessmentStrategyDelegateService);
|
|
@ -0,0 +1,214 @@
|
|||
// (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 { CoreSiteSchema } from '@services/sites';
|
||||
import { AddonModWorkshopAction } from '../workshop';
|
||||
|
||||
/**
|
||||
* Database variables for AddonModWorkshopOfflineProvider.
|
||||
*/
|
||||
export const SUBMISSIONS_TABLE = 'addon_mod_workshop_submissions';
|
||||
export const ASSESSMENTS_TABLE = 'addon_mod_workshop_assessments';
|
||||
export const EVALUATE_SUBMISSIONS_TABLE = 'addon_mod_workshop_evaluate_submissions';
|
||||
export const EVALUATE_ASSESSMENTS_TABLE = 'addon_mod_workshop_evaluate_assessments';
|
||||
|
||||
export const ADDON_MOD_WORKSHOP_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'AddonModWorkshopOfflineProvider',
|
||||
version: 1,
|
||||
tables: [
|
||||
{
|
||||
name: SUBMISSIONS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'workshopid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'action',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'submissionid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'attachmentsid',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'timemodified',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['workshopid', 'action'],
|
||||
},
|
||||
{
|
||||
name: ASSESSMENTS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'workshopid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'assessmentid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'inputdata',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'timemodified',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['workshopid', 'assessmentid'],
|
||||
},
|
||||
{
|
||||
name: EVALUATE_SUBMISSIONS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'workshopid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'submissionid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'timemodified',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'feedbacktext',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'published',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'gradeover',
|
||||
type: 'TEXT',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['workshopid', 'submissionid'],
|
||||
},
|
||||
{
|
||||
name: EVALUATE_ASSESSMENTS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'workshopid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'assessmentid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'timemodified',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'feedbacktext',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'weight',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'gradinggradeover',
|
||||
type: 'TEXT',
|
||||
},
|
||||
],
|
||||
primaryKeys: ['workshopid', 'assessmentid'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Data about workshop submissions to sync.
|
||||
*/
|
||||
export type AddonModWorkshopSubmissionDBRecord = {
|
||||
workshopid: number; // Primary key.
|
||||
action: AddonModWorkshopAction; // Primary key.
|
||||
submissionid: number;
|
||||
courseid: number;
|
||||
title: string;
|
||||
content: string;
|
||||
attachmentsid: string;
|
||||
timemodified: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data about workshop assessments to sync.
|
||||
*/
|
||||
export type AddonModWorkshopAssessmentDBRecord = {
|
||||
workshopid: number; // Primary key.
|
||||
assessmentid: number; // Primary key.
|
||||
courseid: number;
|
||||
inputdata: string;
|
||||
timemodified: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data about workshop evaluate submissions to sync.
|
||||
*/
|
||||
export type AddonModWorkshopEvaluateSubmissionDBRecord = {
|
||||
workshopid: number; // Primary key.
|
||||
submissionid: number; // Primary key.
|
||||
courseid: number;
|
||||
timemodified: number;
|
||||
feedbacktext: string;
|
||||
published: number;
|
||||
gradeover: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data about workshop evaluate assessments to sync.
|
||||
*/
|
||||
export type AddonModWorkshopEvaluateAssessmentDBRecord = {
|
||||
workshopid: number; // Primary key.
|
||||
assessmentid: number; // Primary key.
|
||||
courseid: number;
|
||||
timemodified: number;
|
||||
feedbacktext: string;
|
||||
weight: number;
|
||||
gradinggradeover: string;
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModWorkshopProvider, AddonModWorkshop } from '../workshop';
|
||||
/**
|
||||
* Handler to treat links to workshop.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
|
||||
|
||||
name = 'AddonModWorkshopLinkHandler';
|
||||
|
||||
constructor() {
|
||||
super(AddonModWorkshopProvider.COMPONENT, 'workshop', 'w');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
isEnabled(siteId: string): Promise<boolean> {
|
||||
return AddonModWorkshop.isPluginEnabled(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopIndexLinkHandler = makeSingleton(AddonModWorkshopIndexLinkHandlerService);
|
|
@ -0,0 +1,42 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModWorkshop } from '../workshop';
|
||||
|
||||
/**
|
||||
* Handler to treat links to workshop list page.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopListLinkHandlerService extends CoreContentLinksModuleListHandler {
|
||||
|
||||
name = 'AddonModWorkshopListLinkHandler';
|
||||
|
||||
constructor() {
|
||||
super('AddonModWorkshop', 'workshop');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return AddonModWorkshop.isPluginEnabled();
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopListLinkHandler = makeSingleton(AddonModWorkshopListLinkHandlerService);
|
|
@ -0,0 +1,79 @@
|
|||
// (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 { CoreConstants } from '@/core/constants';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModWorkshopIndexComponent } from '../../components/index';
|
||||
import { AddonModWorkshop } from '../workshop';
|
||||
|
||||
/**
|
||||
* Handler to support workshop modules.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopModuleHandlerService implements CoreCourseModuleHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'mod_workshop';
|
||||
|
||||
name = 'AddonModWorkshop';
|
||||
modName = 'workshop';
|
||||
|
||||
supportedFeatures = {
|
||||
[CoreConstants.FEATURE_GROUPS]: true,
|
||||
[CoreConstants.FEATURE_GROUPINGS]: true,
|
||||
[CoreConstants.FEATURE_MOD_INTRO]: true,
|
||||
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
|
||||
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
|
||||
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
||||
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
|
||||
[CoreConstants.FEATURE_PLAGIARISM]: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return AddonModWorkshop.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
|
||||
return {
|
||||
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
|
||||
title: module.name,
|
||||
class: 'addon-mod_workshop-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
|
||||
options = options || {};
|
||||
options.params = options.params || {};
|
||||
Object.assign(options.params, { module });
|
||||
const routeParams = '/' + courseId + '/' + module.id;
|
||||
|
||||
CoreNavigator.navigateToSitePath(AddonModWorkshopModuleHandlerService.PAGE_NAME + routeParams, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getMainComponent(): Promise<Type<unknown>> {
|
||||
return AddonModWorkshopIndexComponent;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopModuleHandler = makeSingleton(AddonModWorkshopModuleHandlerService);
|
|
@ -0,0 +1,399 @@
|
|||
// (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 { AddonModDataSyncResult } from '@addons/mod/data/services/data-sync';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
||||
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreGroup, CoreGroups } from '@services/groups';
|
||||
import { CoreSites, CoreSitesReadingStrategy, CoreSitesCommonWSOptions } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreWSExternalFile, CoreWSFile } from '@services/ws';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import {
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshop,
|
||||
AddonModWorkshopPhase,
|
||||
AddonModWorkshopGradesData,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
} from '../workshop';
|
||||
import { AddonModWorkshopHelper } from '../workshop-helper';
|
||||
import { AddonModWorkshopSync } from '../workshop-sync';
|
||||
|
||||
/**
|
||||
* Handler to prefetch workshops.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
|
||||
|
||||
name = 'AddonModWorkshop';
|
||||
modName = 'workshop';
|
||||
component = AddonModWorkshopProvider.COMPONENT;
|
||||
updatesNames = new RegExp('^configuration$|^.*files$|^completion|^gradeitems$|^outcomes$|^submissions$|^assessments$' +
|
||||
'|^assessmentgrades$|^usersubmissions$|^userassessments$|^userassessmentgrades$|^userassessmentgrades$');
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSFile[]> {
|
||||
const info = await this.getWorkshopInfoHelper(module, courseId, { omitFail: true });
|
||||
|
||||
return info.files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get all workshop info just once.
|
||||
*
|
||||
* @param module Module to get the files.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved with the info fetched.
|
||||
*/
|
||||
protected async getWorkshopInfoHelper(
|
||||
module: CoreCourseAnyModuleData,
|
||||
courseId: number,
|
||||
options: AddonModWorkshopGetInfoOptions = {},
|
||||
): Promise<{ workshop?: AddonModWorkshopData; groups: CoreGroup[]; files: CoreWSFile[]}> {
|
||||
let groups: CoreGroup[] = [];
|
||||
let files: CoreWSFile[] = [];
|
||||
let workshop: AddonModWorkshopData | undefined;
|
||||
let access: AddonModWorkshopGetWorkshopAccessInformationWSResponse | undefined;
|
||||
|
||||
const modOptions = {
|
||||
cmId: module.id,
|
||||
...options, // Include all options.
|
||||
};
|
||||
|
||||
try {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
const userId = site.getUserId();
|
||||
const workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
|
||||
|
||||
files = this.getIntroFilesFromInstance(module, workshop);
|
||||
files = files.concat(workshop.instructauthorsfiles || []).concat(workshop.instructreviewersfiles || []);
|
||||
|
||||
access = await AddonModWorkshop.getWorkshopAccessInformation(workshop.id, modOptions);
|
||||
if (access.canviewallsubmissions) {
|
||||
const groupInfo = await CoreGroups.getActivityGroupInfo(module.id, false, undefined, options.siteId);
|
||||
if (!groupInfo.groups || groupInfo.groups.length == 0) {
|
||||
groupInfo.groups = [{ id: 0, name: '' }];
|
||||
}
|
||||
groups = groupInfo.groups;
|
||||
}
|
||||
|
||||
const phases = await AddonModWorkshop.getUserPlanPhases(workshop.id, modOptions);
|
||||
|
||||
// Get submission phase info.
|
||||
const submissionPhase = phases[AddonModWorkshopPhase.PHASE_SUBMISSION];
|
||||
const canSubmit = AddonModWorkshopHelper.canSubmit(workshop, access, submissionPhase.tasks);
|
||||
const canAssess = AddonModWorkshopHelper.canAssess(workshop, access);
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (canSubmit) {
|
||||
promises.push(AddonModWorkshopHelper.getUserSubmission(workshop.id, {
|
||||
userId,
|
||||
cmId: module.id,
|
||||
}).then((submission) => {
|
||||
if (submission) {
|
||||
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
|
||||
}
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
|
||||
promises.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions).then(async (submissions) => {
|
||||
|
||||
await Promise.all(submissions.map(async (submission) => {
|
||||
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
|
||||
|
||||
const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop!.id, submission.id, {
|
||||
cmId: module.id,
|
||||
});
|
||||
|
||||
assessments.forEach((assessment) => {
|
||||
files = files.concat(assessment.feedbackattachmentfiles)
|
||||
.concat(assessment.feedbackcontentfiles);
|
||||
});
|
||||
|
||||
if (workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
|
||||
await Promise.all(assessments.map((assessment) =>
|
||||
AddonModWorkshopHelper.getReviewerAssessmentById(workshop!.id, assessment.id)));
|
||||
}
|
||||
}));
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
// Get assessment files.
|
||||
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
|
||||
promises.push(AddonModWorkshopHelper.getReviewerAssessments(workshop.id, modOptions).then((assessments) => {
|
||||
assessments.forEach((assessment) => {
|
||||
files = files.concat(<CoreWSExternalFile[]>assessment.feedbackattachmentfiles)
|
||||
.concat(assessment.feedbackcontentfiles);
|
||||
});
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return {
|
||||
workshop,
|
||||
groups,
|
||||
files: files.filter((file) => typeof file !== 'undefined'),
|
||||
};
|
||||
} catch (error) {
|
||||
if (options.omitFail) {
|
||||
// Any error, return the info we have.
|
||||
return {
|
||||
workshop,
|
||||
groups,
|
||||
files: files.filter((file) => typeof file !== 'undefined'),
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||
await AddonModWorkshop.invalidateContent(moduleId, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @return Whether the module can be downloaded. The promise should never be rejected.
|
||||
*/
|
||||
async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
||||
const workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, {
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
});
|
||||
|
||||
const accessData = await AddonModWorkshop.getWorkshopAccessInformation(workshop.id, { cmId: module.id });
|
||||
|
||||
// Check if workshop is setup by phase.
|
||||
return accessData.canswitchphase || workshop.phase > AddonModWorkshopPhase.PHASE_SETUP;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return AddonModWorkshop.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
|
||||
return this.prefetchPackage(module, courseId, this.prefetchWorkshop.bind(this, module, courseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all the grades reports for all the groups and then returns only unique grades.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param groups Array of groups in the activity.
|
||||
* @param cmId Module ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return All unique entries.
|
||||
*/
|
||||
protected async getAllGradesReport(
|
||||
workshopId: number,
|
||||
groups: CoreGroup[],
|
||||
cmId: number,
|
||||
siteId: string,
|
||||
): Promise<AddonModWorkshopGradesData[]> {
|
||||
const promises: Promise<AddonModWorkshopGradesData[]>[] = [];
|
||||
|
||||
groups.forEach((group) => {
|
||||
promises.push(AddonModWorkshop.fetchAllGradeReports(workshopId, { groupId: group.id, cmId, siteId }));
|
||||
});
|
||||
|
||||
const grades = await Promise.all(promises);
|
||||
const uniqueGrades: Record<number, AddonModWorkshopGradesData> = {};
|
||||
|
||||
grades.forEach((groupGrades) => {
|
||||
groupGrades.forEach((grade) => {
|
||||
if (grade.submissionid) {
|
||||
uniqueGrades[grade.submissionid] = grade;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return CoreUtils.objectToArray(uniqueGrades);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a workshop.
|
||||
*
|
||||
* @param module The module object returned by WS.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchWorkshop(module: CoreCourseAnyModuleData, courseId: number, siteId: string): Promise<void> {
|
||||
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
const userIds: number[] = [];
|
||||
const commonOptions = {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
const modOptions = {
|
||||
cmId: module.id,
|
||||
...commonOptions, // Include all common options.
|
||||
};
|
||||
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const currentUserId = site.getUserId();
|
||||
|
||||
// Prefetch the workshop data.
|
||||
const info = await this.getWorkshopInfoHelper(module, courseId, commonOptions);
|
||||
const workshop = info.workshop!;
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const assessmentIds: number[] = [];
|
||||
|
||||
promises.push(CoreFilepool.addFilesToQueue(siteId, info.files, this.component, module.id));
|
||||
|
||||
promises.push(AddonModWorkshop.getWorkshopAccessInformation(workshop.id, modOptions).then(async (access) => {
|
||||
const phases = await AddonModWorkshop.getUserPlanPhases(workshop.id, modOptions);
|
||||
|
||||
// Get submission phase info.
|
||||
const submissionPhase = phases[AddonModWorkshopPhase.PHASE_SUBMISSION];
|
||||
const canSubmit = AddonModWorkshopHelper.canSubmit(workshop, access, submissionPhase.tasks);
|
||||
const canAssess = AddonModWorkshopHelper.canAssess(workshop, access);
|
||||
const promises2: Promise<unknown>[] = [];
|
||||
|
||||
if (canSubmit) {
|
||||
promises2.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions));
|
||||
// Add userId to the profiles to prefetch.
|
||||
userIds.push(currentUserId);
|
||||
}
|
||||
|
||||
let reportPromise: Promise<unknown> = Promise.resolve();
|
||||
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
|
||||
// eslint-disable-next-line promise/no-nesting
|
||||
reportPromise = this.getAllGradesReport(workshop.id, info.groups, module.id, siteId).then((grades) => {
|
||||
grades.forEach((grade) => {
|
||||
userIds.push(grade.userid);
|
||||
grade.submissiongradeoverby && userIds.push(grade.submissiongradeoverby);
|
||||
|
||||
grade.reviewedby && grade.reviewedby.forEach((assessment) => {
|
||||
userIds.push(assessment.userid);
|
||||
assessmentIds[assessment.assessmentid] = assessment.assessmentid;
|
||||
});
|
||||
|
||||
grade.reviewerof && grade.reviewerof.forEach((assessment) => {
|
||||
userIds.push(assessment.userid);
|
||||
assessmentIds[assessment.assessmentid] = assessment.assessmentid;
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
|
||||
// Wait the report promise to finish to override assessments array if needed.
|
||||
reportPromise = reportPromise.finally(async () => {
|
||||
const revAssessments = await AddonModWorkshopHelper.getReviewerAssessments(workshop.id, {
|
||||
userId: currentUserId,
|
||||
cmId: module.id,
|
||||
siteId,
|
||||
});
|
||||
|
||||
let files: CoreWSExternalFile[] = []; // Files in each submission.
|
||||
|
||||
revAssessments.forEach((assessment) => {
|
||||
if (assessment.submission?.authorid == currentUserId) {
|
||||
promises.push(AddonModWorkshop.getAssessment(
|
||||
workshop.id,
|
||||
assessment.id,
|
||||
modOptions,
|
||||
));
|
||||
}
|
||||
userIds.push(assessment.reviewerid);
|
||||
userIds.push(assessment.gradinggradeoverby);
|
||||
assessmentIds[assessment.id] = assessment.id;
|
||||
|
||||
files = files.concat(assessment.submission?.attachmentfiles || [])
|
||||
.concat(assessment.submission?.contentfiles || []);
|
||||
});
|
||||
|
||||
await CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id);
|
||||
});
|
||||
}
|
||||
|
||||
reportPromise = reportPromise.finally(() => {
|
||||
if (assessmentIds.length > 0) {
|
||||
return Promise.all(assessmentIds.map((assessmentId) =>
|
||||
AddonModWorkshop.getAssessmentForm(workshop.id, assessmentId, modOptions)));
|
||||
}
|
||||
});
|
||||
promises2.push(reportPromise);
|
||||
|
||||
if (workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
|
||||
promises2.push(AddonModWorkshop.getGrades(workshop.id, modOptions));
|
||||
if (access.canviewpublishedsubmissions) {
|
||||
promises2.push(AddonModWorkshop.getSubmissions(workshop.id, modOptions));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises2);
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
// Add Basic Info to manage links.
|
||||
promises.push(CoreCourse.getModuleBasicInfoByInstance(workshop.id, 'workshop', siteId));
|
||||
promises.push(CoreCourse.getModuleBasicGradeInfo(module.id, siteId));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Prefetch user profiles.
|
||||
await CoreUser.prefetchProfiles(userIds, courseId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModDataSyncResult> {
|
||||
return AddonModWorkshopSync.syncWorkshop(module.instance!, siteId);
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopPrefetchHandler = makeSingleton(AddonModWorkshopPrefetchHandlerService);
|
||||
|
||||
/**
|
||||
* Options to pass to getWorkshopInfoHelper.
|
||||
*/
|
||||
export type AddonModWorkshopGetInfoOptions = CoreSitesCommonWSOptions & {
|
||||
omitFail?: boolean; // True to always return even if fails.
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreCronHandler } from '@services/cron';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModWorkshopSync } from '../workshop-sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopSyncCronHandlerService implements CoreCronHandler {
|
||||
|
||||
name = 'AddonModWorkshopSyncCronHandler';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
execute(siteId?: string, force?: boolean): Promise<void> {
|
||||
return AddonModWorkshopSync.syncAllWorkshops(siteId, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getInterval(): number {
|
||||
return AddonModWorkshopSync.syncInterval;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopSyncCronHandler = makeSingleton(AddonModWorkshopSyncCronHandlerService);
|
|
@ -0,0 +1,638 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { FileEntry } from '@ionic-native/file';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreFileEntry } from '@services/file-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import { AddonModWorkshopAssessmentStrategyFieldErrors } from '../components/assessment-strategy/assessment-strategy';
|
||||
import { AddonWorkshopAssessmentStrategyDelegate } from './assessment-strategy-delegate';
|
||||
import {
|
||||
AddonModWorkshopExampleMode,
|
||||
AddonModWorkshopPhase,
|
||||
AddonModWorkshopUserOptions,
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshop,
|
||||
AddonModWorkshopSubmissionData,
|
||||
AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
AddonModWorkshopPhaseTaskData,
|
||||
AddonModWorkshopSubmissionAssessmentData,
|
||||
AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
AddonModWorkshopAction,
|
||||
AddonModWorkshopOverallFeedbackMode,
|
||||
AddonModWorkshopGetAssessmentFormFieldsParsedData,
|
||||
} from './workshop';
|
||||
import { AddonModWorkshopOffline, AddonModWorkshopOfflineSubmission } from './workshop-offline';
|
||||
|
||||
/**
|
||||
* Helper to gather some common functions for workshop.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopHelperProvider {
|
||||
|
||||
/**
|
||||
* Get a task by code.
|
||||
*
|
||||
* @param tasks Array of tasks.
|
||||
* @param taskCode Unique task code.
|
||||
* @return Task requested
|
||||
*/
|
||||
getTask(tasks: AddonModWorkshopPhaseTaskData[], taskCode: string): AddonModWorkshopPhaseTaskData | undefined {
|
||||
return tasks.find((task) => task.code == taskCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is task code is done.
|
||||
*
|
||||
* @param tasks Array of tasks.
|
||||
* @param taskCode Unique task code.
|
||||
* @return True if task is completed.
|
||||
*/
|
||||
isTaskDone(tasks: AddonModWorkshopPhaseTaskData[], taskCode: string): boolean {
|
||||
const task = this.getTask(tasks, taskCode);
|
||||
|
||||
if (task) {
|
||||
return !!task.completed;
|
||||
}
|
||||
|
||||
// Task not found, assume true.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if a user can submit a workshop.
|
||||
*
|
||||
* @param workshop Workshop info.
|
||||
* @param access Access information.
|
||||
* @param tasks Array of tasks.
|
||||
* @return True if the user can submit the workshop.
|
||||
*/
|
||||
canSubmit(
|
||||
workshop: AddonModWorkshopData,
|
||||
access: AddonModWorkshopGetWorkshopAccessInformationWSResponse,
|
||||
tasks: AddonModWorkshopPhaseTaskData[],
|
||||
): boolean {
|
||||
const examplesMust = workshop.useexamples &&
|
||||
workshop.examplesmode == AddonModWorkshopExampleMode.EXAMPLES_BEFORE_SUBMISSION;
|
||||
const examplesDone = access.canmanageexamples ||
|
||||
workshop.examplesmode == AddonModWorkshopExampleMode.EXAMPLES_VOLUNTARY ||
|
||||
this.isTaskDone(tasks, 'examples');
|
||||
|
||||
return workshop.phase > AddonModWorkshopPhase.PHASE_SETUP && access.cansubmit && (!examplesMust || examplesDone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if a user can assess a workshop.
|
||||
*
|
||||
* @param workshop Workshop info.
|
||||
* @param access Access information.
|
||||
* @return True if the user can assess the workshop.
|
||||
*/
|
||||
canAssess(workshop: AddonModWorkshopData, access: AddonModWorkshopGetWorkshopAccessInformationWSResponse): boolean {
|
||||
const examplesMust = workshop.useexamples &&
|
||||
workshop.examplesmode == AddonModWorkshopExampleMode.EXAMPLES_BEFORE_ASSESSMENT;
|
||||
|
||||
const examplesDone = access.canmanageexamples;
|
||||
|
||||
return !examplesMust || examplesDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a particular user submission from the submission list.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param options Other options.
|
||||
* @return Resolved with the submission, resolved with false if not found.
|
||||
*/
|
||||
async getUserSubmission(
|
||||
workshopId: number,
|
||||
options: AddonModWorkshopUserOptions = {},
|
||||
): Promise<AddonModWorkshopSubmissionData | undefined> {
|
||||
const userId = options.userId || CoreSites.getCurrentSiteUserId();
|
||||
|
||||
const submissions = await AddonModWorkshop.getSubmissions(workshopId, options);
|
||||
|
||||
return submissions.find((submission) => submission.authorid == userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a particular submission. It will use prefetched data if fetch fails.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param submissionId Submission ID.
|
||||
* @param options Other options.
|
||||
* @return Resolved with the submission, resolved with false if not found.
|
||||
*/
|
||||
async getSubmissionById(
|
||||
workshopId: number,
|
||||
submissionId: number,
|
||||
options: AddonModWorkshopUserOptions = {},
|
||||
): Promise<AddonModWorkshopSubmissionData> {
|
||||
try {
|
||||
return await AddonModWorkshop.getSubmission(workshopId, submissionId, options);
|
||||
} catch {
|
||||
const submissions = await AddonModWorkshop.getSubmissions(workshopId, options);
|
||||
|
||||
const submission = submissions.find((submission) => submission.id == submissionId);
|
||||
|
||||
if (!submission) {
|
||||
throw new CoreError('Submission not found');
|
||||
}
|
||||
|
||||
return submission;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a particular assesment. It will use prefetched data if fetch fails. It will add assessment form data.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param options Other options.
|
||||
* @return Resolved with the assessment.
|
||||
*/
|
||||
async getReviewerAssessmentById(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
options: AddonModWorkshopUserOptions = {},
|
||||
): Promise<AddonModWorkshopSubmissionAssessmentWithFormData> {
|
||||
let assessment: AddonModWorkshopSubmissionAssessmentWithFormData | undefined;
|
||||
|
||||
try {
|
||||
assessment = await AddonModWorkshop.getAssessment(workshopId, assessmentId, options);
|
||||
} catch (error) {
|
||||
const assessments = await AddonModWorkshop.getReviewerAssessments(workshopId, options);
|
||||
assessment = assessments.find((assessment_1) => assessment_1.id == assessmentId);
|
||||
|
||||
if (!assessment) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
assessment.form = await AddonModWorkshop.getAssessmentForm(workshopId, assessmentId, options);
|
||||
|
||||
return assessment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the assessment of the given user and all the related data.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when the workshop data is retrieved.
|
||||
*/
|
||||
async getReviewerAssessments(
|
||||
workshopId: number,
|
||||
options: AddonModWorkshopUserOptions = {},
|
||||
): Promise<AddonModWorkshopSubmissionAssessmentWithFormData[]> {
|
||||
options.siteId = options.siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
const assessments: AddonModWorkshopSubmissionAssessmentWithFormData[] =
|
||||
await AddonModWorkshop.getReviewerAssessments(workshopId, options);
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
assessments.forEach((assessment) => {
|
||||
promises.push(this.getSubmissionById(workshopId, assessment.submissionid, options).then((submission) => {
|
||||
assessment.submission = submission;
|
||||
|
||||
return;
|
||||
}));
|
||||
promises.push(AddonModWorkshop.getAssessmentForm(workshopId, assessment.id, options).then((assessmentForm) => {
|
||||
assessment.form = assessmentForm;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
return assessments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete stored attachment files for a submission.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when deleted.
|
||||
*/
|
||||
async deleteSubmissionStoredFiles(workshopId: number, siteId?: string): Promise<void> {
|
||||
const folderPath = await AddonModWorkshopOffline.getSubmissionFolder(workshopId, siteId);
|
||||
|
||||
// Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists.
|
||||
await CoreUtils.ignoreErrors(CoreFile.removeDir(folderPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||
* to be submitted later.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param files List of files.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
async storeSubmissionFiles(
|
||||
workshopId: number,
|
||||
files: CoreFileEntry[],
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult> {
|
||||
// Get the folder where to store the files.
|
||||
const folderPath = await AddonModWorkshopOffline.getSubmissionFolder(workshopId, siteId);
|
||||
|
||||
return CoreFileUploader.storeFilesToUpload(folderPath, files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload or store some files for a submission, depending if the user is offline or not.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param submissionId If not editing, it will refer to timecreated.
|
||||
* @param files List of files.
|
||||
* @param editing If the submission is being edited or added otherwise.
|
||||
* @param offline True if files sould be stored for offline, false to upload them.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success.
|
||||
*/
|
||||
uploadOrStoreSubmissionFiles(
|
||||
workshopId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: true,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult>;
|
||||
uploadOrStoreSubmissionFiles(
|
||||
workshopId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: false,
|
||||
siteId?: string,
|
||||
): Promise<number>;
|
||||
uploadOrStoreSubmissionFiles(
|
||||
workshopId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: boolean,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult | number> {
|
||||
if (offline) {
|
||||
return this.storeSubmissionFiles(workshopId, files, siteId);
|
||||
}
|
||||
|
||||
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of stored attachment files for a submission. See AddonModWorkshopHelperProvider#storeFiles.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param submissionId If not editing, it will refer to timecreated.
|
||||
* @param editing If the submission is being edited or added otherwise.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getStoredSubmissionFiles(
|
||||
workshopId: number,
|
||||
siteId?: string,
|
||||
): Promise<FileEntry[]> {
|
||||
const folderPath = await AddonModWorkshopOffline.getSubmissionFolder(workshopId, siteId);
|
||||
|
||||
// Ignore not found files.
|
||||
return CoreUtils.ignoreErrors(CoreFileUploader.getStoredFiles(folderPath), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of stored attachment files for a submission and online files also. See AddonModWorkshopHelperProvider#storeFiles.
|
||||
*
|
||||
* @param filesObject Files object combining offline and online information.
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getSubmissionFilesFromOfflineFilesObject(
|
||||
filesObject: CoreFileUploaderStoreFilesResult,
|
||||
workshopId: number,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileEntry[]> {
|
||||
const folderPath = await AddonModWorkshopOffline.getSubmissionFolder(workshopId, siteId);
|
||||
|
||||
return CoreFileUploader.getStoredFilesFromOfflineFilesObject(filesObject, folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete stored attachment files for an assessment.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when deleted.
|
||||
*/
|
||||
async deleteAssessmentStoredFiles(workshopId: number, assessmentId: number, siteId?: string): Promise<void> {
|
||||
const folderPath = await AddonModWorkshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId);
|
||||
|
||||
// Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists.
|
||||
await CoreUtils.ignoreErrors(CoreFile.removeDir(folderPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||
* to be submitted later.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param files List of files.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
async storeAssessmentFiles(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
files: CoreFileEntry[],
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult> {
|
||||
// Get the folder where to store the files.
|
||||
const folderPath = await AddonModWorkshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId);
|
||||
|
||||
return CoreFileUploader.storeFilesToUpload(folderPath, files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload or store some files for an assessment, depending if the user is offline or not.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId ID.
|
||||
* @param files List of files.
|
||||
* @param offline True if files sould be stored for offline, false to upload them.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if success.
|
||||
*/
|
||||
uploadOrStoreAssessmentFiles(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: true,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult>;
|
||||
uploadOrStoreAssessmentFiles(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: false,
|
||||
siteId?: string,
|
||||
): Promise<number>
|
||||
uploadOrStoreAssessmentFiles(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
files: CoreFileEntry[],
|
||||
offline: boolean,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileUploaderStoreFilesResult | number> {
|
||||
if (offline) {
|
||||
return this.storeAssessmentFiles(workshopId, assessmentId, files, siteId);
|
||||
}
|
||||
|
||||
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of stored attachment files for an assessment. See AddonModWorkshopHelperProvider#storeFiles.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getStoredAssessmentFiles(workshopId: number, assessmentId: number, siteId?: string): Promise<FileEntry[]> {
|
||||
const folderPath = await AddonModWorkshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId);
|
||||
|
||||
// Ignore not found files.
|
||||
return CoreUtils.ignoreErrors(CoreFileUploader.getStoredFiles(folderPath), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of stored attachment files for an assessment and online files also. See AddonModWorkshopHelperProvider#storeFiles.
|
||||
*
|
||||
* @param filesObject Files object combining offline and online information.
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async getAssessmentFilesFromOfflineFilesObject(
|
||||
filesObject: CoreFileUploaderStoreFilesResult,
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
siteId?: string,
|
||||
): Promise<CoreFileEntry[]> {
|
||||
const folderPath = await AddonModWorkshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId);
|
||||
|
||||
return CoreFileUploader.getStoredFilesFromOfflineFilesObject(filesObject, folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies offline data to submission.
|
||||
*
|
||||
* @param submission Submission object to be modified.
|
||||
* @param actions Offline actions to be applied to the given submission.
|
||||
* @return Promise resolved with the files.
|
||||
*/
|
||||
async applyOfflineData(
|
||||
submission?: AddonModWorkshopSubmissionDataWithOfflineData,
|
||||
actions: AddonModWorkshopOfflineSubmission[] = [],
|
||||
): Promise<AddonModWorkshopSubmissionDataWithOfflineData | undefined> {
|
||||
if (actions.length == 0) {
|
||||
return submission;
|
||||
}
|
||||
|
||||
if (typeof submission == 'undefined') {
|
||||
submission = {
|
||||
id: 0,
|
||||
workshopid: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
timemodified: 0,
|
||||
example: false,
|
||||
authorid: 0,
|
||||
timecreated: 0,
|
||||
contenttrust: 0,
|
||||
attachment: 0,
|
||||
published: false,
|
||||
late: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let attachmentsId: CoreFileUploaderStoreFilesResult | undefined;
|
||||
const workshopId = actions[0].workshopid;
|
||||
|
||||
actions.forEach((action) => {
|
||||
switch (action.action) {
|
||||
case AddonModWorkshopAction.ADD:
|
||||
case AddonModWorkshopAction.UPDATE:
|
||||
submission!.title = action.title;
|
||||
submission!.content = action.content;
|
||||
submission!.title = action.title;
|
||||
submission!.courseid = action.courseid;
|
||||
submission!.submissionmodified = action.timemodified / 1000;
|
||||
submission!.offline = true;
|
||||
attachmentsId = action.attachmentsid as CoreFileUploaderStoreFilesResult;
|
||||
break;
|
||||
case AddonModWorkshopAction.DELETE:
|
||||
submission!.deleted = true;
|
||||
submission!.submissionmodified = action.timemodified / 1000;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
// Check offline files for latest attachmentsid.
|
||||
if (attachmentsId) {
|
||||
submission.attachmentfiles =
|
||||
await this.getSubmissionFilesFromOfflineFilesObject(attachmentsId, workshopId);
|
||||
} else {
|
||||
submission.attachmentfiles = [];
|
||||
}
|
||||
|
||||
return submission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare assessment data to be sent to the server.
|
||||
*
|
||||
* @param workshop Workshop object.
|
||||
* @param selectedValues Assessment current values
|
||||
* @param feedbackText Feedback text.
|
||||
* @param feedbackFiles Feedback attachments.
|
||||
* @param form Assessment form original data.
|
||||
* @param attachmentsId The draft file area id for attachments.
|
||||
* @return Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||
*/
|
||||
async prepareAssessmentData(
|
||||
workshop: AddonModWorkshopData,
|
||||
selectedValues: AddonModWorkshopGetAssessmentFormFieldsParsedData[],
|
||||
feedbackText: string,
|
||||
form: AddonModWorkshopGetAssessmentFormDefinitionData,
|
||||
attachmentsId: CoreFileUploaderStoreFilesResult | number = 0,
|
||||
): Promise<CoreFormFields<unknown>> {
|
||||
|
||||
if (workshop.overallfeedbackmode == AddonModWorkshopOverallFeedbackMode.ENABLED_REQUIRED && !feedbackText) {
|
||||
const errors: AddonModWorkshopAssessmentStrategyFieldErrors =
|
||||
{ feedbackauthor: Translate.instant('core.err_required') };
|
||||
throw errors;
|
||||
}
|
||||
|
||||
const data =
|
||||
(await AddonWorkshopAssessmentStrategyDelegate.prepareAssessmentData(workshop.strategy!, selectedValues, form)) || {};
|
||||
data.feedbackauthor = feedbackText;
|
||||
data.feedbackauthorattachmentsid = attachmentsId;
|
||||
data.nodims = form.dimenssionscount;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the real value of a grade based on real_grade_value.
|
||||
*
|
||||
* @param value Percentual value from 0 to 100.
|
||||
* @param max The maximal grade.
|
||||
* @param decimals Decimals to show in the formatted grade.
|
||||
* @return Real grade formatted.
|
||||
*/
|
||||
protected realGradeValueHelper(value?: number | string, max = 0, decimals = 0): string | undefined {
|
||||
if (typeof value == 'string') {
|
||||
// Already treated.
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value == null || typeof value == 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (max == 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
value = CoreTextUtils.roundToDecimals(max * value / 100, decimals);
|
||||
|
||||
return CoreUtils.formatFloat(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the real value of a grades of an assessment.
|
||||
*
|
||||
* @param workshop Workshop object.
|
||||
* @param assessment Assessment data.
|
||||
* @return Assessment with real grades.
|
||||
*/
|
||||
realGradeValue(
|
||||
workshop: AddonModWorkshopData,
|
||||
assessment: AddonModWorkshopSubmissionAssessmentWithFormData,
|
||||
): AddonModWorkshopSubmissionAssessmentWithFormData {
|
||||
assessment.grade = this.realGradeValueHelper(assessment.grade, workshop.grade, workshop.gradedecimals);
|
||||
assessment.gradinggrade = this.realGradeValueHelper(assessment.gradinggrade, workshop.gradinggrade, workshop.gradedecimals);
|
||||
|
||||
assessment.gradinggradeover = this.realGradeValueHelper(
|
||||
assessment.gradinggradeover,
|
||||
workshop.gradinggrade,
|
||||
workshop.gradedecimals,
|
||||
);
|
||||
|
||||
return assessment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check grade should be shown
|
||||
*
|
||||
* @param grade Grade to be shown
|
||||
* @return If grade should be shown or not.
|
||||
*/
|
||||
showGrade(grade?: number|string): boolean {
|
||||
return typeof grade !== 'undefined' && grade !== null;
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopHelper = makeSingleton(AddonModWorkshopHelperProvider);
|
||||
|
||||
export type AddonModWorkshopSubmissionAssessmentWithFormData =
|
||||
Omit<AddonModWorkshopSubmissionAssessmentData, 'grade'|'gradinggrade'|'gradinggradeover'|'feedbackattachmentfiles'> & {
|
||||
form?: AddonModWorkshopGetAssessmentFormDefinitionData;
|
||||
submission?: AddonModWorkshopSubmissionData;
|
||||
offline?: boolean;
|
||||
strategy?: string;
|
||||
grade?: string | number;
|
||||
gradinggrade?: string | number;
|
||||
gradinggradeover?: string | number;
|
||||
ownAssessment?: boolean;
|
||||
feedbackauthor?: string;
|
||||
feedbackattachmentfiles: CoreFileEntry[]; // Feedbackattachmentfiles.
|
||||
};
|
||||
|
||||
export type AddonModWorkshopSubmissionDataWithOfflineData = Omit<AddonModWorkshopSubmissionData, 'attachmentfiles'> & {
|
||||
courseid?: number;
|
||||
submissionmodified?: number;
|
||||
offline?: boolean;
|
||||
deleted?: boolean;
|
||||
attachmentfiles?: CoreFileEntry[];
|
||||
reviewedby?: AddonModWorkshopSubmissionAssessmentWithFormData[];
|
||||
reviewerof?: AddonModWorkshopSubmissionAssessmentWithFormData[];
|
||||
gradinggrade?: number;
|
||||
reviewedbydone?: number;
|
||||
reviewerofdone?: number;
|
||||
reviewedbycount?: number;
|
||||
reviewerofcount?: number;
|
||||
};
|
|
@ -0,0 +1,684 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreFormFields } from '@singletons/form';
|
||||
import {
|
||||
AddonModWorkshopAssessmentDBRecord,
|
||||
AddonModWorkshopEvaluateAssessmentDBRecord,
|
||||
AddonModWorkshopEvaluateSubmissionDBRecord,
|
||||
AddonModWorkshopSubmissionDBRecord,
|
||||
ASSESSMENTS_TABLE,
|
||||
EVALUATE_ASSESSMENTS_TABLE,
|
||||
EVALUATE_SUBMISSIONS_TABLE,
|
||||
SUBMISSIONS_TABLE,
|
||||
} from './database/workshop';
|
||||
import { AddonModWorkshopAction } from './workshop';
|
||||
|
||||
/**
|
||||
* Service to handle offline workshop.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopOfflineProvider {
|
||||
|
||||
/**
|
||||
* Get all the workshops ids that have something to be synced.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with workshops id that have something to be synced.
|
||||
*/
|
||||
async getAllWorkshops(siteId?: string): Promise<number[]> {
|
||||
const promiseResults = await Promise.all([
|
||||
this.getAllSubmissions(siteId),
|
||||
this.getAllAssessments(siteId),
|
||||
this.getAllEvaluateSubmissions(siteId),
|
||||
this.getAllEvaluateAssessments(siteId),
|
||||
]);
|
||||
|
||||
const workshopIds: Record<number, number> = {};
|
||||
|
||||
// Get workshops from any offline object all should have workshopid.
|
||||
promiseResults.forEach((offlineObjects) => {
|
||||
offlineObjects.forEach((offlineObject: AddonModWorkshopOfflineSubmission | AddonModWorkshopOfflineAssessment |
|
||||
AddonModWorkshopOfflineEvaluateSubmission | AddonModWorkshopOfflineEvaluateAssessment) => {
|
||||
workshopIds[offlineObject.workshopid] = offlineObject.workshopid;
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(workshopIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is an offline data to be synced.
|
||||
*
|
||||
* @param workshopId Workshop ID to remove.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with boolean: true if has offline data, false otherwise.
|
||||
*/
|
||||
async hasWorkshopOfflineData(workshopId: number, siteId?: string): Promise<boolean> {
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
this.getSubmissions(workshopId, siteId),
|
||||
this.getAssessments(workshopId, siteId),
|
||||
this.getEvaluateSubmissions(workshopId, siteId),
|
||||
this.getEvaluateAssessments(workshopId, siteId),
|
||||
]);
|
||||
|
||||
return results.some((result) => result && result.length);
|
||||
} catch {
|
||||
// No offline data found.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workshop submission action.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param submissionId Submission ID.
|
||||
* @param action Action to be done.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async deleteSubmissionAction(
|
||||
workshopId: number,
|
||||
action: AddonModWorkshopAction,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopSubmissionDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
action: action,
|
||||
};
|
||||
|
||||
await site.getDb().deleteRecords(SUBMISSIONS_TABLE, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all workshop submission actions.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async deleteAllSubmissionActions(workshopId: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopSubmissionDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
};
|
||||
|
||||
await site.getDb().deleteRecords(SUBMISSIONS_TABLE, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the all the submissions to be synced.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the objects to be synced.
|
||||
*/
|
||||
async getAllSubmissions(siteId?: string): Promise<AddonModWorkshopOfflineSubmission[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModWorkshopSubmissionDBRecord>(SUBMISSIONS_TABLE);
|
||||
|
||||
return records.map(this.parseSubmissionRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the submissions of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getSubmissions(workshopId: number, siteId?: string): Promise<AddonModWorkshopOfflineSubmission[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopSubmissionDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
};
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModWorkshopSubmissionDBRecord>(SUBMISSIONS_TABLE, conditions);
|
||||
|
||||
return records.map(this.parseSubmissionRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an specific action of a submission of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param action Action to be done.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getSubmissionAction(
|
||||
workshopId: number,
|
||||
action: AddonModWorkshopAction,
|
||||
siteId?: string,
|
||||
): Promise<AddonModWorkshopOfflineSubmission> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopSubmissionDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
action: action,
|
||||
};
|
||||
|
||||
const record = await site.getDb().getRecord<AddonModWorkshopSubmissionDBRecord>(SUBMISSIONS_TABLE, conditions);
|
||||
|
||||
return this.parseSubmissionRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for adding a submission action to a workshop.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param courseId Course ID the workshop belongs to.
|
||||
* @param title The submission title.
|
||||
* @param content The submission text content.
|
||||
* @param attachmentsId Stored attachments.
|
||||
* @param submissionId Submission Id, if action is add, the time the submission was created.
|
||||
* If set to 0, current time is used.
|
||||
* @param action Action to be done. ['add', 'update', 'delete']
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when submission action is successfully saved.
|
||||
*/
|
||||
async saveSubmission(
|
||||
workshopId: number,
|
||||
courseId: number,
|
||||
title: string,
|
||||
content: string,
|
||||
attachmentsId: CoreFileUploaderStoreFilesResult | undefined,
|
||||
submissionId = 0,
|
||||
action: AddonModWorkshopAction,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const timemodified = CoreTimeUtils.timestamp();
|
||||
|
||||
const submission: AddonModWorkshopSubmissionDBRecord = {
|
||||
workshopid: workshopId,
|
||||
courseid: courseId,
|
||||
title: title,
|
||||
content: content,
|
||||
attachmentsid: JSON.stringify(attachmentsId),
|
||||
action: action,
|
||||
submissionid: submissionId,
|
||||
timemodified: timemodified,
|
||||
};
|
||||
|
||||
await site.getDb().insertRecord(SUBMISSIONS_TABLE, submission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "attachments" column of a submission record.
|
||||
*
|
||||
* @param record Submission record, modified in place.
|
||||
*/
|
||||
protected parseSubmissionRecord(record: AddonModWorkshopSubmissionDBRecord): AddonModWorkshopOfflineSubmission {
|
||||
return {
|
||||
...record,
|
||||
attachmentsid: CoreTextUtils.parseJSON(record.attachmentsid),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workshop assessment.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async deleteAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopAssessmentDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
assessmentid: assessmentId,
|
||||
};
|
||||
|
||||
await site.getDb().deleteRecords(ASSESSMENTS_TABLE, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the all the assessments to be synced.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the objects to be synced.
|
||||
*/
|
||||
async getAllAssessments(siteId?: string): Promise<AddonModWorkshopOfflineAssessment[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModWorkshopAssessmentDBRecord>(ASSESSMENTS_TABLE);
|
||||
|
||||
return records.map(this.parseAssessmentRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the assessments of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getAssessments(workshopId: number, siteId?: string): Promise<AddonModWorkshopOfflineAssessment[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopAssessmentDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
};
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModWorkshopAssessmentDBRecord>(ASSESSMENTS_TABLE, conditions);
|
||||
|
||||
return records.map(this.parseAssessmentRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an specific assessment of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise<AddonModWorkshopOfflineAssessment> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopAssessmentDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
assessmentid: assessmentId,
|
||||
};
|
||||
|
||||
const record = await site.getDb().getRecord<AddonModWorkshopAssessmentDBRecord>(ASSESSMENTS_TABLE, conditions);
|
||||
|
||||
return this.parseAssessmentRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for adding an assessment to a workshop.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param courseId Course ID the workshop belongs to.
|
||||
* @param inputData Assessment data.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when assessment is successfully saved.
|
||||
*/
|
||||
async saveAssessment(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
courseId: number,
|
||||
inputData: CoreFormFields,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const assessment: AddonModWorkshopAssessmentDBRecord = {
|
||||
workshopid: workshopId,
|
||||
courseid: courseId,
|
||||
inputdata: JSON.stringify(inputData),
|
||||
assessmentid: assessmentId,
|
||||
timemodified: CoreTimeUtils.timestamp(),
|
||||
};
|
||||
|
||||
await site.getDb().insertRecord(ASSESSMENTS_TABLE, assessment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "inpudata" column of an assessment record.
|
||||
*
|
||||
* @param record Assessnent record, modified in place.
|
||||
*/
|
||||
protected parseAssessmentRecord(record: AddonModWorkshopAssessmentDBRecord): AddonModWorkshopOfflineAssessment {
|
||||
return {
|
||||
...record,
|
||||
inputdata: CoreTextUtils.parseJSON(record.inputdata),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workshop evaluate submission.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param submissionId Submission ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async deleteEvaluateSubmission(workshopId: number, submissionId: number, siteId?: string): Promise<void> {
|
||||
const conditions: Partial<AddonModWorkshopEvaluateSubmissionDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
submissionid: submissionId,
|
||||
};
|
||||
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
await site.getDb().deleteRecords(EVALUATE_SUBMISSIONS_TABLE, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the all the evaluate submissions to be synced.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the objects to be synced.
|
||||
*/
|
||||
async getAllEvaluateSubmissions(siteId?: string): Promise<AddonModWorkshopOfflineEvaluateSubmission[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModWorkshopEvaluateSubmissionDBRecord>(EVALUATE_SUBMISSIONS_TABLE);
|
||||
|
||||
return records.map(this.parseEvaluateSubmissionRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the evaluate submissions of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getEvaluateSubmissions(workshopId: number, siteId?: string): Promise<AddonModWorkshopOfflineEvaluateSubmission[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopEvaluateSubmissionDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
};
|
||||
|
||||
const records =
|
||||
await site.getDb().getRecords<AddonModWorkshopEvaluateSubmissionDBRecord>(EVALUATE_SUBMISSIONS_TABLE, conditions);
|
||||
|
||||
return records.map(this.parseEvaluateSubmissionRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an specific evaluate submission of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param submissionId Submission ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getEvaluateSubmission(
|
||||
workshopId: number,
|
||||
submissionId: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModWorkshopOfflineEvaluateSubmission> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopEvaluateSubmissionDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
submissionid: submissionId,
|
||||
};
|
||||
|
||||
const record =
|
||||
await site.getDb().getRecord<AddonModWorkshopEvaluateSubmissionDBRecord>(EVALUATE_SUBMISSIONS_TABLE, conditions);
|
||||
|
||||
return this.parseEvaluateSubmissionRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for evaluation a submission to a workshop.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param submissionId Submission ID.
|
||||
* @param courseId Course ID the workshop belongs to.
|
||||
* @param feedbackText The feedback for the author.
|
||||
* @param published Whether to publish the submission for other users.
|
||||
* @param gradeOver The new submission grade (empty for no overriding the grade).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when submission evaluation is successfully saved.
|
||||
*/
|
||||
async saveEvaluateSubmission(
|
||||
workshopId: number,
|
||||
submissionId: number,
|
||||
courseId: number,
|
||||
feedbackText = '',
|
||||
published?: boolean,
|
||||
gradeOver?: string,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const submission: AddonModWorkshopEvaluateSubmissionDBRecord = {
|
||||
workshopid: workshopId,
|
||||
courseid: courseId,
|
||||
submissionid: submissionId,
|
||||
timemodified: CoreTimeUtils.timestamp(),
|
||||
feedbacktext: feedbackText,
|
||||
published: Number(published),
|
||||
gradeover: JSON.stringify(gradeOver),
|
||||
};
|
||||
|
||||
await site.getDb().insertRecord(EVALUATE_SUBMISSIONS_TABLE, submission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "published" and "gradeover" columns of an evaluate submission record.
|
||||
*
|
||||
* @param record Evaluate submission record, modified in place.
|
||||
*/
|
||||
protected parseEvaluateSubmissionRecord(
|
||||
record: AddonModWorkshopEvaluateSubmissionDBRecord,
|
||||
): AddonModWorkshopOfflineEvaluateSubmission {
|
||||
return {
|
||||
...record,
|
||||
published: Boolean(record.published),
|
||||
gradeover: CoreTextUtils.parseJSON(record.gradeover),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workshop evaluate assessment.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
async deleteEvaluateAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopEvaluateAssessmentDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
assessmentid: assessmentId,
|
||||
};
|
||||
|
||||
await site.getDb().deleteRecords(EVALUATE_ASSESSMENTS_TABLE, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the all the evaluate assessments to be synced.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the objects to be synced.
|
||||
*/
|
||||
async getAllEvaluateAssessments(siteId?: string): Promise<AddonModWorkshopOfflineEvaluateAssessment[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const records = await site.getDb().getRecords<AddonModWorkshopEvaluateAssessmentDBRecord>(EVALUATE_ASSESSMENTS_TABLE);
|
||||
|
||||
return records.map(this.parseEvaluateAssessmentRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the evaluate assessments of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getEvaluateAssessments(workshopId: number, siteId?: string): Promise<AddonModWorkshopOfflineEvaluateAssessment[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopEvaluateAssessmentDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
};
|
||||
|
||||
const records =
|
||||
await site.getDb().getRecords<AddonModWorkshopEvaluateAssessmentDBRecord>(EVALUATE_ASSESSMENTS_TABLE, conditions);
|
||||
|
||||
return records.map(this.parseEvaluateAssessmentRecord.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an specific evaluate assessment of a workshop to be synced.
|
||||
*
|
||||
* @param workshopId ID of the workshop.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the object to be synced.
|
||||
*/
|
||||
async getEvaluateAssessment(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModWorkshopOfflineEvaluateAssessment> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const conditions: Partial<AddonModWorkshopEvaluateAssessmentDBRecord> = {
|
||||
workshopid: workshopId,
|
||||
assessmentid: assessmentId,
|
||||
};
|
||||
|
||||
const record =
|
||||
await site.getDb().getRecord<AddonModWorkshopEvaluateAssessmentDBRecord>(EVALUATE_ASSESSMENTS_TABLE, conditions);
|
||||
|
||||
return this.parseEvaluateAssessmentRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for evaluating an assessment to a workshop.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param courseId Course ID the workshop belongs to.
|
||||
* @param feedbackText The feedback for the reviewer.
|
||||
* @param weight The new weight for the assessment.
|
||||
* @param gradingGradeOver The new grading grade (empty for no overriding the grade).
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when assessment evaluation is successfully saved.
|
||||
*/
|
||||
async saveEvaluateAssessment(
|
||||
workshopId: number,
|
||||
assessmentId: number,
|
||||
courseId: number,
|
||||
feedbackText?: string,
|
||||
weight = 0,
|
||||
gradingGradeOver?: string,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const assessment: AddonModWorkshopEvaluateAssessmentDBRecord = {
|
||||
workshopid: workshopId,
|
||||
courseid: courseId,
|
||||
assessmentid: assessmentId,
|
||||
timemodified: CoreTimeUtils.timestamp(),
|
||||
feedbacktext: feedbackText || '',
|
||||
weight: weight,
|
||||
gradinggradeover: JSON.stringify(gradingGradeOver),
|
||||
};
|
||||
|
||||
await site.getDb().insertRecord(EVALUATE_ASSESSMENTS_TABLE, assessment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "gradinggradeover" column of an evaluate assessment record.
|
||||
*
|
||||
* @param record Evaluate assessment record, modified in place.
|
||||
*/
|
||||
protected parseEvaluateAssessmentRecord(
|
||||
record: AddonModWorkshopEvaluateAssessmentDBRecord,
|
||||
): AddonModWorkshopOfflineEvaluateAssessment {
|
||||
return {
|
||||
...record,
|
||||
gradinggradeover: CoreTextUtils.parseJSON(record.gradinggradeover),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for offline attachments in a workshop.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
async getWorkshopFolder(workshopId: number, siteId?: string): Promise<string> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const siteFolderPath = CoreFile.getSiteFolder(site.getId());
|
||||
const workshopFolderPath = 'offlineworkshop/' + workshopId + '/';
|
||||
|
||||
return CoreTextUtils.concatenatePaths(siteFolderPath, workshopFolderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for offline submissions.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
async getSubmissionFolder(workshopId: number, siteId?: string): Promise<string> {
|
||||
const folderPath = await this.getWorkshopFolder(workshopId, siteId);
|
||||
|
||||
return CoreTextUtils.concatenatePaths(folderPath, 'submission');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for offline assessment.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param assessmentId Assessment ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the path.
|
||||
*/
|
||||
async getAssessmentFolder(workshopId: number, assessmentId: number, siteId?: string): Promise<string> {
|
||||
let folderPath = await this.getWorkshopFolder(workshopId, siteId);
|
||||
|
||||
folderPath += 'assessment/';
|
||||
|
||||
return CoreTextUtils.concatenatePaths(folderPath, String(assessmentId));
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopOffline = makeSingleton(AddonModWorkshopOfflineProvider);
|
||||
|
||||
export type AddonModWorkshopOfflineSubmission = Omit<AddonModWorkshopSubmissionDBRecord, 'attachmentsid'> & {
|
||||
attachmentsid?: CoreFileUploaderStoreFilesResult;
|
||||
};
|
||||
|
||||
export type AddonModWorkshopOfflineAssessment = Omit<AddonModWorkshopAssessmentDBRecord, 'inputdata'> & {
|
||||
inputdata: CoreFormFields;
|
||||
};
|
||||
|
||||
export type AddonModWorkshopOfflineEvaluateSubmission =
|
||||
Omit<AddonModWorkshopEvaluateSubmissionDBRecord, 'published' | 'gradeover'> & {
|
||||
published: boolean;
|
||||
gradeover: string;
|
||||
};
|
||||
|
||||
export type AddonModWorkshopOfflineEvaluateAssessment =
|
||||
Omit<AddonModWorkshopEvaluateAssessmentDBRecord, 'gradinggradeover'> & {
|
||||
gradinggradeover: string;
|
||||
};
|
|
@ -0,0 +1,631 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreSyncBaseProvider, CoreSyncBlockedError } from '@classes/base-sync';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreFileEntry } from '@services/file-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate, makeSingleton } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { AddonModWorkshop,
|
||||
AddonModWorkshopAction,
|
||||
AddonModWorkshopData,
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshopSubmissionType,
|
||||
} from './workshop';
|
||||
import { AddonModWorkshopHelper } from './workshop-helper';
|
||||
import { AddonModWorkshopOffline,
|
||||
AddonModWorkshopOfflineAssessment,
|
||||
AddonModWorkshopOfflineEvaluateAssessment,
|
||||
AddonModWorkshopOfflineEvaluateSubmission,
|
||||
AddonModWorkshopOfflineSubmission,
|
||||
} from './workshop-offline';
|
||||
|
||||
/**
|
||||
* Service to sync workshops.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider<AddonModWorkshopSyncResult> {
|
||||
|
||||
static readonly AUTO_SYNCED = 'addon_mod_workshop_autom_synced';
|
||||
static readonly MANUAL_SYNCED = 'addon_mod_workshop_manual_synced';
|
||||
|
||||
protected componentTranslatableString = 'workshop';
|
||||
|
||||
constructor() {
|
||||
super('AddonModWorkshopSyncProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an workshop has data to synchronize.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with boolean: true if has data to sync, false otherwise.
|
||||
*/
|
||||
hasDataToSync(workshopId: number, siteId?: string): Promise<boolean> {
|
||||
return AddonModWorkshopOffline.hasWorkshopOfflineData(workshopId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all workshops that need it and haven't been synchronized in a while.
|
||||
*
|
||||
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @return Promise resolved when the sync is done.
|
||||
*/
|
||||
syncAllWorkshops(siteId?: string, force?: boolean): Promise<void> {
|
||||
return this.syncOnSites('all workshops', this.syncAllWorkshopsFunc.bind(this, !!force), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all workshops on a site.
|
||||
*
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @param siteId Site ID to sync.
|
||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected async syncAllWorkshopsFunc(force: boolean, siteId: string): Promise<void> {
|
||||
const workshopIds = await AddonModWorkshopOffline.getAllWorkshops(siteId);
|
||||
|
||||
// Sync all workshops that haven't been synced for a while.
|
||||
const promises = workshopIds.map(async (workshopId) => {
|
||||
const data = force
|
||||
? await this.syncWorkshop(workshopId, siteId)
|
||||
: await this.syncWorkshopIfNeeded(workshopId, siteId);
|
||||
|
||||
if (data && data.updated) {
|
||||
// Sync done. Send event.
|
||||
CoreEvents.trigger(AddonModWorkshopSyncProvider.AUTO_SYNCED, {
|
||||
workshopId: workshopId,
|
||||
warnings: data.warnings,
|
||||
}, siteId);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a workshop only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the workshop is synced or if it doesn't need to be synced.
|
||||
*/
|
||||
async syncWorkshopIfNeeded(workshopId: number, siteId?: string): Promise<AddonModWorkshopSyncResult | undefined> {
|
||||
const needed = await this.isSyncNeeded(workshopId, siteId);
|
||||
|
||||
if (needed) {
|
||||
return this.syncWorkshop(workshopId, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize a workshop.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
syncWorkshop(workshopId: number, siteId?: string): Promise<AddonModWorkshopSyncResult> {
|
||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||
|
||||
if (this.isSyncing(workshopId, siteId)) {
|
||||
// There's already a sync ongoing for this discussion, return the promise.
|
||||
return this.getOngoingSync(workshopId, siteId)!;
|
||||
}
|
||||
|
||||
// Verify that workshop isn't blocked.
|
||||
if (CoreSync.isBlocked(AddonModWorkshopProvider.COMPONENT, workshopId, siteId)) {
|
||||
this.logger.debug(`Cannot sync workshop '${workshopId}' because it is blocked.`);
|
||||
|
||||
throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||
}
|
||||
|
||||
this.logger.debug(`Try to sync workshop '${workshopId}' in site ${siteId}'`);
|
||||
|
||||
const syncPromise = this.performSyncWorkshop(workshopId, siteId);
|
||||
|
||||
return this.addOngoingSync(workshopId, syncPromise, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the workshop sync.
|
||||
*
|
||||
* @param workshopId Workshop ID.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
protected async performSyncWorkshop(workshopId: number, siteId: string): Promise<AddonModWorkshopSyncResult> {
|
||||
const result: AddonModWorkshopSyncResult = {
|
||||
warnings: [],
|
||||
updated: false,
|
||||
};
|
||||
|
||||
// Sync offline logs.
|
||||
await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModWorkshopProvider.COMPONENT, workshopId, siteId));
|
||||
|
||||
// Get offline submissions to be sent.
|
||||
const syncs = await Promise.all([
|
||||
// Get offline submissions to be sent.
|
||||
CoreUtils.ignoreErrors(AddonModWorkshopOffline.getSubmissions(workshopId, siteId), []),
|
||||
// Get offline submission assessments to be sent.
|
||||
CoreUtils.ignoreErrors(AddonModWorkshopOffline.getAssessments(workshopId, siteId), []),
|
||||
// Get offline submission evaluations to be sent.
|
||||
CoreUtils.ignoreErrors(AddonModWorkshopOffline.getEvaluateSubmissions(workshopId, siteId), []),
|
||||
// Get offline assessment evaluations to be sent.
|
||||
CoreUtils.ignoreErrors(AddonModWorkshopOffline.getEvaluateAssessments(workshopId, siteId), []),
|
||||
]);
|
||||
|
||||
let courseId: number | undefined;
|
||||
|
||||
// Get courseId from the first object
|
||||
for (const x in syncs) {
|
||||
if (syncs[x].length > 0 && syncs[x][0].courseid) {
|
||||
courseId = syncs[x][0].courseid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!courseId) {
|
||||
// Sync finished, set sync time.
|
||||
await CoreUtils.ignoreErrors(this.setSyncTime(workshopId, siteId));
|
||||
|
||||
// Nothing to sync.
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!CoreApp.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
throw new CoreNetworkError();
|
||||
}
|
||||
|
||||
const workshop = await AddonModWorkshop.getWorkshopById(courseId, workshopId, { siteId });
|
||||
|
||||
const submissionsActions: AddonModWorkshopOfflineSubmission[] = syncs[0];
|
||||
const assessments: AddonModWorkshopOfflineAssessment[] = syncs[1];
|
||||
const submissionEvaluations: AddonModWorkshopOfflineEvaluateSubmission[] = syncs[2];
|
||||
const assessmentEvaluations: AddonModWorkshopOfflineEvaluateAssessment[] = syncs[3];
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(this.syncSubmission(workshop, submissionsActions, result, siteId).then(() => {
|
||||
result.updated = true;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
assessments.forEach((assessment) => {
|
||||
promises.push(this.syncAssessment(workshop, assessment, result, siteId).then(() => {
|
||||
result.updated = true;
|
||||
|
||||
return;
|
||||
}));
|
||||
});
|
||||
|
||||
submissionEvaluations.forEach((evaluation) => {
|
||||
promises.push(this.syncEvaluateSubmission(workshop, evaluation, result, siteId).then(() => {
|
||||
result.updated = true;
|
||||
|
||||
return;
|
||||
}));
|
||||
});
|
||||
|
||||
assessmentEvaluations.forEach((evaluation) => {
|
||||
promises.push(this.syncEvaluateAssessment(workshop, evaluation, result, siteId).then(() => {
|
||||
result.updated = true;
|
||||
|
||||
return;
|
||||
}));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
await CoreUtils.ignoreErrors(AddonModWorkshop.invalidateContentById(workshopId, courseId, siteId));
|
||||
}
|
||||
|
||||
// Sync finished, set sync time.
|
||||
await CoreUtils.ignoreErrors(this.setSyncTime(workshopId, siteId));
|
||||
|
||||
// All done, return the warnings.
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a submission.
|
||||
*
|
||||
* @param workshop Workshop.
|
||||
* @param submissionActions Submission actions offline data.
|
||||
* @param result Object with the result of the sync.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected async syncSubmission(
|
||||
workshop: AddonModWorkshopData,
|
||||
submissionActions: AddonModWorkshopOfflineSubmission[],
|
||||
result: AddonModWorkshopSyncResult,
|
||||
siteId: string,
|
||||
): Promise<void> {
|
||||
let discardError: string | undefined;
|
||||
|
||||
// Sort entries by timemodified.
|
||||
submissionActions = submissionActions.sort((a, b) => a.timemodified - b.timemodified);
|
||||
|
||||
let timemodified = 0;
|
||||
let submissionId = submissionActions[0].submissionid;
|
||||
|
||||
if (submissionId > 0) {
|
||||
// Is editing.
|
||||
try {
|
||||
const submission = await AddonModWorkshop.getSubmission(workshop.id, submissionId, {
|
||||
cmId: workshop.coursemodule,
|
||||
siteId,
|
||||
});
|
||||
|
||||
timemodified = submission.timemodified;
|
||||
} catch {
|
||||
timemodified = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (timemodified < 0 || timemodified >= submissionActions[0].timemodified) {
|
||||
// The entry was not found in Moodle or the entry has been modified, discard the action.
|
||||
result.updated = true;
|
||||
discardError = Translate.instant('addon.mod_workshop.warningsubmissionmodified');
|
||||
|
||||
await AddonModWorkshopOffline.deleteAllSubmissionActions(workshop.id, siteId);
|
||||
|
||||
this.addOfflineDataDeletedWarning(result.warnings, workshop.name, discardError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
submissionActions.forEach(async (action) => {
|
||||
submissionId = action.submissionid > 0 ? action.submissionid : submissionId;
|
||||
|
||||
try {
|
||||
let attachmentsId: number | undefined;
|
||||
|
||||
// Upload attachments first if any.
|
||||
if (action.attachmentsid) {
|
||||
const files = await AddonModWorkshopHelper.getSubmissionFilesFromOfflineFilesObject(
|
||||
action.attachmentsid,
|
||||
workshop.id,
|
||||
siteId,
|
||||
);
|
||||
|
||||
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles(
|
||||
workshop.id,
|
||||
files,
|
||||
false,
|
||||
siteId,
|
||||
);
|
||||
} else {
|
||||
// Remove all files.
|
||||
attachmentsId = await AddonModWorkshopHelper.uploadOrStoreSubmissionFiles(
|
||||
workshop.id,
|
||||
[],
|
||||
false,
|
||||
siteId,
|
||||
);
|
||||
}
|
||||
|
||||
if (workshop.submissiontypefile == AddonModWorkshopSubmissionType.SUBMISSION_TYPE_DISABLED) {
|
||||
attachmentsId = undefined;
|
||||
}
|
||||
|
||||
// Perform the action.
|
||||
switch (action.action) {
|
||||
case AddonModWorkshopAction.ADD:
|
||||
submissionId = await AddonModWorkshop.addSubmissionOnline(
|
||||
workshop.id,
|
||||
action.title,
|
||||
action.content,
|
||||
attachmentsId,
|
||||
siteId,
|
||||
);
|
||||
case AddonModWorkshopAction.UPDATE:
|
||||
await AddonModWorkshop.updateSubmissionOnline(
|
||||
submissionId,
|
||||
action.title,
|
||||
action.content,
|
||||
attachmentsId,
|
||||
siteId,
|
||||
);
|
||||
case AddonModWorkshopAction.DELETE:
|
||||
await AddonModWorkshop.deleteSubmissionOnline(submissionId, siteId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||
discardError = CoreTextUtils.getErrorMessageFromError(error);
|
||||
}
|
||||
|
||||
// Couldn't connect to server, reject.
|
||||
throw error;
|
||||
|
||||
}
|
||||
// Delete the offline data.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModWorkshopOffline.deleteSubmissionAction(
|
||||
action.workshopid,
|
||||
action.action,
|
||||
siteId,
|
||||
);
|
||||
|
||||
// Delete stored files.
|
||||
if (action.action == AddonModWorkshopAction.ADD || action.action == AddonModWorkshopAction.UPDATE) {
|
||||
|
||||
return AddonModWorkshopHelper.deleteSubmissionStoredFiles(
|
||||
action.workshopid,
|
||||
siteId,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (discardError) {
|
||||
// Submission was discarded, add a warning.
|
||||
this.addOfflineDataDeletedWarning(result.warnings, workshop.name, discardError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize an assessment.
|
||||
*
|
||||
* @param workshop Workshop.
|
||||
* @param assessment Assessment offline data.
|
||||
* @param result Object with the result of the sync.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected async syncAssessment(
|
||||
workshop: AddonModWorkshopData,
|
||||
assessmentData: AddonModWorkshopOfflineAssessment,
|
||||
result: AddonModWorkshopSyncResult,
|
||||
siteId: string,
|
||||
): Promise<void> {
|
||||
let discardError: string | undefined;
|
||||
const assessmentId = assessmentData.assessmentid;
|
||||
|
||||
let timemodified = 0;
|
||||
|
||||
try {
|
||||
const assessment = await AddonModWorkshop.getAssessment(workshop.id, assessmentId, {
|
||||
cmId: workshop.coursemodule,
|
||||
siteId,
|
||||
});
|
||||
|
||||
timemodified = assessment.timemodified;
|
||||
} catch {
|
||||
timemodified = -1;
|
||||
}
|
||||
|
||||
if (timemodified < 0 || timemodified >= assessmentData.timemodified) {
|
||||
// The entry was not found in Moodle or the entry has been modified, discard the action.
|
||||
result.updated = true;
|
||||
discardError = Translate.instant('addon.mod_workshop.warningassessmentmodified');
|
||||
|
||||
await AddonModWorkshopOffline.deleteAssessment(workshop.id, assessmentId, siteId);
|
||||
|
||||
this.addOfflineDataDeletedWarning(result.warnings, workshop.name, discardError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let attachmentsId = 0;
|
||||
const inputData = assessmentData.inputdata;
|
||||
|
||||
try {
|
||||
let files: CoreFileEntry[] = [];
|
||||
// Upload attachments first if any.
|
||||
if (inputData.feedbackauthorattachmentsid) {
|
||||
files = await AddonModWorkshopHelper.getAssessmentFilesFromOfflineFilesObject(
|
||||
<CoreFileUploaderStoreFilesResult>inputData.feedbackauthorattachmentsid,
|
||||
workshop.id,
|
||||
assessmentId,
|
||||
siteId,
|
||||
);
|
||||
}
|
||||
|
||||
attachmentsId =
|
||||
await AddonModWorkshopHelper.uploadOrStoreAssessmentFiles(workshop.id, assessmentId, files, false, siteId);
|
||||
|
||||
inputData.feedbackauthorattachmentsid = attachmentsId || 0;
|
||||
|
||||
await AddonModWorkshop.updateAssessmentOnline(assessmentId, inputData, siteId);
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||
discardError = CoreTextUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the offline data.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModWorkshopOffline.deleteAssessment(workshop.id, assessmentId, siteId);
|
||||
await AddonModWorkshopHelper.deleteAssessmentStoredFiles(workshop.id, assessmentId, siteId);
|
||||
|
||||
if (discardError) {
|
||||
// Assessment was discarded, add a warning.
|
||||
this.addOfflineDataDeletedWarning(result.warnings, workshop.name, discardError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a submission evaluation.
|
||||
*
|
||||
* @param workshop Workshop.
|
||||
* @param evaluate Submission evaluation offline data.
|
||||
* @param result Object with the result of the sync.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected async syncEvaluateSubmission(
|
||||
workshop: AddonModWorkshopData,
|
||||
evaluate: AddonModWorkshopOfflineEvaluateSubmission,
|
||||
result: AddonModWorkshopSyncResult,
|
||||
siteId: string,
|
||||
): Promise<void> {
|
||||
let discardError: string | undefined;
|
||||
const submissionId = evaluate.submissionid;
|
||||
|
||||
let timemodified = 0;
|
||||
|
||||
try {
|
||||
const submission = await AddonModWorkshop.getSubmission(workshop.id, submissionId, {
|
||||
cmId: workshop.coursemodule,
|
||||
siteId,
|
||||
});
|
||||
|
||||
timemodified = submission.timemodified;
|
||||
} catch {
|
||||
timemodified = -1;
|
||||
}
|
||||
|
||||
if (timemodified < 0 || timemodified >= evaluate.timemodified) {
|
||||
// The entry was not found in Moodle or the entry has been modified, discard the action.
|
||||
result.updated = true;
|
||||
discardError = Translate.instant('addon.mod_workshop.warningsubmissionmodified');
|
||||
|
||||
await AddonModWorkshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId);
|
||||
|
||||
this.addOfflineDataDeletedWarning(result.warnings, workshop.name, discardError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AddonModWorkshop.evaluateSubmissionOnline(
|
||||
submissionId,
|
||||
evaluate.feedbacktext,
|
||||
evaluate.published,
|
||||
evaluate.gradeover,
|
||||
siteId,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||
discardError = CoreTextUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the offline data.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModWorkshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId);
|
||||
|
||||
if (discardError) {
|
||||
// Assessment was discarded, add a warning.
|
||||
this.addOfflineDataDeletedWarning(result.warnings, workshop.name, discardError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a assessment evaluation.
|
||||
*
|
||||
* @param workshop Workshop.
|
||||
* @param evaluate Assessment evaluation offline data.
|
||||
* @param result Object with the result of the sync.
|
||||
* @param siteId Site ID.
|
||||
* @return Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected async syncEvaluateAssessment(
|
||||
workshop: AddonModWorkshopData,
|
||||
evaluate: AddonModWorkshopOfflineEvaluateAssessment,
|
||||
result: AddonModWorkshopSyncResult,
|
||||
siteId: string,
|
||||
): Promise<void> {
|
||||
let discardError: string | undefined;
|
||||
const assessmentId = evaluate.assessmentid;
|
||||
|
||||
let timemodified = 0;
|
||||
|
||||
try {
|
||||
const assessment = await AddonModWorkshop.getAssessment(workshop.id, assessmentId, {
|
||||
cmId: workshop.coursemodule,
|
||||
siteId,
|
||||
});
|
||||
|
||||
timemodified = assessment.timemodified;
|
||||
} catch {
|
||||
timemodified = -1;
|
||||
}
|
||||
|
||||
if (timemodified < 0 || timemodified >= evaluate.timemodified) {
|
||||
// The entry was not found in Moodle or the entry has been modified, discard the action.
|
||||
result.updated = true;
|
||||
discardError = Translate.instant('addon.mod_workshop.warningassessmentmodified');
|
||||
|
||||
return AddonModWorkshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId);
|
||||
}
|
||||
|
||||
try {
|
||||
await AddonModWorkshop.evaluateAssessmentOnline(
|
||||
assessmentId,
|
||||
evaluate.feedbacktext,
|
||||
evaluate.weight,
|
||||
evaluate.gradinggradeover,
|
||||
siteId,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error && CoreUtils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||
discardError = CoreTextUtils.getErrorMessageFromError(error);
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the offline data.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModWorkshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId);
|
||||
|
||||
if (discardError) {
|
||||
// Assessment was discarded, add a warning.
|
||||
this.addOfflineDataDeletedWarning(result.warnings, workshop.name, discardError);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonModWorkshopSync = makeSingleton(AddonModWorkshopSyncProvider);
|
||||
|
||||
export type AddonModWorkshopAutoSyncData = {
|
||||
workshopId: number;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
export type AddonModWorkshopSyncResult = {
|
||||
warnings: string[];
|
||||
updated: boolean;
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,63 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { CanLeaveGuard } from '@guards/can-leave';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonModWorkshopIndexPage } from './pages/index/index';
|
||||
import { AddonModWorkshopComponentsModule } from './components/components.module';
|
||||
import { AddonModWorkshopSubmissionPage } from './pages/submission/submission';
|
||||
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
|
||||
import { AddonModWorkshopAssessmentPage } from './pages/assessment/assessment';
|
||||
import { AddonModWorkshopEditSubmissionPage } from './pages/edit-submission/edit-submission';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModWorkshopIndexPage,
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/:submissionId',
|
||||
component: AddonModWorkshopSubmissionPage,
|
||||
canDeactivate: [CanLeaveGuard],
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/:submissionId/edit', // @todo
|
||||
component: AddonModWorkshopEditSubmissionPage,
|
||||
canDeactivate: [CanLeaveGuard],
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/:submissionId/:assessmentId',
|
||||
component: AddonModWorkshopAssessmentPage,
|
||||
canDeactivate: [CanLeaveGuard],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
AddonModWorkshopComponentsModule,
|
||||
CoreEditorComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonModWorkshopIndexPage,
|
||||
AddonModWorkshopSubmissionPage,
|
||||
AddonModWorkshopAssessmentPage,
|
||||
AddonModWorkshopEditSubmissionPage,
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopLazyModule {}
|
|
@ -0,0 +1,79 @@
|
|||
// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreCronDelegate } from '@services/cron';
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
import { AddonModWorkshopAssessmentStrategyModule } from './assessment/assessment.module';
|
||||
import { AddonModWorkshopComponentsModule } from './components/components.module';
|
||||
import { AddonWorkshopAssessmentStrategyDelegateService } from './services/assessment-strategy-delegate';
|
||||
import { ADDON_MOD_WORKSHOP_OFFLINE_SITE_SCHEMA } from './services/database/workshop';
|
||||
import { AddonModWorkshopIndexLinkHandler } from './services/handlers/index-link';
|
||||
import { AddonModWorkshopListLinkHandler } from './services/handlers/list-link';
|
||||
import { AddonModWorkshopModuleHandler, AddonModWorkshopModuleHandlerService } from './services/handlers/module';
|
||||
import { AddonModWorkshopPrefetchHandler } from './services/handlers/prefetch';
|
||||
import { AddonModWorkshopSyncCronHandler } from './services/handlers/sync-cron';
|
||||
import { AddonModWorkshopProvider } from './services/workshop';
|
||||
import { AddonModWorkshopHelperProvider } from './services/workshop-helper';
|
||||
import { AddonModWorkshopOfflineProvider } from './services/workshop-offline';
|
||||
import { AddonModWorkshopSyncProvider } from './services/workshop-sync';
|
||||
|
||||
// List of providers (without handlers).
|
||||
export const ADDON_MOD_WORKSHOP_SERVICES: Type<unknown>[] = [
|
||||
AddonModWorkshopProvider,
|
||||
AddonModWorkshopOfflineProvider,
|
||||
AddonModWorkshopSyncProvider,
|
||||
AddonModWorkshopHelperProvider,
|
||||
AddonWorkshopAssessmentStrategyDelegateService,
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: AddonModWorkshopModuleHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('./workshop-lazy.module').then(m => m.AddonModWorkshopLazyModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
AddonModWorkshopComponentsModule,
|
||||
AddonModWorkshopAssessmentStrategyModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: CORE_SITE_SCHEMAS,
|
||||
useValue: [ADDON_MOD_WORKSHOP_OFFLINE_SITE_SCHEMA],
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => () => {
|
||||
CoreCourseModuleDelegate.registerHandler(AddonModWorkshopModuleHandler.instance);
|
||||
CoreCourseModulePrefetchDelegate.registerHandler(AddonModWorkshopPrefetchHandler.instance);
|
||||
CoreCronDelegate.register(AddonModWorkshopSyncCronHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(AddonModWorkshopIndexLinkHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(AddonModWorkshopListLinkHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AddonModWorkshopModule {}
|
|
@ -20,12 +20,11 @@ import { AddonQbehaviourDeferredFeedbackModule } from './deferredfeedback/deferr
|
|||
import { AddonQbehaviourImmediateCBMModule } from './immediatecbm/immediatecbm.module';
|
||||
import { AddonQbehaviourImmediateFeedbackModule } from './immediatefeedback/immediatefeedback.module';
|
||||
import { AddonQbehaviourInformationItemModule } from './informationitem/informationitem.module';
|
||||
import { AddonQbehaviourInteractiveModule } from './interactive/interactive.module';
|
||||
import { AddonQbehaviourInteractiveCountbackModule } from './interactivecountback/interactivecountback.module';
|
||||
import { AddonQbehaviourInteractiveModule } from './interactive/interactive.module';
|
||||
import { AddonQbehaviourManualGradedModule } from './manualgraded/manualgraded.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonQbehaviourAdaptiveModule,
|
||||
AddonQbehaviourAdaptiveNoPenaltyModule,
|
||||
|
@ -34,12 +33,9 @@ import { AddonQbehaviourManualGradedModule } from './manualgraded/manualgraded.m
|
|||
AddonQbehaviourImmediateCBMModule,
|
||||
AddonQbehaviourImmediateFeedbackModule,
|
||||
AddonQbehaviourInformationItemModule,
|
||||
AddonQbehaviourInteractiveModule,
|
||||
AddonQbehaviourInteractiveCountbackModule,
|
||||
AddonQbehaviourInteractiveModule,
|
||||
AddonQbehaviourManualGradedModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
export class AddonQbehaviourModule { }
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AddonQtypeCalculatedModule } from './calculated/calculated.module';
|
||||
import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module';
|
||||
import { AddonQtypeCalculatedSimpleModule } from './calculatedsimple/calculatedsimple.module';
|
||||
|
@ -31,7 +32,6 @@ import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module';
|
|||
import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonQtypeCalculatedModule,
|
||||
AddonQtypeCalculatedMultiModule,
|
||||
|
@ -50,8 +50,5 @@ import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module';
|
|||
AddonQtypeShortAnswerModule,
|
||||
AddonQtypeTrueFalseModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
export class AddonQtypeModule { }
|
||||
|
|
|
@ -21,8 +21,6 @@ export const ADDON_REMOTETHEMES_SERVICES: Type<unknown>[] = [
|
|||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<span [core-mark-required]="required">{{ field.name }}</span>
|
||||
<core-input-errors [control]="control"></core-input-errors>
|
||||
</ion-label>
|
||||
<core-rich-text-editor item-content [control]="control" [placeholder]="field.name" [autoSave]="true"
|
||||
<core-rich-text-editor [control]="control" [placeholder]="field.name" [autoSave]="true"
|
||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [elementId]="modelName">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
|
|
|
@ -16,18 +16,16 @@ import { NgModule } from '@angular/core';
|
|||
import { AddonUserProfileFieldCheckboxModule } from './checkbox/checkbox.module';
|
||||
import { AddonUserProfileFieldDatetimeModule } from './datetime/datetime.module';
|
||||
import { AddonUserProfileFieldMenuModule } from './menu/menu.module';
|
||||
import { AddonUserProfileFieldTextModule } from './text/text.module';
|
||||
import { AddonUserProfileFieldTextareaModule } from './textarea/textarea.module';
|
||||
import { AddonUserProfileFieldTextModule } from './text/text.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonUserProfileFieldCheckboxModule,
|
||||
AddonUserProfileFieldDatetimeModule,
|
||||
AddonUserProfileFieldMenuModule,
|
||||
AddonUserProfileFieldTextModule,
|
||||
AddonUserProfileFieldTextareaModule,
|
||||
AddonUserProfileFieldTextModule,
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
export class AddonUserProfileFieldModule { }
|
||||
|
|
|
@ -18,80 +18,80 @@ import { IonicModule } from '@ionic/angular';
|
|||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
|
||||
import { CoreAttachmentsComponent } from './attachments/attachments';
|
||||
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
||||
import { CoreChartComponent } from './chart/chart';
|
||||
import { CoreChronoComponent } from './chrono/chrono';
|
||||
import { CoreDownloadRefreshComponent } from './download-refresh/download-refresh';
|
||||
import { CoreFileComponent } from './file/file';
|
||||
import { CoreIconComponent } from './icon/icon';
|
||||
import { CoreIframeComponent } from './iframe/iframe';
|
||||
import { CoreInputErrorsComponent } from './input-errors/input-errors';
|
||||
import { CoreLoadingComponent } from './loading/loading';
|
||||
import { CoreMarkRequiredComponent } from './mark-required/mark-required';
|
||||
import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
|
||||
import { CoreRecaptchaModalComponent } from './recaptcha/recaptcha-modal';
|
||||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||
import { CoreSplitViewComponent } from './split-view/split-view';
|
||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||
import { CoreTabsComponent } from './tabs/tabs';
|
||||
import { CoreTabComponent } from './tabs/tab';
|
||||
import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet';
|
||||
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
|
||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||
import { CoreContextMenuComponent } from './context-menu/context-menu';
|
||||
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
|
||||
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
|
||||
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
||||
import { CoreDownloadRefreshComponent } from './download-refresh/download-refresh';
|
||||
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
||||
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
|
||||
import { CoreTimerComponent } from './timer/timer';
|
||||
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { CoreAttachmentsComponent } from './attachments/attachments';
|
||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||
import { CoreFileComponent } from './file/file';
|
||||
import { CoreFilesComponent } from './files/files';
|
||||
import { CoreIconComponent } from './icon/icon';
|
||||
import { CoreIframeComponent } from './iframe/iframe';
|
||||
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
|
||||
import { CoreInputErrorsComponent } from './input-errors/input-errors';
|
||||
import { CoreLoadingComponent } from './loading/loading';
|
||||
import { CoreLocalFileComponent } from './local-file/local-file';
|
||||
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
||||
import { CoreMarkRequiredComponent } from './mark-required/mark-required';
|
||||
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||
import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
|
||||
import { CoreRecaptchaModalComponent } from './recaptcha/recaptcha-modal';
|
||||
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
|
||||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||
import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||
import { CoreChartComponent } from './chart/chart';
|
||||
import { CoreSplitViewComponent } from './split-view/split-view';
|
||||
import { CoreStyleComponent } from './style/style';
|
||||
import { CoreTabComponent } from './tabs/tab';
|
||||
import { CoreTabsComponent } from './tabs/tabs';
|
||||
import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet';
|
||||
import { CoreTimerComponent } from './timer/timer';
|
||||
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreAttachmentsComponent,
|
||||
CoreBSTooltipComponent,
|
||||
CoreChartComponent,
|
||||
CoreChronoComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreFileComponent,
|
||||
CoreIconComponent,
|
||||
CoreIframeComponent,
|
||||
CoreInputErrorsComponent,
|
||||
CoreLoadingComponent,
|
||||
CoreMarkRequiredComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreRecaptchaModalComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreSplitViewComponent,
|
||||
CoreStyleComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreTabsComponent,
|
||||
CoreTabComponent,
|
||||
CoreTabsOutletComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreUserAvatarComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreDynamicComponent,
|
||||
CoreSendMessageFormComponent,
|
||||
CoreTimerComponent,
|
||||
CoreNavigationBarComponent,
|
||||
CoreAttachmentsComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreFileComponent,
|
||||
CoreFilesComponent,
|
||||
CoreIconComponent,
|
||||
CoreIframeComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreInputErrorsComponent,
|
||||
CoreLoadingComponent,
|
||||
CoreLocalFileComponent,
|
||||
CoreBSTooltipComponent,
|
||||
CoreMarkRequiredComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreNavigationBarComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreRecaptchaModalComponent,
|
||||
CoreSendMessageFormComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreSitePickerComponent,
|
||||
CoreChartComponent,
|
||||
CoreSplitViewComponent,
|
||||
CoreStyleComponent,
|
||||
CoreTabComponent,
|
||||
CoreTabsComponent,
|
||||
CoreTabsOutletComponent,
|
||||
CoreTimerComponent,
|
||||
CoreUserAvatarComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -102,40 +102,40 @@ import { CoreStyleComponent } from './style/style';
|
|||
CorePipesModule,
|
||||
],
|
||||
exports: [
|
||||
CoreAttachmentsComponent,
|
||||
CoreBSTooltipComponent,
|
||||
CoreChartComponent,
|
||||
CoreChronoComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreFileComponent,
|
||||
CoreIconComponent,
|
||||
CoreIframeComponent,
|
||||
CoreInputErrorsComponent,
|
||||
CoreLoadingComponent,
|
||||
CoreMarkRequiredComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreRecaptchaModalComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreSplitViewComponent,
|
||||
CoreStyleComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreTabsComponent,
|
||||
CoreTabComponent,
|
||||
CoreTabsOutletComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreUserAvatarComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreDynamicComponent,
|
||||
CoreSendMessageFormComponent,
|
||||
CoreTimerComponent,
|
||||
CoreNavigationBarComponent,
|
||||
CoreAttachmentsComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreFileComponent,
|
||||
CoreFilesComponent,
|
||||
CoreIconComponent,
|
||||
CoreIframeComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreInputErrorsComponent,
|
||||
CoreLoadingComponent,
|
||||
CoreLocalFileComponent,
|
||||
CoreBSTooltipComponent,
|
||||
CoreMarkRequiredComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreNavigationBarComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreRecaptchaModalComponent,
|
||||
CoreSendMessageFormComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreSitePickerComponent,
|
||||
CoreChartComponent,
|
||||
CoreSplitViewComponent,
|
||||
CoreStyleComponent,
|
||||
CoreTabComponent,
|
||||
CoreTabsComponent,
|
||||
CoreTabsOutletComponent,
|
||||
CoreTimerComponent,
|
||||
CoreUserAvatarComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -27,15 +27,12 @@ import { Translate } from '@singletons';
|
|||
*
|
||||
* Please notice that the inputs need to have a FormControl to make it work. That FormControl needs to be passed to this component.
|
||||
*
|
||||
* If this component is placed in the same ion-item as a ion-label or ion-input, then it should have the attribute "item-content",
|
||||
* otherwise Ionic will remove it.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <ion-item class="ion-text-wrap">
|
||||
* <ion-label stacked core-mark-required="true">{{ 'core.login.username' | translate }}</ion-label>
|
||||
* <ion-input type="text" name="username" formControlName="username"></ion-input>
|
||||
* <core-input-errors item-content [control]="myForm.controls.username" [errorMessages]="usernameErrors"></core-input-errors>
|
||||
* <core-input-errors [control]="myForm.controls.username" [errorMessages]="usernameErrors"></core-input-errors>
|
||||
* </ion-item>
|
||||
*/
|
||||
@Component({
|
||||
|
|
|
@ -21,34 +21,34 @@ import { CoreInterceptor } from './classes/interceptor';
|
|||
import { getDatabaseProviders } from './services/database';
|
||||
import { getInitializerProviders } from './initializers';
|
||||
|
||||
import { CoreDbProvider } from '@services/db';
|
||||
import { CoreAppProvider } from '@services/app';
|
||||
import { CoreConfigProvider } from '@services/config';
|
||||
import { CoreLangProvider } from '@services/lang';
|
||||
import { CoreTextUtilsProvider } from '@services/utils/text';
|
||||
import { CoreCronDelegateService } from '@services/cron';
|
||||
import { CoreCustomURLSchemesProvider } from '@services/urlschemes';
|
||||
import { CoreDbProvider } from '@services/db';
|
||||
import { CoreDomUtilsProvider } from '@services/utils/dom';
|
||||
import { CoreFileHelperProvider } from '@services/file-helper';
|
||||
import { CoreFilepoolProvider } from '@services/filepool';
|
||||
import { CoreFileProvider } from '@services/file';
|
||||
import { CoreFileSessionProvider } from '@services/file-session';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
import { CoreGeolocationProvider } from '@services/geolocation';
|
||||
import { CoreGroupsProvider } from '@services/groups';
|
||||
import { CoreIframeUtilsProvider } from '@services/utils/iframe';
|
||||
import { CoreLangProvider } from '@services/lang';
|
||||
import { CoreLocalNotificationsProvider } from '@services/local-notifications';
|
||||
import { CoreMimetypeUtilsProvider } from '@services/utils/mimetype';
|
||||
import { CoreNavigatorService } from '@services/navigator';
|
||||
import { CorePluginFileDelegateService } from '@services/plugin-file-delegate';
|
||||
import { CoreScreenService } from '@services/screen';
|
||||
import { CoreSitesProvider } from '@services/sites';
|
||||
import { CoreSyncProvider } from '@services/sync';
|
||||
import { CoreTextUtilsProvider } from '@services/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@services/utils/time';
|
||||
import { CoreUpdateManagerProvider } from '@services/update-manager';
|
||||
import { CoreUrlUtilsProvider } from '@services/utils/url';
|
||||
import { CoreUtilsProvider } from '@services/utils/utils';
|
||||
import { CoreMimetypeUtilsProvider } from '@services/utils/mimetype';
|
||||
import { CoreFileProvider } from '@services/file';
|
||||
import { CoreWSProvider } from '@services/ws';
|
||||
import { CoreSitesProvider } from '@services/sites';
|
||||
import { CoreLocalNotificationsProvider } from '@services/local-notifications';
|
||||
import { CoreGroupsProvider } from '@services/groups';
|
||||
import { CoreCronDelegateService } from '@services/cron';
|
||||
import { CoreFileSessionProvider } from '@services/file-session';
|
||||
import { CoreFilepoolProvider } from '@services/filepool';
|
||||
import { CoreUpdateManagerProvider } from '@services/update-manager';
|
||||
import { CorePluginFileDelegateService } from '@services/plugin-file-delegate';
|
||||
import { CoreSyncProvider } from '@services/sync';
|
||||
import { CoreFileHelperProvider } from '@services/file-helper';
|
||||
import { CoreGeolocationProvider } from '@services/geolocation';
|
||||
import { CoreNavigatorService } from '@services/navigator';
|
||||
import { CoreScreenService } from '@services/screen';
|
||||
import { CoreCustomURLSchemesProvider } from '@services/urlschemes';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
|
||||
export const CORE_SERVICES: Type<unknown>[] = [
|
||||
CoreAppProvider,
|
||||
|
@ -56,26 +56,26 @@ export const CORE_SERVICES: Type<unknown>[] = [
|
|||
CoreCronDelegateService,
|
||||
CoreCustomURLSchemesProvider,
|
||||
CoreDbProvider,
|
||||
CoreDomUtilsProvider,
|
||||
CoreFileHelperProvider,
|
||||
CoreFileSessionProvider,
|
||||
CoreFileProvider,
|
||||
CoreFilepoolProvider,
|
||||
CoreFileProvider,
|
||||
CoreFileSessionProvider,
|
||||
CoreForms,
|
||||
CoreGeolocationProvider,
|
||||
CoreGroupsProvider,
|
||||
CoreIframeUtilsProvider,
|
||||
CoreLangProvider,
|
||||
CoreLocalNotificationsProvider,
|
||||
CoreMimetypeUtilsProvider,
|
||||
CoreNavigatorService,
|
||||
CorePluginFileDelegateService,
|
||||
CoreScreenService,
|
||||
CoreSitesProvider,
|
||||
CoreSyncProvider,
|
||||
CoreUpdateManagerProvider,
|
||||
CoreDomUtilsProvider,
|
||||
CoreForms,
|
||||
CoreIframeUtilsProvider,
|
||||
CoreMimetypeUtilsProvider,
|
||||
CoreTextUtilsProvider,
|
||||
CoreTimeUtilsProvider,
|
||||
CoreUpdateManagerProvider,
|
||||
CoreUrlUtilsProvider,
|
||||
CoreUtilsProvider,
|
||||
CoreWSProvider,
|
||||
|
|
|
@ -15,41 +15,40 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreAutoFocusDirective } from './auto-focus';
|
||||
import { CoreAutoRowsDirective } from './auto-rows';
|
||||
import { CoreExternalContentDirective } from './external-content';
|
||||
import { CoreFabDirective } from './fab';
|
||||
import { CoreFaIconDirective } from './fa-icon';
|
||||
import { CoreFormatTextDirective } from './format-text';
|
||||
import { CoreLinkDirective } from './link';
|
||||
import { CoreLongPressDirective } from './long-press';
|
||||
import { CoreSupressEventsDirective } from './supress-events';
|
||||
import { CoreFaIconDirective } from './fa-icon';
|
||||
import { CoreUserLinkDirective } from './user-link';
|
||||
import { CoreAutoRowsDirective } from './auto-rows';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreAutoFocusDirective,
|
||||
CoreAutoRowsDirective,
|
||||
CoreExternalContentDirective,
|
||||
CoreFabDirective,
|
||||
CoreFaIconDirective,
|
||||
CoreFormatTextDirective,
|
||||
CoreLinkDirective,
|
||||
CoreLongPressDirective,
|
||||
CoreSupressEventsDirective,
|
||||
CoreFabDirective,
|
||||
CoreFaIconDirective,
|
||||
CoreUserLinkDirective,
|
||||
CoreAutoRowsDirective,
|
||||
],
|
||||
imports: [],
|
||||
exports: [
|
||||
CoreAutoFocusDirective,
|
||||
CoreAutoRowsDirective,
|
||||
CoreExternalContentDirective,
|
||||
CoreFabDirective,
|
||||
CoreFaIconDirective,
|
||||
CoreFormatTextDirective,
|
||||
CoreLinkDirective,
|
||||
CoreLongPressDirective,
|
||||
CoreSupressEventsDirective,
|
||||
CoreFabDirective,
|
||||
CoreFaIconDirective,
|
||||
CoreUserLinkDirective,
|
||||
CoreAutoRowsDirective,
|
||||
],
|
||||
})
|
||||
export class CoreDirectivesModule {}
|
||||
|
|
|
@ -22,7 +22,5 @@ export const CORE_BLOCK_SERVICES: Type<unknown>[] = [
|
|||
CoreBlockHelperProvider,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
providers: [],
|
||||
})
|
||||
@NgModule({})
|
||||
export class CoreBlockModule {}
|
||||
|
|
|
@ -142,7 +142,7 @@ import { ADDON_MOD_SCORM_SERVICES } from '@addons/mod/scorm/scorm.module';
|
|||
import { ADDON_MOD_SURVEY_SERVICES } from '@addons/mod/survey/survey.module';
|
||||
import { ADDON_MOD_URL_SERVICES } from '@addons/mod/url/url.module';
|
||||
import { ADDON_MOD_WIKI_SERVICES } from '@addons/mod/wiki/wiki.module';
|
||||
// @todo import { ADDON_MOD_WORKSHOP_SERVICES } from '@addons/mod/workshop/workshop.module';
|
||||
import { ADDON_MOD_WORKSHOP_SERVICES } from '@addons/mod/workshop/workshop.module';
|
||||
import { ADDON_NOTES_SERVICES } from '@addons/notes/notes.module';
|
||||
import { ADDON_NOTIFICATIONS_SERVICES } from '@addons/notifications/notifications.module';
|
||||
import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.module';
|
||||
|
@ -150,7 +150,7 @@ import { ADDON_REMOTETHEMES_SERVICES } from '@addons/remotethemes/remotethemes.m
|
|||
|
||||
// Import some addon modules that define components, directives and pipes. Only import the important ones.
|
||||
import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module';
|
||||
// @todo import { AddonModWorkshopComponentsModule } from '@addons/mod/workshop/components/components.module';
|
||||
import { AddonModWorkshopComponentsModule } from '@addons/mod/workshop/components/components.module';
|
||||
|
||||
/**
|
||||
* Service to provide functionalities regarding compiling dynamic HTML and Javascript.
|
||||
|
@ -172,7 +172,7 @@ export class CoreCompileProvider {
|
|||
CoreSharedModule, CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreUserComponentsModule,
|
||||
CoreCourseDirectivesModule, CoreQuestionComponentsModule, AddonModAssignComponentsModule,
|
||||
CoreBlockComponentsModule, CoreEditorComponentsModule, CoreSearchComponentsModule, CoreSitePluginsDirectivesModule,
|
||||
// @todo AddonModWorkshopComponentsModule,
|
||||
AddonModWorkshopComponentsModule,
|
||||
];
|
||||
|
||||
constructor(protected injector: Injector, compilerFactory: JitCompilerFactory) {
|
||||
|
@ -308,7 +308,7 @@ export class CoreCompileProvider {
|
|||
...ADDON_MOD_SURVEY_SERVICES,
|
||||
...ADDON_MOD_URL_SERVICES,
|
||||
...ADDON_MOD_WIKI_SERVICES,
|
||||
// @todo ...ADDON_MOD_WORKSHOP_SERVICES,
|
||||
...ADDON_MOD_WORKSHOP_SERVICES,
|
||||
...ADDON_NOTES_SERVICES,
|
||||
...ADDON_NOTIFICATIONS_SERVICES,
|
||||
...ADDON_PRIVATEFILES_SERVICES,
|
||||
|
|
|
@ -22,10 +22,5 @@ export const CORE_CONTENTLINKS_SERVICES: Type<unknown>[] = [
|
|||
CoreContentLinksHelperProvider,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
@NgModule({})
|
||||
export class CoreContentLinksModule {}
|
||||
|
|
|
@ -67,12 +67,12 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
|||
prefetchStatus?: string; // Used when calling fillContextMenu.
|
||||
prefetchText?: string; // Used when calling fillContextMenu.
|
||||
size?: string; // Used when calling fillContextMenu.
|
||||
isDestroyed?: boolean; // Whether the component is destroyed, used when calling fillContextMenu.
|
||||
isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu.
|
||||
contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
|
||||
contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
|
||||
|
||||
protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
|
||||
protected isCurrentView?: boolean; // Whether the component is in the current view.
|
||||
protected isCurrentView = false; // Whether the component is in the current view.
|
||||
protected siteId?: string; // Current Site ID.
|
||||
protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called.
|
||||
protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
|
||||
|
|
|
@ -20,7 +20,6 @@ import { CoreCourseDownloadModuleMainFileDirective } from './download-module-mai
|
|||
declarations: [
|
||||
CoreCourseDownloadModuleMainFileDirective,
|
||||
],
|
||||
imports: [],
|
||||
exports: [
|
||||
CoreCourseDownloadModuleMainFileDirective,
|
||||
],
|
||||
|
|
|
@ -20,14 +20,11 @@ import { CoreCourseFormatTopicsModule } from './topics/topics.module';
|
|||
import { CoreCourseFormatWeeksModule } from './weeks/weeks.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
CoreCourseFormatSingleActivityModule,
|
||||
CoreCourseFormatSocialModule,
|
||||
CoreCourseFormatTopicsModule,
|
||||
CoreCourseFormatWeeksModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class CoreCourseFormatModule { }
|
||||
|
|
|
@ -18,8 +18,6 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
|
|||
import { CoreCourseFormatSocialHandler } from './services/handlers/social-format';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
|
|
|
@ -18,8 +18,6 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
|
|||
import { CoreCourseFormatTopicsHandler } from './services/handlers/topics-format';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue