diff --git a/src/addon/mod/assign/assign.module.ts b/src/addon/mod/assign/assign.module.ts index d3a41cfbf..cb3d922fb 100644 --- a/src/addon/mod/assign/assign.module.ts +++ b/src/addon/mod/assign/assign.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { CoreCronDelegate } from '@providers/cron'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { AddonModAssignProvider } from './providers/assign'; import { AddonModAssignOfflineProvider } from './providers/assign-offline'; @@ -23,6 +24,7 @@ import { AddonModAssignFeedbackDelegate } from './providers/feedback-delegate'; import { AddonModAssignSubmissionDelegate } from './providers/submission-delegate'; import { AddonModAssignDefaultFeedbackHandler } from './providers/default-feedback-handler'; import { AddonModAssignDefaultSubmissionHandler } from './providers/default-submission-handler'; +import { AddonModAssignModuleHandler } from './providers/module-handler'; import { AddonModAssignPrefetchHandler } from './providers/prefetch-handler'; import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler'; @@ -38,13 +40,16 @@ import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler'; AddonModAssignSubmissionDelegate, AddonModAssignDefaultFeedbackHandler, AddonModAssignDefaultSubmissionHandler, + AddonModAssignModuleHandler, AddonModAssignPrefetchHandler, AddonModAssignSyncCronHandler ] }) export class AddonModAssignModule { - constructor(prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModAssignPrefetchHandler, + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModAssignModuleHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModAssignPrefetchHandler, cronDelegate: CoreCronDelegate, syncHandler: AddonModAssignSyncCronHandler) { + moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); cronDelegate.register(syncHandler); } diff --git a/src/addon/mod/assign/components/components.module.ts b/src/addon/mod/assign/components/components.module.ts new file mode 100644 index 000000000..b1911094b --- /dev/null +++ b/src/addon/mod/assign/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 { AddonModAssignIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModAssignIndexComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonModAssignIndexComponent + ], + entryComponents: [ + AddonModAssignIndexComponent + ] +}) +export class AddonModAssignComponentsModule {} diff --git a/src/addon/mod/assign/components/index/index.html b/src/addon/mod/assign/components/index/index.html new file mode 100644 index 000000000..c92079a16 --- /dev/null +++ b/src/addon/mod/assign/components/index/index.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + {{ note }} + + + + + + + + +
+ + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} +
+ + + + +

{{ 'addon.mod_assign.timeremaining' | translate }}

+

{{ timeRemaining }}

+
+ +

{{ 'addon.mod_assign.latesubmissions' | translate }}

+

{{ lateSubmissions }}

+
+ + + +

{{ 'addon.mod_assign.numberofteams' | translate }}

+

{{ 'addon.mod_assign.numberofparticipants' | translate }}

+ + {{ summary.participantcount }} + +
+ + + +

{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}

+ + {{ summary.submissiondraftscount }} + +
+ + + +

{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}

+ + {{ summary.submissionssubmittedcount }} + +
+ + + +

{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}

+ + {{ summary.submissionsneedgradingcount }} + +
+ + +
+ + {{ 'addon.mod_assign.ungroupedusers' | translate }} +
+
+ + + +
diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts new file mode 100644 index 000000000..ea7497ff5 --- /dev/null +++ b/src/addon/mod/assign/components/index/index.ts @@ -0,0 +1,312 @@ +// (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, Optional, Injector } from '@angular/core'; +import { Content, NavController } from 'ionic-angular'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { AddonModAssignProvider } from '../../providers/assign'; +import { AddonModAssignHelperProvider } from '../../providers/helper'; +import { AddonModAssignOfflineProvider } from '../../providers/assign-offline'; +import { AddonModAssignSyncProvider } from '../../providers/assign-sync'; +import * as moment from 'moment'; + +/** + * Component that displays an assignment. + */ +@Component({ + selector: 'addon-mod-assign-index', + templateUrl: 'index.html', +}) +export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent { + component = AddonModAssignProvider.COMPONENT; + moduleName = 'assign'; + + assign: any; // The assign object. + canViewSubmissions: boolean; // Whether the user can view all submissions. + timeRemaining: string; // Message about time remaining to submit. + lateSubmissions: string; // Message about late submissions. + showNumbers = true; // Whether to show number of submissions with each status. + summary: any; // The summary. + needsGradingAvalaible: boolean; // Whether we can see the submissions that need grading. + + // Status. + submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED; + submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT; + needGrading = AddonModAssignProvider.NEED_GRADING; + + protected userId: number; // Current user ID. + protected syncEventName = AddonModAssignSyncProvider.AUTO_SYNCED; + + // Observers. + protected savedObserver; + protected submittedObserver; + protected gradedObserver; + + constructor(injector: Injector, protected assignProvider: AddonModAssignProvider, @Optional() content: Content, + protected assignHelper: AddonModAssignHelperProvider, protected assignOffline: AddonModAssignOfflineProvider, + protected syncProvider: AddonModAssignSyncProvider, protected timeUtils: CoreTimeUtilsProvider, + protected groupsProvider: CoreGroupsProvider, protected navCtrl: NavController) { + super(injector, content); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.userId = this.sitesProvider.getCurrentSiteUserId(); + + this.loadContent(false, true).then(() => { + this.assignProvider.logView(this.assign.id).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }).catch(() => { + // Ignore errors. + }); + + if (!this.canViewSubmissions) { + // User can only see his submission, log view the user submission. + this.assignProvider.logSubmissionView(this.assign.id).catch(() => { + // Ignore errors. + }); + } else { + // User can see all submissions, log grading view. + this.assignProvider.logGradingView(this.assign.id).catch(() => { + // Ignore errors. + }); + } + }); + + // Listen to events. + this.savedObserver = this.eventsProvider.on(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, (data) => { + if (this.assign && data.assignmentId == this.assign.id && data.userId == this.userId) { + // Assignment submission saved, refresh data. + this.showLoadingAndRefresh(true, false); + } + }, this.siteId); + + this.submittedObserver = this.eventsProvider.on(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, (data) => { + if (this.assign && data.assignmentId == this.assign.id && data.userId == this.userId) { + // Assignment submitted, check completion. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + } + }, this.siteId); + + this.gradedObserver = this.eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => { + if (this.assign && data.assignmentId == this.assign.id && data.userId == this.userId) { + // Assignment graded, refresh data. + this.showLoadingAndRefresh(true, false); + } + }, this.siteId); + } + + /** + * Expand the description. + */ + expandDescription(ev?: Event): void { + ev && ev.preventDefault(); + ev && ev.stopPropagation(); + + if (this.assign && (this.description || this.assign.introattachments)) { + this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, + this.module.id, this.assign.introattachments); + } + } + + /** + * Get assignment data. + * + * @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 { + + // Get assignment data. + return this.assignProvider.getAssignment(this.courseId, this.module.id).then((assignData) => { + this.assign = assignData; + + this.dataRetrieved.emit(this.assign); + this.description = this.assign.intro || this.description; + + if (sync) { + // Try to synchronize the assign. + return this.syncActivity(showErrors).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + // Check if there's any offline data for this assign. + return this.assignOffline.hasAssignOfflineData(this.assign.id); + }).then((hasOffline) => { + this.hasOffline = hasOffline; + + // Get assignment submissions. + return this.assignProvider.getSubmissions(this.assign.id).then((data) => { + const time = this.timeUtils.timestamp(); + + this.canViewSubmissions = data.canviewsubmissions; + + if (data.canviewsubmissions) { + + // Calculate the messages to display about time remaining and late submissions. + if (this.assign.duedate > 0) { + if (this.assign.duedate - time <= 0) { + this.timeRemaining = this.translate.instant('addon.mod_assign.assignmentisdue'); + } else { + this.timeRemaining = this.timeUtils.formatDuration(this.assign.duedate - time, 3); + + if (this.assign.cutoffdate) { + if (this.assign.cutoffdate > time) { + const dateFormat = this.translate.instant('core.dfmediumdate'); + + this.lateSubmissions = this.translate.instant('addon.mod_assign.latesubmissionsaccepted', + {$a: moment(this.assign.cutoffdate * 1000).format(dateFormat)}); + } else { + this.lateSubmissions = this.translate.instant('addon.mod_assign.nomoresubmissionsaccepted'); + } + } else { + this.lateSubmissions = ''; + } + } + } else { + this.timeRemaining = ''; + this.lateSubmissions = ''; + } + + // Check if groupmode is enabled to avoid showing wrong numbers. + return this.groupsProvider.activityHasGroups(this.assign.cmid).then((hasGroups) => { + this.showNumbers = !hasGroups; + + return this.assignProvider.getSubmissionStatus(this.assign.id).then((response) => { + this.summary = response.gradingsummary; + + this.needsGradingAvalaible = response.gradingsummary.submissionsneedgradingcount > 0 && + this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.2'); + }); + }); + } + }); + }).then(() => { + // All data obtained, now fill the context menu. + this.fillContextMenu(refresh); + }); + } + + /** + * Go to view a list of submissions. + * + * @param {string} status Status to see. + * @param {number} count Number of submissions with the status. + */ + goToSubmissionList(status: string, count: number): void { + if (typeof status == 'undefined') { + this.navCtrl.push('AddonModAssignSubmissionListPage', { + courseId: this.courseId, + moduleId: this.module.id, + moduleName: this.moduleName + }); + } else if (count || !this.showNumbers) { + this.navCtrl.push('AddonModAssignSubmissionListPage', { + status: status, + courseId: this.courseId, + moduleId: this.module.id, + moduleName: this.moduleName + }); + } + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param {any} result Data returned by the sync function. + * @return {boolean} If succeed or not. + */ + protected hasSyncSucceed(result: any): boolean { + if (result.updated) { + // Sync done, trigger event. + this.eventsProvider.trigger(AddonModAssignSyncProvider.MANUAL_SYNCED, { + assignId: this.assign.id, + warnings: result.warnings + }, this.siteId); + } + + return result.updated; + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + const promises = []; + + promises.push(this.assignProvider.invalidateAssignmentData(this.courseId)); + + if (this.assign) { + promises.push(this.assignProvider.invalidateAllSubmissionData(this.assign.id)); + + if (this.canViewSubmissions) { + promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id)); + } + } + + return Promise.all(promises).finally(() => { + // @todo $scope.$broadcast(mmaModAssignSubmissionInvalidatedEvent); + }); + } + + /** + * 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.assign && syncEventData.assignId == this.assign.id) { + if (syncEventData.warnings && syncEventData.warnings.length) { + // Show warnings. + this.domUtils.showErrorModal(syncEventData.warnings[0]); + } + + return true; + } + + return false; + } + + /** + * Performs the sync of the activity. + * + * @return {Promise} Promise resolved when done. + */ + protected sync(): Promise { + return this.syncProvider.syncAssign(this.assign.id); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.savedObserver && this.savedObserver.off(); + this.submittedObserver && this.submittedObserver.off(); + this.gradedObserver && this.gradedObserver.off(); + } +} diff --git a/src/addon/mod/assign/lang/en.json b/src/addon/mod/assign/lang/en.json new file mode 100644 index 000000000..e04437755 --- /dev/null +++ b/src/addon/mod/assign/lang/en.json @@ -0,0 +1,99 @@ +{ + "acceptsubmissionstatement": "Please accept the submission statement.", + "addattempt": "Allow another attempt", + "addnewattempt": "Add a new attempt", + "addnewattemptfromprevious": "Add a new attempt based on previous submission", + "addsubmission": "Add submission", + "allowsubmissionsfromdate": "Allow submissions from", + "allowsubmissionsfromdatesummary": "This assignment will accept submissions from {{$a}}", + "allowsubmissionsanddescriptionfromdatesummary": "The assignment details and submission form will be available from {{$a}}", + "applytoteam": "Apply grades and feedback to entire group", + "assignmentisdue": "Assignment is due", + "attemptnumber": "Attempt number", + "attemptreopenmethod": "Attempts reopened", + "attemptreopenmethod_manual": "Manually", + "attemptreopenmethod_untilpass": "Automatically until pass", + "attemptsettings": "Attempt settings", + "cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.", + "cannoteditduetostatementsubmission": "You can't add or edit a submission in the app because the submission statement could not be retrieved from the site.", + "cannotsubmitduetostatementsubmission": "You can't make a submission in the app because the submission statement could not be retrieved from the site.", + "confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.", + "currentgrade": "Current grade in gradebook", + "cutoffdate": "Cut-off date", + "currentattempt": "This is attempt {{$a}}.", + "currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).", + "defaultteam": "Default group", + "duedate": "Due date", + "duedateno": "No due date", + "duedatereached": "The due date for this assignment has now passed", + "editingstatus": "Editing status", + "editsubmission": "Edit submission", + "erroreditpluginsnotsupported": "You can't add or edit a submission in the app because certain plugins are not yet supported for editing.", + "errorshowinginformation": "Submission information cannot be displayed.", + "extensionduedate": "Extension due date", + "feedbacknotsupported": "This feedback is not supported by the app and may not contain all the information.", + "grade": "Grade", + "graded": "Graded", + "gradedby": "Graded by", + "gradenotsynced": "Grade not synced", + "gradedon": "Graded on", + "gradeoutof": "Grade out of {{$a}}", + "gradingstatus": "Grading status", + "groupsubmissionsettings": "Group submission settings", + "hiddenuser": "Participant", + "latesubmissions": "Late submissions", + "latesubmissionsaccepted": "Allowed until {{$a}}", + "markingworkflowstate": "Marking workflow state", + "markingworkflowstateinmarking": "In marking", + "markingworkflowstateinreview": "In review", + "markingworkflowstatenotmarked": "Not marked", + "markingworkflowstatereadyforreview": "Marking completed", + "markingworkflowstatereadyforrelease": "Ready for release", + "markingworkflowstatereleased": "Released", + "multipleteams": "Member of more than one group", + "noattempt": "No attempt", + "nomoresubmissionsaccepted": "Only allowed for participants who have been granted an extension", + "noonlinesubmissions": "This assignment does not require you to submit anything online", + "nosubmission": "Nothing has been submitted for this assignment", + "notallparticipantsareshown": "Participants who have not made a submission are not shown.", + "noteam": "Not a member of any group", + "notgraded": "Not graded", + "numberofdraftsubmissions": "Drafts", + "numberofparticipants": "Participants", + "numberofsubmittedassignments": "Submitted", + "numberofsubmissionsneedgrading": "Needs grading", + "numberofteams": "Groups", + "numwords": "({{$a}} words)", + "outof": "{{$a.current}} out of {{$a.total}}", + "overdue": "Assignment is overdue by: {{$a}}", + "savechanges": "Save changes", + "submissioneditable": "Student can edit this submission", + "submissionnoteditable": "Student cannot edit this submission", + "submissionnotsupported": "This submission is not supported by the app and may not contain all the information.", + "submission": "Submission", + "submissionslocked": "This assignment is not accepting submissions", + "submissionstatus_draft": "Draft (not submitted)", + "submissionstatusheading": "Submission status", + "submissionstatus_marked": "Graded", + "submissionstatus_new": "No submission", + "submissionstatus_reopened": "Reopened", + "submissionstatus_submitted": "Submitted for grading", + "submissionstatus_": "No submission", + "submissionstatus": "Submission status", + "submissionstatusheading": "Submission status", + "submissionteam": "Group", + "submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.", + "submitassignment": "Submit assignment", + "submittedearly": "Assignment was submitted {{$a}} early", + "submittedlate": "Assignment was submitted {{$a}} late", + "timemodified": "Last modified", + "timeremaining": "Time remaining", + "ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.", + "unlimitedattempts": "Unlimited", + "userwithid": "User with ID {{id}}", + "userswhoneedtosubmit": "Users who need to submit: {{$a}}", + "viewsubmission": "View submission", + "warningsubmissionmodified": "The user submission was modified on the site.", + "warningsubmissiongrademodified": "The submission grade was modified on the site.", + "wordlimit": "Word limit" +} \ No newline at end of file diff --git a/src/addon/mod/assign/pages/index/index.html b/src/addon/mod/assign/pages/index/index.html new file mode 100644 index 000000000..e66a37dd0 --- /dev/null +++ b/src/addon/mod/assign/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/assign/pages/index/index.module.ts b/src/addon/mod/assign/pages/index/index.module.ts new file mode 100644 index 000000000..ebaca9739 --- /dev/null +++ b/src/addon/mod/assign/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 { AddonModAssignComponentsModule } from '../../components/components.module'; +import { AddonModAssignIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModAssignIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModAssignComponentsModule, + IonicPageModule.forChild(AddonModAssignIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModAssignIndexPageModule {} diff --git a/src/addon/mod/assign/pages/index/index.ts b/src/addon/mod/assign/pages/index/index.ts new file mode 100644 index 000000000..f9b960f67 --- /dev/null +++ b/src/addon/mod/assign/pages/index/index.ts @@ -0,0 +1,48 @@ +// (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 { AddonModAssignIndexComponent } from '../../components/index/index'; + +/** + * Page that displays an assign. + */ +@IonicPage({ segment: 'addon-mod-assign-index' }) +@Component({ + selector: 'page-addon-mod-assign-index', + templateUrl: 'index.html', +}) +export class AddonModAssignIndexPage { + @ViewChild(AddonModAssignIndexComponent) assignComponent: AddonModAssignIndexComponent; + + title: string; + module: any; + courseId: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.title = this.module.name; + } + + /** + * Update some data based on the assign instance. + * + * @param {any} assign Assign instance. + */ + updateData(assign: any): void { + this.title = assign.name || this.title; + } +} diff --git a/src/addon/mod/assign/providers/index-link-handler.ts b/src/addon/mod/assign/providers/index-link-handler.ts new file mode 100644 index 000000000..f129eb1b9 --- /dev/null +++ b/src/addon/mod/assign/providers/index-link-handler.ts @@ -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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; + +/** + * Handler to treat links to assign index page. + */ +@Injectable() +export class AddonModAssignIndexLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModAssignIndexLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, 'AddonModAssign', 'assign'); + } +} diff --git a/src/addon/mod/assign/providers/module-handler.ts b/src/addon/mod/assign/providers/module-handler.ts new file mode 100644 index 000000000..4de424c27 --- /dev/null +++ b/src/addon/mod/assign/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 { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonModAssignProvider } from './assign'; +import { AddonModAssignIndexComponent } from '../components/index/index'; + +/** + * Handler to support assign modules. + */ +@Injectable() +export class AddonModAssignModuleHandler implements CoreCourseModuleHandler { + name = 'AddonModAssign'; + modName = 'assign'; + + constructor(private courseProvider: CoreCourseProvider, private assignProvider: AddonModAssignProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean { + return this.assignProvider.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('assign'), + title: module.name, + class: 'addon-mod_assign-handler', + showDownloadButton: true, + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModAssignIndexPage', {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 AddonModAssignIndexComponent; + } +} diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index 3c52e925f..b1dd2c9bf 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -48,9 +48,9 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo protected hasAnsweredOnline = false; protected now: number; - constructor(injector: Injector, private choiceProvider: AddonModChoiceProvider, @Optional() private content: Content, + constructor(injector: Injector, private choiceProvider: AddonModChoiceProvider, @Optional() content: Content, private choiceOffline: AddonModChoiceOfflineProvider, private choiceSync: AddonModChoiceSyncProvider) { - super(injector); + super(injector, content); } /** diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts index 6e31b5a9f..cd90d9d65 100644 --- a/src/addon/mod/feedback/components/index/index.ts +++ b/src/addon/mod/feedback/components/index/index.ts @@ -65,11 +65,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity protected submitObserver: any; - constructor(injector: Injector, private feedbackProvider: AddonModFeedbackProvider, @Optional() private content: Content, + constructor(injector: Injector, private feedbackProvider: AddonModFeedbackProvider, @Optional() content: Content, private feedbackOffline: AddonModFeedbackOfflineProvider, private groupsProvider: CoreGroupsProvider, private feedbackSync: AddonModFeedbackSyncProvider, private navCtrl: NavController, private feedbackHelper: AddonModFeedbackHelperProvider) { - super(injector); + super(injector, content); // Listen for form submit events. this.submitObserver = this.eventsProvider.on(AddonModFeedbackProvider.FORM_SUBMITTED, (data) => { diff --git a/src/addon/mod/quiz/components/index/index.ts b/src/addon/mod/quiz/components/index/index.ts index 59561a7c9..2e60facfa 100644 --- a/src/addon/mod/quiz/components/index/index.ts +++ b/src/addon/mod/quiz/components/index/index.ts @@ -68,12 +68,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp protected finishedObserver: any; // It will observe attempt finished events. protected hasPlayed = false; // Whether the user has gone to the quiz player (attempted). - constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() protected content: Content, + constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() content: Content, protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider, protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate, protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate) { - super(injector); + super(injector, content); } /** diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index 731ce8b66..9197331e3 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -38,10 +38,10 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo protected userId: number; protected syncEventName = AddonModSurveySyncProvider.AUTO_SYNCED; - constructor(injector: Injector, private surveyProvider: AddonModSurveyProvider, @Optional() private content: Content, + constructor(injector: Injector, private surveyProvider: AddonModSurveyProvider, @Optional() content: Content, private surveyHelper: AddonModSurveyHelperProvider, private surveyOffline: AddonModSurveyOfflineProvider, private surveySync: AddonModSurveySyncProvider) { - super(injector); + super(injector, content); } /** @@ -83,8 +83,6 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo */ protected isRefreshSyncNeeded(syncEventData: any): boolean { if (this.survey && syncEventData.surveyId == this.survey.id && syncEventData.userId == this.userId) { - this.content.scrollToTop(); - return true; } @@ -189,9 +187,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo } return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => { - this.content.scrollToTop(); - - return this.refreshContent(false); + return this.showLoadingAndRefresh(false); }).finally(() => { modal.dismiss(); }); diff --git a/src/addon/mod/survey/providers/module-handler.ts b/src/addon/mod/survey/providers/module-handler.ts index 4cefa0b32..01a526702 100644 --- a/src/addon/mod/survey/providers/module-handler.ts +++ b/src/addon/mod/survey/providers/module-handler.ts @@ -50,6 +50,7 @@ export class AddonModSurveyModuleHandler implements CoreCourseModuleHandler { icon: this.courseProvider.getModuleIconSrc('survey'), title: module.name, class: 'addon-mod_survey-handler', + showDownloadButton: true, action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { navCtrl.push('AddonModSurveyIndexPage', {module: module, courseId: courseId}, options); } diff --git a/src/app/app.scss b/src/app/app.scss index fc70cf588..642918b05 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -636,6 +636,11 @@ canvas[core-chart] { background-image: url("data:image/svg+xml;charset=utf-8,") !important; } +// For list where some items have detail icon and some others don't. +.core-list-align-detail-right .item .item-inner { + @include padding-horizontal(null, 32px); +} + [ion-fixed] { width: 100%; } diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 3db66af55..875fec056 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Injector } from '@angular/core'; +import { Content } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; @@ -47,7 +48,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR protected eventsProvider: CoreEventsProvider; protected modulePrefetchProvider: CoreCourseModulePrefetchDelegate; - constructor(injector: Injector) { + constructor(injector: Injector, protected content?: Content) { super(injector); this.sitesProvider = injector.get(CoreSitesProvider); @@ -118,10 +119,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR */ protected autoSyncEventReceived(syncEventData: any): void { if (this.isRefreshSyncNeeded(syncEventData)) { - this.loaded = false; - // Refresh the data. - this.refreshContent(false); + this.showLoadingAndRefresh(false); } } @@ -146,6 +145,22 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR }); } + /** + * Show loading and perform the refresh content function. + * + * @param {boolean} [sync=false] If the refresh needs syncing. + * @param {boolean} [showErrors=false] Wether to show errors to the user or hide them. + * @return {Promise} Resolved when done. + */ + protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise { + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + this.loaded = false; + this.content && this.content.scrollToTop(); + + return this.refreshContent(true, showErrors); + } + /** * Download the component contents. * diff --git a/src/core/viewer/pages/text/text.html b/src/core/viewer/pages/text/text.html index b00ee1f27..f2419198c 100644 --- a/src/core/viewer/pages/text/text.html +++ b/src/core/viewer/pages/text/text.html @@ -11,4 +11,8 @@ + + + + diff --git a/src/core/viewer/pages/text/text.module.ts b/src/core/viewer/pages/text/text.module.ts index 2cfce877b..08a594bc3 100644 --- a/src/core/viewer/pages/text/text.module.ts +++ b/src/core/viewer/pages/text/text.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreViewerTextPage } from './text'; +import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; /** @@ -26,6 +27,7 @@ import { CoreDirectivesModule } from '@directives/directives.module'; CoreViewerTextPage ], imports: [ + CoreComponentsModule, CoreDirectivesModule, IonicPageModule.forChild(CoreViewerTextPage), TranslateModule.forChild() diff --git a/src/core/viewer/pages/text/text.ts b/src/core/viewer/pages/text/text.ts index 98d1bf6ad..9139d5473 100644 --- a/src/core/viewer/pages/text/text.ts +++ b/src/core/viewer/pages/text/text.ts @@ -29,12 +29,14 @@ export class CoreViewerTextPage { content: string; // Page content. component: string; // Component to use in format-text. componentId: string | number; // Component ID to use in format-text. + files: any[]; // List of files. constructor(private viewCtrl: ViewController, params: NavParams, textUtils: CoreTextUtilsProvider) { this.title = params.get('title'); this.content = params.get('content'); this.component = params.get('component'); this.componentId = params.get('componentId'); + this.files = params.get('files'); } /** diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index aa6b2e678..a79cc2f1c 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -211,6 +211,11 @@ export class CoreFormatTextDirective implements OnChanges { this.element.style.maxHeight = this.maxHeight + 'px'; this.element.addEventListener('click', (e) => { + if (e.defaultPrevented) { + // Ignore it if the event was prevented by some other listener. + return; + } + e.preventDefault(); e.stopPropagation(); diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 3020d4ce6..9e47ed8ea 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -303,14 +303,16 @@ export class CoreTextUtilsProvider { * @param {string} text Content of the text to be expanded. * @param {string} [component] Component to link the embedded files to. * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {any[]} [files] List of files to display along with the text. */ - expandText(title: string, text: string, component?: string, componentId?: string | number): void { + expandText(title: string, text: string, component?: string, componentId?: string | number, files?: any[]): void { if (text.length > 0) { const params: any = { title: title, content: text, component: component, - componentId: componentId + componentId: componentId, + files: files }; // Open a modal with the contents.