MOBILE-3657 workshop: Add workshop activity services

main
Pau Ferrer Ocaña 2021-03-31 16:33:46 +02:00
parent a6574c5b11
commit 8e7b148205
15 changed files with 4922 additions and 7 deletions

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

@ -35,6 +35,7 @@ import { AddonModScormModule } from './scorm/scorm.module';
import { AddonModSurveyModule } from './survey/survey.module';
import { AddonModUrlModule } from './url/url.module';
import { AddonModWikiModule } from './wiki/wiki.module';
import { AddonModWorkshopModule } from './workshop/workshop.module';
@NgModule({
imports: [
@ -59,6 +60,7 @@ import { AddonModWikiModule } from './wiki/wiki.module';
AddonModSurveyModule,
AddonModUrlModule,
AddonModWikiModule,
AddonModWorkshopModule,
],
})
export class AddonModModule { }

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

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

@ -1158,12 +1158,12 @@ export class CoreUtilsProvider {
* @param keyPrefix Key prefix if neededs to delete it.
* @return Object.
*/
objectToKeyValueMap(
objectToKeyValueMap<T = unknown>(
objects: Record<string, unknown>[],
keyName: string,
valueName: string,
keyPrefix?: string,
): {[name: string]: unknown} | undefined {
): {[name: string]: T} | undefined {
if (!objects) {
return;
}