MOBILE-2354 workshop: Index page

main
Pau Ferrer Ocaña 2018-05-30 16:03:36 +02:00 committed by Albert Gasset
parent 3652d0591d
commit 0591c35a0b
17 changed files with 1146 additions and 16 deletions

View File

@ -0,0 +1,45 @@
// (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 { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModWorkshopIndexComponent } from './index/index';
@NgModule({
declarations: [
AddonModWorkshopIndexComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonModWorkshopIndexComponent
],
entryComponents: [
AddonModWorkshopIndexComponent
]
})
export class AddonModWorkshopComponentsModule {}

View File

@ -0,0 +1,176 @@
<!-- 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 *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 *ngIf="phases && phases[selectedPhase] && phases[selectedPhase].tasks.length">
<ion-item text-wrap *ngFor="let task of phases[selectedPhase].tasks" [ngClass]="{'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 *ngIf="canSubmit">
<ion-item text-wrap *ngIf="!submission">
<h2>{{ 'addon.mod_workshop.yoursubmission' | translate }}</h2>
<p>{{ 'addon.mod_workshop.noyoursubmission' | translate }}</p>
</ion-item>
<!-- <addon-mod-workshop-submission *ngIf="submission" [submission]="submission" [courseId]="workshop.course" [module]="module" [workshop]="workshop" [access]="access"></addon-mod-workshop-submission> -->
</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 *ngIf="canAssess && assessments.length">
<ion-item text-wrap>
<h2>{{ 'addon.mod_workshop.assignedassessments' | translate }}</h2>
</ion-item>
<!-- <addon-mod-workshop-submission *ngFor="let assessment of assessments" [submission]="assessment.submission" [assessment]="assessment" [courseId]="workshop.course" [module]="module" [workshop]="workshop" [access]="access" summary="true"></addon-mod-workshop-submission> -->
</ion-card >
</ng-container>
<ion-card *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 *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 *ngIf="publishedSubmissions.length">
<ion-item text-wrap>
<h2>{{ 'addon.mod_workshop.publishedsubmissions' | translate }}</h2>
</ion-item>
<!-- <addon-mod-workshop-submission *ngFor="let submission of publishedSubmissions" [submission]="submission" [courseId]="workshop.course" [module]="module" [workshop]="workshop" [access]="access" summary="true"></addon-mod-workshop-submission> -->
</ion-card>
</ng-container>
<!-- MULTIPLE PHASES SUBMISSION OR GREATER only teachers -->
<ion-card *ngIf="workshop.phase == selectedPhase && access.canviewallsubmissions && selectedPhase >= workshopPhases.PHASE_SUBMISSION && 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>
<!-- <addon-mod-workshop-submission *ngFor="submission of grades" [submission]="submission" [courseId]="workshop.course" [module]="module" [workshop]="workshop" [access]="access" summary="true"></addon-mod-workshop-submission>-->
<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,485 @@
// (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: '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;
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.content && this.content.scrollToTop();
this.loaded = false;
this.refreshContent(true, false);
});
}
/**
* 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);
});
});
}
/**
* 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 === module.id) {
this.content && this.content.scrollToTop();
this.loaded = false;
this.refreshContent(true, false);
// 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;
}
}
});
}
return Promise.resolve();
}).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: module,
access: this.access,
courseId: this.courseId,
submission: this.submission
};
if (this.submission.id) {
params['submissionId'] = 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,60 @@
{
"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.",
"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,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

@ -268,7 +268,7 @@ export class AddonModWorkshopHelperProvider {
}
/**
* Get a list of stored attachment files for a submission. See $mmaModWorkshopHelper#storeFiles.
* 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.
@ -286,7 +286,7 @@ export class AddonModWorkshopHelperProvider {
}
/**
* Get a list of stored attachment files for a submission and online files also. See $mmaModWorkshopHelper#storeFiles.
* 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.
@ -355,7 +355,7 @@ export class AddonModWorkshopHelperProvider {
}
/**
* Get a list of stored attachment files for an assessment. See $mmaModWorkshopHelper#storeFiles.
* Get a list of stored attachment files for an assessment. See AddonModWorkshopHelperProvider#storeFiles.
*
* @param {number} workshopId Workshop ID.
* @param {number} assessmentId Assessment ID.
@ -372,7 +372,7 @@ export class AddonModWorkshopHelperProvider {
}
/**
* Get a list of stored attachment files for an assessment and online files also. See $mmaModWorkshopHelper#storeFiles.
* 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.

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

@ -418,7 +418,7 @@ export class AddonModWorkshopOfflineProvider {
getAllAssessments(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(this.ASSESSMENTS_TABLE).then((records) => {
records.forEach(this.parseAssessnentRecord.bind(this));
records.forEach(this.parseAssessmentRecord.bind(this));
return records;
});
@ -439,7 +439,7 @@ export class AddonModWorkshopOfflineProvider {
};
return site.getDb().getRecords(this.ASSESSMENTS_TABLE, conditions).then((records) => {
records.forEach(this.parseAssessnentRecord.bind(this));
records.forEach(this.parseAssessmentRecord.bind(this));
return records;
});
@ -462,7 +462,7 @@ export class AddonModWorkshopOfflineProvider {
};
return site.getDb().getRecord(this.ASSESSMENTS_TABLE, conditions).then((record) => {
this.parseAssessnentRecord(record);
this.parseAssessmentRecord(record);
return record;
});
@ -498,7 +498,7 @@ export class AddonModWorkshopOfflineProvider {
*
* @param {any} record Assessnent record, modified in place.
*/
protected parseAssessnentRecord(record: any): void {
protected parseAssessmentRecord(record: any): void {
record.inputdata = this.textUtils.parseJSON(record.inputdata);
}

View File

@ -24,7 +24,7 @@ import { AddonModWorkshopOfflineProvider } from './offline';
*/
@Injectable()
export class AddonModWorkshopProvider {
static COMPONENT = 'mmaWorkshopUrl';
static COMPONENT = 'mmaModWorkshop';
static PER_PAGE = 10;
static PHASE_SETUP = 10;
static PHASE_SUBMISSION = 20;
@ -35,6 +35,9 @@ export class AddonModWorkshopProvider {
static EXAMPLES_BEFORE_SUBMISSION: 1;
static EXAMPLES_BEFORE_ASSESSMENT: 2;
static SUBMISSION_CHANGED = 'addon_mod_workshop_submission_changed';
static ASSESSMENT_SAVED = 'addon_mod_workshop_assessment_saved';
protected ROOT_CACHE_KEY = 'mmaModWorkshop:';
constructor(
@ -346,12 +349,7 @@ export class AddonModWorkshopProvider {
return site.read('mod_workshop_get_user_plan', params, preSets).then((response) => {
if (response && response.userplan && response.userplan.phases) {
const phases = {};
response.userplan.phases.forEach((phase) => {
phases[phase.code] = phase;
});
return phases;
return this.utils.arrayToObject(response.userplan.phases, 'code');
}
return Promise.reject(null);
@ -1295,7 +1293,7 @@ export class AddonModWorkshopProvider {
/**
* Invalidate the prefetched content except files.
* To invalidate files, use $mmaModWorkshop#invalidateFiles.
* To invalidate files, use AddonModWorkshopProvider#invalidateFiles.
*
* @param {number} moduleId The module ID.
* @param {number} courseId Course ID.

View File

@ -0,0 +1,49 @@
// (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 { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
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';
@NgModule({
declarations: [
],
imports: [
AddonModWorkshopComponentsModule
],
providers: [
AddonModWorkshopProvider,
AddonModWorkshopModuleHandler,
AddonModWorkshopLinkHandler,
AddonModWorkshopOfflineProvider,
AddonModWorkshopSyncProvider,
AddonModWorkshopHelperProvider,
AddonWorkshopAssessmentStrategyDelegate
]
})
export class AddonModWorkshopModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModWorkshopModuleHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModWorkshopLinkHandler) {
moduleDelegate.registerHandler(moduleHandler);
contentLinksDelegate.registerHandler(linkHandler);
}
}

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,