From 0591c35a0b97937a2fbb6e77a8f90d259f1ef088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 30 May 2018 16:03:36 +0200 Subject: [PATCH] MOBILE-2354 workshop: Index page --- .../workshop/components/components.module.ts | 45 ++ .../mod/workshop/components/index/index.html | 176 +++++++ .../mod/workshop/components/index/index.ts | 485 ++++++++++++++++++ src/addon/mod/workshop/lang/en.json | 60 +++ src/addon/mod/workshop/pages/index/index.html | 16 + .../mod/workshop/pages/index/index.module.ts | 33 ++ src/addon/mod/workshop/pages/index/index.ts | 50 ++ src/addon/mod/workshop/pages/phase/phase.html | 25 + .../mod/workshop/pages/phase/phase.module.ts | 33 ++ src/addon/mod/workshop/pages/phase/phase.ts | 56 ++ src/addon/mod/workshop/providers/helper.ts | 8 +- .../mod/workshop/providers/link-handler.ts | 30 ++ .../mod/workshop/providers/module-handler.ts | 72 +++ src/addon/mod/workshop/providers/offline.ts | 8 +- src/addon/mod/workshop/providers/workshop.ts | 14 +- src/addon/mod/workshop/workshop.module.ts | 49 ++ src/app/app.module.ts | 2 + 17 files changed, 1146 insertions(+), 16 deletions(-) create mode 100644 src/addon/mod/workshop/components/components.module.ts create mode 100644 src/addon/mod/workshop/components/index/index.html create mode 100644 src/addon/mod/workshop/components/index/index.ts create mode 100644 src/addon/mod/workshop/lang/en.json create mode 100644 src/addon/mod/workshop/pages/index/index.html create mode 100644 src/addon/mod/workshop/pages/index/index.module.ts create mode 100644 src/addon/mod/workshop/pages/index/index.ts create mode 100644 src/addon/mod/workshop/pages/phase/phase.html create mode 100644 src/addon/mod/workshop/pages/phase/phase.module.ts create mode 100644 src/addon/mod/workshop/pages/phase/phase.ts create mode 100644 src/addon/mod/workshop/providers/link-handler.ts create mode 100644 src/addon/mod/workshop/providers/module-handler.ts create mode 100644 src/addon/mod/workshop/workshop.module.ts diff --git a/src/addon/mod/workshop/components/components.module.ts b/src/addon/mod/workshop/components/components.module.ts new file mode 100644 index 000000000..6111f4bb4 --- /dev/null +++ b/src/addon/mod/workshop/components/components.module.ts @@ -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 {} diff --git a/src/addon/mod/workshop/components/index/index.html b/src/addon/mod/workshop/components/index/index.html new file mode 100644 index 000000000..15f9ed06c --- /dev/null +++ b/src/addon/mod/workshop/components/index/index.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + +

{{ phases[selectedPhase].title }}

+

{{ 'addon.mod_workshop.userplancurrentphase' | translate }}

+ +
+ + + {{ 'addon.mod_workshop.switchphase' + selectedPhase | translate }} + + +
+ + + + + + + + +

{{task.title}}

+

+ +
+
+ + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ +
+ + + + +

{{ 'addon.mod_workshop.areainstructauthors' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.yoursubmission' | translate }}

+

{{ 'addon.mod_workshop.noyoursubmission' | translate }}

+
+ + +
+ + + + + + + + +
+ + + + + +

{{ 'addon.mod_workshop.areainstructreviewers' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.assignedassessments' | translate }}

+
+ +
+
+ + + +

{{ 'addon.mod_workshop.yoursubmission' | translate }}

+
+ +

{{ 'addon.mod_workshop.assignedassessments' | translate }}

+
+
+ + + + + +

{{ 'addon.mod_workshop.conclusion' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.yourgrades' | translate }}

+
+ +

{{ 'addon.mod_workshop.submissiongrade' | translate }}

+ +
+ +

{{ 'addon.mod_workshop.gradinggrade' | translate }}

+ +
+
+ + + +

{{ 'addon.mod_workshop.publishedsubmissions' | translate }}

+
+ +
+
+ + + + +

{{ 'addon.mod_workshop.submissionsreport' | translate }}

+
+ +

{{ 'addon.mod_workshop.gradesreport' | translate }}

+
+ + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + + + + + + + + + + + + + +
+
+
diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts new file mode 100644 index 000000000..ecbb91bbf --- /dev/null +++ b/src/addon/mod/workshop/components/index/index.ts @@ -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} Resolved when done. + */ + protected invalidateContent(): Promise { + 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} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + 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} Resolved when done. + */ + gotoSubmissionsPage(page: number): Promise { + 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} Promise resolved when done. + */ + setGroup(groupId: number): Promise { + this.group = groupId; + + return this.gotoSubmissionsPage(0); + } + + /** + * Convenience function to set current phase information. + * + * @return {Promise} Promise resolved when done. + */ + protected setPhaseInfo(): Promise { + 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} Promise resolved when done. + */ + protected sync(): Promise { + 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(); + } +} diff --git a/src/addon/mod/workshop/lang/en.json b/src/addon/mod/workshop/lang/en.json new file mode 100644 index 000000000..57243db4a --- /dev/null +++ b/src/addon/mod/workshop/lang/en.json @@ -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" +} diff --git a/src/addon/mod/workshop/pages/index/index.html b/src/addon/mod/workshop/pages/index/index.html new file mode 100644 index 000000000..6a977cfa3 --- /dev/null +++ b/src/addon/mod/workshop/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/workshop/pages/index/index.module.ts b/src/addon/mod/workshop/pages/index/index.module.ts new file mode 100644 index 000000000..b18d9e2ef --- /dev/null +++ b/src/addon/mod/workshop/pages/index/index.module.ts @@ -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 {} diff --git a/src/addon/mod/workshop/pages/index/index.ts b/src/addon/mod/workshop/pages/index/index.ts new file mode 100644 index 000000000..db244a7d1 --- /dev/null +++ b/src/addon/mod/workshop/pages/index/index.ts @@ -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; + } +} diff --git a/src/addon/mod/workshop/pages/phase/phase.html b/src/addon/mod/workshop/pages/phase/phase.html new file mode 100644 index 000000000..a8ff1f37a --- /dev/null +++ b/src/addon/mod/workshop/pages/phase/phase.html @@ -0,0 +1,25 @@ + + + {{ 'addon.mod_workshop.selectphase' | translate }} + + + + + + + + + + {{ phase.title }} +

{{ 'addon.mod_workshop.userplancurrentphase' | translate }}

+
+ +
+ + {{ phase.title }} + +
+
+
diff --git a/src/addon/mod/workshop/pages/phase/phase.module.ts b/src/addon/mod/workshop/pages/phase/phase.module.ts new file mode 100644 index 000000000..2085e6ca9 --- /dev/null +++ b/src/addon/mod/workshop/pages/phase/phase.module.ts @@ -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 {} diff --git a/src/addon/mod/workshop/pages/phase/phase.ts b/src/addon/mod/workshop/pages/phase/phase.ts new file mode 100644 index 000000000..f1fe2fdcc --- /dev/null +++ b/src/addon/mod/workshop/pages/phase/phase.ts @@ -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; + } +} diff --git a/src/addon/mod/workshop/providers/helper.ts b/src/addon/mod/workshop/providers/helper.ts index 48736cd8b..4007b223a 100644 --- a/src/addon/mod/workshop/providers/helper.ts +++ b/src/addon/mod/workshop/providers/helper.ts @@ -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. diff --git a/src/addon/mod/workshop/providers/link-handler.ts b/src/addon/mod/workshop/providers/link-handler.ts new file mode 100644 index 000000000..6c06a0ca6 --- /dev/null +++ b/src/addon/mod/workshop/providers/link-handler.ts @@ -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'); + } +} diff --git a/src/addon/mod/workshop/providers/module-handler.ts b/src/addon/mod/workshop/providers/module-handler.ts new file mode 100644 index 000000000..bb275e5d9 --- /dev/null +++ b/src/addon/mod/workshop/providers/module-handler.ts @@ -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} Whether or not the handler is enabled on a site level. + */ + isEnabled(): Promise { + 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; + } +} diff --git a/src/addon/mod/workshop/providers/offline.ts b/src/addon/mod/workshop/providers/offline.ts index 6ac09becb..c74a1b48e 100644 --- a/src/addon/mod/workshop/providers/offline.ts +++ b/src/addon/mod/workshop/providers/offline.ts @@ -418,7 +418,7 @@ export class AddonModWorkshopOfflineProvider { getAllAssessments(siteId?: string): Promise { 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); } diff --git a/src/addon/mod/workshop/providers/workshop.ts b/src/addon/mod/workshop/providers/workshop.ts index 5ce7d3a79..9962b836d 100644 --- a/src/addon/mod/workshop/providers/workshop.ts +++ b/src/addon/mod/workshop/providers/workshop.ts @@ -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. diff --git a/src/addon/mod/workshop/workshop.module.ts b/src/addon/mod/workshop/workshop.module.ts new file mode 100644 index 000000000..2653be857 --- /dev/null +++ b/src/addon/mod/workshop/workshop.module.ts @@ -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); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8737ec72f..d11018cb9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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,