Merge pull request #1339 from albertgasset/MOBILE-2354

Mobile 2354
main
Juan Leyva 2018-06-15 10:22:20 +02:00 committed by GitHub
commit 286abdbef4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 8792 additions and 16 deletions

View File

@ -293,7 +293,8 @@ var templatesSrc = [
'./src/core/**/components/**/*.html',
'./src/core/**/component/**/*.html',
// 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';

View File

@ -1033,7 +1033,7 @@ export class AddonMessagesProvider {
return this.sendMessagesOnline(messages, siteId).then((response) => {
if (response && response[0] && response[0].msgid === -1) {
// 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(() => {

View File

@ -11,7 +11,7 @@
</button>
</div>
<ion-note *ngIf="!isSent" color="dark">
<ion-icon name="clock"></ion-icon>
<ion-icon name="time"></ion-icon>
{{ 'core.notsent' | translate }}
</ion-note>
</div>

View File

@ -1189,7 +1189,7 @@ export class AddonModFeedbackProvider {
};
return site.write('mod_feedback_process_page', params).catch((error) => {
return this.utils.createFakeWSError(error);
return Promise.reject(this.utils.createFakeWSError(error));
}).then((response) => {
// Invalidate and update current values because they will change.
return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => {

View File

@ -179,7 +179,7 @@ export class AddonModForumProvider {
return site.write('mod_forum_add_discussion', params).then((response) => {
// Other errors ocurring.
if (!response || !response.discussionid) {
return this.utils.createFakeWSError('');
return Promise.reject(this.utils.createFakeWSError(''));
} else {
return response.discussionid;
}
@ -694,7 +694,7 @@ export class AddonModForumProvider {
return site.write('mod_forum_add_discussion_post', params).then((response) => {
if (!response || !response.postid) {
return this.utils.createFakeWSError('');
return Promise.reject(this.utils.createFakeWSError(''));
} else {
return response.postid;
}

View File

@ -812,7 +812,7 @@ export class AddonModGlossaryProvider {
return response.entryid;
}
return this.utils.createFakeWSError('');
return Promise.reject(this.utils.createFakeWSError(''));
});
});
}

View File

@ -268,7 +268,7 @@ export class AddonModSurveyProvider {
return site.write('mod_survey_submit_answers', params).then((response) => {
if (!response.status) {
return this.utils.createFakeWSError('');
return Promise.reject(this.utils.createFakeWSError(''));
}
});
});

View File

@ -482,8 +482,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy {
pageId: this.pageId,
subwikiId: this.subwikiId,
pageTitle: title,
siteId: this.sitesProvider.getCurrentSiteId()
});
}, this.sitesProvider.getCurrentSiteId());
});
} else {
// Page stored in offline. Go to see the offline page.

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
{
"dimensioncommentfor": "Comment for {{$a}}",
"dimensiongradefor": "Grade for {{$a}}",
"dimensionnumber": "Aspect {{$a}}",
"mustchoosegrade": "You have to select a grade for this aspect"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@
</ion-avatar>
<h2>{{note.userfullname}}</h2>
<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 text-wrap>
<core-format-text [clean]="true" [text]="note.content"></core-format-text>

View File

@ -111,3 +111,11 @@
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;
}

View File

@ -112,3 +112,12 @@
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;
}

View File

@ -96,6 +96,7 @@ import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module';
import { AddonModScormModule } from '@addon/mod/scorm/scorm.module';
import { AddonModUrlModule } from '@addon/mod/url/url.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 { AddonModWikiModule } from '@addon/mod/wiki/wiki.module';
import { AddonMessageOutputModule } from '@addon/messageoutput/messageoutput.module';
@ -202,6 +203,7 @@ export const CORE_PROVIDERS: any[] = [
AddonModScormModule,
AddonModUrlModule,
AddonModSurveyModule,
AddonModWorkshopModule,
AddonModImscpModule,
AddonModWikiModule,
AddonMessageOutputModule,

View File

@ -47,3 +47,11 @@
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;
}

View File

@ -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_URL_PROVIDERS } from '@addon/mod/url/url.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_NOTIFICATIONS_PROVIDERS } from '@addon/notifications/notifications.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 { 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.
@ -136,6 +138,7 @@ export class CoreCompileProvider {
IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
CoreCourseDirectivesModule, CoreSitePluginsDirectivesModule, CoreQuestionComponentsModule, AddonModAssignComponentsModule,
AddonModWorkshopComponentsModule
];
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_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_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);
// We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.

View File

@ -20,6 +20,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreGradesProvider } from './grades';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
@ -32,7 +33,7 @@ export class CoreGradesHelperProvider {
constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider,
private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider,
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');
}
@ -446,4 +447,55 @@ export class CoreGradesHelperProvider {
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([]);
}
}

View File

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

View File

@ -0,0 +1 @@
<core-compile-html [text]="content" [jsData]="jsData" (created)="componentCreated($event)"></core-compile-html>

View File

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

View File

@ -43,6 +43,7 @@ import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/deleg
import { AddonModQuizAccessRuleDelegate } from '@addon/mod/quiz/providers/access-rules-delegate';
import { AddonModAssignFeedbackDelegate } from '@addon/mod/assign/providers/feedback-delegate';
import { AddonModAssignSubmissionDelegate } from '@addon/mod/assign/providers/submission-delegate';
import { AddonWorkshopAssessmentStrategyDelegate } from '@addon/mod/workshop/providers/assessment-strategy-delegate';
// Handler classes.
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 { CoreSitePluginsAssignFeedbackHandler } from '../classes/handlers/assign-feedback-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
@ -85,7 +87,8 @@ export class CoreSitePluginsHelperProvider {
private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider,
private messageOutputDelegate: AddonMessageOutputDelegate, private accessRulesDelegate: AddonModQuizAccessRuleDelegate,
private assignSubmissionDelegate: AddonModAssignSubmissionDelegate, private translate: TranslateService,
private assignFeedbackDelegate: AddonModAssignFeedbackDelegate) {
private assignFeedbackDelegate: AddonModAssignFeedbackDelegate,
private workshopAssessmentStrategyDelegate: AddonWorkshopAssessmentStrategyDelegate) {
this.logger = logger.getInstance('CoreSitePluginsHelperProvider');
@ -483,6 +486,10 @@ export class CoreSitePluginsHelperProvider {
promise = Promise.resolve(this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema));
break;
case 'AddonWorkshopAssessmentStrategyDelegate':
promise = Promise.resolve(this.registerWorkshopAssessmentStrategyHandler(plugin, handlerName, handlerSchema));
break;
default:
// Nothing to do.
promise = Promise.resolve();
@ -864,4 +871,24 @@ export class CoreSitePluginsHelperProvider {
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);
});
}
}

View File

@ -47,8 +47,11 @@ export class CoreAutoRowsDirective {
/**
* Resize after content.
*/
ngAfterViewContent(): void {
this.resize();
ngAfterViewInit(): void {
// Wait for rendering of child views.
setTimeout(() => {
this.resize();
}, 300);
}
/**