MOBILE-2334 assign: Implement assign index page

main
Dani Palou 2018-04-11 13:49:59 +02:00
parent 7ab247cf7c
commit 0bb96f0e80
22 changed files with 798 additions and 20 deletions

View File

@ -14,6 +14,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CoreCronDelegate } from '@providers/cron'; import { CoreCronDelegate } from '@providers/cron';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { AddonModAssignProvider } from './providers/assign'; import { AddonModAssignProvider } from './providers/assign';
import { AddonModAssignOfflineProvider } from './providers/assign-offline'; import { AddonModAssignOfflineProvider } from './providers/assign-offline';
@ -23,6 +24,7 @@ import { AddonModAssignFeedbackDelegate } from './providers/feedback-delegate';
import { AddonModAssignSubmissionDelegate } from './providers/submission-delegate'; import { AddonModAssignSubmissionDelegate } from './providers/submission-delegate';
import { AddonModAssignDefaultFeedbackHandler } from './providers/default-feedback-handler'; import { AddonModAssignDefaultFeedbackHandler } from './providers/default-feedback-handler';
import { AddonModAssignDefaultSubmissionHandler } from './providers/default-submission-handler'; import { AddonModAssignDefaultSubmissionHandler } from './providers/default-submission-handler';
import { AddonModAssignModuleHandler } from './providers/module-handler';
import { AddonModAssignPrefetchHandler } from './providers/prefetch-handler'; import { AddonModAssignPrefetchHandler } from './providers/prefetch-handler';
import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler'; import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler';
@ -38,13 +40,16 @@ import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler';
AddonModAssignSubmissionDelegate, AddonModAssignSubmissionDelegate,
AddonModAssignDefaultFeedbackHandler, AddonModAssignDefaultFeedbackHandler,
AddonModAssignDefaultSubmissionHandler, AddonModAssignDefaultSubmissionHandler,
AddonModAssignModuleHandler,
AddonModAssignPrefetchHandler, AddonModAssignPrefetchHandler,
AddonModAssignSyncCronHandler AddonModAssignSyncCronHandler
] ]
}) })
export class AddonModAssignModule { export class AddonModAssignModule {
constructor(prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModAssignPrefetchHandler, constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModAssignModuleHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModAssignPrefetchHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModAssignSyncCronHandler) { cronDelegate: CoreCronDelegate, syncHandler: AddonModAssignSyncCronHandler) {
moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler); prefetchDelegate.registerHandler(prefetchHandler);
cronDelegate.register(syncHandler); cronDelegate.register(syncHandler);
} }

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 { AddonModAssignIndexComponent } from './index/index';
@NgModule({
declarations: [
AddonModAssignIndexComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonModAssignIndexComponent
],
entryComponents: [
AddonModAssignIndexComponent
]
})
export class AddonModAssignComponentsModule {}

View File

@ -0,0 +1,87 @@
<!-- 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="assign && (description || (assign.introattachments && assign.introattachments.length))" [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">
<!-- Description and intro attachments. -->
<ion-card *ngIf="description">
<ion-item text-wrap>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120" (click)="expandDescription($event)"></core-format-text>
<ion-note *ngIf="note" item-end>{{ note }}</ion-note>
</ion-item>
</ion-card>
<ion-card *ngIf="assign && assign.introattachments && assign.introattachments.length">
<core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId"></core-file>
</ion-card>
<!-- Assign has something offline. -->
<div *ngIf="hasOffline" class="core-warning-card" icon-start>
<ion-icon name="warning"></ion-icon>
{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
</div>
<!-- User can view all submissions (teacher). -->
<ion-card *ngIf="assign && canViewSubmissions" class="core-list-align-detail-right">
<ion-item text-wrap *ngIf="timeRemaining">
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p>{{ timeRemaining }}</p>
</ion-item>
<ion-item text-wrap *ngIf="lateSubmissions">
<h2>{{ 'addon.mod_assign.latesubmissions' | translate }}</h2>
<p>{{ lateSubmissions }}</p>
</ion-item>
<!-- Summary of all submissions. -->
<a ion-item text-wrap *ngIf="summary && summary.participantcount" (click)="goToSubmissionList()">
<h2 *ngIf="assign.teamsubmission">{{ 'addon.mod_assign.numberofteams' | translate }}</h2>
<h2 *ngIf="!assign.teamsubmission">{{ 'addon.mod_assign.numberofparticipants' | translate }}</h2>
<ion-badge item-end *ngIf="showNumbers" color="primary">
{{ summary.participantcount }}
</ion-badge>
</a>
<!-- Summary of submissions with draft status. -->
<a ion-item text-wrap *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled" [attr.detail-none]="(showNumbers && !summary.submissiondraftscount) ? true : null" (click)="goToSubmissionList(submissionStatusDraft, summary.submissiondraftscount)">
<h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2>
<ion-badge item-end *ngIf="showNumbers" color="primary">
{{ summary.submissiondraftscount }}
</ion-badge>
</a>
<!-- Summary of submissions with submitted status. -->
<a ion-item text-wrap *ngIf="summary && summary.submissionsenabled" [attr.detail-none]="(showNumbers && !summary.submissionssubmittedcount) ? true : null" (click)="goToSubmissionList(submissionStatusSubmitted, summary.submissionssubmittedcount)">
<h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2>
<ion-badge item-end *ngIf="showNumbers" color="primary">
{{ summary.submissionssubmittedcount }}
</ion-badge>
</a>
<!-- Summary of submissions that need grading. -->
<a ion-item text-wrap *ngIf="summary && summary.submissionsenabled && !assign.teamsubmission && showNumbers" [attr.detail-none]="needsGradingAvalaible ? null : true" (click)="goToSubmissionList(needGrading, needsGradingAvalaible)">
<h2>{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}</h2>
<ion-badge item-end color="primary">
{{ summary.submissionsneedgradingcount }}
</ion-badge>
</a>
<!-- Ungrouped users. -->
<div *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card" icon-start>
<ion-icon name="information"></ion-icon>
{{ 'addon.mod_assign.ungroupedusers' | translate }}
</div>
</ion-card>
<!-- @todo <addon-mod-assign-submission *ngIf="!canViewSubmissions" [courseId]="courseId" [moduleId]="module.id"></addon-mod-assign-submission> -->
</core-loading>

View File

@ -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<any>} Promise resolved when done.
*/
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
// 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<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
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<any>} Promise resolved when done.
*/
protected sync(): Promise<any> {
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();
}
}

View File

@ -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 <strong>{{$a}}</strong>",
"allowsubmissionsanddescriptionfromdatesummary": "The assignment details and submission form will be available from <strong>{{$a}}</strong>",
"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": "<font color=\"red\">Assignment is overdue by: {{$a}}</font>",
"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"
}

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]="assignComponent.loaded" (ionRefresh)="assignComponent.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-assign-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-assign-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 { AddonModAssignComponentsModule } from '../../components/components.module';
import { AddonModAssignIndexPage } from './index';
@NgModule({
declarations: [
AddonModAssignIndexPage,
],
imports: [
CoreDirectivesModule,
AddonModAssignComponentsModule,
IonicPageModule.forChild(AddonModAssignIndexPage),
TranslateModule.forChild()
],
})
export class AddonModAssignIndexPageModule {}

View File

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

View File

@ -0,0 +1,29 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { 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');
}
}

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

View File

@ -48,9 +48,9 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
protected hasAnsweredOnline = false; protected hasAnsweredOnline = false;
protected now: number; 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) { private choiceOffline: AddonModChoiceOfflineProvider, private choiceSync: AddonModChoiceSyncProvider) {
super(injector); super(injector, content);
} }
/** /**

View File

@ -65,11 +65,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
protected submitObserver: any; 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 feedbackOffline: AddonModFeedbackOfflineProvider, private groupsProvider: CoreGroupsProvider,
private feedbackSync: AddonModFeedbackSyncProvider, private navCtrl: NavController, private feedbackSync: AddonModFeedbackSyncProvider, private navCtrl: NavController,
private feedbackHelper: AddonModFeedbackHelperProvider) { private feedbackHelper: AddonModFeedbackHelperProvider) {
super(injector); super(injector, content);
// Listen for form submit events. // Listen for form submit events.
this.submitObserver = this.eventsProvider.on(AddonModFeedbackProvider.FORM_SUBMITTED, (data) => { this.submitObserver = this.eventsProvider.on(AddonModFeedbackProvider.FORM_SUBMITTED, (data) => {

View File

@ -68,12 +68,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
protected finishedObserver: any; // It will observe attempt finished events. protected finishedObserver: any; // It will observe attempt finished events.
protected hasPlayed = false; // Whether the user has gone to the quiz player (attempted). 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 quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider,
protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate, protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController, protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController,
protected prefetchDelegate: CoreCourseModulePrefetchDelegate) { protected prefetchDelegate: CoreCourseModulePrefetchDelegate) {
super(injector); super(injector, content);
} }
/** /**

View File

@ -38,10 +38,10 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
protected userId: number; protected userId: number;
protected syncEventName = AddonModSurveySyncProvider.AUTO_SYNCED; 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 surveyHelper: AddonModSurveyHelperProvider, private surveyOffline: AddonModSurveyOfflineProvider,
private surveySync: AddonModSurveySyncProvider) { private surveySync: AddonModSurveySyncProvider) {
super(injector); super(injector, content);
} }
/** /**
@ -83,8 +83,6 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
*/ */
protected isRefreshSyncNeeded(syncEventData: any): boolean { protected isRefreshSyncNeeded(syncEventData: any): boolean {
if (this.survey && syncEventData.surveyId == this.survey.id && syncEventData.userId == this.userId) { if (this.survey && syncEventData.surveyId == this.survey.id && syncEventData.userId == this.userId) {
this.content.scrollToTop();
return true; 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(() => { return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => {
this.content.scrollToTop(); return this.showLoadingAndRefresh(false);
return this.refreshContent(false);
}).finally(() => { }).finally(() => {
modal.dismiss(); modal.dismiss();
}); });

View File

@ -50,6 +50,7 @@ export class AddonModSurveyModuleHandler implements CoreCourseModuleHandler {
icon: this.courseProvider.getModuleIconSrc('survey'), icon: this.courseProvider.getModuleIconSrc('survey'),
title: module.name, title: module.name,
class: 'addon-mod_survey-handler', class: 'addon-mod_survey-handler',
showDownloadButton: true,
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
navCtrl.push('AddonModSurveyIndexPage', {module: module, courseId: courseId}, options); navCtrl.push('AddonModSurveyIndexPage', {module: module, courseId: courseId}, options);
} }

View File

@ -636,6 +636,11 @@ canvas[core-chart] {
background-image: url("data:image/svg+xml;charset=utf-8,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2020'><path%20d='M2,20l-2-2l8-8L0,2l2-2l10,10L2,20z'%20fill='%23FFFFFF'/></svg>") !important; background-image: url("data:image/svg+xml;charset=utf-8,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2020'><path%20d='M2,20l-2-2l8-8L0,2l2-2l10,10L2,20z'%20fill='%23FFFFFF'/></svg>") !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] { [ion-fixed] {
width: 100%; width: 100%;
} }

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
import { Injector } from '@angular/core'; import { Injector } from '@angular/core';
import { Content } from 'ionic-angular';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
@ -47,7 +48,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
protected eventsProvider: CoreEventsProvider; protected eventsProvider: CoreEventsProvider;
protected modulePrefetchProvider: CoreCourseModulePrefetchDelegate; protected modulePrefetchProvider: CoreCourseModulePrefetchDelegate;
constructor(injector: Injector) { constructor(injector: Injector, protected content?: Content) {
super(injector); super(injector);
this.sitesProvider = injector.get(CoreSitesProvider); this.sitesProvider = injector.get(CoreSitesProvider);
@ -118,10 +119,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
*/ */
protected autoSyncEventReceived(syncEventData: any): void { protected autoSyncEventReceived(syncEventData: any): void {
if (this.isRefreshSyncNeeded(syncEventData)) { if (this.isRefreshSyncNeeded(syncEventData)) {
this.loaded = false;
// Refresh the data. // 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<any>} Resolved when done.
*/
protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise<any> {
this.refreshIcon = 'spinner';
this.syncIcon = 'spinner';
this.loaded = false;
this.content && this.content.scrollToTop();
return this.refreshContent(true, showErrors);
}
/** /**
* Download the component contents. * Download the component contents.
* *

View File

@ -11,4 +11,8 @@
</ion-header> </ion-header>
<ion-content padding> <ion-content padding>
<core-format-text [text]="content" [component]="component" [componentId]="componentId"></core-format-text> <core-format-text [text]="content" [component]="component" [componentId]="componentId"></core-format-text>
<ion-card *ngIf="files && files.length">
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="componentId"></core-file>
</ion-card>
</ion-content> </ion-content>

View File

@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular'; import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CoreViewerTextPage } from './text'; import { CoreViewerTextPage } from './text';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreDirectivesModule } from '@directives/directives.module';
/** /**
@ -26,6 +27,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
CoreViewerTextPage CoreViewerTextPage
], ],
imports: [ imports: [
CoreComponentsModule,
CoreDirectivesModule, CoreDirectivesModule,
IonicPageModule.forChild(CoreViewerTextPage), IonicPageModule.forChild(CoreViewerTextPage),
TranslateModule.forChild() TranslateModule.forChild()

View File

@ -29,12 +29,14 @@ export class CoreViewerTextPage {
content: string; // Page content. content: string; // Page content.
component: string; // Component to use in format-text. component: string; // Component to use in format-text.
componentId: string | number; // Component ID 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) { constructor(private viewCtrl: ViewController, params: NavParams, textUtils: CoreTextUtilsProvider) {
this.title = params.get('title'); this.title = params.get('title');
this.content = params.get('content'); this.content = params.get('content');
this.component = params.get('component'); this.component = params.get('component');
this.componentId = params.get('componentId'); this.componentId = params.get('componentId');
this.files = params.get('files');
} }
/** /**

View File

@ -211,6 +211,11 @@ export class CoreFormatTextDirective implements OnChanges {
this.element.style.maxHeight = this.maxHeight + 'px'; this.element.style.maxHeight = this.maxHeight + 'px';
this.element.addEventListener('click', (e) => { this.element.addEventListener('click', (e) => {
if (e.defaultPrevented) {
// Ignore it if the event was prevented by some other listener.
return;
}
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@ -303,14 +303,16 @@ export class CoreTextUtilsProvider {
* @param {string} text Content of the text to be expanded. * @param {string} text Content of the text to be expanded.
* @param {string} [component] Component to link the embedded files to. * @param {string} [component] Component to link the embedded files to.
* @param {string|number} [componentId] An ID to use in conjunction with the component. * @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) { if (text.length > 0) {
const params: any = { const params: any = {
title: title, title: title,
content: text, content: text,
component: component, component: component,
componentId: componentId componentId: componentId,
files: files
}; };
// Open a modal with the contents. // Open a modal with the contents.