commit
286abdbef4
|
@ -293,7 +293,8 @@ var templatesSrc = [
|
||||||
'./src/core/**/components/**/*.html',
|
'./src/core/**/components/**/*.html',
|
||||||
'./src/core/**/component/**/*.html',
|
'./src/core/**/component/**/*.html',
|
||||||
// Only some addon components are injected to compile to decrease load time. Copy only the ones that are needed.
|
// Only some addon components are injected to compile to decrease load time. Copy only the ones that are needed.
|
||||||
'./src/addon/mod/assign/components/**/*.html'
|
'./src/addon/mod/assign/components/**/*.html',
|
||||||
|
'./src/addon/mod/workshop/components/**/*.html'
|
||||||
],
|
],
|
||||||
templatesDest = './www/templates';
|
templatesDest = './www/templates';
|
||||||
|
|
||||||
|
|
|
@ -1033,7 +1033,7 @@ export class AddonMessagesProvider {
|
||||||
return this.sendMessagesOnline(messages, siteId).then((response) => {
|
return this.sendMessagesOnline(messages, siteId).then((response) => {
|
||||||
if (response && response[0] && response[0].msgid === -1) {
|
if (response && response[0] && response[0].msgid === -1) {
|
||||||
// There was an error, and it should be translated already.
|
// There was an error, and it should be translated already.
|
||||||
return this.utils.createFakeWSError(response[0].errormessage);
|
return Promise.reject(this.utils.createFakeWSError(response[0].errormessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.invalidateDiscussionCache(toUserId, siteId).catch(() => {
|
return this.invalidateDiscussionCache(toUserId, siteId).catch(() => {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ion-note *ngIf="!isSent" color="dark">
|
<ion-note *ngIf="!isSent" color="dark">
|
||||||
<ion-icon name="clock"></ion-icon>
|
<ion-icon name="time"></ion-icon>
|
||||||
{{ 'core.notsent' | translate }}
|
{{ 'core.notsent' | translate }}
|
||||||
</ion-note>
|
</ion-note>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1189,7 +1189,7 @@ export class AddonModFeedbackProvider {
|
||||||
};
|
};
|
||||||
|
|
||||||
return site.write('mod_feedback_process_page', params).catch((error) => {
|
return site.write('mod_feedback_process_page', params).catch((error) => {
|
||||||
return this.utils.createFakeWSError(error);
|
return Promise.reject(this.utils.createFakeWSError(error));
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
// Invalidate and update current values because they will change.
|
// Invalidate and update current values because they will change.
|
||||||
return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => {
|
return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => {
|
||||||
|
|
|
@ -179,7 +179,7 @@ export class AddonModForumProvider {
|
||||||
return site.write('mod_forum_add_discussion', params).then((response) => {
|
return site.write('mod_forum_add_discussion', params).then((response) => {
|
||||||
// Other errors ocurring.
|
// Other errors ocurring.
|
||||||
if (!response || !response.discussionid) {
|
if (!response || !response.discussionid) {
|
||||||
return this.utils.createFakeWSError('');
|
return Promise.reject(this.utils.createFakeWSError(''));
|
||||||
} else {
|
} else {
|
||||||
return response.discussionid;
|
return response.discussionid;
|
||||||
}
|
}
|
||||||
|
@ -694,7 +694,7 @@ export class AddonModForumProvider {
|
||||||
|
|
||||||
return site.write('mod_forum_add_discussion_post', params).then((response) => {
|
return site.write('mod_forum_add_discussion_post', params).then((response) => {
|
||||||
if (!response || !response.postid) {
|
if (!response || !response.postid) {
|
||||||
return this.utils.createFakeWSError('');
|
return Promise.reject(this.utils.createFakeWSError(''));
|
||||||
} else {
|
} else {
|
||||||
return response.postid;
|
return response.postid;
|
||||||
}
|
}
|
||||||
|
|
|
@ -812,7 +812,7 @@ export class AddonModGlossaryProvider {
|
||||||
return response.entryid;
|
return response.entryid;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.utils.createFakeWSError('');
|
return Promise.reject(this.utils.createFakeWSError(''));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,7 +268,7 @@ export class AddonModSurveyProvider {
|
||||||
|
|
||||||
return site.write('mod_survey_submit_answers', params).then((response) => {
|
return site.write('mod_survey_submit_answers', params).then((response) => {
|
||||||
if (!response.status) {
|
if (!response.status) {
|
||||||
return this.utils.createFakeWSError('');
|
return Promise.reject(this.utils.createFakeWSError(''));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -482,8 +482,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
|
||||||
pageId: this.pageId,
|
pageId: this.pageId,
|
||||||
subwikiId: this.subwikiId,
|
subwikiId: this.subwikiId,
|
||||||
pageTitle: title,
|
pageTitle: title,
|
||||||
siteId: this.sitesProvider.getCurrentSiteId()
|
}, this.sitesProvider.getCurrentSiteId());
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Page stored in offline. Go to see the offline page.
|
// Page stored in offline. Go to see the offline page.
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from './component/accumulative';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyAccumulativeHandler } from './providers/handler';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopAssessmentStrategyAccumulativeComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModWorkshopAssessmentStrategyAccumulativeHandler
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModWorkshopAssessmentStrategyAccumulativeComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModWorkshopAssessmentStrategyAccumulativeComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyAccumulativeModule {
|
||||||
|
constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate,
|
||||||
|
strategyHandler: AddonModWorkshopAssessmentStrategyAccumulativeHandler) {
|
||||||
|
strategyDelegate.registerHandler(strategyHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<ion-card *ngFor="let field of assessment.form.fields; let n = index">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ field.dimtitle }}</h2>
|
||||||
|
<core-format-text [text]="field.description"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="edit && field.grades">
|
||||||
|
<ion-label [core-mark-required]="true" stacked>{{ 'addon.mod_workshop_assessment_accumulative.dimensiongradefor' | translate : {'$a': field.dimtitle } }}</ion-label>
|
||||||
|
<ion-select [(ngModel)]="selectedValues[n].grade">
|
||||||
|
<ion-option *ngFor="let grade of field.grades" [value]="grade.value">{{grade.label}}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
<core-input-errors item-content *ngIf="fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]"></core-input-errors>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="!edit && field.grades" text-wrap>
|
||||||
|
<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-item>
|
||||||
|
<ion-item *ngIf="edit">
|
||||||
|
<ion-label 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" text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop_assessment_accumulative.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2>
|
||||||
|
<p><core-format-text [text]="selectedValues[n].peercomment"></core-format-text></p>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
|
@ -0,0 +1,26 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for accumulative assessment strategy.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-workshop-assessment-strategy-accumulative',
|
||||||
|
templateUrl: 'accumulative.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyAccumulativeComponent extends AddonModWorkshopAssessmentStrategyComponentBase {
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"dimensioncommentfor": "Comment for {{$a}}",
|
||||||
|
"dimensiongradefor": "Grade for {{$a}}",
|
||||||
|
"dimensionnumber": "Aspect {{$a}}",
|
||||||
|
"mustchoosegrade": "You have to select a grade for this aspect"
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Injector } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
|
||||||
|
import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyAccumulativeComponent } from '../component/accumulative';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for accumulative assessment strategy plugin.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopAssessmentStrategyAccumulativeHandler implements AddonWorkshopAssessmentStrategyHandler {
|
||||||
|
name = 'AddonModWorkshopAssessmentStrategyAccumulative';
|
||||||
|
strategyName = 'accumulative';
|
||||||
|
|
||||||
|
constructor(private translate: TranslateService, private gradesHelper: CoreGradesHelperProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Injector.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector): any | Promise<any> {
|
||||||
|
return AddonModWorkshopAssessmentStrategyAccumulativeComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare original values to be shown and compared.
|
||||||
|
*
|
||||||
|
* @param {any} form Original data of the form.
|
||||||
|
* @param {number} workshopId WorkShop Id
|
||||||
|
* @return {Promise<any[]>} Promise resolved with original values sorted.
|
||||||
|
*/
|
||||||
|
getOriginalValues(form: any, workshopId: number): Promise<any[]> {
|
||||||
|
const defaultGrade = this.translate.instant('core.choosedots'),
|
||||||
|
originalValues = [],
|
||||||
|
promises = [];
|
||||||
|
|
||||||
|
form.fields.forEach((field, n) => {
|
||||||
|
field.dimtitle = this.translate.instant(
|
||||||
|
'addon.mod_workshop_assessment_accumulative.dimensionnumber', {$a: field.number});
|
||||||
|
|
||||||
|
const scale = parseInt(field.grade, 10) < 0 ? form.dimensionsinfo[n].scale : null;
|
||||||
|
|
||||||
|
if (!form.current[n]) {
|
||||||
|
form.current[n] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
originalValues[n] = {
|
||||||
|
peercomment: form.current[n].peercomment || '',
|
||||||
|
number: field.number
|
||||||
|
};
|
||||||
|
|
||||||
|
form.current[n].grade = form.current[n].grade ? parseInt(form.current[n].grade, 10) : -1;
|
||||||
|
|
||||||
|
promises.push(this.gradesHelper.makeGradesMenu(field.grade, workshopId, defaultGrade, -1, scale).then((grades) => {
|
||||||
|
field.grades = grades;
|
||||||
|
originalValues[n].grade = form.current[n].grade;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
return originalValues;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||||
|
*
|
||||||
|
* @param {any[]} originalValues Original values of the form.
|
||||||
|
* @param {any[]} currentValues Current values of the form.
|
||||||
|
* @return {boolean} True if data has changed, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataChanged(originalValues: any[], currentValues: any[]): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {any{}} currentValues Current values of the form.
|
||||||
|
* @param {any} form Assessment form data.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(currentValues: any[], form: any): Promise<any> {
|
||||||
|
const data = {};
|
||||||
|
const errors = {};
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
form.fields.forEach((field, idx) => {
|
||||||
|
const grade = parseInt(currentValues[idx].grade, 10);
|
||||||
|
if (!isNaN(grade) && grade >= 0) {
|
||||||
|
data['grade__idx_' + idx] = grade;
|
||||||
|
} else {
|
||||||
|
errors['grade_' + idx] = this.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) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyAccumulativeModule } from './accumulative/accumulative.module';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyCommentsModule } from './comments/comments.module';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyNumErrorsModule } from './numerrors/numerrors.module';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyRubricModule } from './rubric/rubric.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
AddonModWorkshopAssessmentStrategyAccumulativeModule,
|
||||||
|
AddonModWorkshopAssessmentStrategyCommentsModule,
|
||||||
|
AddonModWorkshopAssessmentStrategyNumErrorsModule,
|
||||||
|
AddonModWorkshopAssessmentStrategyRubricModule,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyModule {}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyCommentsComponent } from './component/comments';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyCommentsHandler } from './providers/handler';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopAssessmentStrategyCommentsComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModWorkshopAssessmentStrategyCommentsHandler
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModWorkshopAssessmentStrategyCommentsComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModWorkshopAssessmentStrategyCommentsComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyCommentsModule {
|
||||||
|
constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate,
|
||||||
|
strategyHandler: AddonModWorkshopAssessmentStrategyCommentsHandler) {
|
||||||
|
strategyDelegate.registerHandler(strategyHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<ion-card *ngFor="let field of assessment.form.fields; let n = index">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ field.dimtitle }}</h2>
|
||||||
|
<core-format-text [text]="field.description"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="edit">
|
||||||
|
<ion-label stacked [core-mark-required]="true">{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</ion-label>
|
||||||
|
<ion-textarea aria-multiline="true" [(ngModel)]="selectedValues[n].peercomment" core-auto-rows></ion-textarea>
|
||||||
|
<core-input-errors item-content *ngIf="fieldErrors['peercomment_' + n]" [errorText]="fieldErrors['peercomment_' + n]"></core-input-errors>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="!edit" text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop_assessment_comments.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2>
|
||||||
|
<p><core-format-text [text]="selectedValues[n].peercomment"></core-format-text></p>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
|
@ -0,0 +1,26 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for comments assessment strategy.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-workshop-assessment-strategy-comments',
|
||||||
|
templateUrl: 'comments.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyCommentsComponent extends AddonModWorkshopAssessmentStrategyComponentBase {
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"dimensioncommentfor": "Comment for {{$a}}",
|
||||||
|
"dimensionnumber": "Aspect {{$a}}"
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Injector } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyCommentsComponent } from '../component/comments';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for comments assessment strategy plugin.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopAssessmentStrategyCommentsHandler implements AddonWorkshopAssessmentStrategyHandler {
|
||||||
|
name = 'AddonModWorkshopAssessmentStrategyComments';
|
||||||
|
strategyName = 'comments';
|
||||||
|
|
||||||
|
constructor(private translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Injector.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector): any | Promise<any> {
|
||||||
|
return AddonModWorkshopAssessmentStrategyCommentsComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare original values to be shown and compared.
|
||||||
|
*
|
||||||
|
* @param {any} form Original data of the form.
|
||||||
|
* @param {number} workshopId Workshop Id
|
||||||
|
* @return {Promise<any[]>} Promise resolved with original values sorted.
|
||||||
|
*/
|
||||||
|
getOriginalValues(form: any, workshopId: number): Promise<any[]> {
|
||||||
|
const originalValues = [];
|
||||||
|
|
||||||
|
form.fields.forEach((field, n) => {
|
||||||
|
field.dimtitle = this.translate.instant('addon.mod_workshop_assessment_comments.dimensionnumber', {$a: field.number});
|
||||||
|
|
||||||
|
if (!form.current[n]) {
|
||||||
|
form.current[n] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
originalValues[n] = {
|
||||||
|
peercomment: form.current[n].peercomment || '',
|
||||||
|
number: field.number
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(originalValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||||
|
*
|
||||||
|
* @param {any[]} originalValues Original values of the form.
|
||||||
|
* @param {any[]} currentValues Current values of the form.
|
||||||
|
* @return {boolean} True if data has changed, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataChanged(originalValues: any[], currentValues: any[]): boolean {
|
||||||
|
for (const x in originalValues) {
|
||||||
|
if (originalValues[x].peercomment != currentValues[x].peercomment) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {any{}} currentValues Current values of the form.
|
||||||
|
* @param {any} form Assessment form data.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(currentValues: any[], form: any): Promise<any> {
|
||||||
|
const data = {};
|
||||||
|
const errors = {};
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
form.fields.forEach((field, idx) => {
|
||||||
|
if (currentValues[idx].peercomment) {
|
||||||
|
data['peercomment__idx_' + idx] = currentValues[idx].peercomment;
|
||||||
|
} else {
|
||||||
|
errors['peercomment_' + idx] = this.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) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<ion-card *ngFor="let field of assessment.form.fields; let n = index">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ field.dimtitle }}</h2>
|
||||||
|
<core-format-text [text]="field.description"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ion-list radio-group [(ngModel)]="selectedValues[n].grade" [name]="'grade_' + n">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label stacked [core-mark-required]="edit">{{ 'addon.mod_workshop.yourassessmentfor' | translate : {'$a': field.dimtitle } }}</ion-label>
|
||||||
|
<core-input-errors item-content *ngIf="edit && fieldErrors['grade_' + n]" [errorText]="fieldErrors['grade_' + n]"></core-input-errors>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label><core-format-text [text]="field.grade0"></core-format-text></ion-label>
|
||||||
|
<ion-radio [value]="-1" [disabled]="!edit"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label><core-format-text [text]="field.grade1"></core-format-text></ion-label>
|
||||||
|
<ion-radio [value]="1" [disabled]="!edit"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
<ion-item *ngIf="edit">
|
||||||
|
<ion-label 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" text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop_assessment_numerrors.dimensioncommentfor' | translate : {'$a': field.dimtitle } }}</h2>
|
||||||
|
<p><core-format-text [text]="selectedValues[n].peercomment"></core-format-text></p>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
|
@ -0,0 +1,26 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for numerrors assessment strategy.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-workshop-assessment-strategy-numerrors',
|
||||||
|
templateUrl: 'numerrors.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyNumErrorsComponent extends AddonModWorkshopAssessmentStrategyComponentBase {
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"dimensioncommentfor": "Comment for {{$a}}",
|
||||||
|
"dimensiongradefor": "Grade for {{$a}}",
|
||||||
|
"dimensionnumber": "Assertion {{$a}}"
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from './component/numerrors';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyNumErrorsHandler } from './providers/handler';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopAssessmentStrategyNumErrorsComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModWorkshopAssessmentStrategyNumErrorsHandler
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModWorkshopAssessmentStrategyNumErrorsComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModWorkshopAssessmentStrategyNumErrorsComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyNumErrorsModule {
|
||||||
|
constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate,
|
||||||
|
strategyHandler: AddonModWorkshopAssessmentStrategyNumErrorsHandler) {
|
||||||
|
strategyDelegate.registerHandler(strategyHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Injector } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyNumErrorsComponent } from '../component/numerrors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for numerrors assessment strategy plugin.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopAssessmentStrategyNumErrorsHandler implements AddonWorkshopAssessmentStrategyHandler {
|
||||||
|
name = 'AddonModWorkshopAssessmentStrategyNumErrors';
|
||||||
|
strategyName = 'numerrors';
|
||||||
|
|
||||||
|
constructor(private translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Injector.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector): any | Promise<any> {
|
||||||
|
return AddonModWorkshopAssessmentStrategyNumErrorsComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare original values to be shown and compared.
|
||||||
|
*
|
||||||
|
* @param {any} form Original data of the form.
|
||||||
|
* @param {number} workshopId Workshop Id
|
||||||
|
* @return {Promise<any[]>} Promise resolved with original values sorted.
|
||||||
|
*/
|
||||||
|
getOriginalValues(form: any, workshopId: number): Promise<any[]> {
|
||||||
|
const originalValues = [];
|
||||||
|
|
||||||
|
form.fields.forEach((field, n) => {
|
||||||
|
field.dimtitle = this.translate.instant('addon.mod_workshop_assessment_numerrors.dimensionnumber', {$a: field.number});
|
||||||
|
|
||||||
|
if (!form.current[n]) {
|
||||||
|
form.current[n] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
originalValues[n] = {
|
||||||
|
peercomment: form.current[n].peercomment || '',
|
||||||
|
number: field.number,
|
||||||
|
grade: form.current[n].grade || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(originalValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||||
|
*
|
||||||
|
* @param {any[]} originalValues Original values of the form.
|
||||||
|
* @param {any[]} currentValues Current values of the form.
|
||||||
|
* @return {boolean} True if data has changed, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataChanged(originalValues: any[], currentValues: any[]): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {any{}} currentValues Current values of the form.
|
||||||
|
* @param {any} form Assessment form data.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(currentValues: any[], form: any): Promise<any> {
|
||||||
|
const data = {};
|
||||||
|
const errors = {};
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
form.fields.forEach((field, idx) => {
|
||||||
|
const grade = parseInt(currentValues[idx].grade);
|
||||||
|
if (!isNaN(grade) && grade >= 0) {
|
||||||
|
data['grade__idx_' + idx] = grade;
|
||||||
|
} else {
|
||||||
|
errors['grade_' + idx] = this.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) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<ion-card *ngFor="let field of assessment.form.fields; let n = index">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2 [core-mark-required]="edit">{{ field.dimtitle }}</h2>
|
||||||
|
<core-format-text [text]="field.description"></core-format-text>
|
||||||
|
<core-input-errors *ngIf="edit && fieldErrors['chosenlevelid_' + n]" [errorText]="fieldErrors['chosenlevelid_' + n]"></core-input-errors>
|
||||||
|
</ion-item>
|
||||||
|
<ion-list 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"></core-format-text></p></ion-label>
|
||||||
|
<ion-radio [value]="subfield.levelid" [disabled]="!edit"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-card>
|
|
@ -0,0 +1,26 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { AddonModWorkshopAssessmentStrategyComponentBase } from '../../../classes/assessment-strategy-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for rubric assessment strategy.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-workshop-assessment-strategy-rubric',
|
||||||
|
templateUrl: 'rubric.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyRubricComponent extends AddonModWorkshopAssessmentStrategyComponentBase {
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"dimensionnumber": "Criterion {{$a}}",
|
||||||
|
"mustchooseone": "You have to select one of these items"
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Injector } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { AddonWorkshopAssessmentStrategyHandler } from '../../../providers/assessment-strategy-delegate';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyRubricComponent } from '../component/rubric';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for rubric assessment strategy plugin.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopAssessmentStrategyRubricHandler implements AddonWorkshopAssessmentStrategyHandler {
|
||||||
|
name = 'AddonModWorkshopAssessmentStrategyRubric';
|
||||||
|
strategyName = 'rubric';
|
||||||
|
|
||||||
|
constructor(private translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Injector.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector): any | Promise<any> {
|
||||||
|
return AddonModWorkshopAssessmentStrategyRubricComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare original values to be shown and compared.
|
||||||
|
*
|
||||||
|
* @param {any} form Original data of the form.
|
||||||
|
* @param {number} workshopId Workshop Id
|
||||||
|
* @return {Promise<any[]>} Promise resolved with original values sorted.
|
||||||
|
*/
|
||||||
|
getOriginalValues(form: any, workshopId: number): Promise<any[]> {
|
||||||
|
const originalValues = [];
|
||||||
|
|
||||||
|
form.fields.forEach((field, n) => {
|
||||||
|
field.dimtitle = this.translate.instant('addon.mod_workshop_assessment_rubric.dimensionnumber', {$a: field.number});
|
||||||
|
|
||||||
|
if (!form.current[n]) {
|
||||||
|
form.current[n] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
originalValues[n] = {
|
||||||
|
chosenlevelid: form.current[n].chosenlevelid || '',
|
||||||
|
number: field.number
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(originalValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||||
|
*
|
||||||
|
* @param {any[]} originalValues Original values of the form.
|
||||||
|
* @param {any[]} currentValues Current values of the form.
|
||||||
|
* @return {boolean} True if data has changed, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataChanged(originalValues: any[], currentValues: any[]): boolean {
|
||||||
|
for (const x in originalValues) {
|
||||||
|
if (originalValues[x].chosenlevelid != (currentValues[x].chosenlevelid || '')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {any{}} currentValues Current values of the form.
|
||||||
|
* @param {any} form Assessment form data.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(currentValues: any[], form: any): Promise<any> {
|
||||||
|
const data = {};
|
||||||
|
const errors = {};
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
form.fields.forEach((field, idx) => {
|
||||||
|
const id = parseInt(currentValues[idx].chosenlevelid, 10);
|
||||||
|
if (!isNaN(id) && id >= 0) {
|
||||||
|
data['chosenlevelid__idx_' + idx] = id;
|
||||||
|
} else {
|
||||||
|
errors['chosenlevelid_' + idx] = this.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) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyRubricComponent } from './component/rubric';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyRubricHandler } from './providers/handler';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopAssessmentStrategyRubricComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModWorkshopAssessmentStrategyRubricHandler
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModWorkshopAssessmentStrategyRubricComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModWorkshopAssessmentStrategyRubricComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentStrategyRubricModule {
|
||||||
|
constructor(strategyDelegate: AddonWorkshopAssessmentStrategyDelegate,
|
||||||
|
strategyHandler: AddonModWorkshopAssessmentStrategyRubricHandler) {
|
||||||
|
strategyDelegate.registerHandler(strategyHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { Input } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for component to render an assessment strategy.
|
||||||
|
*/
|
||||||
|
export class AddonModWorkshopAssessmentStrategyComponentBase {
|
||||||
|
@Input() workshopId: number;
|
||||||
|
@Input() assessment: any;
|
||||||
|
@Input() edit: boolean;
|
||||||
|
@Input() selectedValues: any[];
|
||||||
|
@Input() fieldErrors: any;
|
||||||
|
@Input() strategy: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
<h3 padding>{{ 'addon.mod_workshop.assessmentform' | translate }}</h3>
|
||||||
|
|
||||||
|
<form name="mma-mod_workshop-assessment-form">
|
||||||
|
<core-loading [hideUntil]="assessmentStrategyLoaded">
|
||||||
|
<ng-container *ngIf="componentClass && assessmentStrategyLoaded">
|
||||||
|
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="core-info-card" *ngIf="notSupported">
|
||||||
|
{{ 'addon.mod_workshop.assessmentstrategynotsupported' | translate:{$a: strategy} }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-card *ngIf="assessmentStrategyLoaded && overallFeedkback && (edit || data.assessment.feedbackauthor || data.assessment.feedbackattachmentfiles.length) ">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.overallfeedback' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item stacked *ngIf="edit">
|
||||||
|
<ion-label stacked [core-mark-required]="overallFeedkbackRequired">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
|
||||||
|
<core-rich-text-editor item-content [control]="feedbackControl" [component]="component" [componentId]="workshop.coursemodule" (contentChanged)="onFeedbackChange($event)"></core-rich-text-editor>
|
||||||
|
<core-input-errors item-content *ngIf="overallFeedkbackRequired && fieldErrors['feedbackauthor']" [errorText]="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 stacked [core-mark-required]="true">{{ 'addon.mod_workshop.assessmentweight' | translate }}</ion-label>
|
||||||
|
<ion-select [(ngModel)]="weight">
|
||||||
|
<ion-option *ngFor="let w of weights" [value]="w">{{w}}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="!edit && data.assessment.feedbackauthor">
|
||||||
|
<core-format-text [component]="component" [componentId]="componentId" [text]="data.assessment.feedbackauthor"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="!edit && workshop.overallfeedbackfiles && data.assessment.feedbackattachmentfiles && data.assessment.feedbackattachmentfiles.length">
|
||||||
|
<ng-container *ngFor="let attachment of data.assessment.feedbackattachmentfiles">
|
||||||
|
<!-- Files already attached to the submission. -->
|
||||||
|
<core-file *ngIf="!attachment.name" [file]="attachment" [component]="component" [componentId]="componentId"></core-file>
|
||||||
|
<!-- Files stored in offline to be sent later. -->
|
||||||
|
<core-local-file *ngIf="attachment.name" [file]="attachment"></core-local-file>
|
||||||
|
</ng-container>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
</core-loading>
|
||||||
|
</form>
|
|
@ -0,0 +1,364 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Injector } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreFileSessionProvider } from '@providers/file-session';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||||
|
import { AddonModWorkshopProvider } from '../../providers/workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from '../../providers/assessment-strategy-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
|
||||||
|
@Input() workshop: any;
|
||||||
|
@Input() access: any;
|
||||||
|
@Input() assessmentId: number;
|
||||||
|
@Input() userId: number;
|
||||||
|
@Input() strategy: string;
|
||||||
|
@Input() edit?: boolean;
|
||||||
|
|
||||||
|
componentClass: any;
|
||||||
|
data = {
|
||||||
|
workshopId: 0,
|
||||||
|
assessment: null,
|
||||||
|
edit: false,
|
||||||
|
selectedValues: [],
|
||||||
|
fieldErrors: {},
|
||||||
|
strategy: ''
|
||||||
|
};
|
||||||
|
assessmentStrategyLoaded = false;
|
||||||
|
notSupported = false;
|
||||||
|
feedbackText = '';
|
||||||
|
feedbackControl = new FormControl();
|
||||||
|
overallFeedkback = false;
|
||||||
|
overallFeedkbackRequired = false;
|
||||||
|
component = AddonModWorkshopProvider.COMPONENT;
|
||||||
|
componentId: number;
|
||||||
|
weights: any[];
|
||||||
|
weight: number;
|
||||||
|
|
||||||
|
protected rteEnabled: boolean;
|
||||||
|
protected obsInvalidated: any;
|
||||||
|
protected hasOffline: boolean;
|
||||||
|
protected originalData = {
|
||||||
|
text: '',
|
||||||
|
files: [],
|
||||||
|
weight: 1,
|
||||||
|
selectedValues: []
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private translate: TranslateService,
|
||||||
|
private injector: Injector,
|
||||||
|
private eventsProvider: CoreEventsProvider,
|
||||||
|
private fileSessionProvider: CoreFileSessionProvider,
|
||||||
|
private syncProvider: CoreSyncProvider,
|
||||||
|
private domUtils: CoreDomUtilsProvider,
|
||||||
|
private textUtils: CoreTextUtilsProvider,
|
||||||
|
private utils: CoreUtilsProvider,
|
||||||
|
private sitesProvider: CoreSitesProvider,
|
||||||
|
private uploaderProvider: CoreFileUploaderProvider,
|
||||||
|
private workshopProvider: AddonModWorkshopProvider,
|
||||||
|
private workshopHelper: AddonModWorkshopHelperProvider,
|
||||||
|
private workshopOffline: AddonModWorkshopOfflineProvider,
|
||||||
|
private strategyDelegate: AddonWorkshopAssessmentStrategyDelegate) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): 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.componentClass = this.strategyDelegate.getComponentForPlugin(this.injector, this.strategy);
|
||||||
|
if (this.componentClass) {
|
||||||
|
this.overallFeedkback = !!this.workshop.overallfeedbackmode;
|
||||||
|
this.overallFeedkbackRequired = this.workshop.overallfeedbackmode == 2;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
// Check if rich text editor is enabled.
|
||||||
|
if (this.edit) {
|
||||||
|
// Block the workshop.
|
||||||
|
this.syncProvider.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshop.id);
|
||||||
|
|
||||||
|
promise = this.domUtils.isRichTextEditorEnabled();
|
||||||
|
} else {
|
||||||
|
// We aren't editing, so no rich text editor.
|
||||||
|
promise = Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then((enabled) => {
|
||||||
|
this.rteEnabled = enabled;
|
||||||
|
|
||||||
|
return this.load();
|
||||||
|
}).then(() => {
|
||||||
|
this.obsInvalidated = this.eventsProvider.on(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED,
|
||||||
|
this.load.bind(this), this.sitesProvider.getCurrentSiteId());
|
||||||
|
}).finally(() => {
|
||||||
|
this.assessmentStrategyLoaded = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Helper data and fallback.
|
||||||
|
this.notSupported = !this.strategyDelegate.isPluginSupported(this.strategy);
|
||||||
|
this.assessmentStrategyLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to load the assessment data.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promised resvoled when data is loaded.
|
||||||
|
*/
|
||||||
|
protected load(): Promise<any> {
|
||||||
|
return this.workshopHelper.getReviewerAssessmentById(this.workshop.id, this.assessmentId, this.userId)
|
||||||
|
.then((assessmentData) => {
|
||||||
|
this.data.assessment = assessmentData;
|
||||||
|
|
||||||
|
let promise;
|
||||||
|
if (this.edit) {
|
||||||
|
promise = this.workshopOffline.getAssessment(this.workshop.id, this.assessmentId).then((offlineAssessment) => {
|
||||||
|
const offlineData = offlineAssessment.inputdata;
|
||||||
|
|
||||||
|
this.hasOffline = true;
|
||||||
|
|
||||||
|
assessmentData.feedbackauthor = offlineData.feedbackauthor;
|
||||||
|
|
||||||
|
if (this.access.canallocate) {
|
||||||
|
assessmentData.weight = offlineData.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override assessment plugins values.
|
||||||
|
assessmentData.form.current = this.workshopProvider.parseFields(
|
||||||
|
this.utils.objectToArrayOfObjects(offlineData, 'name', 'value'));
|
||||||
|
|
||||||
|
// Override offline files.
|
||||||
|
if (offlineData) {
|
||||||
|
return this.workshopHelper.getAssessmentFilesFromOfflineFilesObject(
|
||||||
|
offlineData.feedbackauthorattachmentsid, this.workshop.id, this.assessmentId)
|
||||||
|
.then((files) => {
|
||||||
|
assessmentData.feedbackattachmentfiles = files;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.hasOffline = false;
|
||||||
|
// Ignore errors.
|
||||||
|
}).finally(() => {
|
||||||
|
this.feedbackText = assessmentData.feedbackauthor;
|
||||||
|
this.feedbackControl.setValue(this.feedbackText);
|
||||||
|
|
||||||
|
this.originalData.text = this.data.assessment.feedbackauthor;
|
||||||
|
|
||||||
|
if (this.access.canallocate) {
|
||||||
|
this.originalData.weight = assessmentData.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.originalData.files = [];
|
||||||
|
assessmentData.feedbackattachmentfiles.forEach((file) => {
|
||||||
|
let filename;
|
||||||
|
if (file.filename) {
|
||||||
|
filename = file.filename;
|
||||||
|
} else {
|
||||||
|
// We don't have filename, extract it from the path.
|
||||||
|
filename = file.filepath[0] == '/' ? file.filepath.substr(1) : file.filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.originalData.files.push({
|
||||||
|
filename : filename,
|
||||||
|
fileurl: file.fileurl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(() => {
|
||||||
|
return this.strategyDelegate.getOriginalValues(this.strategy, assessmentData.form, this.workshop.id)
|
||||||
|
.then((values) => {
|
||||||
|
this.data.selectedValues = values;
|
||||||
|
}).finally(() => {
|
||||||
|
this.originalData.selectedValues = this.utils.clone(this.data.selectedValues);
|
||||||
|
if (this.edit) {
|
||||||
|
this.fileSessionProvider.setFiles(AddonModWorkshopProvider.COMPONENT,
|
||||||
|
this.workshop.id + '_' + this.assessmentId, assessmentData.feedbackattachmentfiles);
|
||||||
|
if (this.access.canallocate) {
|
||||||
|
this.weight = assessmentData.weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data has changed.
|
||||||
|
*
|
||||||
|
* @return {boolean} True if data has changed.
|
||||||
|
*/
|
||||||
|
hasDataChanged(): boolean {
|
||||||
|
if (!this.assessmentStrategyLoaded) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare feedback text.
|
||||||
|
const text = this.textUtils.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 = this.fileSessionProvider.getFiles(AddonModWorkshopProvider.COMPONENT,
|
||||||
|
this.workshop.id + '_' + this.assessmentId) || [];
|
||||||
|
if (this.uploaderProvider.areFileListDifferent(files, this.originalData.files)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.strategyDelegate.hasDataChanged(this.workshop, this.originalData.selectedValues, this.data.selectedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the assessment.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done, rejected if assessment could not be saved.
|
||||||
|
*/
|
||||||
|
saveAssessment(): Promise<any> {
|
||||||
|
const files = this.fileSessionProvider.getFiles(AddonModWorkshopProvider.COMPONENT,
|
||||||
|
this.workshop.id + '_' + this.assessmentId) || [];
|
||||||
|
let saveOffline = false;
|
||||||
|
let allowOffline = !files.length;
|
||||||
|
|
||||||
|
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
|
this.data.fieldErrors = {};
|
||||||
|
|
||||||
|
// Upload attachments first if any.
|
||||||
|
return this.workshopHelper.uploadOrStoreAssessmentFiles(this.workshop.id, this.assessmentId, files,
|
||||||
|
saveOffline).catch(() => {
|
||||||
|
// Cannot upload them in online, save them in offline.
|
||||||
|
saveOffline = true;
|
||||||
|
allowOffline = true;
|
||||||
|
|
||||||
|
return this.workshopHelper.uploadOrStoreAssessmentFiles(this.workshop.id, this.assessmentId, files, saveOffline);
|
||||||
|
}).then((attachmentsId) => {
|
||||||
|
const text = this.textUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment.feedbackcontentfiles || []);
|
||||||
|
|
||||||
|
return this.workshopHelper.prepareAssessmentData(this.workshop, this.data.selectedValues, text, files,
|
||||||
|
this.data.assessment.form, attachmentsId).catch((errors) => {
|
||||||
|
this.data.fieldErrors = errors;
|
||||||
|
|
||||||
|
return Promise.reject(this.translate.instant('core.errorinvalidform'));
|
||||||
|
});
|
||||||
|
}).then((assessmentData) => {
|
||||||
|
if (saveOffline) {
|
||||||
|
// Save assessment in offline.
|
||||||
|
return this.workshopOffline.saveAssessment(this.workshop.id, this.assessmentId, this.workshop.course,
|
||||||
|
assessmentData).then(() => {
|
||||||
|
// Don't return anything.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send it to server.
|
||||||
|
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||||
|
return this.workshopProvider.updateAssessment(this.workshop.id, this.assessmentId, this.workshop.course,
|
||||||
|
assessmentData, false, allowOffline);
|
||||||
|
}).then((grade) => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
// If sent to the server, invalidate and clean.
|
||||||
|
if (grade) {
|
||||||
|
promises.push(this.workshopHelper.deleteAssessmentStoredFiles(this.workshop.id, this.assessmentId));
|
||||||
|
promises.push(this.workshopProvider.invalidateAssessmentFormData(this.workshop.id, this.assessmentId));
|
||||||
|
promises.push(this.workshopProvider.invalidateAssessmentData(this.workshop.id, this.assessmentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
}).finally(() => {
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, {
|
||||||
|
workshopId: this.workshop.id,
|
||||||
|
assessmentId: this.assessmentId,
|
||||||
|
userId: this.sitesProvider.getCurrentSiteUserId(),
|
||||||
|
}, this.sitesProvider.getCurrentSiteId());
|
||||||
|
|
||||||
|
if (files) {
|
||||||
|
// Delete the local files from the tmp folder.
|
||||||
|
this.uploaderProvider.clearTmpFiles(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'Error saving assessment.');
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feedback text changed.
|
||||||
|
*
|
||||||
|
* @param {string} text The new text.
|
||||||
|
*/
|
||||||
|
onFeedbackChange(text: string): void {
|
||||||
|
this.feedbackText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.obsInvalidated && this.obsInvalidated.off();
|
||||||
|
|
||||||
|
if (this.data.assessment.feedbackattachmentfiles) {
|
||||||
|
// Delete the local files from the tmp folder.
|
||||||
|
this.uploaderProvider.clearTmpFiles(this.data.assessment.feedbackattachmentfiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<ion-item *ngIf="summary" text-wrap [attr.detail-push]="canViewAssessment && !canSelfAssess? true : null" (click)="gotoAssessment()">
|
||||||
|
<ion-avatar item-start>
|
||||||
|
<img [src]="profile && profile.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: profile && profile.fullname}" core-user-link [courseId]="courseId" [userId]="profile && profile.id" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<button ion-button block *ngIf="canSelfAssess && !showGrade(assessment.grade)" (click)="gotoOwnAssessment()">{{ 'addon.mod_workshop.assess' | translate }}</button>
|
||||||
|
<button ion-button block *ngIf="canSelfAssess && showGrade(assessment.grade)" (click)="gotoOwnAssessment()">{{ 'addon.mod_workshop.reassess' | translate }}</button>
|
||||||
|
<ion-note item-end *ngIf="offline">
|
||||||
|
<ion-icon name="time"></ion-icon>{{ 'core.notsent' | translate }}
|
||||||
|
</ion-note>
|
||||||
|
</ion-item>
|
||||||
|
</core-loading>
|
|
@ -0,0 +1,35 @@
|
||||||
|
addon-mod-workshop-assessment {
|
||||||
|
.item-md.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-md-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-ios.item-block .item-inner {
|
||||||
|
border-bottom: $hairlines-width solid $list-ios-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-wp.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-wp-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child .item .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.with-borders addon-mod-workshop-assessment {
|
||||||
|
.item-md.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-md-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-ios.item-block .item-inner {
|
||||||
|
border-bottom: $hairlines-width solid $list-ios-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-wp.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-wp-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child .item .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { NavController } from 'ionic-angular';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from '../../providers/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: any;
|
||||||
|
@Input() summary?: boolean;
|
||||||
|
@Input() courseId: number;
|
||||||
|
@Input() submission: any;
|
||||||
|
@Input() module?: any;
|
||||||
|
@Input() workshop: any;
|
||||||
|
@Input() access: any;
|
||||||
|
|
||||||
|
canViewAssessment = false;
|
||||||
|
canSelfAssess = false;
|
||||||
|
profile: any;
|
||||||
|
showGrade: any;
|
||||||
|
offline = false;
|
||||||
|
loaded = false;
|
||||||
|
|
||||||
|
protected currentUserId: number;
|
||||||
|
protected assessmentId: number;
|
||||||
|
|
||||||
|
constructor(private workshopOffline: AddonModWorkshopOfflineProvider, private workshopHelper: AddonModWorkshopHelperProvider,
|
||||||
|
private navCtrl: NavController, private userProvider: CoreUserProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
|
sitesProvider: CoreSitesProvider) {
|
||||||
|
this.currentUserId = sitesProvider.getCurrentSiteUserId();
|
||||||
|
this.showGrade = this.workshopHelper.showGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
const canAssess = this.access && this.access.assessingallowed,
|
||||||
|
userId = this.assessment.userid || this.assessment.reviewerid,
|
||||||
|
promises = [];
|
||||||
|
|
||||||
|
this.assessmentId = this.assessment.assessmentid || this.assessment.id;
|
||||||
|
this.canViewAssessment = this.assessment.grade;
|
||||||
|
this.canSelfAssess = canAssess && userId == this.currentUserId;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
promises.push(this.userProvider.getProfile(userId, this.courseId, true).then((profile) => {
|
||||||
|
this.profile = profile;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let assessOffline;
|
||||||
|
if (userId == this.currentUserId) {
|
||||||
|
assessOffline = this.workshopOffline.getAssessment(this.workshop.id, this.assessmentId) .then((offlineAssess) => {
|
||||||
|
this.offline = true;
|
||||||
|
this.assessment.weight = offlineAssess.inputdata.weight;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
assessOffline = this.workshopOffline.getEvaluateAssessment(this.workshop.id, this.assessmentId)
|
||||||
|
.then((offlineAssess) => {
|
||||||
|
this.offline = true;
|
||||||
|
this.assessment.gradinggradeover = offlineAssess.gradinggradeover;
|
||||||
|
this.assessment.weight = offlineAssess.weight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(assessOffline.catch(() => {
|
||||||
|
this.offline = false;
|
||||||
|
// Ignore errors.
|
||||||
|
}));
|
||||||
|
|
||||||
|
Promise.all(promises).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the assessment.
|
||||||
|
*/
|
||||||
|
gotoAssessment(): void {
|
||||||
|
if (!this.canSelfAssess && this.canViewAssessment) {
|
||||||
|
const params = {
|
||||||
|
assessment: this.assessment,
|
||||||
|
submission: this.submission,
|
||||||
|
profile: this.profile,
|
||||||
|
courseId: this.courseId,
|
||||||
|
assessmentId: this.assessmentId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.submission) {
|
||||||
|
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
|
this.workshopHelper.getSubmissionById(this.workshop.id, this.assessment.submissionid)
|
||||||
|
.then((submissionData) => {
|
||||||
|
|
||||||
|
params.submission = submissionData;
|
||||||
|
this.navCtrl.push('AddonModWorkshopAssessmentPage', params);
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'Cannot load submission');
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.navCtrl.push('AddonModWorkshopAssessmentPage', params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to my own assessment.
|
||||||
|
*/
|
||||||
|
gotoOwnAssessment(): void {
|
||||||
|
if (this.canSelfAssess) {
|
||||||
|
const params = {
|
||||||
|
module: this.module,
|
||||||
|
workshop: this.workshop,
|
||||||
|
access: this.access,
|
||||||
|
courseId: this.courseId,
|
||||||
|
profile: this.profile,
|
||||||
|
submission: this.submission,
|
||||||
|
assessment: this.assessment
|
||||||
|
};
|
||||||
|
|
||||||
|
this.navCtrl.push('AddonModWorkshopSubmissionPage', params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||||
|
import { AddonModWorkshopIndexComponent } from './index/index';
|
||||||
|
import { AddonModWorkshopSubmissionComponent } from './submission/submission';
|
||||||
|
import { AddonModWorkshopAssessmentComponent } from './assessment/assessment';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyComponent } from './assessment-strategy/assessment-strategy';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopIndexComponent,
|
||||||
|
AddonModWorkshopSubmissionComponent,
|
||||||
|
AddonModWorkshopAssessmentComponent,
|
||||||
|
AddonModWorkshopAssessmentStrategyComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CorePipesModule,
|
||||||
|
CoreCourseComponentsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModWorkshopIndexComponent,
|
||||||
|
AddonModWorkshopSubmissionComponent,
|
||||||
|
AddonModWorkshopAssessmentComponent,
|
||||||
|
AddonModWorkshopAssessmentStrategyComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModWorkshopIndexComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopComponentsModule {}
|
|
@ -0,0 +1,184 @@
|
||||||
|
<!-- Buttons to add to the header. -->
|
||||||
|
<core-navbar-buttons end>
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></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()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</core-navbar-buttons>
|
||||||
|
|
||||||
|
<!-- Content. -->
|
||||||
|
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||||
|
<core-course-module-description *ngIf="description && selectedPhase == workshopPhases.PHASE_SETUP" [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
|
||||||
|
|
||||||
|
<ion-card class="with-borders" *ngIf="phases">
|
||||||
|
<ion-item (click)="selectPhase()">
|
||||||
|
<h2 stacked text-wrap>{{ phases[selectedPhase].title }}</h2>
|
||||||
|
<p text-wrap *ngIf="phases[selectedPhase].code == workshop.phase">{{ 'addon.mod_workshop.userplancurrentphase' | translate }}</p>
|
||||||
|
<ion-icon item-end name="arrow-dropdown"></ion-icon>
|
||||||
|
</ion-item>
|
||||||
|
<a ion-item text-wrap *ngIf="phases[selectedPhase].switchUrl" [href]="phases[selectedPhase].switchUrl" detail-none>
|
||||||
|
<ion-icon item-start name="swap"></ion-icon>
|
||||||
|
{{ 'addon.mod_workshop.switchphase' + selectedPhase | translate }}
|
||||||
|
<ion-icon item-end name="open"></ion-icon>
|
||||||
|
</a>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="with-borders" *ngIf="phases && phases[selectedPhase] && phases[selectedPhase].tasks && phases[selectedPhase].tasks.length">
|
||||||
|
<ion-item text-wrap *ngFor="let task of phases[selectedPhase].tasks" [class.item-dimmed]="selectedPhase != workshop.phase" (click)="runTask(task)" detail-none>
|
||||||
|
<ion-icon item-start name="radio-button-off" *ngIf="task.completed == null"></ion-icon>
|
||||||
|
<ion-icon item-start name="close-circle" color="danger" *ngIf="task.completed == ''"></ion-icon>
|
||||||
|
<ion-icon item-start name="information-circle" color="info" *ngIf="task.completed == 'info'"></ion-icon>
|
||||||
|
<ion-icon item-start name="checkmark-circle" color="success" *ngIf="task.completed == '1'"></ion-icon>
|
||||||
|
|
||||||
|
<h2>{{task.title}}</h2>
|
||||||
|
<p *ngIf="task.details"><core-format-text [text]="task.details"></core-format-text></p>
|
||||||
|
<ion-icon item-end *ngIf="task.link && !task.support" name="open"></ion-icon>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Has something offline. -->
|
||||||
|
<div class="core-warning-card" icon-start *ngIf="hasOffline">
|
||||||
|
<ion-icon name="warning"></ion-icon>
|
||||||
|
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="access && workshop && workshop.phase >= selectedPhase">
|
||||||
|
<!-- SUBMISSION PHASE -->
|
||||||
|
<ng-container *ngIf="selectedPhase == workshopPhases.PHASE_SUBMISSION">
|
||||||
|
<ion-card *ngIf="workshop.instructauthors">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.areainstructauthors' | translate }}</h2>
|
||||||
|
<core-format-text fullOnClick="true" [component]="component" [componentId]="workshop.cmid" [text]="workshop.instructauthors"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="with-borders" *ngIf="canSubmit">
|
||||||
|
<ion-item text-wrap *ngIf="!submission">
|
||||||
|
<h2>{{ 'addon.mod_workshop.yoursubmission' | translate }}</h2>
|
||||||
|
<p>{{ 'addon.mod_workshop.noyoursubmission' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ng-container *ngIf="submission">
|
||||||
|
<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 == selectedPhase">
|
||||||
|
<ion-item text-wrap *ngIf="canSubmit && ((access.creatingsubmissionallowed && !submission) || (access.modifyingsubmissionallowed && submission))">
|
||||||
|
<button ion-button icon-start block *ngIf="access.creatingsubmissionallowed && !submission" (click)="runTaskByCode('submit')">
|
||||||
|
<ion-icon name="add"></ion-icon>
|
||||||
|
{{ 'addon.mod_workshop.createsubmission' | translate }}
|
||||||
|
</button>
|
||||||
|
<button ion-button icon-start block *ngIf="access.modifyingsubmissionallowed && submission" (click)="runTaskByCode('submit')">
|
||||||
|
<ion-icon name="create"></ion-icon>
|
||||||
|
{{ 'addon.mod_workshop.editsubmission' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- ASSESSMENT PHASE -->
|
||||||
|
<ng-container *ngIf="selectedPhase == workshopPhases.PHASE_ASSESSMENT">
|
||||||
|
<ion-card *ngIf="workshop.instructreviewers">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.areainstructreviewers' | translate }}</h2>
|
||||||
|
<core-format-text fullOnClick="true" [component]="component" [componentId]="workshop.cmid" [text]="workshop.instructreviewers"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="with-borders" *ngIf="canAssess && assessments && assessments.length">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.assignedassessments' | translate }}</h2>
|
||||||
|
</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"></addon-mod-workshop-submission>
|
||||||
|
</ng-container>
|
||||||
|
</ion-card >
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ion-card class="with-borders" *ngIf="!access.canviewallsubmissions && selectedPhase == workshop.phase && (canSubmit || canAssess) && selectedPhase == workshopPhases.PHASE_EVALUATION">
|
||||||
|
<ion-item text-wrap *ngIf="submission" (click)="switchPhase(workshopPhases.PHASE_SUBMISSION)" detail-push>
|
||||||
|
<h2>{{ 'addon.mod_workshop.yoursubmission' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="canAssess" (click)="switchPhase(workshopPhases.PHASE_ASSESSMENT)" detail-push>
|
||||||
|
<h2>{{ 'addon.mod_workshop.assignedassessments' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- CLOSED PHASE -->
|
||||||
|
<ng-container *ngIf="selectedPhase == workshopPhases.PHASE_CLOSED">
|
||||||
|
<ion-card *ngIf="workshop.conclusion">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.conclusion' | translate }}</h2>
|
||||||
|
<core-format-text fullOnClick="true" [component]="component" [componentId]="workshop.cmid" [text]="workshop.conclusion"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="with-borders" *ngIf="userGrades">
|
||||||
|
<ion-item-divider color="light" text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.yourgrades' | translate }}</h2>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item text-wrap *ngIf="userGrades.submissionlongstrgrade" (click)="switchPhase(workshopPhases.PHASE_SUBMISSION)" detail-push>
|
||||||
|
<h2>{{ 'addon.mod_workshop.submissiongrade' | translate }}</h2>
|
||||||
|
<core-format-text [text]="userGrades.submissionlongstrgrade"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="userGrades.assessmentlongstrgrade" (click)="switchPhase(workshopPhases.PHASE_ASSESSMENT)" detail-push>
|
||||||
|
<h2>{{ 'addon.mod_workshop.gradinggrade' | translate }}</h2>
|
||||||
|
<core-format-text [text]="userGrades.assessmentlongstrgrade"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="with-borders" *ngIf="publishedSubmissions && publishedSubmissions.length">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.publishedsubmissions' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ng-container *ngFor="let submission of publishedSubmissions">
|
||||||
|
<addon-mod-workshop-submission [submission]="submission" [courseId]="workshop.course" [module]="module" [workshop]="workshop" [access]="access" summary="true"></addon-mod-workshop-submission>
|
||||||
|
</ng-container>
|
||||||
|
</ion-card>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- MULTIPLE PHASES SUBMISSION OR GREATER only teachers -->
|
||||||
|
<ion-card class="with-borders" *ngIf="workshop.phase == selectedPhase && access.canviewallsubmissions && selectedPhase >= workshopPhases.PHASE_SUBMISSION && grades && grades.length">
|
||||||
|
<ion-item text-wrap *ngIf="selectedPhase == workshopPhases.PHASE_SUBMISSION">
|
||||||
|
<h2>{{ 'addon.mod_workshop.submissionsreport' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="selectedPhase > workshopPhases.PHASE_SUBMISSION">
|
||||||
|
<h2>{{ 'addon.mod_workshop.gradesreport' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item 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)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-workshop-groupslabel">
|
||||||
|
<ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-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"></addon-mod-workshop-submission>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ion-grid *ngIf="page > 0 || hasNextPage">
|
||||||
|
<ion-row align-items-center>
|
||||||
|
<ion-col *ngIf="page > 0">
|
||||||
|
<button ion-button block outline icon-start (click)="gotoSubmissionsPage(page - 1)">>
|
||||||
|
<ion-icon name="arrow-back"></ion-icon>
|
||||||
|
{{ 'core.previous' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="hasNextPage">
|
||||||
|
<button ion-button block icon-end (click)="gotoSubmissionsPage(page + 1)">
|
||||||
|
{{ 'core.next' | translate }}
|
||||||
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ion-card>
|
||||||
|
</div>
|
||||||
|
</core-loading>
|
|
@ -0,0 +1,483 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Optional, Injector } from '@angular/core';
|
||||||
|
import { Content, ModalController, NavController, Platform } from 'ionic-angular';
|
||||||
|
import { CoreGroupInfo, CoreGroupsProvider } from '@providers/groups';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||||
|
import { AddonModWorkshopProvider } from '../../providers/workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModWorkshopSyncProvider } from '../../providers/sync';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a workshop index page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-workshop-index',
|
||||||
|
templateUrl: 'addon-mod-workshop-index.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivityComponent {
|
||||||
|
@Input() group = 0;
|
||||||
|
|
||||||
|
moduleName = 'workshop';
|
||||||
|
workshop: any;
|
||||||
|
page = 0;
|
||||||
|
access: any;
|
||||||
|
phases: any;
|
||||||
|
grades: any;
|
||||||
|
assessments: any;
|
||||||
|
userGrades: any;
|
||||||
|
publishedSubmissions: any;
|
||||||
|
selectedPhase: number;
|
||||||
|
submission: any;
|
||||||
|
groupInfo: CoreGroupInfo = {
|
||||||
|
groups: [],
|
||||||
|
separateGroups: false,
|
||||||
|
visibleGroups: false
|
||||||
|
};
|
||||||
|
canSubmit = false;
|
||||||
|
canAssess = false;
|
||||||
|
hasNextPage = false;
|
||||||
|
|
||||||
|
workshopPhases = {
|
||||||
|
PHASE_SETUP: AddonModWorkshopProvider.PHASE_SETUP,
|
||||||
|
PHASE_SUBMISSION: AddonModWorkshopProvider.PHASE_SUBMISSION,
|
||||||
|
PHASE_ASSESSMENT: AddonModWorkshopProvider.PHASE_ASSESSMENT,
|
||||||
|
PHASE_EVALUATION: AddonModWorkshopProvider.PHASE_EVALUATION,
|
||||||
|
PHASE_CLOSED: AddonModWorkshopProvider.PHASE_CLOSED
|
||||||
|
};
|
||||||
|
|
||||||
|
protected offlineSubmissions = [];
|
||||||
|
protected supportedTasks = { // Add here native supported tasks.
|
||||||
|
submit: true
|
||||||
|
};
|
||||||
|
protected obsSubmissionChanged: any;
|
||||||
|
protected obsAssessmentSaved: any;
|
||||||
|
protected appResumeSubscription: any;
|
||||||
|
protected syncObserver: any;
|
||||||
|
|
||||||
|
constructor(injector: Injector, private workshopProvider: AddonModWorkshopProvider, @Optional() content: Content,
|
||||||
|
private workshopOffline: AddonModWorkshopOfflineProvider, private groupsProvider: CoreGroupsProvider,
|
||||||
|
private navCtrl: NavController, private modalCtrl: ModalController, private utils: CoreUtilsProvider,
|
||||||
|
platform: Platform, private workshopHelper: AddonModWorkshopHelperProvider,
|
||||||
|
private workshopSync: AddonModWorkshopSyncProvider) {
|
||||||
|
super(injector, content);
|
||||||
|
|
||||||
|
// Listen to submission and assessment changes.
|
||||||
|
this.obsSubmissionChanged = this.eventsProvider.on(AddonModWorkshopProvider.SUBMISSION_CHANGED, (data) => {
|
||||||
|
this.eventReceived(data);
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Listen to submission and assessment changes.
|
||||||
|
this.obsAssessmentSaved = this.eventsProvider.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 = this.eventsProvider.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
|
||||||
|
// Update just when all database is synced.
|
||||||
|
this.eventReceived(data);
|
||||||
|
}, this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.loadContent(false, true).then(() => {
|
||||||
|
if (!this.workshop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.workshopProvider.logView(this.workshop.id).then(() => {
|
||||||
|
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||||
|
}).catch((error) => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when we receive an event of submission changes.
|
||||||
|
*
|
||||||
|
* @param {any} data Data received by the event.
|
||||||
|
*/
|
||||||
|
protected eventReceived(data: any): void {
|
||||||
|
if ((this.workshop && this.workshop.id === data.workshopId) || data.cmId === this.module.id) {
|
||||||
|
this.showLoadingAndRefresh(true);
|
||||||
|
|
||||||
|
// Check completion since it could be configured to complete once the user adds a new discussion or replies.
|
||||||
|
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected invalidateContent(): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.workshopProvider.invalidateWorkshopData(this.courseId));
|
||||||
|
if (this.workshop) {
|
||||||
|
promises.push(this.workshopProvider.invalidateWorkshopAccessInformationData(this.workshop.id));
|
||||||
|
promises.push(this.workshopProvider.invalidateUserPlanPhasesData(this.workshop.id));
|
||||||
|
if (this.canSubmit) {
|
||||||
|
promises.push(this.workshopProvider.invalidateSubmissionsData(this.workshop.id));
|
||||||
|
}
|
||||||
|
if (this.access.canviewallsubmissions) {
|
||||||
|
promises.push(this.workshopProvider.invalidateGradeReportData(this.workshop.id));
|
||||||
|
promises.push(this.groupsProvider.invalidateActivityAllowedGroups(this.workshop.coursemodule));
|
||||||
|
promises.push(this.groupsProvider.invalidateActivityGroupMode(this.workshop.coursemodule));
|
||||||
|
}
|
||||||
|
if (this.canAssess) {
|
||||||
|
promises.push(this.workshopProvider.invalidateReviewerAssesmentsData(this.workshop.id));
|
||||||
|
}
|
||||||
|
promises.push(this.workshopProvider.invalidateGradesData(this.workshop.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares sync event data with current data to check if refresh content is needed.
|
||||||
|
*
|
||||||
|
* @param {any} syncEventData Data receiven on sync observer.
|
||||||
|
* @return {boolean} True if refresh is needed, false otherwise.
|
||||||
|
*/
|
||||||
|
protected isRefreshSyncNeeded(syncEventData: any): boolean {
|
||||||
|
if (this.workshop && syncEventData.workshopId == this.workshop.id) {
|
||||||
|
// Refresh the data.
|
||||||
|
this.content.scrollToTop();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download feedback contents.
|
||||||
|
*
|
||||||
|
* @param {boolean} [refresh=false] If it's refreshing content.
|
||||||
|
* @param {boolean} [sync=false] If the refresh is needs syncing.
|
||||||
|
* @param {boolean} [showErrors=false] If show errors to the user of hide them.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||||
|
return this.workshopProvider.getWorkshop(this.courseId, this.module.id).then((workshop) => {
|
||||||
|
this.workshop = workshop;
|
||||||
|
|
||||||
|
this.selectedPhase = workshop.phase;
|
||||||
|
|
||||||
|
this.description = workshop.intro || workshop.description;
|
||||||
|
this.dataRetrieved.emit(workshop);
|
||||||
|
|
||||||
|
if (sync) {
|
||||||
|
// Try to synchronize the feedback.
|
||||||
|
return this.syncActivity(showErrors);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Check if there are answers stored in offline.
|
||||||
|
return this.workshopProvider.getWorkshopAccessInformation(this.workshop.id);
|
||||||
|
}).then((accessData) => {
|
||||||
|
this.access = accessData;
|
||||||
|
|
||||||
|
if (accessData.canviewallsubmissions) {
|
||||||
|
return this.groupsProvider.getActivityGroupInfo(this.workshop.coursemodule,
|
||||||
|
accessData.canviewallsubmissions).then((groupInfo) => {
|
||||||
|
this.groupInfo = groupInfo;
|
||||||
|
|
||||||
|
// Check selected group is accessible.
|
||||||
|
if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
|
||||||
|
const found = groupInfo.groups.some((group) => {
|
||||||
|
return group.id == this.group;
|
||||||
|
});
|
||||||
|
if (!found) {
|
||||||
|
this.group = groupInfo.groups[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
return this.workshopProvider.getUserPlanPhases(this.workshop.id);
|
||||||
|
}).then((phases) => {
|
||||||
|
this.phases = phases;
|
||||||
|
|
||||||
|
// Treat phases.
|
||||||
|
for (const x in phases) {
|
||||||
|
phases[x].tasks.forEach((task) => {
|
||||||
|
if (!task.link && (task.code == 'examples' || task.code == 'prepareexamples')) {
|
||||||
|
// Add links to manage examples.
|
||||||
|
task.link = this.externalUrl;
|
||||||
|
} else if (task.link && typeof this.supportedTasks[task.code] !== 'undefined') {
|
||||||
|
task.support = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const action = phases[x].actions.find((action) => {
|
||||||
|
return action.url && action.type == 'switchphase';
|
||||||
|
});
|
||||||
|
phases[x].switchUrl = action ? action.url : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are info stored in offline.
|
||||||
|
return this.workshopOffline.hasWorkshopOfflineData(this.workshop.id).then((hasOffline) => {
|
||||||
|
this.hasOffline = hasOffline;
|
||||||
|
if (hasOffline) {
|
||||||
|
return this.workshopOffline.getSubmissions(this.workshop.id).then((submissionsActions) => {
|
||||||
|
this.offlineSubmissions = submissionsActions;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.offlineSubmissions = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
return this.setPhaseInfo();
|
||||||
|
}).then(() => {
|
||||||
|
// All data obtained, now fill the context menu.
|
||||||
|
this.fillContextMenu(refresh);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves and shows submissions grade page.
|
||||||
|
*
|
||||||
|
* @param {number} page Page number to be retrieved.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
gotoSubmissionsPage(page: number): Promise<any> {
|
||||||
|
return this.workshopProvider.getGradesReport(this.workshop.id, this.group, page).then((report) => {
|
||||||
|
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;
|
||||||
|
|
||||||
|
this.grades = report.grades || [];
|
||||||
|
|
||||||
|
this.grades.forEach((submission) => {
|
||||||
|
const actions = this.workshopHelper.filterSubmissionActions(this.offlineSubmissions, submission.submissionid
|
||||||
|
|| false);
|
||||||
|
submission = this.workshopHelper.applyOfflineData(submission, actions);
|
||||||
|
|
||||||
|
return this.workshopHelper.applyOfflineData(submission, actions).then((offlineSubmission) => {
|
||||||
|
submission = offlineSubmission;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open task.
|
||||||
|
*
|
||||||
|
* @param {any} task Task to be done.
|
||||||
|
*/
|
||||||
|
runTask(task: any): void {
|
||||||
|
if (task.support) {
|
||||||
|
if (task.code == 'submit' && this.canSubmit && ((this.access.creatingsubmissionallowed && !this.submission) ||
|
||||||
|
(this.access.modifyingsubmissionallowed && this.submission))) {
|
||||||
|
const params = {
|
||||||
|
module: this.module,
|
||||||
|
access: this.access,
|
||||||
|
courseId: this.courseId,
|
||||||
|
submissionId: this.submission && this.submission.id
|
||||||
|
};
|
||||||
|
|
||||||
|
this.navCtrl.push('AddonModWorkshopEditSubmissionPage', params);
|
||||||
|
}
|
||||||
|
} else if (task.link) {
|
||||||
|
this.utils.openInBrowser(task.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run task link on current phase.
|
||||||
|
*
|
||||||
|
* @param {string} taskCode Code related to the task to run.
|
||||||
|
*/
|
||||||
|
runTaskByCode(taskCode: string): void {
|
||||||
|
const task = this.workshopHelper.getTask(this.phases[this.workshop.phase].tasks, taskCode);
|
||||||
|
|
||||||
|
return task ? this.runTask(task) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select Phase to be shown.
|
||||||
|
*/
|
||||||
|
selectPhase(): void {
|
||||||
|
if (this.phases) {
|
||||||
|
const modal = this.modalCtrl.create('AddonModWorkshopPhaseSelectorPage', {
|
||||||
|
phases: this.utils.objectToArray(this.phases),
|
||||||
|
selected: this.selectedPhase,
|
||||||
|
workshopPhase: this.workshop.phase
|
||||||
|
});
|
||||||
|
modal.onDidDismiss((phase) => {
|
||||||
|
// Add data to search object.
|
||||||
|
typeof phase != 'undefined' && this.switchPhase(phase);
|
||||||
|
});
|
||||||
|
modal.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set group to see the workshop.
|
||||||
|
* @param {number} groupId Group Id.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
setGroup(groupId: number): Promise<any> {
|
||||||
|
this.group = groupId;
|
||||||
|
|
||||||
|
return this.gotoSubmissionsPage(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to set current phase information.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected setPhaseInfo(): Promise<any> {
|
||||||
|
this.submission = false;
|
||||||
|
this.canAssess = false;
|
||||||
|
this.assessments = false;
|
||||||
|
this.userGrades = false;
|
||||||
|
this.publishedSubmissions = false;
|
||||||
|
|
||||||
|
this.canSubmit = this.workshopHelper.canSubmit(this.workshop, this.access,
|
||||||
|
this.phases[AddonModWorkshopProvider.PHASE_SUBMISSION].tasks);
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
if (this.canSubmit) {
|
||||||
|
promises.push(this.workshopHelper.getUserSubmission(this.workshop.id).then((submission) => {
|
||||||
|
const actions = this.workshopHelper.filterSubmissionActions(this.offlineSubmissions, submission.id || false);
|
||||||
|
|
||||||
|
return this.workshopHelper.applyOfflineData(submission, actions).then((submission) => {
|
||||||
|
this.submission = submission;
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.access.canviewallsubmissions && this.workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) {
|
||||||
|
promises.push(this.gotoSubmissionsPage(this.page));
|
||||||
|
}
|
||||||
|
|
||||||
|
let assessPromise = Promise.resolve();
|
||||||
|
|
||||||
|
if (this.workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT) {
|
||||||
|
this.canAssess = this.workshopHelper.canAssess(this.workshop, this.access);
|
||||||
|
if (this.canAssess) {
|
||||||
|
assessPromise = this.workshopHelper.getReviewerAssessments(this.workshop.id).then((assessments) => {
|
||||||
|
const p2 = [];
|
||||||
|
|
||||||
|
assessments.forEach((assessment) => {
|
||||||
|
assessment.strategy = this.workshop.strategy;
|
||||||
|
if (this.hasOffline) {
|
||||||
|
p2.push(this.workshopOffline.getAssessment(this.workshop.id, assessment.id)
|
||||||
|
.then((offlineAssessment) => {
|
||||||
|
assessment.offline = true;
|
||||||
|
assessment.timemodified = Math.floor(offlineAssessment.timemodified / 1000);
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(p2).then(() => {
|
||||||
|
this.assessments = assessments;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
promises.push(assessPromise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) {
|
||||||
|
promises.push(this.workshopProvider.getGrades(this.workshop.id).then((grades) => {
|
||||||
|
this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : false;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (this.access.canviewpublishedsubmissions) {
|
||||||
|
promises.push(assessPromise.then(() => {
|
||||||
|
return this.workshopProvider.getSubmissions(this.workshop.id).then((submissions) => {
|
||||||
|
this.publishedSubmissions = submissions.filter((submission) => {
|
||||||
|
if (submission.published) {
|
||||||
|
this.assessments.forEach((assessment) => {
|
||||||
|
submission.reviewedby = [];
|
||||||
|
if (assessment.submissionid == submission.id) {
|
||||||
|
submission.reviewedby.push(this.workshopHelper.realGradeValue(this.workshop, assessment));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch shown phase.
|
||||||
|
*
|
||||||
|
* @param {number} phase Selected phase.
|
||||||
|
*/
|
||||||
|
switchPhase(phase: number): void {
|
||||||
|
this.selectedPhase = phase;
|
||||||
|
this.page = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the sync of the activity.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected sync(): Promise<any> {
|
||||||
|
return this.workshopSync.syncWorkshop(this.workshop.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sync has succeed from result sync data.
|
||||||
|
*
|
||||||
|
* @param {any} result Data returned on the sync function.
|
||||||
|
* @return {boolean} If suceed or not.
|
||||||
|
*/
|
||||||
|
protected hasSyncSucceed(result: any): boolean {
|
||||||
|
return result.updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.obsSubmissionChanged && this.obsSubmissionChanged.off();
|
||||||
|
this.obsAssessmentSaved && this.obsAssessmentSaved.off();
|
||||||
|
this.appResumeSubscription && this.appResumeSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<div *ngIf="!summary">
|
||||||
|
<ion-list-header text-wrap>
|
||||||
|
<ion-avatar item-start>
|
||||||
|
<img [src]="profile && profile.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: profile && profile.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
<h2>{{submission.title}}</h2>
|
||||||
|
<p *ngIf="profile && profile.fullname">{{profile.fullname}}</p>
|
||||||
|
<p *ngIf="showGrade(submission.submissiongrade)" [class.addon-has-overriden-grade]="showGrade(submission.submissiongradeover)">
|
||||||
|
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.submissiongrade}}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="showGrade(submission.submissiongradeover)" class="addon-overriden-grade">
|
||||||
|
{{ 'addon.mod_workshop.gradeover' | translate }}: {{submission.submissiongradeover}}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="access.canviewallsubmissions && showGrade(submission.gradinggrade)">
|
||||||
|
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{submission.gradinggrade}}
|
||||||
|
</p>
|
||||||
|
<ion-note item-end *ngIf="!submission.timemodified">
|
||||||
|
<ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}
|
||||||
|
</ion-note>
|
||||||
|
<ion-note item-end *ngIf="submission.timemodified">
|
||||||
|
{{submission.timemodified | coreDateDayOrTime}}
|
||||||
|
<ng-container *ngIf="submission.offline"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</ng-container>
|
||||||
|
<ng-container *ngIf="submission.deleted"><ion-icon name="trash"></ion-icon> {{ 'core.deletedoffline' | translate }}</ng-container>
|
||||||
|
</ion-note>
|
||||||
|
</ion-list-header>
|
||||||
|
<ion-item text-wrap *ngIf="submission.content">
|
||||||
|
<core-format-text [component]="component" [componentId]="componentId" [text]="submission.content"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngFor="let attachment of submission.attachmentfiles">
|
||||||
|
<!-- Files already attached to the submission. -->
|
||||||
|
<core-file *ngIf="!attachment.name" [file]="attachment" [component]="component" [componentId]="componentId"></core-file>
|
||||||
|
<!-- Files stored in offline to be sent later. -->
|
||||||
|
<core-local-file *ngIf="attachment.name" [file]="attachment"></core-local-file>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="viewDetails && submission.feedbackauthor">
|
||||||
|
<img [src]="evaluateByProfile && evaluateByProfile.profileimageurl" core-external-content core-user-link [courseId]="courseId" [userId]="evaluateByProfile && evaluateByProfile.id" [alt]="'core.pictureof' | translate:{$a: evaluateByProfile && evaluateByProfile.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'"/>
|
||||||
|
|
||||||
|
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}</h2>
|
||||||
|
<core-format-text [text]="submission.feedbackauthor"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="viewDetails">
|
||||||
|
<button ion-button block (click)="gotoSubmission()">
|
||||||
|
{{ 'core.showmore' | translate }}
|
||||||
|
<ion-icon name="arrow-forward" item-end></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item text-wrap *ngIf="summary" [attr.detail-push]="submission.timemodified? true : null" (click)="gotoSubmission()">
|
||||||
|
<ion-avatar item-start>
|
||||||
|
<img [src]="profile && profile.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: profile && profile.fullname}" core-user-link [courseId]="courseId" [userId]="profile && profile.id" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
|
||||||
|
<h2>{{submission.title}}</h2>
|
||||||
|
<p *ngIf="profile && profile.fullname">{{profile.fullname}}</p>
|
||||||
|
<p *ngIf="submission.reviewedbycount">
|
||||||
|
{{ 'addon.mod_workshop.receivedgrades' | translate }}: {{submission.reviewedbycount}} / {{submission.reviewedby.length}}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="submission.reviewerofcount">
|
||||||
|
{{ 'addon.mod_workshop.givengrades' | translate }}: {{submission.reviewerofcount}} / {{submission.reviewerof.length}}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="!showGrade(submission.submissiongradeover) && showGrade(submission.submissiongrade)">
|
||||||
|
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.submissiongrade}}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="showGrade(submission.submissiongradeover)" class="addon-overriden-grade">
|
||||||
|
{{ 'addon.mod_workshop.submissiongradeof' | translate:{$a: workshop.grade } }}: {{submission.submissiongradeover}}
|
||||||
|
</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-note item-end *ngIf="submission.timemodified">
|
||||||
|
{{submission.timemodified | coreDateDayOrTime}}
|
||||||
|
<div *ngIf="offline"><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</div>
|
||||||
|
<div *ngIf="submission.deleted"><ion-icon name="trash"></ion-icon> {{ 'core.deletedoffline' | translate }}</div>
|
||||||
|
</ion-note>
|
||||||
|
</ion-item>
|
||||||
|
</core-loading>
|
|
@ -0,0 +1,35 @@
|
||||||
|
addon-mod-workshop-submission {
|
||||||
|
.item-md.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-md-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-ios.item-block .item-inner {
|
||||||
|
border-bottom: $hairlines-width solid $list-ios-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-wp.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-wp-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child .item .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.with-borders addon-mod-workshop-submission {
|
||||||
|
.item-md.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-md-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-ios.item-block .item-inner {
|
||||||
|
border-bottom: $hairlines-width solid $list-ios-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-wp.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-wp-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child .item .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { NavController } from 'ionic-angular';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { AddonModWorkshopProvider } from '../../providers/workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays workshop submission.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-workshop-submission',
|
||||||
|
templateUrl: 'addon-mod-workshop-submission.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopSubmissionComponent implements OnInit {
|
||||||
|
@Input() submission: any;
|
||||||
|
@Input() module: any;
|
||||||
|
@Input() workshop: any;
|
||||||
|
@Input() access: any;
|
||||||
|
@Input() courseId: number;
|
||||||
|
@Input() assessment?: any;
|
||||||
|
@Input() summary?: boolean;
|
||||||
|
|
||||||
|
component = AddonModWorkshopProvider.COMPONENT;
|
||||||
|
componentId: number;
|
||||||
|
userId: number;
|
||||||
|
loaded = false;
|
||||||
|
offline = false;
|
||||||
|
viewDetails = false;
|
||||||
|
profile: any;
|
||||||
|
showGrade: any;
|
||||||
|
evaluateByProfile: any;
|
||||||
|
|
||||||
|
constructor(private workshopOffline: AddonModWorkshopOfflineProvider, private workshopHelper: AddonModWorkshopHelperProvider,
|
||||||
|
private navCtrl: NavController, private userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) {
|
||||||
|
this.userId = sitesProvider.getCurrentSiteUserId();
|
||||||
|
this.showGrade = this.workshopHelper.showGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.componentId = this.module.instance;
|
||||||
|
this.userId = this.submission.authorid || this.submission.userid || this.userId;
|
||||||
|
this.submission.title = this.submission.title || this.submission.submissiontitle;
|
||||||
|
this.submission.timemodified = this.submission.timemodified || this.submission.submissionmodified;
|
||||||
|
this.submission.id = this.submission.id || this.submission.submissionid;
|
||||||
|
|
||||||
|
if (this.workshop.phase == AddonModWorkshopProvider.PHASE_ASSESSMENT) {
|
||||||
|
if (this.submission.reviewedby && this.submission.reviewedby.length) {
|
||||||
|
this.submission.reviewedbycount = this.submission.reviewedby.reduce((a, b) => {
|
||||||
|
return a + (b.grade ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.submission.reviewerof && this.submission.reviewerof.length) {
|
||||||
|
this.submission.reviewerofcount = this.submission.reviewerof.reduce((a, b) => {
|
||||||
|
return a + (b.grade ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
this.offline = (this.submission && this.submission.offline) || (this.assessment && this.assessment.offline);
|
||||||
|
|
||||||
|
if (this.submission.id) {
|
||||||
|
promises.push(this.workshopOffline.getEvaluateSubmission(this.workshop.id, this.submission.id)
|
||||||
|
.then((offlineSubmission) => {
|
||||||
|
this.submission.submissiongradeover = offlineSubmission.gradeover;
|
||||||
|
this.offline = true;
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.userId) {
|
||||||
|
promises.push(this.userProvider.getProfile(this.userId, this.courseId, true).then((profile) => {
|
||||||
|
this.profile = profile;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.viewDetails = !this.summary && this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED &&
|
||||||
|
this.navCtrl.getActive().name !== 'AddonModWorkshopSubmissionPage';
|
||||||
|
|
||||||
|
if (this.viewDetails && this.submission.gradeoverby) {
|
||||||
|
promises.push(this.userProvider.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
|
||||||
|
this.evaluateByProfile = profile;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(promises).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the submission.
|
||||||
|
*/
|
||||||
|
gotoSubmission(): void {
|
||||||
|
if (this.submission.timemodified) {
|
||||||
|
const params = {
|
||||||
|
module: this.module,
|
||||||
|
workshop: this.workshop,
|
||||||
|
access: this.access,
|
||||||
|
courseId: this.courseId,
|
||||||
|
profile: this.profile,
|
||||||
|
submission: this.submission,
|
||||||
|
assessment: this.assessment,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.navCtrl.push('AddonModWorkshopSubmissionPage', params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"conclusion": "Conclusion",
|
||||||
|
"createsubmission": "Start preparing your 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",
|
||||||
|
"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",
|
||||||
|
"selectphase": "Select phase",
|
||||||
|
"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",
|
||||||
|
"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"
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||||
|
<ion-buttons end [hidden]="!evaluating">
|
||||||
|
<button ion-button clear (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
|
||||||
|
{{ 'core.save' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshAssessment($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<ion-avatar item-start *ngIf="profile">
|
||||||
|
<img [src]="profile.profileimageurl" core-external-content core-user-link [courseId]="courseId" [userId]="profile.id" [alt]="'core.pictureof' | translate:{$a: profile.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
<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="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-item>
|
||||||
|
|
||||||
|
<addon-mod-workshop-assessment-strategy *ngIf="assessment && assessmentId && showGrade(assessment.grade) && workshop && access && profile" [workshop]="workshop" [access]="access" [assessmentId]="assessmentId" [userId]="profile.id" [strategy]="strategy"></addon-mod-workshop-assessment-strategy>
|
||||||
|
|
||||||
|
<form ion-list [formGroup]="evaluateForm" *ngIf="evaluating">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.assessmentsettings' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="access.canallocate">
|
||||||
|
<ion-label stacked core-mark-required="true">{{ 'addon.mod_workshop.assessmentweight' | translate }}</ion-label>
|
||||||
|
<ion-select formControlName="weight" required="true">
|
||||||
|
<ion-option *ngFor="let w of weights" [value]="w">{{ w }}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.gradinggradecalculated' | translate }}</h2>
|
||||||
|
<p>{{ assessment.gradinggrade }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="access.canoverridegrades">
|
||||||
|
<ion-label stacked>{{ 'addon.mod_workshop.gradinggradeover' | translate }}</ion-label>
|
||||||
|
<ion-select formControlName="grade">
|
||||||
|
<ion-option *ngFor="let grade of evaluationGrades" [value]="grade.value">{{grade.label}}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="access.canoverridegrades">
|
||||||
|
<ion-label stacked>{{ 'addon.mod_workshop.feedbackreviewer' | translate }}</ion-label>
|
||||||
|
<core-rich-text-editor item-content [control]="evaluateForm.controls['text']" formControlName="text"></core-rich-text-editor>
|
||||||
|
</ion-item>
|
||||||
|
</form>
|
||||||
|
<ion-list *ngIf="!evaluating && evaluate && evaluate.text">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<ion-avatar item-start *ngIf="evaluateGradingByProfile">
|
||||||
|
<img [src]="evaluateGradingByProfile.profileimageurl" core-external-content core-user-link [courseId]="courseId" [userId]="evaluateGradingByProfile.id" [alt]="'core.pictureof' | translate:{$a: evaluateGradingByProfile.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
<h2 *ngIf="evaluateGradingByProfile && evaluateGradingByProfile.fullname">{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }}</h2>
|
||||||
|
<core-format-text [text]="evaluate.text"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModWorkshopAssessmentPage } from './assessment';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopAssessmentPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreComponentsModule,
|
||||||
|
AddonModWorkshopComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModWorkshopAssessmentPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentPageModule {}
|
|
@ -0,0 +1,375 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams, NavController } from 'ionic-angular';
|
||||||
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
|
||||||
|
import { AddonModWorkshopProvider } from '../../providers/workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
|
||||||
|
import { AddonModWorkshopSyncProvider } from '../../providers/sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a workshop assessment.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-workshop-assessment' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-workshop-assessment-page',
|
||||||
|
templateUrl: 'assessment.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
assessment: any;
|
||||||
|
submission: any;
|
||||||
|
profile: any;
|
||||||
|
courseId: number;
|
||||||
|
access: any;
|
||||||
|
assessmentId: number;
|
||||||
|
evaluating = false;
|
||||||
|
loaded = false;
|
||||||
|
showGrade: any;
|
||||||
|
evaluateForm: FormGroup;
|
||||||
|
maxGrade: number;
|
||||||
|
workshop: any;
|
||||||
|
strategy: any;
|
||||||
|
title: string;
|
||||||
|
evaluate = {
|
||||||
|
text: '',
|
||||||
|
grade: -1,
|
||||||
|
weight: 1
|
||||||
|
};
|
||||||
|
weights = [];
|
||||||
|
evaluateByProfile: any;
|
||||||
|
evaluationGrades: any;
|
||||||
|
|
||||||
|
protected workshopId: number;
|
||||||
|
protected originalEvaluation: any = {};
|
||||||
|
protected hasOffline = false;
|
||||||
|
protected syncObserver: any;
|
||||||
|
protected isDestroyed = false;
|
||||||
|
protected siteId: string;
|
||||||
|
protected currentUserId: number;
|
||||||
|
protected forceLeave = false;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected courseProvider: CoreCourseProvider,
|
||||||
|
protected workshopProvider: AddonModWorkshopProvider, protected workshopOffline: AddonModWorkshopOfflineProvider,
|
||||||
|
protected workshopHelper: AddonModWorkshopHelperProvider, protected navCtrl: NavController,
|
||||||
|
protected syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, protected fb: FormBuilder,
|
||||||
|
protected translate: TranslateService, protected eventsProvider: CoreEventsProvider,
|
||||||
|
protected domUtils: CoreDomUtilsProvider, protected gradesHelper: CoreGradesHelperProvider,
|
||||||
|
protected userProvider: CoreUserProvider) {
|
||||||
|
|
||||||
|
this.assessment = navParams.get('assessment');
|
||||||
|
this.submission = navParams.get('submission') || {};
|
||||||
|
this.profile = navParams.get('profile');
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
|
||||||
|
this.assessmentId = this.assessment.assessmentid || this.assessment.id;
|
||||||
|
this.workshopId = this.submission.workshopid || null;
|
||||||
|
this.siteId = sitesProvider.getCurrentSiteId();
|
||||||
|
this.currentUserId = sitesProvider.getCurrentSiteUserId();
|
||||||
|
|
||||||
|
this.showGrade = this.workshopHelper.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 = this.eventsProvider.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.fetchAssessmentData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we can leave the page or not.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||||
|
*/
|
||||||
|
ionViewCanLeave(): boolean | Promise<void> {
|
||||||
|
if (this.forceLeave || !this.evaluating) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasEvaluationChanged()) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show confirmation if some data has been modified.
|
||||||
|
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the assessment data.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchAssessmentData(): Promise<void> {
|
||||||
|
return this.workshopProvider.getWorkshopById(this.courseId, this.workshopId).then((workshopData) => {
|
||||||
|
this.workshop = workshopData;
|
||||||
|
this.title = this.workshop.name;
|
||||||
|
this.strategy = this.workshop.strategy;
|
||||||
|
|
||||||
|
return this.courseProvider.getModuleBasicGradeInfo(workshopData.coursemodule);
|
||||||
|
}).then((gradeInfo) => {
|
||||||
|
this.maxGrade = gradeInfo.grade;
|
||||||
|
|
||||||
|
return this.workshopProvider.getWorkshopAccessInformation(this.workshopId);
|
||||||
|
}).then((accessData) => {
|
||||||
|
this.access = accessData;
|
||||||
|
|
||||||
|
// Load Weights selector.
|
||||||
|
if (this.assessmentId && (accessData.canallocate || accessData.canoverridegrades)) {
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
// Block the workshop.
|
||||||
|
this.syncProvider.blockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.evaluating = true;
|
||||||
|
} else {
|
||||||
|
this.evaluating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.evaluating || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) {
|
||||||
|
// Get all info of the assessment.
|
||||||
|
return this.workshopHelper.getReviewerAssessmentById(this.workshopId, this.assessmentId, this.profile.id)
|
||||||
|
.then((assessment) => {
|
||||||
|
let defaultGrade, promise;
|
||||||
|
|
||||||
|
this.assessment = this.workshopHelper.realGradeValue(this.workshop, assessment);
|
||||||
|
this.evaluate.text = this.assessment.feedbackreviewer || '';
|
||||||
|
this.evaluate.weight = this.assessment.weight;
|
||||||
|
|
||||||
|
if (this.evaluating) {
|
||||||
|
if (accessData.canallocate) {
|
||||||
|
this.weights = [];
|
||||||
|
for (let i = 16; i >= 0; i--) {
|
||||||
|
this.weights[i] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessData.canoverridegrades) {
|
||||||
|
defaultGrade = this.translate.instant('addon.mod_workshop.notoverridden');
|
||||||
|
promise = this.gradesHelper.makeGradesMenu(this.workshop.gradinggrade, this.workshopId, defaultGrade,
|
||||||
|
-1).then((grades) => {
|
||||||
|
this.evaluationGrades = grades;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(() => {
|
||||||
|
return this.workshopOffline.getEvaluateAssessment(this.workshopId, this.assessmentId)
|
||||||
|
.then((offlineAssess) => {
|
||||||
|
this.hasOffline = true;
|
||||||
|
this.evaluate.weight = offlineAssess.weight;
|
||||||
|
if (accessData.canoverridegrades) {
|
||||||
|
this.evaluate.text = offlineAssess.feedbacktext || '';
|
||||||
|
this.evaluate.grade = offlineAssess.gradinggradeover || -1;
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.hasOffline = false;
|
||||||
|
// No offline, load online.
|
||||||
|
if (accessData.canoverridegrades) {
|
||||||
|
this.evaluate.text = this.assessment.feedbackreviewer || '';
|
||||||
|
this.evaluate.grade = this.assessment.gradinggradeover || -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.originalEvaluation.weight = this.evaluate.weight;
|
||||||
|
if (accessData.canoverridegrades) {
|
||||||
|
this.originalEvaluation.text = this.evaluate.text;
|
||||||
|
this.originalEvaluation.grade = this.evaluate.grade;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.evaluateForm.controls['weight'].setValue(this.evaluate.weight);
|
||||||
|
if (accessData.canoverridegrades) {
|
||||||
|
this.evaluateForm.controls['grade'].setValue(this.evaluate.grade);
|
||||||
|
this.evaluateForm.controls['text'].setValue(this.evaluate.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && this.assessment.gradinggradeoverby) {
|
||||||
|
return this.userProvider.getProfile(this.assessment.gradinggradeoverby, this.courseId, true)
|
||||||
|
.then((profile) => {
|
||||||
|
this.evaluateByProfile = profile;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'mm.course.errorgetmodule', true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force leaving the page, without checking for changes.
|
||||||
|
*/
|
||||||
|
protected forceLeavePage(): void {
|
||||||
|
this.forceLeave = true;
|
||||||
|
this.navCtrl.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data has changed.
|
||||||
|
*
|
||||||
|
* @return {boolean} 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 {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected refreshAllData(): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.workshopProvider.invalidateWorkshopData(this.courseId));
|
||||||
|
promises.push(this.workshopProvider.invalidateWorkshopAccessInformationData(this.workshopId));
|
||||||
|
promises.push(this.workshopProvider.invalidateReviewerAssesmentsData(this.workshopId));
|
||||||
|
|
||||||
|
if (this.assessmentId) {
|
||||||
|
promises.push(this.workshopProvider.invalidateAssessmentFormData(this.workshopId, this.assessmentId));
|
||||||
|
promises.push(this.workshopProvider.invalidateAssessmentData(this.workshopId, this.assessmentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).finally(() => {
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, this.siteId);
|
||||||
|
|
||||||
|
return this.fetchAssessmentData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull to refresh.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshAssessment(refresher: any): void {
|
||||||
|
if (this.loaded) {
|
||||||
|
this.refreshAllData().finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the assessment evaluation.
|
||||||
|
*/
|
||||||
|
saveEvaluation(): void {
|
||||||
|
// Check if data has changed.
|
||||||
|
if (this.hasEvaluationChanged()) {
|
||||||
|
this.sendEvaluation().then(() => {
|
||||||
|
this.forceLeavePage();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Nothing to save, just go back.
|
||||||
|
this.forceLeavePage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the evaluation to be saved on the server.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected sendEvaluation(): Promise<any> {
|
||||||
|
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
|
// Check if rich text editor is enabled or not.
|
||||||
|
return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => {
|
||||||
|
const inputData = this.evaluateForm.value;
|
||||||
|
inputData.grade = inputData.grade >= 0 ? inputData.grade : '';
|
||||||
|
if (!rteEnabled) {
|
||||||
|
// Rich text editor not enabled, add some HTML to the message if needed.
|
||||||
|
inputData.text = this.textUtils.formatHtmlLines(inputData.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send it to server.
|
||||||
|
return this.workshopProvider.evaluateAssessment(this.workshopId, this.assessmentId, this.courseId, inputData.text,
|
||||||
|
inputData.weight, inputData.grade);
|
||||||
|
}).then(() => {
|
||||||
|
const data = {
|
||||||
|
workshopId: this.workshopId,
|
||||||
|
assessmentId: this.assessmentId,
|
||||||
|
userId: this.currentUserId
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.workshopProvider.invalidateAssessmentData(this.workshopId, this.assessmentId).finally(() => {
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_SAVED, data, this.siteId);
|
||||||
|
});
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'Cannot save assessment evaluation');
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
|
||||||
|
this.syncObserver && this.syncObserver.off();
|
||||||
|
// Restore original back functions.
|
||||||
|
this.syncProvider.unblockOperation(AddonModWorkshopProvider.COMPONENT, this.workshopId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title>{{ 'addon.mod_workshop.editsubmission' | translate }}</ion-title>
|
||||||
|
<ion-buttons end>
|
||||||
|
<button ion-button clear (click)="save()" [attr.aria-label]="'core.save' | translate">
|
||||||
|
{{ 'core.save' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSubmission($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<form ion-list [formGroup]="editForm" *ngIf="workshop">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<ion-label stacked core-mark-required="true">{{ 'addon.mod_workshop.submissiontitle' | translate }}</ion-label>
|
||||||
|
<ion-input name="title" type="text" [placeholder]="'addon.mod_workshop.submissiontitle' | translate" formControlName="title"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label stacked>{{ 'addon.mod_workshop.submissioncontent' | translate }}</ion-label>
|
||||||
|
<core-rich-text-editor item-content [control]="editForm.controls['content']" formControlName="content" [placeholder]="'addon.mod_workshop.submissioncontent' | translate" name="content" [component]="component" [componentId]="componentId"></core-rich-text-editor>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<core-attachments *ngIf="workshop.nattachments > 0" [files]="submission.attachmentfiles" [maxSize]="workshop.maxbytes" [maxSubmissions]="workshop.nattachments" [component]="component" [componentId]="workshop.cmid" allowOffline="true" [acceptedTypes]="workshop.submissionfiletypes"></core-attachments>
|
||||||
|
</form>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopEditSubmissionPage } from './edit-submission';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopEditSubmissionPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModWorkshopEditSubmissionPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopEditSubmissionPageModule {}
|
|
@ -0,0 +1,398 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams, NavController } from 'ionic-angular';
|
||||||
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
import { CoreFileSessionProvider } from '@providers/file-session';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||||
|
import { AddonModWorkshopProvider } from '../../providers/workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the workshop edit submission.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-workshop-edit-submission' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-workshop-edit-submission',
|
||||||
|
templateUrl: 'edit-submission.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
module: any;
|
||||||
|
courseId: number;
|
||||||
|
access: any;
|
||||||
|
submission = {
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
attachmentfiles: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
loaded = false;
|
||||||
|
component = AddonModWorkshopProvider.COMPONENT;
|
||||||
|
componentId: number;
|
||||||
|
editForm: FormGroup; // The form group.
|
||||||
|
|
||||||
|
protected workshopId: number;
|
||||||
|
protected submissionId: number;
|
||||||
|
protected userId: number;
|
||||||
|
protected originalData: any = {};
|
||||||
|
protected hasOffline = false;
|
||||||
|
protected editing = false;
|
||||||
|
protected forceLeave = false;
|
||||||
|
protected siteId: string;
|
||||||
|
protected workshop: any;
|
||||||
|
protected isDestroyed = false;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected fileUploaderProvider: CoreFileUploaderProvider,
|
||||||
|
protected workshopProvider: AddonModWorkshopProvider, protected workshopOffline: AddonModWorkshopOfflineProvider,
|
||||||
|
protected workshopHelper: AddonModWorkshopHelperProvider, protected navCtrl: NavController,
|
||||||
|
protected fileSessionprovider: CoreFileSessionProvider, protected syncProvider: CoreSyncProvider,
|
||||||
|
protected textUtils: CoreTextUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected fb: FormBuilder,
|
||||||
|
protected translate: TranslateService, protected eventsProvider: CoreEventsProvider) {
|
||||||
|
this.module = navParams.get('module');
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.access = navParams.get('access');
|
||||||
|
this.submissionId = navParams.get('submissionId');
|
||||||
|
|
||||||
|
this.workshopId = this.module.instance;
|
||||||
|
this.componentId = this.module.id;
|
||||||
|
this.userId = sitesProvider.getCurrentSiteUserId();
|
||||||
|
this.siteId = sitesProvider.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 {
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
// Block the workshop.
|
||||||
|
this.syncProvider.blockOperation(this.component, this.workshopId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchSubmissionData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we can leave the page or not.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||||
|
*/
|
||||||
|
ionViewCanLeave(): boolean | Promise<void> {
|
||||||
|
if (this.forceLeave) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
// Check if data has changed.
|
||||||
|
if (!this.hasDataChanged()) {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
} else {
|
||||||
|
// Show confirmation if some data has been modified.
|
||||||
|
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(() => {
|
||||||
|
if (this.submission.attachmentfiles) {
|
||||||
|
// Delete the local files from the tmp folder.
|
||||||
|
this.fileUploaderProvider.clearTmpFiles(this.submission.attachmentfiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the submission data.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchSubmissionData(): Promise<void> {
|
||||||
|
return this.workshopProvider.getWorkshop(this.courseId, this.module.id).then((workshopData) => {
|
||||||
|
this.workshop = workshopData;
|
||||||
|
|
||||||
|
if (this.submissionId > 0) {
|
||||||
|
this.editing = true;
|
||||||
|
|
||||||
|
return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => {
|
||||||
|
this.submission = submissionData;
|
||||||
|
|
||||||
|
const canEdit = (this.userId == submissionData.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).then(() => {
|
||||||
|
return this.workshopOffline.getSubmissions(this.workshopId).then((submissionsActions) => {
|
||||||
|
if (submissionsActions && submissionsActions.length) {
|
||||||
|
this.hasOffline = true;
|
||||||
|
const actions = this.workshopHelper.filterSubmissionActions(submissionsActions, this.editing ?
|
||||||
|
this.submission.id : 0);
|
||||||
|
|
||||||
|
return this.workshopHelper.applyOfflineData(this.submission, actions);
|
||||||
|
} else {
|
||||||
|
this.hasOffline = false;
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.originalData.title = this.submission.title;
|
||||||
|
this.originalData.content = this.submission.content;
|
||||||
|
this.originalData.attachmentfiles = [];
|
||||||
|
|
||||||
|
this.submission.attachmentfiles.forEach((file) => {
|
||||||
|
let filename;
|
||||||
|
if (file.filename) {
|
||||||
|
filename = file.filename;
|
||||||
|
} else {
|
||||||
|
// We don't have filename, extract it from the path.
|
||||||
|
filename = file.filepath[0] == '/' ? file.filepath.substr(1) : file.filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.originalData.attachmentfiles.push({
|
||||||
|
filename : filename,
|
||||||
|
fileurl: file.fileurl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
this.editForm.controls['title'].setValue(this.submission.title);
|
||||||
|
this.editForm.controls['content'].setValue(this.submission.content);
|
||||||
|
|
||||||
|
const submissionId = this.submission.id || 'newsub';
|
||||||
|
this.fileSessionprovider.setFiles(this.component,
|
||||||
|
this.workshopId + '_' + submissionId, this.submission.attachmentfiles || []);
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
}).catch((message) => {
|
||||||
|
this.loaded = false;
|
||||||
|
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
|
||||||
|
this.forceLeavePage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force leaving the page, without checking for changes.
|
||||||
|
*/
|
||||||
|
protected forceLeavePage(): void {
|
||||||
|
this.forceLeave = true;
|
||||||
|
this.navCtrl.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the form input data.
|
||||||
|
*
|
||||||
|
* @return {any} Object with all the info.
|
||||||
|
*/
|
||||||
|
protected getInputData(): any {
|
||||||
|
const submissionId = this.submission.id || 'newsub';
|
||||||
|
|
||||||
|
const values = this.editForm.value;
|
||||||
|
values['attachmentfiles'] = this.fileSessionprovider.getFiles(this.component, this.workshopId + '_' + submissionId) || [];
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data has changed.
|
||||||
|
*
|
||||||
|
* @return {boolean} True if changed or false if not.
|
||||||
|
*/
|
||||||
|
protected hasDataChanged(): boolean {
|
||||||
|
if (!this.loaded) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputData = this.getInputData();
|
||||||
|
if (!this.originalData || typeof this.originalData.title == 'undefined') {
|
||||||
|
// There is no original data, assume it hasn't changed.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.originalData.title != inputData.title || this.originalData.content != inputData.content) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fileUploaderProvider.areFileListDifferent(inputData.attachmentfiles, this.originalData.attachmentfiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull to refresh.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshSubmission(refresher: any): void {
|
||||||
|
if (this.loaded) {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submission.id));
|
||||||
|
promises.push(this.workshopProvider.invalidateSubmissionsData(this.workshopId));
|
||||||
|
|
||||||
|
Promise.all(promises).finally(() => {
|
||||||
|
return this.fetchSubmissionData();
|
||||||
|
}).finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the submission.
|
||||||
|
*/
|
||||||
|
save(): void {
|
||||||
|
// Check if data has changed.
|
||||||
|
if (this.hasDataChanged()) {
|
||||||
|
this.saveSubmission().then(() => {
|
||||||
|
// 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 {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected saveSubmission(): Promise<any> {
|
||||||
|
const inputData = this.getInputData();
|
||||||
|
|
||||||
|
if (!inputData.title) {
|
||||||
|
this.domUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredtitle');
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
if (!inputData.content) {
|
||||||
|
this.domUtils.showAlertTranslated('core.notice', 'addon.mod_workshop.submissionrequiredcontent');
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let allowOffline = true,
|
||||||
|
saveOffline = false;
|
||||||
|
|
||||||
|
const modal = this.domUtils.showModalLoading('core.sending', true),
|
||||||
|
submissionId = this.submission.id;
|
||||||
|
|
||||||
|
// Check if rich text editor is enabled or not.
|
||||||
|
return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => {
|
||||||
|
if (!rteEnabled) {
|
||||||
|
// Rich text editor not enabled, add some HTML to the message if needed.
|
||||||
|
inputData.content = this.textUtils.formatHtmlLines(inputData.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload attachments first if any.
|
||||||
|
allowOffline = !inputData.attachmentfiles.length;
|
||||||
|
|
||||||
|
return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, this.submission.id, inputData.attachmentfiles,
|
||||||
|
this.editing, saveOffline).catch(() => {
|
||||||
|
// Cannot upload them in online, save them in offline.
|
||||||
|
saveOffline = true;
|
||||||
|
allowOffline = true;
|
||||||
|
|
||||||
|
return this.workshopHelper.uploadOrStoreSubmissionFiles(this.workshopId, this.submission.id,
|
||||||
|
inputData.attachmentfiles, this.editing, saveOffline);
|
||||||
|
});
|
||||||
|
}).then((attachmentsId) => {
|
||||||
|
if (this.editing) {
|
||||||
|
if (saveOffline) {
|
||||||
|
// Save submission in offline.
|
||||||
|
return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title,
|
||||||
|
inputData.content, attachmentsId, submissionId, 'update').then(() => {
|
||||||
|
// Don't return anything.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send it to server.
|
||||||
|
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||||
|
return this.workshopProvider.updateSubmission(this.workshopId, submissionId, this.courseId, inputData.title,
|
||||||
|
inputData.content, attachmentsId, undefined, allowOffline);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveOffline) {
|
||||||
|
// Save submission in offline.
|
||||||
|
return this.workshopOffline.saveSubmission(this.workshopId, this.courseId, inputData.title, inputData.content,
|
||||||
|
attachmentsId, submissionId, 'add').then(() => {
|
||||||
|
// Don't return anything.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send it to server.
|
||||||
|
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||||
|
return this.workshopProvider.addSubmission(this.workshopId, this.courseId, inputData.title, inputData.content,
|
||||||
|
attachmentsId, undefined, submissionId, allowOffline);
|
||||||
|
}).then((newSubmissionId) => {
|
||||||
|
const data = {
|
||||||
|
workshopId: this.workshopId,
|
||||||
|
cmId: this.module.cmid
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newSubmissionId && submissionId) {
|
||||||
|
// Data sent to server, delete stored files (if any).
|
||||||
|
this.workshopOffline.deleteSubmissionAction(this.workshopId, submissionId, this.editing ? 'update' : 'add');
|
||||||
|
this.workshopHelper.deleteSubmissionStoredFiles(this.workshopId, submissionId, this.editing);
|
||||||
|
data['submissionId'] = newSubmissionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = newSubmissionId ? this.workshopProvider.invalidateSubmissionData(this.workshopId, newSubmissionId) :
|
||||||
|
Promise.resolve();
|
||||||
|
|
||||||
|
return promise.finally(() => {
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||||
|
|
||||||
|
// Delete the local files from the tmp folder.
|
||||||
|
this.fileUploaderProvider.clearTmpFiles(inputData.attachmentfiles);
|
||||||
|
});
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'Cannot save submission');
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
this.syncProvider.unblockOperation(this.component, this.workshopId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||||
|
|
||||||
|
<ion-buttons end>
|
||||||
|
<!-- The buttons defined by the component will be added in here. -->
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="workshopComponent.loaded" (ionRefresh)="workshopComponent.doRefresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<addon-mod-workshop-index [module]="module" [courseId]="courseId" [group]="selectedGroup" (dataRetrieved)="updateData($event)"></addon-mod-workshop-index>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModWorkshopIndexPage } from './index';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopIndexPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
AddonModWorkshopComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModWorkshopIndexPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopIndexPageModule {}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, ViewChild } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams } from 'ionic-angular';
|
||||||
|
import { AddonModWorkshopIndexComponent } from '../../components/index/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a workshop.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-workshop-index' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-workshop-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopIndexPage {
|
||||||
|
@ViewChild(AddonModWorkshopIndexComponent) workshopComponent: AddonModWorkshopIndexComponent;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
module: any;
|
||||||
|
courseId: number;
|
||||||
|
selectedGroup: number;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams) {
|
||||||
|
this.module = navParams.get('module') || {};
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.selectedGroup = navParams.get('group') || 0;
|
||||||
|
this.title = this.module.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update some data based on the workshop instance.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop instance.
|
||||||
|
*/
|
||||||
|
updateData(workshop: any): void {
|
||||||
|
this.title = workshop.name || this.title;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title>{{ 'addon.mod_workshop.selectphase' | translate }}</ion-title>
|
||||||
|
<ion-buttons end>
|
||||||
|
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||||
|
<ion-icon name="close"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-list radio-group [(ngModel)]="selected" (ionChange)="switchPhase()">
|
||||||
|
<ng-container *ngFor="let phase of phases">
|
||||||
|
<ion-item *ngIf="workshopPhase >= phase.code || phase.tasks.length || phase.switchUrl">
|
||||||
|
<ion-label>{{ phase.title }}
|
||||||
|
<p text-wrap *ngIf="workshopPhase == phase.code">{{ 'addon.mod_workshop.userplancurrentphase' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-radio [value]="phase.code"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="!(workshopPhase >= phase.code || phase.tasks.length || phase.switchUrl)">
|
||||||
|
{{ phase.title }}
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopPhaseSelectorPage } from './phase';
|
||||||
|
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopPhaseSelectorPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreCompileHtmlComponentModule,
|
||||||
|
IonicPageModule.forChild(AddonModWorkshopPhaseSelectorPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopPhaseSelectorPageModule {}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPage, NavParams, ViewController } from 'ionic-angular';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the phase selector modal.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-workshop-phase-selector' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-workshop-phase-selector',
|
||||||
|
templateUrl: 'phase.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopPhaseSelectorPage {
|
||||||
|
selected: number;
|
||||||
|
phases: any;
|
||||||
|
workshopPhase: number;
|
||||||
|
protected original: number;
|
||||||
|
|
||||||
|
constructor(params: NavParams, private viewCtrl: ViewController) {
|
||||||
|
this.selected = params.get('selected');
|
||||||
|
this.original = this.selected;
|
||||||
|
this.phases = params.get('phases');
|
||||||
|
this.workshopPhase = params.get('workshopPhase');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal.
|
||||||
|
*/
|
||||||
|
closeModal(): void {
|
||||||
|
this.viewCtrl.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select phase.
|
||||||
|
*/
|
||||||
|
switchPhase(): void {
|
||||||
|
// This is a quick hack to avoid the first switch phase call done just when opening the modal.
|
||||||
|
if (this.original != this.selected) {
|
||||||
|
this.viewCtrl.dismiss(this.selected);
|
||||||
|
}
|
||||||
|
this.original = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||||
|
<ion-buttons end [hidden]="!loaded">
|
||||||
|
<button *ngIf="assessmentId" ion-button clear (click)="saveAssessment()" [attr.aria-label]="'core.save' | translate">
|
||||||
|
{{ 'core.save' | translate }}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="canAddFeedback" ion-button clear (click)="saveEvaluation()" [attr.aria-label]="'core.save' | translate">
|
||||||
|
{{ 'core.save' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSubmission($event)">
|
||||||
|
<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 text-wrap *ngIf="canEdit || canDelete">
|
||||||
|
<button ion-button block icon-start *ngIf="canEdit" (click)="editSubmission()">
|
||||||
|
<ion-icon name="create"></ion-icon>
|
||||||
|
{{ 'addon.mod_workshop.editsubmission' | translate }}
|
||||||
|
</button>
|
||||||
|
<button ion-button block icon-start *ngIf="!submission.deleted && canDelete" color="danger" (click)="deleteSubmission()">
|
||||||
|
<ion-icon name="trash"></ion-icon>
|
||||||
|
{{ 'addon.mod_workshop.deletesubmission' | translate }}
|
||||||
|
</button>
|
||||||
|
<button ion-button block icon-start outline *ngIf="submission.deleted && canDelete" color="danger" (click)="undoDeleteSubmission()">
|
||||||
|
<ion-icon name="undo"></ion-icon>
|
||||||
|
{{ 'core.restore' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-list *ngIf="!canAddFeedback && evaluate && evaluate.text">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<ion-avatar item-start *ngIf="evaluateByProfile">
|
||||||
|
<img [src]="evaluateByProfile.profileimageurl" core-external-content core-user-link [courseId]="courseId" [userId]="evaluateByProfile.id" [alt]="'core.pictureof' | translate:{$a: evaluateByProfile.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
<h2 *ngIf="evaluateByProfile && evaluateByProfile.fullname">{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateByProfile.fullname} }}</h2>
|
||||||
|
<core-format-text [text]="evaluate.text"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-list *ngIf="ownAssessment && !assessment">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.yourassessment' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<addon-mod-workshop-assessment [submission]="submission" [assessment]="ownAssessment" [courseId]="courseId" summary="true" [access]="access" [module]="module" [workshop]="workshop"></addon-mod-workshop-assessment>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-list *ngIf="submissionInfo && submissionInfo.reviewedby && submissionInfo.reviewedby.length && !assessment">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.receivedgrades' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ng-container *ngFor="let reviewer of submissionInfo.reviewedby">
|
||||||
|
<addon-mod-workshop-assessment *ngIf="!reviewer.ownAssessment" [submission]="submission" [assessment]="reviewer" [courseId]="courseId" summary="true" [access]="access" [workshop]="workshop"></addon-mod-workshop-assessment>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-list *ngIf="submissionInfo && submissionInfo.reviewerof && submissionInfo.reviewerof.length && !assessment">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.givengrades' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<addon-mod-workshop-assessment *ngFor="let reviewer of submissionInfo.reviewerof" [assessment]="reviewer" [courseId]="courseId" summary="true" [workshop]="workshop" [access]="access"></addon-mod-workshop-assessment>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<form ion-list [formGroup]="feedbackForm" *ngIf="canAddFeedback">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item 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 text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_workshop.gradecalculated' | translate }}</h2>
|
||||||
|
<p>{{ submission.submissiongrade }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<ion-label stacked>{{ 'addon.mod_workshop.gradeover' | translate }}</ion-label>
|
||||||
|
<ion-select formControlName="grade">
|
||||||
|
<ion-option *ngFor="let grade of evaluationGrades" [value]="grade.value">{{grade.label}}</ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label stacked>{{ 'addon.mod_workshop.feedbackauthor' | translate }}</ion-label>
|
||||||
|
<core-rich-text-editor item-content [control]="feedbackForm.controls['text']" formControlName="text"></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 text-wrap>
|
||||||
|
<ion-avatar item-start *ngIf="evaluateGradingByProfile">
|
||||||
|
<img [src]="evaluateGradingByProfile.profileimageurl" core-external-content core-user-link [courseId]="courseId" [userId]="evaluateGradingByProfile.id" [alt]="'core.pictureof' | translate:{$a: evaluateGradingByProfile.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
<h2 *ngIf="evaluateGradingByProfile && evaluateGradingByProfile.fullname">{{ 'addon.mod_workshop.feedbackby' | translate : {$a: evaluateGradingByProfile.fullname} }}</h2>
|
||||||
|
<core-format-text [text]="assessment.feedbackreviewer"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { IonicPageModule } from 'ionic-angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModWorkshopComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModWorkshopSubmissionPage } from './submission';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModWorkshopSubmissionPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreComponentsModule,
|
||||||
|
AddonModWorkshopComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModWorkshopSubmissionPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopSubmissionPageModule {}
|
|
@ -0,0 +1,524 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 } from '@angular/core';
|
||||||
|
import { Content, IonicPage, NavParams, NavController } from 'ionic-angular';
|
||||||
|
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyComponent } from '../../components/assessment-strategy/assessment-strategy';
|
||||||
|
import { AddonModWorkshopProvider } from '../../providers/workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from '../../providers/offline';
|
||||||
|
import { AddonModWorkshopSyncProvider } from '../../providers/sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a workshop submission.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-workshop-submission' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-workshop-submission-page',
|
||||||
|
templateUrl: 'submission.html',
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild(AddonModWorkshopAssessmentStrategyComponent) assessmentStrategy: AddonModWorkshopAssessmentStrategyComponent;
|
||||||
|
|
||||||
|
module: any;
|
||||||
|
workshop: any;
|
||||||
|
access: any;
|
||||||
|
assessment: any;
|
||||||
|
submissionInfo: any;
|
||||||
|
submission: any;
|
||||||
|
|
||||||
|
courseId: number;
|
||||||
|
profile: any;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
loaded = false;
|
||||||
|
ownAssessment = false;
|
||||||
|
strategy: any;
|
||||||
|
assessmentId: number;
|
||||||
|
assessmentUserId: number;
|
||||||
|
evaluate: any;
|
||||||
|
canAddFeedback = false;
|
||||||
|
canEdit = false;
|
||||||
|
canDelete = false;
|
||||||
|
evaluationGrades: any;
|
||||||
|
evaluateGradingByProfile: any;
|
||||||
|
evaluateByProfile: any;
|
||||||
|
feedbackForm: FormGroup; // The form group.
|
||||||
|
|
||||||
|
protected submissionId: number;
|
||||||
|
protected workshopId: number;
|
||||||
|
protected currentUserId: number;
|
||||||
|
protected userId: number;
|
||||||
|
protected siteId: string;
|
||||||
|
protected originalEvaluation = {
|
||||||
|
published: '',
|
||||||
|
text: '',
|
||||||
|
grade: ''
|
||||||
|
};
|
||||||
|
protected hasOffline = false;
|
||||||
|
protected component = AddonModWorkshopProvider.COMPONENT;
|
||||||
|
protected forceLeave = false;
|
||||||
|
protected obsAssessmentSaved: any;
|
||||||
|
protected syncObserver: any;
|
||||||
|
protected isDestroyed = false;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected workshopProvider: AddonModWorkshopProvider,
|
||||||
|
protected workshopOffline: AddonModWorkshopOfflineProvider, protected syncProvider: CoreSyncProvider,
|
||||||
|
protected workshopHelper: AddonModWorkshopHelperProvider, protected navCtrl: NavController,
|
||||||
|
protected textUtils: CoreTextUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected fb: FormBuilder,
|
||||||
|
protected translate: TranslateService, protected eventsProvider: CoreEventsProvider,
|
||||||
|
protected courseProvider: CoreCourseProvider, @Optional() protected content: Content,
|
||||||
|
protected gradesHelper: CoreGradesHelperProvider, protected userProvider: CoreUserProvider) {
|
||||||
|
this.module = navParams.get('module');
|
||||||
|
this.workshop = navParams.get('workshop');
|
||||||
|
this.access = navParams.get('access');
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.profile = navParams.get('profile');
|
||||||
|
this.submissionInfo = navParams.get('submission') || {};
|
||||||
|
this.assessment = navParams.get('assessment') || null;
|
||||||
|
|
||||||
|
this.title = this.module.name;
|
||||||
|
this.workshopId = this.module.instance;
|
||||||
|
this.currentUserId = sitesProvider.getCurrentSiteUserId();
|
||||||
|
this.siteId = sitesProvider.getCurrentSiteId();
|
||||||
|
this.submissionId = this.submissionInfo.submissionid || this.submissionInfo.id;
|
||||||
|
this.userId = this.submissionInfo.userid || null;
|
||||||
|
this.strategy = (this.assessment && this.assessment.strategy) || (this.workshop && this.workshop.strategy);
|
||||||
|
this.assessmentId = this.assessment && (this.assessment.assessmentid || this.assessment.id);
|
||||||
|
this.assessmentUserId = this.assessment && (this.assessment.reviewerid || this.assessment.userid);
|
||||||
|
|
||||||
|
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 = this.eventsProvider.on(AddonModWorkshopProvider.ASSESSMENT_SAVED, (data) => {
|
||||||
|
this.eventReceived(data);
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Refresh workshop on sync.
|
||||||
|
this.syncObserver = this.eventsProvider.on(AddonModWorkshopSyncProvider.AUTO_SYNCED, (data) => {
|
||||||
|
// Update just when all database is synced.
|
||||||
|
this.eventReceived(data);
|
||||||
|
}, this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.fetchSubmissionData().then(() => {
|
||||||
|
this.workshopProvider.logViewSubmission(this.submissionId).then(() => {
|
||||||
|
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we can leave the page or not.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||||
|
*/
|
||||||
|
ionViewCanLeave(): boolean | Promise<void> {
|
||||||
|
const assessmentHasChanged = this.assessmentStrategy && this.assessmentStrategy.hasDataChanged();
|
||||||
|
if (this.forceLeave || (!this.hasEvaluationChanged() && !assessmentHasChanged)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show confirmation if some data has been modified.
|
||||||
|
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Goto edit submission page.
|
||||||
|
*/
|
||||||
|
editSubmission(): void {
|
||||||
|
const params = {
|
||||||
|
module: module,
|
||||||
|
access: this.access,
|
||||||
|
courseid: this.courseId,
|
||||||
|
submissionId: this.submission.id
|
||||||
|
};
|
||||||
|
|
||||||
|
this.navCtrl.push('AddonModWorkshopEditSubmissionPage', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when we receive an event of submission changes.
|
||||||
|
*
|
||||||
|
* @param {any} data Event data received.
|
||||||
|
*/
|
||||||
|
protected eventReceived(data: any): void {
|
||||||
|
if (this.workshopId === data.workshopId) {
|
||||||
|
this.content && this.content.scrollToTop();
|
||||||
|
|
||||||
|
this.loaded = false;
|
||||||
|
this.refreshAllData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the submission data.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchSubmissionData(): Promise<void> {
|
||||||
|
return this.workshopHelper.getSubmissionById(this.workshopId, this.submissionId).then((submissionData) => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
this.submission = submissionData;
|
||||||
|
this.submission.attachmentfiles = submissionData.attachmentfiles || [];
|
||||||
|
this.submission.submissiongrade = this.submissionInfo && this.submissionInfo.submissiongrade;
|
||||||
|
this.submission.gradinggrade = this.submissionInfo && this.submissionInfo.gradinggrade;
|
||||||
|
this.submission.submissiongradeover = this.submissionInfo && this.submissionInfo.submissiongradeover;
|
||||||
|
this.userId = submissionData.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 > AddonModWorkshopProvider.PHASE_ASSESSMENT &&
|
||||||
|
this.workshop.phase < AddonModWorkshopProvider.PHASE_CLOSED && this.access.canoverridegrades;
|
||||||
|
this.ownAssessment = false;
|
||||||
|
|
||||||
|
if (this.access.canviewallassessments) {
|
||||||
|
// Get new data, different that came from stateParams.
|
||||||
|
promises.push(this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId)
|
||||||
|
.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.userid = assessment.reviewerid;
|
||||||
|
assessment = this.workshopHelper.realGradeValue(this.workshop, assessment);
|
||||||
|
|
||||||
|
if (this.currentUserId == assessment.userid) {
|
||||||
|
this.ownAssessment = assessment;
|
||||||
|
assessment.ownAssessment = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
} else if (this.currentUserId == this.userId && this.assessmentId) {
|
||||||
|
// Get new data, different that came from stateParams.
|
||||||
|
promises.push(this.workshopProvider.getAssessment(this.workshopId, this.assessmentId).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;
|
||||||
|
}
|
||||||
|
|
||||||
|
assessment.userid = assessment.reviewerid;
|
||||||
|
assessment = this.workshopHelper.realGradeValue(this.workshop, assessment);
|
||||||
|
|
||||||
|
if (this.currentUserId == assessment.userid) {
|
||||||
|
this.ownAssessment = assessment;
|
||||||
|
assessment.ownAssessment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submissionInfo.reviewedby = [assessment];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) {
|
||||||
|
this.evaluate = {
|
||||||
|
published: submissionData.published,
|
||||||
|
text: submissionData.feedbackauthor || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canAddFeedback) {
|
||||||
|
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
// Block the workshop.
|
||||||
|
this.syncProvider.blockOperation(this.component, this.workshopId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultGrade = this.translate.instant('addon.mod_workshop.notoverridden');
|
||||||
|
|
||||||
|
promises.push(this.gradesHelper.makeGradesMenu(this.workshop.grade, this.workshopId, defaultGrade, -1)
|
||||||
|
.then((grades) => {
|
||||||
|
this.evaluationGrades = grades;
|
||||||
|
|
||||||
|
this.evaluate.grade = {
|
||||||
|
label: this.gradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.submissiongradeover) ||
|
||||||
|
defaultGrade,
|
||||||
|
value: this.submissionInfo.submissiongradeover || -1
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.workshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId)
|
||||||
|
.then((offlineSubmission) => {
|
||||||
|
this.hasOffline = true;
|
||||||
|
this.evaluate.published = offlineSubmission.published;
|
||||||
|
this.evaluate.text = offlineSubmission.feedbacktext;
|
||||||
|
this.evaluate.grade = {
|
||||||
|
label: this.gradesHelper.getGradeLabelFromValue(grades, offlineSubmission.gradeover) || defaultGrade,
|
||||||
|
value: offlineSubmission.gradeover || -1
|
||||||
|
};
|
||||||
|
}).catch(() => {
|
||||||
|
this.hasOffline = false;
|
||||||
|
// Ignore errors.
|
||||||
|
}).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);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
} else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && submissionData.gradeoverby) {
|
||||||
|
promises.push(this.userProvider.getProfile(submissionData.gradeoverby, this.courseId, true).then((profile) => {
|
||||||
|
this.evaluateByProfile = profile;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.assessmentId && !this.access.assessingallowed && this.assessment.feedbackreviewer &&
|
||||||
|
this.assessment.gradinggradeoverby) {
|
||||||
|
promises.push(this.userProvider.getProfile(this.assessment.gradinggradeoverby, this.courseId, true)
|
||||||
|
.then((profile) => {
|
||||||
|
this.evaluateGradingByProfile = profile;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).then(() => {
|
||||||
|
return this.workshopOffline.getSubmissions(this.workshopId).then((submissionsActions) => {
|
||||||
|
const actions = this.workshopHelper.filterSubmissionActions(submissionsActions, this.submissionId);
|
||||||
|
|
||||||
|
return this.workshopHelper.applyOfflineData(this.submission, actions).then((submission) => {
|
||||||
|
this.submission = submission;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force leaving the page, without checking for changes.
|
||||||
|
*/
|
||||||
|
protected forceLeavePage(): void {
|
||||||
|
this.forceLeave = true;
|
||||||
|
this.navCtrl.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data has changed.
|
||||||
|
*
|
||||||
|
* @return {boolean} 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 {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected refreshAllData(): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submissionId));
|
||||||
|
promises.push(this.workshopProvider.invalidateSubmissionsData(this.workshopId));
|
||||||
|
promises.push(this.workshopProvider.invalidateSubmissionAssesmentsData(this.workshopId, this.submissionId));
|
||||||
|
|
||||||
|
if (this.assessmentId) {
|
||||||
|
promises.push(this.workshopProvider.invalidateAssessmentFormData(this.workshopId, this.assessmentId));
|
||||||
|
promises.push(this.workshopProvider.invalidateAssessmentData(this.workshopId, this.assessmentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).finally(() => {
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, this.siteId);
|
||||||
|
|
||||||
|
return this.fetchSubmissionData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull to refresh.
|
||||||
|
*
|
||||||
|
* @param {any} refresher Refresher.
|
||||||
|
*/
|
||||||
|
refreshSubmission(refresher: any): void {
|
||||||
|
if (this.loaded) {
|
||||||
|
this.refreshAllData().finally(() => {
|
||||||
|
refresher.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the assessment.
|
||||||
|
*/
|
||||||
|
saveAssessment(): void {
|
||||||
|
if (this.assessmentStrategy && this.assessmentStrategy.hasDataChanged()) {
|
||||||
|
this.assessmentStrategy.saveAssessment().then(() => {
|
||||||
|
this.forceLeavePage();
|
||||||
|
}).catch(() => {
|
||||||
|
// Error, stay on the page.
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Nothing to save, just go back.
|
||||||
|
this.forceLeavePage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the submission evaluation.
|
||||||
|
*/
|
||||||
|
saveEvaluation(): void {
|
||||||
|
// Check if data has changed.
|
||||||
|
if (this.hasEvaluationChanged()) {
|
||||||
|
this.sendEvaluation().then(() => {
|
||||||
|
this.forceLeavePage();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Nothing to save, just go back.
|
||||||
|
this.forceLeavePage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the evaluation to be saved on the server.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected sendEvaluation(): Promise<any> {
|
||||||
|
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
|
// Check if rich text editor is enabled or not.
|
||||||
|
return this.domUtils.isRichTextEditorEnabled().then((rteEnabled) => {
|
||||||
|
const inputData = this.feedbackForm.value;
|
||||||
|
|
||||||
|
inputData.grade = inputData.grade >= 0 ? inputData.grade : '';
|
||||||
|
if (!rteEnabled) {
|
||||||
|
// Rich text editor not enabled, add some HTML to the message if needed.
|
||||||
|
inputData.text = this.textUtils.formatHtmlLines(inputData.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send it to server.
|
||||||
|
return this.workshopProvider.evaluateSubmission(this.workshopId, this.submissionId, this.courseId, inputData.text,
|
||||||
|
inputData.published, inputData.grade);
|
||||||
|
}).then(() => {
|
||||||
|
const data = {
|
||||||
|
workshopId: this.workshopId,
|
||||||
|
cmId: this.module.cmid,
|
||||||
|
submissionId: this.submissionId
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submissionId).finally(() => {
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||||
|
});
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'Cannot save submission evaluation');
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the submission delete action.
|
||||||
|
*/
|
||||||
|
deleteSubmission(): void {
|
||||||
|
this.domUtils.showConfirm(this.translate.instant('addon.mod_workshop.submissiondeleteconfirm')).then(() => {
|
||||||
|
const modal = this.domUtils.showModalLoading('core.deleting', true);
|
||||||
|
let success = false;
|
||||||
|
this.workshopProvider.deleteSubmission(this.workshopId, this.submissionId, this.courseId).then(() => {
|
||||||
|
success = true;
|
||||||
|
|
||||||
|
return this.workshopProvider.invalidateSubmissionData(this.workshopId, this.submissionId);
|
||||||
|
}).catch((error) => {
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'Cannot delete submission');
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
if (success) {
|
||||||
|
const data = {
|
||||||
|
workshopId: this.workshopId,
|
||||||
|
cmId: this.module.cmid,
|
||||||
|
submissionId: this.submissionId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||||
|
|
||||||
|
this.forceLeavePage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo the submission delete action.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
undoDeleteSubmission(): Promise<any> {
|
||||||
|
return this.workshopOffline.deleteSubmissionAction(this.workshopId, this.submissionId, 'delete').finally(() => {
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
workshopId: this.workshopId,
|
||||||
|
cmId: this.module.cmid,
|
||||||
|
submissionId: this.submissionId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopProvider.SUBMISSION_CHANGED, data, this.siteId);
|
||||||
|
|
||||||
|
return this.refreshAllData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
|
||||||
|
this.syncObserver && this.syncObserver.off();
|
||||||
|
this.obsAssessmentSaved && this.obsAssessmentSaved.off();
|
||||||
|
// Restore original back functions.
|
||||||
|
this.syncProvider.unblockOperation(this.component, this.workshopId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Injector } from '@angular/core';
|
||||||
|
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that all assessment strategy handlers must implement.
|
||||||
|
*/
|
||||||
|
export interface AddonWorkshopAssessmentStrategyHandler extends CoreDelegateHandler {
|
||||||
|
/**
|
||||||
|
* The name of the assessment strategy. E.g. 'accumulative'.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
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 Injector.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent?(injector: Injector): any | Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare original values to be shown and compared.
|
||||||
|
*
|
||||||
|
* @param {any} form Original data of the form.
|
||||||
|
* @param {number} workshopId WorkShop Id
|
||||||
|
* @return {Promise<any[]>} Promise resolved with original values sorted.
|
||||||
|
*/
|
||||||
|
getOriginalValues?(form: any, workshopId: number): Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||||
|
*
|
||||||
|
* @param {any[]} originalValues Original values of the form.
|
||||||
|
* @param {any[]} currentValues Current values of the form.
|
||||||
|
* @return {boolean} True if data has changed, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataChanged?(originalValues: any[], currentValues: any[]): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {any{}} currentValues Current values of the form.
|
||||||
|
* @param {any} form Assessment form data.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(currentValues: any[], form: any): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
export class AddonWorkshopAssessmentStrategyDelegate extends CoreDelegate {
|
||||||
|
|
||||||
|
protected handlerNameProperty = 'strategyName';
|
||||||
|
|
||||||
|
constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider,
|
||||||
|
protected eventsProvider: CoreEventsProvider) {
|
||||||
|
super('AddonWorkshopAssessmentStrategyDelegate', loggerProvider, sitesProvider, eventsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an assessment strategy plugin is supported.
|
||||||
|
*
|
||||||
|
* @param {string} workshopStrategy Assessment strategy name.
|
||||||
|
* @return {boolean} 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 Injector.
|
||||||
|
* @param {string} workshopStrategy Assessment strategy name.
|
||||||
|
* @return {any} The component, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponentForPlugin(injector: Injector, workshopStrategy: string): Promise<any> {
|
||||||
|
return this.executeFunctionOnEnabled(workshopStrategy, 'getComponent', [injector]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare original values to be shown and compared depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {string} workshopStrategy Workshop strategy.
|
||||||
|
* @param {any} form Original data of the form.
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @return {Promise<any[]>} Resolved with original values sorted.
|
||||||
|
*/
|
||||||
|
getOriginalValues(workshopStrategy: string, form: any, workshopId: number): Promise<any[]> {
|
||||||
|
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 {any} workshop Workshop.
|
||||||
|
* @param {any[]} originalValues Original values of the form.
|
||||||
|
* @param {any[]} currentValues Current values of the form.
|
||||||
|
* @return {boolean} True if data has changed, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataChanged(workshop: any, originalValues: any[], currentValues: any[]): boolean {
|
||||||
|
return this.executeFunctionOnEnabled(workshop.strategy, 'hasDataChanged', [originalValues, currentValues]) || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {string} workshopStrategy Workshop strategy to follow.
|
||||||
|
* @param {any{}} currentValues Current values of the form.
|
||||||
|
* @param {any} form Assessment form data.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(workshopStrategy: string, currentValues: any, form: any): Promise<any> {
|
||||||
|
return Promise.resolve(this.executeFunctionOnEnabled(workshopStrategy, 'prepareAssessmentData', [currentValues, form]));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,534 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreFileProvider } from '@providers/file';
|
||||||
|
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { AddonModWorkshopProvider } from './workshop';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from './offline';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from './assessment-strategy-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to gather some common functions for workshop.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopHelperProvider {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private translate: TranslateService,
|
||||||
|
private fileProvider: CoreFileProvider,
|
||||||
|
private uploaderProvider: CoreFileUploaderProvider,
|
||||||
|
private sitesProvider: CoreSitesProvider,
|
||||||
|
private textUtils: CoreTextUtilsProvider,
|
||||||
|
private utils: CoreUtilsProvider,
|
||||||
|
private workshopProvider: AddonModWorkshopProvider,
|
||||||
|
private workshopOffline: AddonModWorkshopOfflineProvider,
|
||||||
|
private strategyDelegate: AddonWorkshopAssessmentStrategyDelegate) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a task by code.
|
||||||
|
*
|
||||||
|
* @param {any[]} tasks Array of tasks.
|
||||||
|
* @param {string} taskCode Unique task code.
|
||||||
|
* @return {any} Task requested
|
||||||
|
*/
|
||||||
|
getTask(tasks: any[], taskCode: string): any {
|
||||||
|
for (const x in tasks) {
|
||||||
|
if (tasks[x].code == taskCode) {
|
||||||
|
return tasks[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check is task code is done.
|
||||||
|
*
|
||||||
|
* @param {any[]} tasks Array of tasks.
|
||||||
|
* @param {string} taskCode Unique task code.
|
||||||
|
* @return {boolean} True if task is completed.
|
||||||
|
*/
|
||||||
|
isTaskDone(tasks: any[], 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 {any} workshop Workshop info.
|
||||||
|
* @param {any} access Access information.
|
||||||
|
* @param {any[]} tasks Array of tasks.
|
||||||
|
* @return {boolean} True if the user can submit the workshop.
|
||||||
|
*/
|
||||||
|
canSubmit(workshop: any, access: any, tasks: any[]): boolean {
|
||||||
|
const examplesMust = workshop.useexamples && workshop.examplesmode == AddonModWorkshopProvider.EXAMPLES_BEFORE_SUBMISSION;
|
||||||
|
const examplesDone = access.canmanageexamples || workshop.examplesmode == AddonModWorkshopProvider.EXAMPLES_VOLUNTARY ||
|
||||||
|
this.isTaskDone(tasks, 'examples');
|
||||||
|
|
||||||
|
return workshop.phase > AddonModWorkshopProvider.PHASE_SETUP && access.cansubmit && (!examplesMust || examplesDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if a user can assess a workshop.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop info.
|
||||||
|
* @param {any} access Access information.
|
||||||
|
* @return {boolean} True if the user can assess the workshop.
|
||||||
|
*/
|
||||||
|
canAssess(workshop: any, access: any): boolean {
|
||||||
|
const examplesMust = workshop.useexamples && workshop.examplesmode == AddonModWorkshopProvider.EXAMPLES_BEFORE_ASSESSMENT;
|
||||||
|
const examplesDone = access.canmanageexamples;
|
||||||
|
|
||||||
|
return !examplesMust || examplesDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a particular user submission from the submission list.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} [userId] User ID. If not defined current user Id.
|
||||||
|
* @return {Promise<any>} Resolved with the submission, resolved with false if not found.
|
||||||
|
*/
|
||||||
|
getUserSubmission(workshopId: number, userId: number = 0): Promise<any> {
|
||||||
|
return this.workshopProvider.getSubmissions(workshopId).then((submissions) => {
|
||||||
|
userId = userId || this.sitesProvider.getCurrentSiteUserId();
|
||||||
|
|
||||||
|
for (const x in submissions) {
|
||||||
|
if (submissions[x].authorid == userId) {
|
||||||
|
return submissions[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a particular submission. It will use prefetched data if fetch fails.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId Submission ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Resolved with the submission, resolved with false if not found.
|
||||||
|
*/
|
||||||
|
getSubmissionById(workshopId: number, submissionId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.workshopProvider.getSubmission(workshopId, submissionId, siteId).catch(() => {
|
||||||
|
return this.workshopProvider.getSubmissions(workshopId, undefined, undefined, undefined, undefined, siteId)
|
||||||
|
.then((submissions) => {
|
||||||
|
for (const x in submissions) {
|
||||||
|
if (submissions[x].id == submissionId) {
|
||||||
|
return submissions[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a particular assesment. It will use prefetched data if fetch fails. It will add assessment form data.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {number} [userId] User ID. If not defined, current user.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Resolved with the assessment.
|
||||||
|
*/
|
||||||
|
getReviewerAssessmentById(workshopId: number, assessmentId: number, userId: number = 0, siteId?: string): Promise<any> {
|
||||||
|
return this.workshopProvider.getAssessment(workshopId, assessmentId, siteId).catch(() => {
|
||||||
|
return this.workshopProvider.getReviewerAssessments(workshopId, userId, undefined, undefined, siteId)
|
||||||
|
.then((assessments) => {
|
||||||
|
for (const x in assessments) {
|
||||||
|
if (assessments[x].id == assessmentId) {
|
||||||
|
return assessments[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}).then((assessment) => {
|
||||||
|
if (!assessment) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.workshopProvider.getAssessmentForm(workshopId, assessmentId, undefined, undefined, undefined, siteId)
|
||||||
|
.then((assessmentForm) => {
|
||||||
|
assessment.form = assessmentForm;
|
||||||
|
|
||||||
|
return assessment;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the assessment of the given user and all the related data.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} [userId=0] User ID. If not defined, current user.
|
||||||
|
* @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache.
|
||||||
|
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved when the workshop data is retrieved.
|
||||||
|
*/
|
||||||
|
getReviewerAssessments(workshopId: number, userId: number = 0, offline: boolean = false, ignoreCache: boolean = false,
|
||||||
|
siteId?: string): Promise<any[]> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
return this.workshopProvider.getReviewerAssessments(workshopId, userId, offline, ignoreCache, siteId)
|
||||||
|
.then((assessments) => {
|
||||||
|
const promises = assessments.map((assessment) => {
|
||||||
|
return this.getSubmissionById(workshopId, assessment.submissionid, siteId).then((submission) => {
|
||||||
|
assessment.submission = submission;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
return assessments;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete stored attachment files for a submission.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId If not editing, it will refer to timecreated.
|
||||||
|
* @param {boolean} editing If the submission is being edited or added otherwise.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when deleted.
|
||||||
|
*/
|
||||||
|
deleteSubmissionStoredFiles(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise<any> {
|
||||||
|
return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => {
|
||||||
|
return this.fileProvider.removeDir(folderPath).catch(() => {
|
||||||
|
// Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||||
|
* to be submitted later.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId If not editing, it will refer to timecreated.
|
||||||
|
* @param {boolean} editing If the submission is being edited or added otherwise.
|
||||||
|
* @param {any[]} files List of files.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
storeSubmissionFiles(workshopId: number, submissionId: number, editing: boolean, files: any[], siteId?: string): Promise<any> {
|
||||||
|
// Get the folder where to store the files.
|
||||||
|
return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => {
|
||||||
|
return this.uploaderProvider.storeFilesToUpload(folderPath, files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload or store some files for a submission, depending if the user is offline or not.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId If not editing, it will refer to timecreated.
|
||||||
|
* @param {any[]} files List of files.
|
||||||
|
* @param {boolean} editing If the submission is being edited or added otherwise.
|
||||||
|
* @param {boolean} offline True if files sould be stored for offline, false to upload them.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if success.
|
||||||
|
*/
|
||||||
|
uploadOrStoreSubmissionFiles(workshopId: number, submissionId: number, files: any[], editing: boolean, offline: boolean,
|
||||||
|
siteId?: string): Promise<any> {
|
||||||
|
if (offline) {
|
||||||
|
return this.storeSubmissionFiles(workshopId, submissionId, editing, files, siteId);
|
||||||
|
} else {
|
||||||
|
return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of stored attachment files for a submission. See AddonModWorkshopHelperProvider#storeFiles.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId If not editing, it will refer to timecreated.
|
||||||
|
* @param {boolean} editing If the submission is being edited or added otherwise.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the files.
|
||||||
|
*/
|
||||||
|
getStoredSubmissionFiles(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise<any[]> {
|
||||||
|
return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => {
|
||||||
|
return this.uploaderProvider.getStoredFiles(folderPath).catch(() => {
|
||||||
|
// Ignore not found files.
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of stored attachment files for a submission and online files also. See AddonModWorkshopHelperProvider#storeFiles.
|
||||||
|
*
|
||||||
|
* @param {any} filesObject Files object combining offline and online information.
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId If not editing, it will refer to timecreated.
|
||||||
|
* @param {boolean} editing If the submission is being edited or added otherwise.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the files.
|
||||||
|
*/
|
||||||
|
getSubmissionFilesFromOfflineFilesObject(filesObject: any, workshopId: number, submissionId: number, editing: boolean,
|
||||||
|
siteId?: string): Promise<any[]> {
|
||||||
|
return this.workshopOffline.getSubmissionFolder(workshopId, submissionId, editing, siteId).then((folderPath) => {
|
||||||
|
return this.uploaderProvider.getStoredFilesFromOfflineFilesObject(filesObject, folderPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete stored attachment files for an assessment.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when deleted.
|
||||||
|
*/
|
||||||
|
deleteAssessmentStoredFiles(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => {
|
||||||
|
return this.fileProvider.removeDir(folderPath).catch(() => {
|
||||||
|
// Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||||
|
* to be submitted later.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {any[]} files List of files.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
storeAssessmentFiles(workshopId: number, assessmentId: number, files: any[], siteId?: string): Promise<any> {
|
||||||
|
// Get the folder where to store the files.
|
||||||
|
return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => {
|
||||||
|
return this.uploaderProvider.storeFilesToUpload(folderPath, files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload or store some files for an assessment, depending if the user is offline or not.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId ID.
|
||||||
|
* @param {any[]} files List of files.
|
||||||
|
* @param {boolean} offline True if files sould be stored for offline, false to upload them.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if success.
|
||||||
|
*/
|
||||||
|
uploadOrStoreAssessmentFiles(workshopId: number, assessmentId: number, files: any[], offline: boolean, siteId?: string):
|
||||||
|
Promise<any> {
|
||||||
|
if (offline) {
|
||||||
|
return this.storeAssessmentFiles(workshopId, assessmentId, files, siteId);
|
||||||
|
} else {
|
||||||
|
return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModWorkshopProvider.COMPONENT, workshopId, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of stored attachment files for an assessment. See AddonModWorkshopHelperProvider#storeFiles.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the files.
|
||||||
|
*/
|
||||||
|
getStoredAssessmentFiles(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => {
|
||||||
|
return this.uploaderProvider.getStoredFiles(folderPath).catch(() => {
|
||||||
|
// Ignore not found files.
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of stored attachment files for an assessment and online files also. See AddonModWorkshopHelperProvider#storeFiles.
|
||||||
|
*
|
||||||
|
* @param {object} filesObject Files object combining offline and online information.
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the files.
|
||||||
|
*/
|
||||||
|
getAssessmentFilesFromOfflineFilesObject(filesObject: any, workshopId: number, assessmentId: number, siteId?: string):
|
||||||
|
Promise<any> {
|
||||||
|
return this.workshopOffline.getAssessmentFolder(workshopId, assessmentId, siteId).then((folderPath) => {
|
||||||
|
return this.uploaderProvider.getStoredFilesFromOfflineFilesObject(filesObject, folderPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the action of a given submission.
|
||||||
|
*
|
||||||
|
* @param {any[]} actions Offline actions to be applied to the given submission.
|
||||||
|
* @param {number} submissionId ID of the submission to filter by or false.
|
||||||
|
* @return {any[]} Promise resolved with the files.
|
||||||
|
*/
|
||||||
|
filterSubmissionActions(actions: any[], submissionId: number): any[] {
|
||||||
|
return actions.filter((action) => {
|
||||||
|
if (submissionId) {
|
||||||
|
return action.submissionid == submissionId;
|
||||||
|
} else {
|
||||||
|
return action.submissionid < 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies offline data to submission.
|
||||||
|
*
|
||||||
|
* @param {any} submission Submission object to be modified.
|
||||||
|
* @param {any[]} actions Offline actions to be applied to the given submission.
|
||||||
|
* @return {Promise<any>} Promise resolved with the files.
|
||||||
|
*/
|
||||||
|
applyOfflineData(submission: any, actions: any[]): Promise<any> {
|
||||||
|
if (actions.length && !submission) {
|
||||||
|
submission = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let editing = true,
|
||||||
|
attachmentsid = false,
|
||||||
|
workshopId;
|
||||||
|
|
||||||
|
actions.forEach((action) => {
|
||||||
|
switch (action.action) {
|
||||||
|
case 'add':
|
||||||
|
submission.id = action.submissionid;
|
||||||
|
editing = false;
|
||||||
|
case 'update':
|
||||||
|
submission.title = action.title;
|
||||||
|
submission.content = action.content;
|
||||||
|
submission.title = action.title;
|
||||||
|
submission.courseid = action.courseid;
|
||||||
|
submission.submissionmodified = parseInt(action.timemodified, 10) / 1000;
|
||||||
|
submission.offline = true;
|
||||||
|
attachmentsid = action.attachmentsid;
|
||||||
|
workshopId = action.workshopid;
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
submission.deleted = true;
|
||||||
|
submission.submissionmodified = parseInt(action.timemodified, 10) / 1000;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check offline files for latest attachmentsid.
|
||||||
|
if (actions.length) {
|
||||||
|
if (attachmentsid) {
|
||||||
|
return this.getSubmissionFilesFromOfflineFilesObject(attachmentsid, workshopId, submission.id, editing)
|
||||||
|
.then((files) => {
|
||||||
|
submission.attachmentfiles = files;
|
||||||
|
|
||||||
|
return submission;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
submission.attachmentfiles = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop object.
|
||||||
|
* @param {any[]} selectedValues Assessment current values
|
||||||
|
* @param {string} feedbackText Feedback text.
|
||||||
|
* @param {any[]} feedbackFiles Feedback attachments.
|
||||||
|
* @param {any} form Assessment form original data.
|
||||||
|
* @param {number} attachmentsId The draft file area id for attachments.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(workshop: any, selectedValues: any[], feedbackText: string, feedbackFiles: any[], form: any,
|
||||||
|
attachmentsId: number): Promise<any> {
|
||||||
|
if (workshop.overallfeedbackmode == 2 && !feedbackText) {
|
||||||
|
return Promise.reject({feedbackauthor: this.translate.instant('core.err_required')});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.strategyDelegate.prepareAssessmentData(workshop.strategy, selectedValues, form).then((data) => {
|
||||||
|
data.feedbackauthor = feedbackText;
|
||||||
|
data.feedbackauthorattachmentsid = attachmentsId || 0;
|
||||||
|
data.nodims = form.dimenssionscount;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the real value of a grade based on real_grade_value.
|
||||||
|
*
|
||||||
|
* @param {number} value Percentual value from 0 to 100.
|
||||||
|
* @param {number} max The maximal grade.
|
||||||
|
* @param {number} decimals Decimals to show in the formatted grade.
|
||||||
|
* @return {string} Real grade formatted.
|
||||||
|
*/
|
||||||
|
protected realGradeValueHelper(value: number, max: number, decimals: number): string {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
} else if (max == 0) {
|
||||||
|
return '0';
|
||||||
|
} else {
|
||||||
|
value = this.textUtils.roundToDecimals(max * value / 100, decimals);
|
||||||
|
|
||||||
|
return this.utils.formatFloat(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the real value of a grades of an assessment.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop object.
|
||||||
|
* @param {any} assessment Assessment data.
|
||||||
|
* @return {any} Assessment with real grades.
|
||||||
|
*/
|
||||||
|
realGradeValue(workshop: any, assessment: any): any {
|
||||||
|
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 {any} grade Grade to be shown
|
||||||
|
* @return {boolean} If grade should be shown or not.
|
||||||
|
*/
|
||||||
|
showGrade(grade: any): boolean {
|
||||||
|
return typeof grade !== 'undefined' && grade !== null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 '@core/contentlinks/classes/module-index-handler';
|
||||||
|
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||||
|
import { AddonModWorkshopProvider } from './workshop';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to workshop.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopLinkHandler extends CoreContentLinksModuleIndexHandler {
|
||||||
|
name = 'AddonModWorkshopLinkHandler';
|
||||||
|
|
||||||
|
constructor(courseHelper: CoreCourseHelperProvider) {
|
||||||
|
super(courseHelper, AddonModWorkshopProvider.COMPONENT, 'workshop');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { NavController, NavOptions } from 'ionic-angular';
|
||||||
|
import { AddonModWorkshopIndexComponent } from '../components/index/index';
|
||||||
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { AddonModWorkshopProvider } from './workshop';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support workshop modules.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopModuleHandler implements CoreCourseModuleHandler {
|
||||||
|
name = 'AddonModWorkshop';
|
||||||
|
modName = 'workshop';
|
||||||
|
|
||||||
|
constructor(private courseProvider: CoreCourseProvider, private workshopProvider: AddonModWorkshopProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): Promise<boolean> {
|
||||||
|
return this.workshopProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data required to display the module in the course contents view.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object.
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @param {number} sectionId The section ID.
|
||||||
|
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||||
|
*/
|
||||||
|
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||||
|
return {
|
||||||
|
icon: this.courseProvider.getModuleIconSrc('workshop'),
|
||||||
|
title: module.name,
|
||||||
|
class: 'addon-mod_workshop-handler',
|
||||||
|
showDownloadButton: true,
|
||||||
|
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||||
|
navCtrl.push('AddonModWorkshopIndexPage', {module: module, courseId: courseId}, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||||
|
* The component returned must implement CoreCourseModuleMainComponent.
|
||||||
|
*
|
||||||
|
* @param {any} course The course object.
|
||||||
|
* @param {any} module The module object.
|
||||||
|
* @return {any} The component to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getMainComponent(course: any, module: any): any {
|
||||||
|
return AddonModWorkshopIndexComponent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,792 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CoreFileProvider } from '@providers/file';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle offline workshop.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopOfflineProvider {
|
||||||
|
|
||||||
|
// Variables for database.
|
||||||
|
static SUBMISSIONS_TABLE = 'addon_mod_workshop_submissions';
|
||||||
|
static ASSESSMENTS_TABLE = 'addon_mod_workshop_assessments';
|
||||||
|
static EVALUATE_SUBMISSIONS_TABLE = 'addon_mod_workshop_evaluate_submissions';
|
||||||
|
static EVALUATE_ASSESSMENTS_TABLE = 'addon_mod_workshop_evaluate_assessments';
|
||||||
|
|
||||||
|
protected tablesSchema = [
|
||||||
|
{
|
||||||
|
name: AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'workshopid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'submissionid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'action',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'courseid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attachmentsid',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timemodified',
|
||||||
|
type: 'INTEGER',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primaryKeys: ['workshopid', 'submissionid', 'action']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: AddonModWorkshopOfflineProvider.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: AddonModWorkshopOfflineProvider.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: AddonModWorkshopOfflineProvider.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']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private fileProvider: CoreFileProvider,
|
||||||
|
private sitesProvider: CoreSitesProvider,
|
||||||
|
private textUtils: CoreTextUtilsProvider,
|
||||||
|
private timeUtils: CoreTimeUtilsProvider) {
|
||||||
|
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the workshops ids that have something to be synced.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<number[]>} Promise resolved with workshops id that have something to be synced.
|
||||||
|
*/
|
||||||
|
getAllWorkshops(siteId?: string): Promise<number[]> {
|
||||||
|
const promises = [
|
||||||
|
this.getAllSubmissions(siteId),
|
||||||
|
this.getAllAssessments(siteId),
|
||||||
|
this.getAllEvaluateSubmissions(siteId),
|
||||||
|
this.getAllEvaluateAssessments(siteId)
|
||||||
|
];
|
||||||
|
|
||||||
|
return Promise.all(promises).then((promiseResults) => {
|
||||||
|
const workshopIds = {};
|
||||||
|
|
||||||
|
// Get workshops from any offline object all should have workshopid.
|
||||||
|
promiseResults.forEach((offlineObjects) => {
|
||||||
|
offlineObjects.forEach((offlineObject) => {
|
||||||
|
workshopIds[offlineObject.workshopid] = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(workshopIds).map((workshopId) => parseInt(workshopId, 10));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is an offline data to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID to remove.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline data, false otherwise.
|
||||||
|
*/
|
||||||
|
hasWorkshopOfflineData(workshopId: number, siteId?: string): Promise<boolean> {
|
||||||
|
const promises = [
|
||||||
|
this.getSubmissions(workshopId, siteId),
|
||||||
|
this.getAssessments(workshopId, siteId),
|
||||||
|
this.getEvaluateSubmissions(workshopId, siteId),
|
||||||
|
this.getEvaluateAssessments(workshopId, siteId)
|
||||||
|
];
|
||||||
|
|
||||||
|
return Promise.all(promises).then((results) => {
|
||||||
|
return results.some((result) => result && result.length);
|
||||||
|
}).catch(() => {
|
||||||
|
// No offline data found.
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workshop submission action.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId Submission ID.
|
||||||
|
* @param {string} action Action to be done.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteSubmissionAction(workshopId: number, submissionId: number, action: string, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
submissionid: submissionId,
|
||||||
|
action: action
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all workshop submission actions.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId Submission ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteAllSubmissionActions(workshopId: number, submissionId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
submissionid: submissionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the all the submissions to be synced.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the objects to be synced.
|
||||||
|
*/
|
||||||
|
getAllSubmissions(siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE).then((records) => {
|
||||||
|
records.forEach(this.parseSubmissionRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the submissions of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getSubmissions(workshopId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions).then((records) => {
|
||||||
|
records.forEach(this.parseSubmissionRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all actions of a submission of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {number} submissionId ID of the submission.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getSubmissionActions(workshopId: number, submissionId: number, siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
submissionid: submissionId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions).then((records) => {
|
||||||
|
records.forEach(this.parseSubmissionRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an specific action of a submission of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {number} submissionId ID of the submission.
|
||||||
|
* @param {string} action Action to be done.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getSubmissionAction(workshopId: number, submissionId: number, action: string, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
submissionid: submissionId,
|
||||||
|
action: action
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecord(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, conditions).then((record) => {
|
||||||
|
this.parseSubmissionRecord(record);
|
||||||
|
|
||||||
|
return record;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offline version for adding a submission action to a workshop.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} courseId Course ID the workshop belongs to.
|
||||||
|
* @param {string} title The submission title.
|
||||||
|
* @param {string} content The submission text content.
|
||||||
|
* @param {any} attachmentsId Stored attachments.
|
||||||
|
* @param {number} submissionId Submission Id, if action is add, the time the submission was created.
|
||||||
|
* If set to 0, current time is used.
|
||||||
|
* @param {string} action Action to be done. ['add', 'update', 'delete']
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when submission action is successfully saved.
|
||||||
|
*/
|
||||||
|
saveSubmission(workshopId: number, courseId: number, title: string, content: string, attachmentsId: any,
|
||||||
|
submissionId: number, action: string, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const timemodified = this.timeUtils.timestamp();
|
||||||
|
const assessment = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
courseid: courseId,
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
attachmentsid: JSON.stringify(attachmentsId),
|
||||||
|
action: action,
|
||||||
|
submissionid: submissionId ? submissionId : -timemodified,
|
||||||
|
timemodified: timemodified
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE, assessment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse "attachments" column of a submission record.
|
||||||
|
*
|
||||||
|
* @param {any} record Submission record, modified in place.
|
||||||
|
*/
|
||||||
|
protected parseSubmissionRecord(record: any): void {
|
||||||
|
record.attachmentsid = this.textUtils.parseJSON(record.attachmentsid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workshop assessment.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
assessmentid: assessmentId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, conditions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the all the assessments to be synced.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the objects to be synced.
|
||||||
|
*/
|
||||||
|
getAllAssessments(siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE).then((records) => {
|
||||||
|
records.forEach(this.parseAssessmentRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the assessments of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getAssessments(workshopId: number, siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, conditions).then((records) => {
|
||||||
|
records.forEach(this.parseAssessmentRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an specific assessment of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
assessmentid: assessmentId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecord(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, conditions).then((record) => {
|
||||||
|
this.parseAssessmentRecord(record);
|
||||||
|
|
||||||
|
return record;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offline version for adding an assessment to a workshop.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {number} courseId Course ID the workshop belongs to.
|
||||||
|
* @param {any} inputData Assessment data.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when assessment is successfully saved.
|
||||||
|
*/
|
||||||
|
saveAssessment(workshopId: number, assessmentId: number, courseId: number, inputData: any, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const assessment = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
courseid: courseId,
|
||||||
|
inputdata: JSON.stringify(inputData),
|
||||||
|
assessmentid: assessmentId,
|
||||||
|
timemodified: this.timeUtils.timestamp()
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE, assessment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse "inpudata" column of an assessment record.
|
||||||
|
*
|
||||||
|
* @param {any} record Assessnent record, modified in place.
|
||||||
|
*/
|
||||||
|
protected parseAssessmentRecord(record: any): void {
|
||||||
|
record.inputdata = this.textUtils.parseJSON(record.inputdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workshop evaluate submission.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId Submission ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteEvaluateSubmission(workshopId: number, submissionId: number, siteId?: string): Promise<any> {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
submissionid: submissionId
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, conditions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the all the evaluate submissions to be synced.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the objects to be synced.
|
||||||
|
*/
|
||||||
|
getAllEvaluateSubmissions(siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE).then((records) => {
|
||||||
|
records.forEach(this.parseEvaluateSubmissionRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the evaluate submissions of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getEvaluateSubmissions(workshopId: number, siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, conditions)
|
||||||
|
.then((records) => {
|
||||||
|
records.forEach(this.parseEvaluateSubmissionRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an specific evaluate submission of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {number} submissionId Submission ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getEvaluateSubmission(workshopId: number, submissionId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
submissionid: submissionId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecord(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, conditions).then((record) => {
|
||||||
|
this.parseEvaluateSubmissionRecord(record);
|
||||||
|
|
||||||
|
return record;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offline version for evaluation a submission to a workshop.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId Submission ID.
|
||||||
|
* @param {number} courseId Course ID the workshop belongs to.
|
||||||
|
* @param {string} feedbackText The feedback for the author.
|
||||||
|
* @param {boolean} published Whether to publish the submission for other users.
|
||||||
|
* @param {any} gradeOver The new submission grade (empty for no overriding the grade).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when submission evaluation is successfully saved.
|
||||||
|
*/
|
||||||
|
saveEvaluateSubmission(workshopId: number, submissionId: number, courseId: number, feedbackText: string, published: boolean,
|
||||||
|
gradeOver: any, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const submission = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
courseid: courseId,
|
||||||
|
submissionid: submissionId,
|
||||||
|
timemodified: this.timeUtils.timestamp(),
|
||||||
|
feedbacktext: feedbackText,
|
||||||
|
published: Number(published),
|
||||||
|
gradeover: JSON.stringify(gradeOver)
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE, submission);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse "published" and "gradeover" columns of an evaluate submission record.
|
||||||
|
*
|
||||||
|
* @param {any} record Evaluate submission record, modified in place.
|
||||||
|
*/
|
||||||
|
protected parseEvaluateSubmissionRecord(record: any): void {
|
||||||
|
record.published = Boolean(record.published);
|
||||||
|
record.gradeover = this.textUtils.parseJSON(record.gradeover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workshop evaluate assessment.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteEvaluateAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
assessmentid: assessmentId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().deleteRecords(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, conditions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the all the evaluate assessments to be synced.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the objects to be synced.
|
||||||
|
*/
|
||||||
|
getAllEvaluateAssessments(siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE).then((records) => {
|
||||||
|
records.forEach(this.parseEvaluateAssessmentRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the evaluate assessments of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getEvaluateAssessments(workshopId: number, siteId?: string): Promise<any[]> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecords(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, conditions)
|
||||||
|
.then((records) => {
|
||||||
|
records.forEach(this.parseEvaluateAssessmentRecord.bind(this));
|
||||||
|
|
||||||
|
return records;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an specific evaluate assessment of a workshop to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId ID of the workshop.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
getEvaluateAssessment(workshopId: number, assessmentId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const conditions = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
assessmentid: assessmentId
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().getRecord(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, conditions).then((record) => {
|
||||||
|
this.parseEvaluateAssessmentRecord(record);
|
||||||
|
|
||||||
|
return record;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offline version for evaluating an assessment to a workshop.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {number} courseId Course ID the workshop belongs to.
|
||||||
|
* @param {string} feedbackText The feedback for the reviewer.
|
||||||
|
* @param {number} weight The new weight for the assessment.
|
||||||
|
* @param {any} gradingGradeOver The new grading grade (empty for no overriding the grade).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when assessment evaluation is successfully saved.
|
||||||
|
*/
|
||||||
|
saveEvaluateAssessment(workshopId: number, assessmentId: number, courseId: number, feedbackText: string, weight: number,
|
||||||
|
gradingGradeOver: any, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const assessment = {
|
||||||
|
workshopid: workshopId,
|
||||||
|
courseid: courseId,
|
||||||
|
assessmentid: assessmentId,
|
||||||
|
timemodified: this.timeUtils.timestamp(),
|
||||||
|
feedbacktext: feedbackText,
|
||||||
|
weight: weight,
|
||||||
|
gradinggradeover: JSON.stringify(gradingGradeOver)
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().insertRecord(AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE, assessment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse "gradinggradeover" column of an evaluate assessment record.
|
||||||
|
*
|
||||||
|
* @param {any} record Evaluate assessment record, modified in place.
|
||||||
|
*/
|
||||||
|
protected parseEvaluateAssessmentRecord(record: any): void {
|
||||||
|
record.gradinggradeover = this.textUtils.parseJSON(record.gradinggradeover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the folder where to store files for offline attachments in a workshop.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<string>} Promise resolved with the path.
|
||||||
|
*/
|
||||||
|
getWorkshopFolder(workshopId: number, siteId?: string): Promise<string> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
|
||||||
|
const siteFolderPath = this.fileProvider.getSiteFolder(site.getId());
|
||||||
|
const workshopFolderPath = 'offlineworkshop/' + workshopId + '/';
|
||||||
|
|
||||||
|
return this.textUtils.concatenatePaths(siteFolderPath, workshopFolderPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the folder where to store files for offline submissions.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} submissionId If not editing, it will refer to timecreated.
|
||||||
|
* @param {boolean} editing If the submission is being edited or added otherwise.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<string>} Promise resolved with the path.
|
||||||
|
*/
|
||||||
|
getSubmissionFolder(workshopId: number, submissionId: number, editing: boolean, siteId?: string): Promise<string> {
|
||||||
|
return this.getWorkshopFolder(workshopId, siteId).then((folderPath) => {
|
||||||
|
folderPath += 'submission/';
|
||||||
|
const folder = editing ? 'update_' + submissionId : 'add';
|
||||||
|
|
||||||
|
return this.textUtils.concatenatePaths(folderPath, folder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the folder where to store files for offline assessment.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {number} assessmentId Assessment ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<string>} Promise resolved with the path.
|
||||||
|
*/
|
||||||
|
getAssessmentFolder(workshopId: number, assessmentId: number, siteId?: string): Promise<string> {
|
||||||
|
return this.getWorkshopFolder(workshopId, siteId).then((folderPath) => {
|
||||||
|
folderPath += 'assessment/';
|
||||||
|
|
||||||
|
return this.textUtils.concatenatePaths(folderPath, String(assessmentId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,368 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Injector } from '@angular/core';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
|
||||||
|
import { CoreGroupsProvider } from '@providers/groups';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { AddonModWorkshopProvider } from './workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from './helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to prefetch workshops.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
|
||||||
|
name = 'AddonModWorkshop';
|
||||||
|
modName = 'workshop';
|
||||||
|
component = AddonModWorkshopProvider.COMPONENT;
|
||||||
|
updatesNames = new RegExp('^configuration$|^.*files$|^completion|^gradeitems$|^outcomes$|^submissions$|^assessments$' +
|
||||||
|
'|^assessmentgrades$|^usersubmissions$|^userassessments$|^userassessmentgrades$|^userassessmentgrades$');
|
||||||
|
|
||||||
|
constructor(injector: Injector,
|
||||||
|
private groupsProvider: CoreGroupsProvider,
|
||||||
|
private userProvider: CoreUserProvider,
|
||||||
|
private workshopProvider: AddonModWorkshopProvider,
|
||||||
|
private workshopHelper: AddonModWorkshopHelperProvider) {
|
||||||
|
super(injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||||
|
* @return {Promise<any>} Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
download(module: any, courseId: number, dirPath?: string): Promise<any> {
|
||||||
|
// Workshop cannot be downloaded right away, only prefetched.
|
||||||
|
return this.prefetch(module, courseId, false, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of files. If not defined, we'll assume they're in module.contents.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {Number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with the list of files.
|
||||||
|
*/
|
||||||
|
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {
|
||||||
|
return this.getWorkshopInfoHelper(module, courseId, true).then((info) => {
|
||||||
|
return info.files;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get all workshop info just once.
|
||||||
|
*
|
||||||
|
* @param {any} module Module to get the files.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} [omitFail=false] True to always return even if fails. Default false.
|
||||||
|
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||||
|
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the info fetched.
|
||||||
|
*/
|
||||||
|
protected getWorkshopInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false,
|
||||||
|
ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||||
|
let workshop,
|
||||||
|
groups = [],
|
||||||
|
files = [],
|
||||||
|
access;
|
||||||
|
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const userId = site.getUserId();
|
||||||
|
|
||||||
|
return this.workshopProvider.getWorkshop(courseId, module.id, siteId, forceCache).then((data) => {
|
||||||
|
files = this.getIntroFilesFromInstance(module, data);
|
||||||
|
files = files.concat(data.instructauthorsfiles).concat(data.instructreviewersfiles);
|
||||||
|
workshop = data;
|
||||||
|
|
||||||
|
return this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId).then((accessData) => {
|
||||||
|
access = accessData;
|
||||||
|
if (access.canviewallsubmissions) {
|
||||||
|
return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => {
|
||||||
|
if (!groupInfo.groups || groupInfo.groups.length == 0) {
|
||||||
|
groupInfo.groups = [{id: 0}];
|
||||||
|
}
|
||||||
|
groups = groupInfo.groups;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => {
|
||||||
|
// Get submission phase info.
|
||||||
|
const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION],
|
||||||
|
canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks),
|
||||||
|
canAssess = this.workshopHelper.canAssess(workshop, access),
|
||||||
|
promises = [];
|
||||||
|
|
||||||
|
if (canSubmit) {
|
||||||
|
promises.push(this.workshopHelper.getUserSubmission(workshop.id, userId).then((submission) => {
|
||||||
|
if (submission) {
|
||||||
|
files = files.concat(submission.contentfiles).concat(submission.attachmentfiles);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) {
|
||||||
|
promises.push(this.workshopProvider.getSubmissions(workshop.id).then((submissions) => {
|
||||||
|
const promises2 = [];
|
||||||
|
submissions.forEach((submission) => {
|
||||||
|
files = files.concat(submission.contentfiles).concat(submission.attachmentfiles);
|
||||||
|
promises2.push(this.workshopProvider.getSubmissionAssessments(workshop.id, submission.id)
|
||||||
|
.then((assessments) => {
|
||||||
|
assessments.forEach((assessment) => {
|
||||||
|
files = files.concat(assessment.feedbackattachmentfiles)
|
||||||
|
.concat(assessment.feedbackcontentfiles);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises2);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get assessment files.
|
||||||
|
if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) {
|
||||||
|
promises.push(this.workshopHelper.getReviewerAssessments(workshop.id).then((assessments) => {
|
||||||
|
assessments.forEach((assessment) => {
|
||||||
|
files = files.concat(assessment.feedbackattachmentfiles).concat(assessment.feedbackcontentfiles);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
return {
|
||||||
|
workshop: workshop,
|
||||||
|
groups: groups,
|
||||||
|
files: files.filter((file) => typeof file !== 'undefined')
|
||||||
|
};
|
||||||
|
}).catch((message): any => {
|
||||||
|
if (omitFail) {
|
||||||
|
// Any error, return the info we have.
|
||||||
|
return {
|
||||||
|
workshop: workshop,
|
||||||
|
groups: groups,
|
||||||
|
files: files.filter((file) => typeof file !== 'undefined')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param {number} moduleId The module ID.
|
||||||
|
* @param {number} courseId The course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||||
|
return this.workshopProvider.invalidateContent(moduleId, courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether the module can be downloaded. The promise should never be rejected.
|
||||||
|
*/
|
||||||
|
isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
|
||||||
|
return this.workshopProvider.getWorkshop(courseId, module.id, undefined, true).then((workshop) => {
|
||||||
|
return this.workshopProvider.getWorkshopAccessInformation(workshop.id).then((accessData) => {
|
||||||
|
// Check if workshop is setup by phase.
|
||||||
|
return accessData.canswitchphase || workshop.phase > AddonModWorkshopProvider.PHASE_SETUP;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.workshopProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
|
||||||
|
return this.prefetchPackage(module, courseId, single, this.prefetchWorkshop.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all the grades reports for all the groups and then returns only unique grades.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {any[]} groups Array of groups in the activity.
|
||||||
|
* @param {string} siteId Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} All unique entries.
|
||||||
|
*/
|
||||||
|
protected getAllGradesReport(workshopId: number, groups: any[], siteId: string): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
groups.forEach((group) => {
|
||||||
|
promises.push(this.workshopProvider.fetchAllGradeReports(
|
||||||
|
workshopId, group.id, undefined, false, false, siteId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then((grades) => {
|
||||||
|
const uniqueGrades = {};
|
||||||
|
|
||||||
|
grades.forEach((groupGrades) => {
|
||||||
|
groupGrades.forEach((grade) => {
|
||||||
|
if (grade.submissionid) {
|
||||||
|
uniqueGrades[grade.submissionid] = grade;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueGrades;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a workshop.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected prefetchWorkshop(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||||
|
const userIds = [];
|
||||||
|
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const currentUserId = site.getUserId();
|
||||||
|
|
||||||
|
// Prefetch the workshop data.
|
||||||
|
return this.getWorkshopInfoHelper(module, courseId, false, false, true, siteId).then((info) => {
|
||||||
|
const workshop = info.workshop,
|
||||||
|
promises = [],
|
||||||
|
assessments = [];
|
||||||
|
|
||||||
|
promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id));
|
||||||
|
promises.push(this.workshopProvider.getWorkshopAccessInformation(workshop.id, false, true, siteId)
|
||||||
|
.then((access) => {
|
||||||
|
return this.workshopProvider.getUserPlanPhases(workshop.id, false, true, siteId).then((phases) => {
|
||||||
|
|
||||||
|
// Get submission phase info.
|
||||||
|
const submissionPhase = phases[AddonModWorkshopProvider.PHASE_SUBMISSION],
|
||||||
|
canSubmit = this.workshopHelper.canSubmit(workshop, access, submissionPhase.tasks),
|
||||||
|
canAssess = this.workshopHelper.canAssess(workshop, access),
|
||||||
|
promises2 = [];
|
||||||
|
|
||||||
|
if (canSubmit) {
|
||||||
|
promises2.push(this.workshopProvider.getSubmissions(workshop.id));
|
||||||
|
// Add userId to the profiles to prefetch.
|
||||||
|
userIds.push(currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let reportPromise = Promise.resolve();
|
||||||
|
if (access.canviewallsubmissions && workshop.phase >= AddonModWorkshopProvider.PHASE_SUBMISSION) {
|
||||||
|
reportPromise = this.getAllGradesReport(workshop.id, info.groups, siteId)
|
||||||
|
.then((grades) => {
|
||||||
|
grades.forEach((grade) => {
|
||||||
|
userIds.push(grade.userid);
|
||||||
|
userIds.push(grade.gradeoverby);
|
||||||
|
|
||||||
|
grade.reviewedby.forEach((assessment) => {
|
||||||
|
userIds.push(assessment.userid);
|
||||||
|
userIds.push(assessment.gradinggradeoverby);
|
||||||
|
assessments[assessment.assessmentid] = assessment;
|
||||||
|
});
|
||||||
|
|
||||||
|
grade.reviewerof.forEach((assessment) => {
|
||||||
|
userIds.push(assessment.userid);
|
||||||
|
userIds.push(assessment.gradinggradeoverby);
|
||||||
|
assessments[assessment.assessmentid] = assessment;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workshop.phase >= AddonModWorkshopProvider.PHASE_ASSESSMENT && canAssess) {
|
||||||
|
// Wait the report promise to finish to override assessments array if needed.
|
||||||
|
reportPromise = reportPromise.finally(() => {
|
||||||
|
return this.workshopHelper.getReviewerAssessments(workshop.id, currentUserId, undefined,
|
||||||
|
undefined, siteId).then((revAssessments) => {
|
||||||
|
let p = Promise.resolve();
|
||||||
|
revAssessments.forEach((assessment) => {
|
||||||
|
if (assessment.submission.authorid == currentUserId) {
|
||||||
|
p = this.workshopProvider.getAssessment(workshop.id, assessment.id);
|
||||||
|
}
|
||||||
|
userIds.push(assessment.reviewerid);
|
||||||
|
userIds.push(assessment.gradinggradeoverby);
|
||||||
|
assessments[assessment.id] = assessment;
|
||||||
|
});
|
||||||
|
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assessments.length > 0) {
|
||||||
|
reportPromise = reportPromise.finally(() => {
|
||||||
|
const promises3 = [];
|
||||||
|
assessments.forEach((assessment, id) => {
|
||||||
|
promises3.push(this.workshopProvider.getAssessmentForm(workshop.id, id, undefined, undefined,
|
||||||
|
undefined, siteId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
promises2.push(reportPromise);
|
||||||
|
|
||||||
|
if (workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) {
|
||||||
|
promises2.push(this.workshopProvider.getGrades(workshop.id));
|
||||||
|
if (access.canviewpublishedsubmissions) {
|
||||||
|
promises2.push(this.workshopProvider.getSubmissions(workshop.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises2);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
// Add Basic Info to manage links.
|
||||||
|
promises.push(this.courseProvider.getModuleBasicInfoByInstance(workshop.id, 'workshop', siteId));
|
||||||
|
promises.push(this.courseProvider.getModuleBasicGradeInfo(module.id, siteId));
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
// Prefetch user profiles.
|
||||||
|
return this.userProvider.prefetchProfiles(userIds, courseId, siteId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 '@providers/cron';
|
||||||
|
import { AddonModWorkshopSyncProvider } from './sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization cron handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopSyncCronHandler implements CoreCronHandler {
|
||||||
|
name = 'AddonModWorkshopSyncCronHandler';
|
||||||
|
|
||||||
|
constructor(private workshopSync: AddonModWorkshopSyncProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] ID of the site affected, undefined for all sites.
|
||||||
|
* @return {Promise<any>} Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
execute(siteId?: string): Promise<any> {
|
||||||
|
return this.workshopSync.syncAllWorkshops(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return {number} Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return AddonModWorkshopSyncProvider.SYNC_TIME;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,565 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { AddonModWorkshopProvider } from './workshop';
|
||||||
|
import { AddonModWorkshopHelperProvider } from './helper';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from './offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to sync workshops.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider {
|
||||||
|
|
||||||
|
static AUTO_SYNCED = 'addon_mod_workshop_autom_synced';
|
||||||
|
static MANUAL_SYNCED = 'addon_mod_workshop_manual_synced';
|
||||||
|
static SYNC_TIME = 300000;
|
||||||
|
|
||||||
|
protected componentTranslate: string;
|
||||||
|
|
||||||
|
constructor(translate: TranslateService,
|
||||||
|
appProvider: CoreAppProvider,
|
||||||
|
courseProvider: CoreCourseProvider,
|
||||||
|
private eventsProvider: CoreEventsProvider,
|
||||||
|
loggerProvider: CoreLoggerProvider,
|
||||||
|
sitesProvider: CoreSitesProvider,
|
||||||
|
syncProvider: CoreSyncProvider,
|
||||||
|
textUtils: CoreTextUtilsProvider,
|
||||||
|
private utils: CoreUtilsProvider,
|
||||||
|
private workshopProvider: AddonModWorkshopProvider,
|
||||||
|
private workshopHelper: AddonModWorkshopHelperProvider,
|
||||||
|
private workshopOffline: AddonModWorkshopOfflineProvider) {
|
||||||
|
|
||||||
|
super('AddonModWorkshopSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||||
|
|
||||||
|
this.componentTranslate = courseProvider.translateModuleName('workshop');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an workshop has data to synchronize.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with boolean: true if has data to sync, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataToSync(workshopId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.workshopOffline.hasWorkshopOfflineData(workshopId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to synchronize all workshops that need it and haven't been synchronized in a while.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @return {Promise<any>} Promise resolved when the sync is done.
|
||||||
|
*/
|
||||||
|
syncAllWorkshops(siteId?: string): Promise<any> {
|
||||||
|
return this.syncOnSites('all workshops', this.syncAllWorkshopsFunc.bind(this), [], siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all workshops on a site.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected syncAllWorkshopsFunc(siteId?: string): Promise<any> {
|
||||||
|
return this.workshopOffline.getAllWorkshops(siteId).then((workshopIds) => {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
// Sync all workshops that haven't been synced for a while.
|
||||||
|
workshopIds.forEach((workshopId) => {
|
||||||
|
promises.push(this.syncWorkshopIfNeeded(workshopId, siteId).then((data) => {
|
||||||
|
if (data && data.updated) {
|
||||||
|
// Sync done. Send event.
|
||||||
|
this.eventsProvider.trigger(AddonModWorkshopSyncProvider.AUTO_SYNCED, {
|
||||||
|
workshopId: workshopId,
|
||||||
|
warnings: data.warnings
|
||||||
|
}, siteId);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a workshop only if a certain time has passed since the last time.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the workshop is synced or if it doesn't need to be synced.
|
||||||
|
*/
|
||||||
|
syncWorkshopIfNeeded(workshopId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.isSyncNeeded(workshopId, siteId).then((needed) => {
|
||||||
|
if (needed) {
|
||||||
|
return this.syncWorkshop(workshopId, siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to synchronize a workshop.
|
||||||
|
*
|
||||||
|
* @param {number} workshopId Workshop ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
syncWorkshop(workshopId: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.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 (this.syncProvider.isBlocked(AddonModWorkshopProvider.COMPONENT, workshopId, siteId)) {
|
||||||
|
this.logger.debug('Cannot sync workshop ' + workshopId + ' because it is blocked.');
|
||||||
|
|
||||||
|
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug('Try to sync workshop ' + workshopId);
|
||||||
|
|
||||||
|
const syncPromises = [];
|
||||||
|
|
||||||
|
// Get offline submissions to be sent.
|
||||||
|
syncPromises.push(this.workshopOffline.getSubmissions(workshopId, siteId).catch(() => {
|
||||||
|
// No offline data found, return empty array.
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get offline submission assessments to be sent.
|
||||||
|
syncPromises.push(this.workshopOffline.getAssessments(workshopId, siteId).catch(() => {
|
||||||
|
// No offline data found, return empty array.
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get offline submission evaluations to be sent.
|
||||||
|
syncPromises.push(this.workshopOffline.getEvaluateSubmissions(workshopId, siteId).catch(() => {
|
||||||
|
// No offline data found, return empty array.
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get offline assessment evaluations to be sent.
|
||||||
|
syncPromises.push(this.workshopOffline.getEvaluateAssessments(workshopId, siteId).catch(() => {
|
||||||
|
// No offline data found, return empty array.
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
warnings: [],
|
||||||
|
updated: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get offline submissions to be sent.
|
||||||
|
const syncPromise = Promise.all(syncPromises).then((syncs) => {
|
||||||
|
let courseId;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Nothing to sync.
|
||||||
|
return;
|
||||||
|
} else if (!this.appProvider.isOnline()) {
|
||||||
|
// Cannot sync in offline.
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.workshopProvider.getWorkshopById(courseId, workshopId, siteId).then((workshop) => {
|
||||||
|
const submissionsActions = syncs[0],
|
||||||
|
assessments = syncs[1],
|
||||||
|
submissionEvaluations = syncs[2],
|
||||||
|
assessmentEvaluations = syncs[3],
|
||||||
|
promises = [],
|
||||||
|
offlineSubmissions = {};
|
||||||
|
|
||||||
|
submissionsActions.forEach((action) => {
|
||||||
|
if (typeof offlineSubmissions[action.submissionid] == 'undefined') {
|
||||||
|
offlineSubmissions[action.submissionid] = [];
|
||||||
|
}
|
||||||
|
offlineSubmissions[action.submissionid].push(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(offlineSubmissions).forEach((submissionId) => {
|
||||||
|
const submissionActions = offlineSubmissions[submissionId];
|
||||||
|
promises.push(this.syncSubmission(workshop, submissionActions, result, siteId).then(() => {
|
||||||
|
result.updated = true;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
assessments.forEach((assessment) => {
|
||||||
|
promises.push(this.syncAssessment(workshop, assessment, result, siteId).then(() => {
|
||||||
|
result.updated = true;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
submissionEvaluations.forEach((evaluation) => {
|
||||||
|
promises.push(this.syncEvaluateSubmission(workshop, evaluation, result, siteId).then(() => {
|
||||||
|
result.updated = true;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
assessmentEvaluations.forEach((evaluation) => {
|
||||||
|
promises.push(this.syncEvaluateAssessment(workshop, evaluation, result, siteId).then(() => {
|
||||||
|
result.updated = true;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).then(() => {
|
||||||
|
if (result.updated) {
|
||||||
|
// Data has been sent to server. Now invalidate the WS calls.
|
||||||
|
return this.workshopProvider.invalidateContentById(workshopId, courseId, siteId).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
// Sync finished, set sync time.
|
||||||
|
return this.setSyncTime(workshopId, siteId).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
// All done, return the warnings.
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.addOngoingSync(workshopId, syncPromise, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a submission.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop.
|
||||||
|
* @param {any[]} submissionActions Submission actions offline data.
|
||||||
|
* @param {any} result Object with the result of the sync.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected syncSubmission(workshop: any, submissionActions: any, result: any, siteId: string): Promise<any> {
|
||||||
|
let discardError;
|
||||||
|
let editing = false;
|
||||||
|
|
||||||
|
// Sort entries by timemodified.
|
||||||
|
submissionActions = submissionActions.sort((a, b) => {
|
||||||
|
return a.timemodified - b.timemodified;
|
||||||
|
});
|
||||||
|
|
||||||
|
let timePromise = null;
|
||||||
|
let submissionId = submissionActions[0].submissionid;
|
||||||
|
|
||||||
|
if (submissionId > 0) {
|
||||||
|
editing = true;
|
||||||
|
timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => {
|
||||||
|
return submission.timemodified;
|
||||||
|
}).catch(() => {
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
timePromise = Promise.resolve(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timePromise.then((timemodified) => {
|
||||||
|
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 = this.translate.instant('addon.mod_workshop.warningsubmissionmodified');
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteAllSubmissionActions(workshop.id, submissionId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise = Promise.resolve();
|
||||||
|
|
||||||
|
submissionActions.forEach((action) => {
|
||||||
|
promise = promise.then(() => {
|
||||||
|
submissionId = action.submissionid > 0 ? action.submissionid : submissionId;
|
||||||
|
|
||||||
|
let fileProm;
|
||||||
|
// Upload attachments first if any.
|
||||||
|
if (action.attachmentsid) {
|
||||||
|
fileProm = this.workshopHelper.getSubmissionFilesFromOfflineFilesObject(action.attachmentsid, workshop.id,
|
||||||
|
submissionId, editing, siteId).then((files) => {
|
||||||
|
return this.workshopHelper.uploadOrStoreSubmissionFiles(workshop.id, submissionId, files, editing,
|
||||||
|
false, siteId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove all files.
|
||||||
|
fileProm = this.workshopHelper.uploadOrStoreSubmissionFiles(workshop.id, submissionId, [], editing, false,
|
||||||
|
siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileProm.then((attachmentsId) => {
|
||||||
|
// Perform the action.
|
||||||
|
switch (action.action) {
|
||||||
|
case 'add':
|
||||||
|
return this.workshopProvider.addSubmissionOnline(workshop.id, action.title, action.content,
|
||||||
|
attachmentsId, siteId).then((newSubmissionId) => {
|
||||||
|
submissionId = newSubmissionId;
|
||||||
|
});
|
||||||
|
case 'update':
|
||||||
|
return this.workshopProvider.updateSubmissionOnline(submissionId, action.title, action.content,
|
||||||
|
attachmentsId, siteId);
|
||||||
|
case 'delete':
|
||||||
|
return this.workshopProvider.deleteSubmissionOnline(submissionId, siteId);
|
||||||
|
default:
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error && this.utils.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||||
|
discardError = error.message || error.error;
|
||||||
|
} else {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Delete the offline data.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteSubmissionAction(action.workshopid, action.submissionid, action.action,
|
||||||
|
siteId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise.then(() => {
|
||||||
|
if (discardError) {
|
||||||
|
// Submission was discarded, add a warning.
|
||||||
|
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: workshop.name,
|
||||||
|
error: discardError
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.warnings.indexOf(message) == -1) {
|
||||||
|
result.warnings.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize an assessment.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop.
|
||||||
|
* @param {any} assessment Assessment offline data.
|
||||||
|
* @param {any} result Object with the result of the sync.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected syncAssessment(workshop: any, assessmentData: any, result: any, siteId: string): Promise<any> {
|
||||||
|
let discardError;
|
||||||
|
const assessmentId = assessmentData.assessmentid;
|
||||||
|
|
||||||
|
const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => {
|
||||||
|
return assessment.timemodified;
|
||||||
|
}).catch(() => {
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return timePromise.then((timemodified) => {
|
||||||
|
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 = this.translate.instant('addon.mod_workshop.warningassessmentmodified');
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileProm;
|
||||||
|
const inputData = assessmentData.inputdata;
|
||||||
|
|
||||||
|
// Upload attachments first if any.
|
||||||
|
if (inputData.feedbackauthorattachmentsid) {
|
||||||
|
fileProm = this.workshopHelper.getAssessmentFilesFromOfflineFilesObject(inputData.feedbackauthorattachmentsid,
|
||||||
|
workshop.id, assessmentId, siteId).then((files) => {
|
||||||
|
return this.workshopHelper.uploadOrStoreAssessmentFiles(workshop.id, assessmentId, files, false, siteId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove all files.
|
||||||
|
fileProm = this.workshopHelper.uploadOrStoreAssessmentFiles(workshop.id, assessmentId, [], false, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileProm.then((attachmentsId) => {
|
||||||
|
inputData.feedbackauthorattachmentsid = attachmentsId || 0;
|
||||||
|
|
||||||
|
return this.workshopProvider.updateAssessmentOnline(assessmentId, inputData, siteId);
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error && this.utils.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||||
|
discardError = error.message || error.error;
|
||||||
|
} else {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Delete the offline data.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteAssessment(workshop.id, assessmentId, siteId);
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
if (discardError) {
|
||||||
|
// Assessment was discarded, add a warning.
|
||||||
|
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: workshop.name,
|
||||||
|
error: discardError
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.warnings.indexOf(message) == -1) {
|
||||||
|
result.warnings.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a submission evaluation.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop.
|
||||||
|
* @param {any} evaluate Submission evaluation offline data.
|
||||||
|
* @param {any} result Object with the result of the sync.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected syncEvaluateSubmission(workshop: any, evaluate: any, result: any, siteId: string): Promise<any> {
|
||||||
|
let discardError;
|
||||||
|
const submissionId = evaluate.submissionid;
|
||||||
|
|
||||||
|
const timePromise = this.workshopProvider.getSubmission(workshop.id, submissionId, siteId).then((submission) => {
|
||||||
|
return submission.timemodified;
|
||||||
|
}).catch(() => {
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return timePromise.then((timemodified) => {
|
||||||
|
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 = this.translate.instant('addon.mod_workshop.warningsubmissionmodified');
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.workshopProvider.evaluateSubmissionOnline(submissionId, evaluate.feedbacktext, evaluate.published,
|
||||||
|
evaluate.gradeover, siteId).catch((error) => {
|
||||||
|
if (error && this.utils.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||||
|
discardError = error.message || error.error;
|
||||||
|
} else {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
return Promise.reject(error && error.error);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Delete the offline data.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteEvaluateSubmission(workshop.id, submissionId, siteId);
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
if (discardError) {
|
||||||
|
// Assessment was discarded, add a warning.
|
||||||
|
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: workshop.name,
|
||||||
|
error: discardError
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.warnings.indexOf(message) == -1) {
|
||||||
|
result.warnings.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a assessment evaluation.
|
||||||
|
*
|
||||||
|
* @param {any} workshop Workshop.
|
||||||
|
* @param {any} evaluate Assessment evaluation offline data.
|
||||||
|
* @param {any} result Object with the result of the sync.
|
||||||
|
* @param {string} siteId Site ID.
|
||||||
|
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected syncEvaluateAssessment(workshop: any, evaluate: any, result: any, siteId: string): Promise<any> {
|
||||||
|
let discardError;
|
||||||
|
const assessmentId = evaluate.assessmentid;
|
||||||
|
|
||||||
|
const timePromise = this.workshopProvider.getAssessment(workshop.id, assessmentId, siteId).then((assessment) => {
|
||||||
|
return assessment.timemodified;
|
||||||
|
}).catch(() => {
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return timePromise.then((timemodified) => {
|
||||||
|
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 = this.translate.instant('addon.mod_workshop.warningassessmentmodified');
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.workshopProvider.evaluateAssessmentOnline(assessmentId, evaluate.feedbacktext, evaluate.weight,
|
||||||
|
evaluate.gradinggradeover, siteId).catch((error) => {
|
||||||
|
if (error && this.utils.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means it cannot be performed. Discard.
|
||||||
|
discardError = error.message || error.error;
|
||||||
|
} else {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
return Promise.reject(error && error.error);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Delete the offline data.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
return this.workshopOffline.deleteEvaluateAssessment(workshop.id, assessmentId, siteId);
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
if (discardError) {
|
||||||
|
// Assessment was discarded, add a warning.
|
||||||
|
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: workshop.name,
|
||||||
|
error: discardError
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.warnings.indexOf(message) == -1) {
|
||||||
|
result.warnings.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,121 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { CoreCronDelegate } from '@providers/cron';
|
||||||
|
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||||
|
import { AddonModWorkshopAssessmentStrategyModule } from './assessment/assessment.module';
|
||||||
|
import { AddonModWorkshopComponentsModule } from './components/components.module';
|
||||||
|
import { AddonModWorkshopModuleHandler } from './providers/module-handler';
|
||||||
|
import { AddonModWorkshopProvider } from './providers/workshop';
|
||||||
|
import { AddonModWorkshopLinkHandler } from './providers/link-handler';
|
||||||
|
import { AddonModWorkshopOfflineProvider } from './providers/offline';
|
||||||
|
import { AddonModWorkshopSyncProvider } from './providers/sync';
|
||||||
|
import { AddonModWorkshopHelperProvider } from './providers/helper';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from './providers/assessment-strategy-delegate';
|
||||||
|
import { AddonModWorkshopPrefetchHandler } from './providers/prefetch-handler';
|
||||||
|
import { AddonModWorkshopSyncCronHandler } from './providers/sync-cron-handler';
|
||||||
|
import { CoreUpdateManagerProvider } from '@providers/update-manager';
|
||||||
|
|
||||||
|
// List of providers (without handlers).
|
||||||
|
export const ADDON_MOD_WORKSHOP_PROVIDERS: any[] = [
|
||||||
|
AddonModWorkshopProvider,
|
||||||
|
AddonModWorkshopOfflineProvider,
|
||||||
|
AddonModWorkshopSyncProvider,
|
||||||
|
AddonModWorkshopHelperProvider,
|
||||||
|
AddonWorkshopAssessmentStrategyDelegate
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AddonModWorkshopComponentsModule,
|
||||||
|
AddonModWorkshopAssessmentStrategyModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModWorkshopProvider,
|
||||||
|
AddonModWorkshopModuleHandler,
|
||||||
|
AddonModWorkshopLinkHandler,
|
||||||
|
AddonModWorkshopOfflineProvider,
|
||||||
|
AddonModWorkshopSyncProvider,
|
||||||
|
AddonModWorkshopHelperProvider,
|
||||||
|
AddonWorkshopAssessmentStrategyDelegate,
|
||||||
|
AddonModWorkshopPrefetchHandler,
|
||||||
|
AddonModWorkshopSyncCronHandler
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModWorkshopModule {
|
||||||
|
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModWorkshopModuleHandler,
|
||||||
|
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler,
|
||||||
|
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModWorkshopPrefetchHandler,
|
||||||
|
cronDelegate: CoreCronDelegate, syncHandler: AddonModWorkshopSyncCronHandler,
|
||||||
|
updateManager: CoreUpdateManagerProvider) {
|
||||||
|
|
||||||
|
moduleDelegate.registerHandler(moduleHandler);
|
||||||
|
contentLinksDelegate.registerHandler(linkHandler);
|
||||||
|
prefetchDelegate.registerHandler(prefetchHandler);
|
||||||
|
cronDelegate.register(syncHandler);
|
||||||
|
|
||||||
|
// Allow migrating the tables from the old app to the new schema.
|
||||||
|
updateManager.registerSiteTablesMigration([
|
||||||
|
{
|
||||||
|
name: 'mma_mod_workshop_offline_submissions',
|
||||||
|
newName: AddonModWorkshopOfflineProvider.SUBMISSIONS_TABLE,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'attachmentsid',
|
||||||
|
type: 'object'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mma_mod_workshop_offline_assessments',
|
||||||
|
newName: AddonModWorkshopOfflineProvider.ASSESSMENTS_TABLE,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'inputdata',
|
||||||
|
type: 'object'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mma_mod_workshop_offline_evaluate_submissions',
|
||||||
|
newName: AddonModWorkshopOfflineProvider.EVALUATE_SUBMISSIONS_TABLE,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'gradeover',
|
||||||
|
type: 'object'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'published',
|
||||||
|
type: 'boolean'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mma_mod_workshop_offline_evaluate_assessments',
|
||||||
|
newName: AddonModWorkshopOfflineProvider.EVALUATE_ASSESSMENTS_TABLE,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'gradinggradeover',
|
||||||
|
type: 'object'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
addon-mod-workshop-submission,
|
||||||
|
addon-mod-workshop-submission-page,
|
||||||
|
addon-mod-workshop-assessment,
|
||||||
|
addon-mod-workshop-assessment-page {
|
||||||
|
|
||||||
|
p.addon-overriden-grade {
|
||||||
|
color: color($colors, success);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.addon-has-overriden-grade {
|
||||||
|
color: color($colors, danger);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<h2>{{note.userfullname}}</h2>
|
<h2>{{note.userfullname}}</h2>
|
||||||
<p *ngIf="!note.offline" item-end>{{note.lastmodified | coreDateDayOrTime}}</p>
|
<p *ngIf="!note.offline" item-end>{{note.lastmodified | coreDateDayOrTime}}</p>
|
||||||
<p *ngIf="note.offline" item-end><ion-icon name="clock"></ion-icon> {{ 'core.notsent' | translate }}</p>
|
<p *ngIf="note.offline" item-end><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item text-wrap>
|
<ion-item text-wrap>
|
||||||
<core-format-text [clean]="true" [text]="note.content"></core-format-text>
|
<core-format-text [clean]="true" [text]="note.content"></core-format-text>
|
||||||
|
|
|
@ -111,3 +111,11 @@
|
||||||
padding-left: 15px * $i + $item-ios-padding-start;
|
padding-left: 15px * $i + $item-ios-padding-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recover borders on items inside cards.
|
||||||
|
.card-ios.with-borders .item-ios.item-block .item-inner {
|
||||||
|
border-bottom: $hairlines-width solid $list-ios-border-color;
|
||||||
|
}
|
||||||
|
.card-ios.with-borders .item-ios:last-child .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
|
@ -112,3 +112,12 @@
|
||||||
padding-left: 15px * $i + $item-md-padding-start;
|
padding-left: 15px * $i + $item-md-padding-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Recover borders on items inside cards.
|
||||||
|
.card-md.with-borders .item-md.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-md-border-color;
|
||||||
|
}
|
||||||
|
.card-md.with-borders .item-md:last-child .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
|
@ -96,6 +96,7 @@ import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module';
|
||||||
import { AddonModScormModule } from '@addon/mod/scorm/scorm.module';
|
import { AddonModScormModule } from '@addon/mod/scorm/scorm.module';
|
||||||
import { AddonModUrlModule } from '@addon/mod/url/url.module';
|
import { AddonModUrlModule } from '@addon/mod/url/url.module';
|
||||||
import { AddonModSurveyModule } from '@addon/mod/survey/survey.module';
|
import { AddonModSurveyModule } from '@addon/mod/survey/survey.module';
|
||||||
|
import { AddonModWorkshopModule } from '@addon/mod/workshop/workshop.module';
|
||||||
import { AddonModImscpModule } from '@addon/mod/imscp/imscp.module';
|
import { AddonModImscpModule } from '@addon/mod/imscp/imscp.module';
|
||||||
import { AddonModWikiModule } from '@addon/mod/wiki/wiki.module';
|
import { AddonModWikiModule } from '@addon/mod/wiki/wiki.module';
|
||||||
import { AddonMessageOutputModule } from '@addon/messageoutput/messageoutput.module';
|
import { AddonMessageOutputModule } from '@addon/messageoutput/messageoutput.module';
|
||||||
|
@ -202,6 +203,7 @@ export const CORE_PROVIDERS: any[] = [
|
||||||
AddonModScormModule,
|
AddonModScormModule,
|
||||||
AddonModUrlModule,
|
AddonModUrlModule,
|
||||||
AddonModSurveyModule,
|
AddonModSurveyModule,
|
||||||
|
AddonModWorkshopModule,
|
||||||
AddonModImscpModule,
|
AddonModImscpModule,
|
||||||
AddonModWikiModule,
|
AddonModWikiModule,
|
||||||
AddonMessageOutputModule,
|
AddonMessageOutputModule,
|
||||||
|
|
|
@ -47,3 +47,11 @@
|
||||||
padding-left: 15px * $i + $item-wp-padding-start;
|
padding-left: 15px * $i + $item-wp-padding-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recover borders on items inside cards.
|
||||||
|
.card-wp.with-borders .item-wp.item-block .item-inner {
|
||||||
|
border-bottom: 1px solid $list-wp-border-color;
|
||||||
|
}
|
||||||
|
.card-wp.with-borders .item-wp:last-child .item-inner {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
|
@ -108,6 +108,7 @@ import { ADDON_MOD_SCORM_PROVIDERS } from '@addon/mod/scorm/scorm.module';
|
||||||
import { ADDON_MOD_SURVEY_PROVIDERS } from '@addon/mod/survey/survey.module';
|
import { ADDON_MOD_SURVEY_PROVIDERS } from '@addon/mod/survey/survey.module';
|
||||||
import { ADDON_MOD_URL_PROVIDERS } from '@addon/mod/url/url.module';
|
import { ADDON_MOD_URL_PROVIDERS } from '@addon/mod/url/url.module';
|
||||||
import { ADDON_MOD_WIKI_PROVIDERS } from '@addon/mod/wiki/wiki.module';
|
import { ADDON_MOD_WIKI_PROVIDERS } from '@addon/mod/wiki/wiki.module';
|
||||||
|
import { ADDON_MOD_WORKSHOP_PROVIDERS } from '@addon/mod/workshop/workshop.module';
|
||||||
import { ADDON_NOTES_PROVIDERS } from '@addon/notes/notes.module';
|
import { ADDON_NOTES_PROVIDERS } from '@addon/notes/notes.module';
|
||||||
import { ADDON_NOTIFICATIONS_PROVIDERS } from '@addon/notifications/notifications.module';
|
import { ADDON_NOTIFICATIONS_PROVIDERS } from '@addon/notifications/notifications.module';
|
||||||
import { ADDON_PUSHNOTIFICATIONS_PROVIDERS } from '@addon/pushnotifications/pushnotifications.module';
|
import { ADDON_PUSHNOTIFICATIONS_PROVIDERS } from '@addon/pushnotifications/pushnotifications.module';
|
||||||
|
@ -115,6 +116,7 @@ import { ADDON_REMOTETHEMES_PROVIDERS } from '@addon/remotethemes/remotethemes.m
|
||||||
|
|
||||||
// Import some addon modules that define components, directives and pipes. Only import the important ones.
|
// Import some addon modules that define components, directives and pipes. Only import the important ones.
|
||||||
import { AddonModAssignComponentsModule } from '@addon/mod/assign/components/components.module';
|
import { AddonModAssignComponentsModule } from '@addon/mod/assign/components/components.module';
|
||||||
|
import { AddonModWorkshopComponentsModule } from '@addon/mod/workshop/components/components.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to provide functionalities regarding compiling dynamic HTML and Javascript.
|
* Service to provide functionalities regarding compiling dynamic HTML and Javascript.
|
||||||
|
@ -136,6 +138,7 @@ export class CoreCompileProvider {
|
||||||
IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
|
IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
|
||||||
CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
|
CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
|
||||||
CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule, CoreQuestionComponentsModule, AddonModAssignComponentsModule,
|
CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule, CoreQuestionComponentsModule, AddonModAssignComponentsModule,
|
||||||
|
AddonModWorkshopComponentsModule
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(protected injector: Injector, logger: CoreLoggerProvider, compilerFactory: JitCompilerFactory) {
|
constructor(protected injector: Injector, logger: CoreLoggerProvider, compilerFactory: JitCompilerFactory) {
|
||||||
|
@ -222,7 +225,7 @@ export class CoreCompileProvider {
|
||||||
.concat(ADDON_MOD_LESSON_PROVIDERS).concat(ADDON_MOD_LTI_PROVIDERS).concat(ADDON_MOD_PAGE_PROVIDERS)
|
.concat(ADDON_MOD_LESSON_PROVIDERS).concat(ADDON_MOD_LTI_PROVIDERS).concat(ADDON_MOD_PAGE_PROVIDERS)
|
||||||
.concat(ADDON_MOD_QUIZ_PROVIDERS).concat(ADDON_MOD_RESOURCE_PROVIDERS).concat(ADDON_MOD_SCORM_PROVIDERS)
|
.concat(ADDON_MOD_QUIZ_PROVIDERS).concat(ADDON_MOD_RESOURCE_PROVIDERS).concat(ADDON_MOD_SCORM_PROVIDERS)
|
||||||
.concat(ADDON_MOD_SURVEY_PROVIDERS).concat(ADDON_MOD_URL_PROVIDERS).concat(ADDON_MOD_WIKI_PROVIDERS)
|
.concat(ADDON_MOD_SURVEY_PROVIDERS).concat(ADDON_MOD_URL_PROVIDERS).concat(ADDON_MOD_WIKI_PROVIDERS)
|
||||||
.concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS)
|
.concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS)
|
||||||
.concat(ADDON_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS);
|
.concat(ADDON_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS);
|
||||||
|
|
||||||
// We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
|
// We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
import { CoreGradesProvider } from './grades';
|
import { CoreGradesProvider } from './grades';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,7 +33,7 @@ export class CoreGradesHelperProvider {
|
||||||
constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider,
|
constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider,
|
||||||
private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider,
|
private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider,
|
||||||
private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider,
|
private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider,
|
||||||
private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider) {
|
private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider) {
|
||||||
this.logger = logger.getInstance('CoreGradesHelperProvider');
|
this.logger = logger.getInstance('CoreGradesHelperProvider');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,4 +447,55 @@ export class CoreGradesHelperProvider {
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array that represents all the current grades that can be chosen using the given grading type.
|
||||||
|
* Negative numbers are scales, zero is no grade, and positive numbers are maximum grades.
|
||||||
|
*
|
||||||
|
* Taken from make_grades_menu on moodlelib.php
|
||||||
|
*
|
||||||
|
* @param {number} gradingType If positive, max grade you can provide. If negative, scale Id.
|
||||||
|
* @param {number} moduleId Module Id needed to retrieve the scale.
|
||||||
|
* @param {string} [defaultLabel] Element that will become default option, if not defined, it won't be added.
|
||||||
|
* @param {any} [defaultValue] Element that will become default option value. Default ''.
|
||||||
|
* @param {string} [scale] Scale csv list String. If not provided, it will take it from the module grade info.
|
||||||
|
* @return {Promise<any[]>} Array with objects with value and label to create a propper HTML select.
|
||||||
|
*/
|
||||||
|
makeGradesMenu(gradingType: number, moduleId: number, defaultLabel: string = '', defaultValue: any = '', scale?: string):
|
||||||
|
Promise<any[]> {
|
||||||
|
if (gradingType < 0) {
|
||||||
|
if (scale) {
|
||||||
|
return Promise.resolve(this.utils.makeMenuFromList(scale, defaultLabel, undefined, defaultValue));
|
||||||
|
} else {
|
||||||
|
return this.courseProvider.getModuleBasicGradeInfo(moduleId).then((gradeInfo) => {
|
||||||
|
if (gradeInfo.scale) {
|
||||||
|
return this.utils.makeMenuFromList(gradeInfo.scale, defaultLabel, undefined, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gradingType > 0) {
|
||||||
|
const grades = [];
|
||||||
|
if (defaultLabel) {
|
||||||
|
// Key as string to avoid resorting of the object.
|
||||||
|
grades.push({
|
||||||
|
label: defaultLabel,
|
||||||
|
value: defaultValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = gradingType; i >= 0; i--) {
|
||||||
|
grades.push({
|
||||||
|
label: i + ' / ' + gradingType,
|
||||||
|
value: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(grades);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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 { Injector } from '@angular/core';
|
||||||
|
import { AddonWorkshopAssessmentStrategyHandler } from '@addon/mod/workshop/providers/assessment-strategy-delegate';
|
||||||
|
import {
|
||||||
|
CoreSitePluginsWorkshopAssessmentStrategyComponent
|
||||||
|
} from '../../components/workshop-assessment-strategy/workshop-assessment-strategy';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to display a workshop assessment strategy site plugin.
|
||||||
|
*/
|
||||||
|
export class CoreSitePluginsWorkshopAssessmentStrategyHandler implements AddonWorkshopAssessmentStrategyHandler {
|
||||||
|
|
||||||
|
constructor(public name: string, public strategyName: string) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the plugin data, either in read or in edit mode.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @param {Injector} injector Injector.
|
||||||
|
* @param {any} plugin The plugin object.
|
||||||
|
* @param {boolean} [edit] Whether the user is editing.
|
||||||
|
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getComponent(injector: Injector): any | Promise<any> {
|
||||||
|
return CoreSitePluginsWorkshopAssessmentStrategyComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare original values to be shown and compared.
|
||||||
|
*
|
||||||
|
* @param {any} form Original data of the form.
|
||||||
|
* @param {number} workshopId WorkShop Id
|
||||||
|
* @return {Promise<any[]>} Promise resolved with original values sorted.
|
||||||
|
*/
|
||||||
|
getOriginalValues(form: any, workshopId: number): Promise<any[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the assessment data has changed for a certain submission and workshop for a this strategy plugin.
|
||||||
|
*
|
||||||
|
* @param {any[]} originalValues Original values of the form.
|
||||||
|
* @param {any[]} currentValues Current values of the form.
|
||||||
|
* @return {boolean} True if data has changed, false otherwise.
|
||||||
|
*/
|
||||||
|
hasDataChanged(originalValues: any[], currentValues: any[]): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare assessment data to be sent to the server depending on the strategy selected.
|
||||||
|
*
|
||||||
|
* @param {any{}} currentValues Current values of the form.
|
||||||
|
* @param {any} form Assessment form data.
|
||||||
|
* @return {Promise<any>} Promise resolved with the data to be sent. Or rejected with the input errors object.
|
||||||
|
*/
|
||||||
|
prepareAssessmentData(currentValues: any[], form: any): Promise<any> {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<core-compile-html [text]="content" [jsData]="jsData" (created)="componentCreated($event)"></core-compile-html>
|
|
@ -0,0 +1,54 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// 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, Input } from '@angular/core';
|
||||||
|
import { CoreSitePluginsProvider } from '../../providers/siteplugins';
|
||||||
|
import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a workshop assessment strategy plugin created using a site plugin.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-siteplugins-workshop-assessment-strategy',
|
||||||
|
templateUrl: 'core-siteplugins-workshop-assessment-strategy.html',
|
||||||
|
})
|
||||||
|
export class CoreSitePluginsWorkshopAssessmentStrategyComponent extends CoreSitePluginsCompileInitComponent implements OnInit {
|
||||||
|
@Input() workshopId: number;
|
||||||
|
@Input() assessment: any;
|
||||||
|
@Input() edit: boolean;
|
||||||
|
@Input() selectedValues: any[];
|
||||||
|
@Input() fieldErrors: any;
|
||||||
|
@Input() strategy: string;
|
||||||
|
|
||||||
|
constructor(sitePluginsProvider: CoreSitePluginsProvider) {
|
||||||
|
super(sitePluginsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Pass the input and output data to the component.
|
||||||
|
this.jsData = {
|
||||||
|
workshopId: this.workshopId,
|
||||||
|
assessment: this.assessment,
|
||||||
|
edit: this.edit,
|
||||||
|
selectedValues: this.selectedValues,
|
||||||
|
fieldErrors: this.fieldErrors,
|
||||||
|
strategy: this.strategy
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getHandlerData('workshopform_' + this.strategy);
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,6 +43,7 @@ import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/deleg
|
||||||
import { AddonModQuizAccessRuleDelegate } from '@addon/mod/quiz/providers/access-rules-delegate';
|
import { AddonModQuizAccessRuleDelegate } from '@addon/mod/quiz/providers/access-rules-delegate';
|
||||||
import { AddonModAssignFeedbackDelegate } from '@addon/mod/assign/providers/feedback-delegate';
|
import { AddonModAssignFeedbackDelegate } from '@addon/mod/assign/providers/feedback-delegate';
|
||||||
import { AddonModAssignSubmissionDelegate } from '@addon/mod/assign/providers/submission-delegate';
|
import { AddonModAssignSubmissionDelegate } from '@addon/mod/assign/providers/submission-delegate';
|
||||||
|
import { AddonWorkshopAssessmentStrategyDelegate } from '@addon/mod/workshop/providers/assessment-strategy-delegate';
|
||||||
|
|
||||||
// Handler classes.
|
// Handler classes.
|
||||||
import { CoreSitePluginsCourseFormatHandler } from '../classes/handlers/course-format-handler';
|
import { CoreSitePluginsCourseFormatHandler } from '../classes/handlers/course-format-handler';
|
||||||
|
@ -59,6 +60,7 @@ import { CoreSitePluginsMessageOutputHandler } from '../classes/handlers/message
|
||||||
import { CoreSitePluginsQuizAccessRuleHandler } from '../classes/handlers/quiz-access-rule-handler';
|
import { CoreSitePluginsQuizAccessRuleHandler } from '../classes/handlers/quiz-access-rule-handler';
|
||||||
import { CoreSitePluginsAssignFeedbackHandler } from '../classes/handlers/assign-feedback-handler';
|
import { CoreSitePluginsAssignFeedbackHandler } from '../classes/handlers/assign-feedback-handler';
|
||||||
import { CoreSitePluginsAssignSubmissionHandler } from '../classes/handlers/assign-submission-handler';
|
import { CoreSitePluginsAssignSubmissionHandler } from '../classes/handlers/assign-submission-handler';
|
||||||
|
import { CoreSitePluginsWorkshopAssessmentStrategyHandler } from '../classes/handlers/workshop-assessment-strategy-handler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site
|
* Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site
|
||||||
|
@ -85,7 +87,8 @@ export class CoreSitePluginsHelperProvider {
|
||||||
private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider,
|
private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider,
|
||||||
private messageOutputDelegate: AddonMessageOutputDelegate, private accessRulesDelegate: AddonModQuizAccessRuleDelegate,
|
private messageOutputDelegate: AddonMessageOutputDelegate, private accessRulesDelegate: AddonModQuizAccessRuleDelegate,
|
||||||
private assignSubmissionDelegate: AddonModAssignSubmissionDelegate, private translate: TranslateService,
|
private assignSubmissionDelegate: AddonModAssignSubmissionDelegate, private translate: TranslateService,
|
||||||
private assignFeedbackDelegate: AddonModAssignFeedbackDelegate) {
|
private assignFeedbackDelegate: AddonModAssignFeedbackDelegate,
|
||||||
|
private workshopAssessmentStrategyDelegate: AddonWorkshopAssessmentStrategyDelegate) {
|
||||||
|
|
||||||
this.logger = logger.getInstance('CoreSitePluginsHelperProvider');
|
this.logger = logger.getInstance('CoreSitePluginsHelperProvider');
|
||||||
|
|
||||||
|
@ -483,6 +486,10 @@ export class CoreSitePluginsHelperProvider {
|
||||||
promise = Promise.resolve(this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema));
|
promise = Promise.resolve(this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'AddonWorkshopAssessmentStrategyDelegate':
|
||||||
|
promise = Promise.resolve(this.registerWorkshopAssessmentStrategyHandler(plugin, handlerName, handlerSchema));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
promise = Promise.resolve();
|
promise = Promise.resolve();
|
||||||
|
@ -864,4 +871,24 @@ export class CoreSitePluginsHelperProvider {
|
||||||
return new CoreSitePluginsUserProfileFieldHandler(uniqueName, fieldType);
|
return new CoreSitePluginsUserProfileFieldHandler(uniqueName, fieldType);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a handler in a plugin, register it in the workshop assessment strategy delegate.
|
||||||
|
*
|
||||||
|
* @param {any} plugin Data of the plugin.
|
||||||
|
* @param {string} handlerName Name of the handler in the plugin.
|
||||||
|
* @param {any} handlerSchema Data about the handler.
|
||||||
|
* @return {string|Promise<string>} A string (or a promise resolved with a string) to identify the handler.
|
||||||
|
*/
|
||||||
|
protected registerWorkshopAssessmentStrategyHandler(plugin: any, handlerName: string, handlerSchema: any)
|
||||||
|
: string | Promise<string> {
|
||||||
|
|
||||||
|
return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.workshopAssessmentStrategyDelegate,
|
||||||
|
(uniqueName: string, result: any) => {
|
||||||
|
|
||||||
|
const strategyName = plugin.component.replace('workshopform_', '');
|
||||||
|
|
||||||
|
return new CoreSitePluginsWorkshopAssessmentStrategyHandler(uniqueName, strategyName);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,11 @@ export class CoreAutoRowsDirective {
|
||||||
/**
|
/**
|
||||||
* Resize after content.
|
* Resize after content.
|
||||||
*/
|
*/
|
||||||
ngAfterViewContent(): void {
|
ngAfterViewInit(): void {
|
||||||
this.resize();
|
// Wait for rendering of child views.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.resize();
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue