Merge pull request #2724 from crazyserver/MOBILE-3657

Mobile 3657
main
Dani Palou 2021-04-26 13:04:43 +02:00 committed by GitHub
commit c2065ef83a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 10580 additions and 388 deletions

View File

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

View File

@ -68,7 +68,8 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "app:build"
"browserTarget": "app:build",
"port": 8100
},
"configurations": {
"production": {

View File

@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,12 +22,8 @@ export const ADDON_MESSAGEOUTPUT_SERVICES: Type<unknown>[] = [
];
@NgModule({
declarations: [
],
imports: [
AddonMessageOutputAirnotifierModule,
],
providers: [
],
})
export class AddonMessageOutputModule {}

View File

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

View File

@ -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'}">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
{
"dimensioncommentfor": "Comment for {{$a}}",
"dimensionnumber": "Aspect {{$a}}"
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
{
"dimensioncommentfor": "Comment for {{$a}}",
"dimensiongradefor": "Grade for {{$a}}",
"dimensionnumber": "Assertion {{$a}}"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
{
"dimensionnumber": "Criterion {{$a}}",
"mustchooseone": "You have to select one of these items"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,8 +21,6 @@ export const ADDON_REMOTETHEMES_SERVICES: Type<unknown>[] = [
];
@NgModule({
declarations: [],
imports: [],
providers: [
{
provide: APP_INITIALIZER,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,5 @@ export const CORE_BLOCK_SERVICES: Type<unknown>[] = [
CoreBlockHelperProvider,
];
@NgModule({
providers: [],
})
@NgModule({})
export class CoreBlockModule {}

View File

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

View File

@ -22,10 +22,5 @@ export const CORE_CONTENTLINKS_SERVICES: Type<unknown>[] = [
CoreContentLinksHelperProvider,
];
@NgModule({
declarations: [],
imports: [],
providers: [],
exports: [],
})
@NgModule({})
export class CoreContentLinksModule {}

View File

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

View File

@ -20,7 +20,6 @@ import { CoreCourseDownloadModuleMainFileDirective } from './download-module-mai
declarations: [
CoreCourseDownloadModuleMainFileDirective,
],
imports: [],
exports: [
CoreCourseDownloadModuleMainFileDirective,
],

View File

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

View File

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

View File

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