commit
1346f86372
File diff suppressed because it is too large
Load Diff
|
@ -78,7 +78,7 @@
|
|||
"zone.js": "0.8.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ionic/app-scripts": "^3.1.5",
|
||||
"@ionic/app-scripts": "^3.1.8",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-clip-empty-files": "^0.1.2",
|
||||
"gulp-rename": "^1.2.2",
|
||||
|
|
|
@ -518,18 +518,18 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
this.domUtils.showConfirm(this.translate.instant(langKey)).then(() => {
|
||||
const modal = this.domUtils.showModalLoading('core.deleting', true);
|
||||
|
||||
this.messagesProvider.deleteMessage(message).then(() => {
|
||||
return this.messagesProvider.deleteMessage(message).then(() => {
|
||||
// Remove message from the list without having to wait for re-fetch.
|
||||
this.messages.splice(index, 1);
|
||||
this.removeMessage(message.hash);
|
||||
this.notifyNewMessage();
|
||||
|
||||
this.fetchData(); // Re-fetch messages to update cached data.
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.messages.errordeletemessage', true);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.messages.errordeletemessage', true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -34,12 +34,12 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
|
|||
|
||||
static AUTO_SYNCED = 'addon_messages_autom_synced';
|
||||
|
||||
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
|
||||
protected appProvider: CoreAppProvider, private messagesOffline: AddonMessagesOfflineProvider,
|
||||
private eventsProvider: CoreEventsProvider, private messagesProvider: AddonMessagesProvider,
|
||||
private userProvider: CoreUserProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
|
||||
syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) {
|
||||
super('AddonMessagesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils);
|
||||
constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider,
|
||||
translate: TranslateService, syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider,
|
||||
private messagesOffline: AddonMessagesOfflineProvider, private eventsProvider: CoreEventsProvider,
|
||||
private messagesProvider: AddonMessagesProvider, private userProvider: CoreUserProvider,
|
||||
private utils: CoreUtilsProvider) {
|
||||
super('AddonMessagesSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -100,8 +100,6 @@ export class AddonMessagesAddContactUserHandler implements CoreUserProfileHandle
|
|||
|
||||
return this.domUtils.showConfirm(template, title, title).then(() => {
|
||||
return this.messagesProvider.removeContact(user.id);
|
||||
}, () => {
|
||||
// Ignore on cancel.
|
||||
});
|
||||
} else {
|
||||
return this.messagesProvider.addContact(user.id);
|
||||
|
|
|
@ -103,8 +103,6 @@ export class AddonMessagesBlockContactUserHandler implements CoreUserProfileHand
|
|||
|
||||
return this.domUtils.showConfirm(template, title, title).then(() => {
|
||||
return this.messagesProvider.blockContact(user.id);
|
||||
}, () => {
|
||||
// Ignore on cancel.
|
||||
});
|
||||
}
|
||||
}).catch((error) => {
|
||||
|
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { AddonModQuizAccessDelayBetweenAttemptsHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessDelayBetweenAttemptsHandler
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessDelayBetweenAttemptsModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessDelayBetweenAttemptsHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
|
||||
/**
|
||||
* Handler to support delay between attempts access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessDelayBetweenAttemptsHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessDelayBetweenAttempts';
|
||||
ruleName = 'quizaccess_delaybetweenattempts';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { AddonModQuizAccessIpAddressHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessIpAddressHandler
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessIpAddressModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessIpAddressHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
|
||||
/**
|
||||
* Handler to support IP address access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessIpAddressHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessIpAddress';
|
||||
ruleName = 'quizaccess_ipaddress';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { AddonModQuizAccessNumAttemptsHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessNumAttemptsHandler
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessNumAttemptsModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessNumAttemptsHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
|
||||
/**
|
||||
* Handler to support num attempts access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessNumAttemptsHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessNumAttempts';
|
||||
ruleName = 'quizaccess_numattempts';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'core.settings.synchronization' | translate }}</p>
|
||||
<p>{{ 'addon.mod_quiz.confirmcontinueoffline' | translate:{$a: quiz.syncTimeReadable} }}</p>
|
||||
</ion-item>
|
|
@ -0,0 +1,42 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Component to render the preflight for offline attempts.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-quiz-acess-offline-attempts',
|
||||
templateUrl: 'offlineattempts.html'
|
||||
})
|
||||
export class AddonModQuizAccessOfflineAttemptsComponent implements OnInit {
|
||||
|
||||
@Input() quiz: any; // The quiz the rule belongs to.
|
||||
@Input() attempt: any; // The attempt being started/continued.
|
||||
@Input() prefetch: boolean; // Whether the user is prefetching the quiz.
|
||||
@Input() siteId: string; // Site ID.
|
||||
@Input() form: FormGroup; // Form where to add the form control.
|
||||
|
||||
constructor(private fb: FormBuilder) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Always set confirmdatasaved to 1. Sending the data means the user accepted.
|
||||
this.form.addControl('confirmdatasaved', this.fb.control(1));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { AddonModQuizAccessOfflineAttemptsHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessOfflineAttemptsComponent } from './component/offlineattempts';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizAccessOfflineAttemptsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessOfflineAttemptsHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModQuizAccessOfflineAttemptsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModQuizAccessOfflineAttemptsComponent
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessOfflineAttemptsModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessOfflineAttemptsHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
import { AddonModQuizSyncProvider } from '../../../providers/quiz-sync';
|
||||
import { AddonModQuizAccessOfflineAttemptsComponent } from '../component/offlineattempts';
|
||||
|
||||
/**
|
||||
* Handler to support offline attempts access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessOfflineAttemptsHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessOfflineAttempts';
|
||||
ruleName = 'quizaccess_offlineattempts';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Add preflight data that doesn't require user interaction. The data should be added to the preflightData param.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} preflightData Object where to add the preflight data.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} Promise resolved when done if async, void if it's synchronous.
|
||||
*/
|
||||
getFixedPreflightData(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise<any> {
|
||||
preflightData.confirmdatasaved = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the access rule preflight.
|
||||
* Implement this if your access rule requires a preflight check with user interaction.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getPreflightComponent(injector: Injector): any | Promise<any> {
|
||||
return AddonModQuizAccessOfflineAttemptsComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
if (prefetch) {
|
||||
// Don't show the warning if the user is prefetching.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!attempt) {
|
||||
// User is starting a new attempt, show the warning.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show warning if last sync was a while ago.
|
||||
return Date.now() - AddonModQuizSyncProvider.SYNC_TIME > quiz.syncTime;
|
||||
}
|
||||
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { AddonModQuizAccessOpenCloseDateHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessOpenCloseDateHandler
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessOpenCloseDateModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessOpenCloseDateHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
import { AddonModQuizProvider } from '../../../providers/quiz';
|
||||
|
||||
/**
|
||||
* Handler to support open/close date access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessOpenCloseDateHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessOpenCloseDate';
|
||||
ruleName = 'quizaccess_openclosedate';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the time left of an attempt should be displayed.
|
||||
*
|
||||
* @param {any} attempt The attempt.
|
||||
* @param {number} endTime The attempt end time (in seconds).
|
||||
* @param {number} timeNow The current time in seconds.
|
||||
* @return {boolean} Whether it should be displayed.
|
||||
*/
|
||||
shouldShowTimeLeft(attempt: any, endTime: number, timeNow: number): boolean {
|
||||
// If this is a teacher preview after the close date, do not show the time.
|
||||
if (attempt.preview && timeNow > endTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show the time left only if it's less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
|
||||
if (timeNow > endTime - AddonModQuizProvider.QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.quizpassword' | translate }}</p>
|
||||
<p>{{ 'addon.mod_quiz.requirepasswordmessage' | translate}}</p>
|
||||
</ion-item>
|
||||
<ion-item [formGroup]="form">
|
||||
<core-show-password item-content [name]="'quizpassword'">
|
||||
<ion-input id="addon-mod_quiz-accessrule-password-input" name="quizpassword" type="password" placeholder="{{ 'addon.mod_quiz.quizpassword' | translate }}" [formControlName]="'quizpassword'"></ion-input>
|
||||
</core-show-password>
|
||||
</ion-item>
|
|
@ -0,0 +1,42 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Component to render the preflight for password.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-quiz-acess-password',
|
||||
templateUrl: 'password.html'
|
||||
})
|
||||
export class AddonModQuizAccessPasswordComponent implements OnInit {
|
||||
|
||||
@Input() quiz: any; // The quiz the rule belongs to.
|
||||
@Input() attempt: any; // The attempt being started/continued.
|
||||
@Input() prefetch: boolean; // Whether the user is prefetching the quiz.
|
||||
@Input() siteId: string; // Site ID.
|
||||
@Input() form: FormGroup; // Form where to add the form control.
|
||||
|
||||
constructor(private fb: FormBuilder) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Add the control for the password.
|
||||
this.form.addControl('quizpassword', this.fb.control(''));
|
||||
}
|
||||
}
|
|
@ -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 { 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 { AddonModQuizAccessPasswordHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessPasswordComponent } from './component/password';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizAccessPasswordComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessPasswordHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModQuizAccessPasswordComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModQuizAccessPasswordComponent
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessPasswordModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessPasswordHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
import { AddonModQuizAccessPasswordComponent } from '../component/password';
|
||||
|
||||
/**
|
||||
* Handler to support password access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessPasswordHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessPassword';
|
||||
ruleName = 'quizaccess_password';
|
||||
|
||||
// Variables for database.
|
||||
protected PASSWORD_TABLE = 'mod_quiz_access_password';
|
||||
protected tableSchema = {
|
||||
name: this.PASSWORD_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'INTEGER',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'TEXT'
|
||||
},
|
||||
{
|
||||
name: 'timemodified',
|
||||
type: 'INTEGER'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider) {
|
||||
this.sitesProvider.createTableFromSchema(this.tableSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add preflight data that doesn't require user interaction. The data should be added to the preflightData param.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} preflightData Object where to add the preflight data.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} Promise resolved when done if async, void if it's synchronous.
|
||||
*/
|
||||
getFixedPreflightData(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise<any> {
|
||||
if (quiz && quiz.id && typeof preflightData.quizpassword == 'undefined') {
|
||||
// Try to get a password stored. If it's found, use it.
|
||||
return this.getPasswordEntry(quiz.id, siteId).then((entry) => {
|
||||
preflightData.quizpassword = entry.password;
|
||||
}).catch(() => {
|
||||
// Don't reject.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a password stored in DB.
|
||||
*
|
||||
* @param {number} quizId Quiz ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with the DB entry on success.
|
||||
*/
|
||||
protected getPasswordEntry(quizId: number, siteId?: string): Promise<any> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.getDb().getRecord(this.PASSWORD_TABLE, {id: quizId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the access rule preflight.
|
||||
* Implement this if your access rule requires a preflight check with user interaction.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getPreflightComponent(injector: Injector): any | Promise<any> {
|
||||
return AddonModQuizAccessPasswordComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
// If there's a password stored don't require the preflight since we'll use the stored one.
|
||||
return this.getPasswordEntry(quiz.id, siteId).then(() => {
|
||||
return false;
|
||||
}).catch(() => {
|
||||
// Not stored.
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when the preflight check has passed. This is a chance to record that fact in some way.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} attempt The attempt started/continued.
|
||||
* @param {any} preflightData Preflight data gathered.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} Promise resolved when done if async, void if it's synchronous.
|
||||
*/
|
||||
notifyPreflightCheckPassed(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string)
|
||||
: void | Promise<any> {
|
||||
|
||||
// The password is right, store it to use it automatically in following executions.
|
||||
if (quiz && quiz.id && typeof preflightData.quizpassword != 'undefined') {
|
||||
return this.storePassword(quiz.id, preflightData.quizpassword, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when the preflight check fails. This is a chance to record that fact in some way.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} attempt The attempt started/continued.
|
||||
* @param {any} preflightData Preflight data gathered.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} Promise resolved when done if async, void if it's synchronous.
|
||||
*/
|
||||
notifyPreflightCheckFailed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string)
|
||||
: void | Promise<any> {
|
||||
|
||||
// The password is wrong, remove it from DB if it's there.
|
||||
if (quiz && quiz.id) {
|
||||
return this.removePassword(quiz.id, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a password from DB.
|
||||
*
|
||||
* @param {number} quizId Quiz ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected removePassword(quizId: number, siteId?: string): Promise<any> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.getDb().deleteRecords(this.PASSWORD_TABLE, {id: quizId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a password in DB.
|
||||
*
|
||||
* @param {number} quizId Quiz ID.
|
||||
* @param {string} password Password.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected storePassword(quizId: number, password: string, siteId?: string): Promise<any> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const entry = {
|
||||
id: quizId,
|
||||
password: password,
|
||||
timemodified: Date.now()
|
||||
};
|
||||
|
||||
return site.getDb().insertRecord(this.PASSWORD_TABLE, entry);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
|
||||
/**
|
||||
* Handler to support safe address access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessSafeBrowserHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessSafeBrowser';
|
||||
ruleName = 'quizaccess_safebrowser';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { AddonModQuizAccessSafeBrowserHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessSafeBrowserHandler
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessSafeBrowserModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessSafeBrowserHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
// (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 { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
|
||||
/**
|
||||
* Handler to support secure window access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessSecureWindowHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessSecureWindow';
|
||||
ruleName = 'quizaccess_securewindow';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { AddonModQuizAccessSecureWindowHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessSecureWindowHandler
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessSecureWindowModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessSecureWindowHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.confirmstartheader' | translate }}</p>
|
||||
<p>{{ 'addon.mod_quiz.confirmstart' | translate:{$a: quiz.readableTimeLimit} }}</p>
|
||||
</ion-item>
|
|
@ -0,0 +1,36 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Component to render the preflight for time limit.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-quiz-acess-time-limit',
|
||||
templateUrl: 'timelimit.html'
|
||||
})
|
||||
export class AddonModQuizAccessTimeLimitComponent {
|
||||
|
||||
@Input() quiz: any; // The quiz the rule belongs to.
|
||||
@Input() attempt: any; // The attempt being started/continued.
|
||||
@Input() prefetch: boolean; // Whether the user is prefetching the quiz.
|
||||
@Input() siteId: string; // Site ID.
|
||||
@Input() form: FormGroup; // Form where to add the form control.
|
||||
|
||||
constructor() {
|
||||
// Nothing to do, we don't need to send anything for time limit.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { AddonModQuizAccessRuleHandler } from '../../../providers/access-rules-delegate';
|
||||
import { AddonModQuizAccessTimeLimitComponent } from '../component/timelimit';
|
||||
|
||||
/**
|
||||
* Handler to support time limit access rule.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessTimeLimitHandler implements AddonModQuizAccessRuleHandler {
|
||||
name = 'AddonModQuizAccessTimeLimit';
|
||||
ruleName = 'quizaccess_timelimit';
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the access rule preflight.
|
||||
* Implement this if your access rule requires a preflight check with user interaction.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getPreflightComponent(injector: Injector): any | Promise<any> {
|
||||
return AddonModQuizAccessTimeLimitComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean> {
|
||||
// Warning only required if the attempt is not already started.
|
||||
return !attempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the time left of an attempt should be displayed.
|
||||
*
|
||||
* @param {any} attempt The attempt.
|
||||
* @param {number} endTime The attempt end time (in seconds).
|
||||
* @param {number} timeNow The current time in seconds.
|
||||
* @return {boolean} Whether it should be displayed.
|
||||
*/
|
||||
shouldShowTimeLeft(attempt: any, endTime: number, timeNow: number): boolean {
|
||||
// If this is a teacher preview after the time limit expires, don't show the time left.
|
||||
return !(attempt.preview && timeNow > endTime);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// (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 { AddonModQuizAccessTimeLimitHandler } from './providers/handler';
|
||||
import { AddonModQuizAccessTimeLimitComponent } from './component/timelimit';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizAccessTimeLimitComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessTimeLimitHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModQuizAccessTimeLimitComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModQuizAccessTimeLimitComponent
|
||||
]
|
||||
})
|
||||
export class AddonModQuizAccessTimeLimitModule {
|
||||
constructor(accessRuleDelegate: AddonModQuizAccessRuleDelegate, handler: AddonModQuizAccessTimeLimitHandler) {
|
||||
accessRuleDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
// (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 { PopoverController, Popover } from 'ionic-angular';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
import { AddonModQuizProvider } from '../providers/quiz';
|
||||
import { AddonModQuizConnectionErrorComponent } from '../components/connection-error/connection-error';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Class to support auto-save in quiz. Every certain seconds, it will check if there are changes in the current page answers
|
||||
* and, if so, it will save them automatically.
|
||||
*/
|
||||
export class AddonModQuizAutoSave {
|
||||
protected CHECK_CHANGES_INTERVAL = 5000;
|
||||
|
||||
protected logger;
|
||||
protected checkChangesInterval; // Interval to check if there are changes in the answers.
|
||||
protected loadPreviousAnswersTimeout; // Timeout to load previous answers.
|
||||
protected autoSaveTimeout; // Timeout to auto-save the answers.
|
||||
protected popover: Popover; // Popover to display there's been an error.
|
||||
protected popoverShown = false; // Whether the popover is shown.
|
||||
protected previousAnswers: any; // The previous answers. It is used to check if answers have changed.
|
||||
protected errorObservable: BehaviorSubject<boolean>; // An observable to notify if there's been an error.
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {string} formName Name of the form where the answers are stored.
|
||||
* @param {string} buttonSelector Selector to find the button to show the connection error.
|
||||
* @param {CoreLoggerProvider} loggerProvider CoreLoggerProvider instance.
|
||||
* @param {PopoverController} popoverCtrl PopoverController instance.
|
||||
* @param {CoreQuestionHelperProvider} questionHelper CoreQuestionHelperProvider instance.
|
||||
* @param {AddonModQuizProvider} quizProvider AddonModQuizProvider instance.
|
||||
*/
|
||||
constructor(protected formName: string, protected buttonSelector: string, loggerProvider: CoreLoggerProvider,
|
||||
protected popoverCtrl: PopoverController, protected questionHelper: CoreQuestionHelperProvider,
|
||||
protected quizProvider: AddonModQuizProvider) {
|
||||
|
||||
this.logger = loggerProvider.getInstance('AddonModQuizAutoSave');
|
||||
|
||||
// Create the popover.
|
||||
this.popover = this.popoverCtrl.create(AddonModQuizConnectionErrorComponent);
|
||||
this.popover.onDidDismiss(() => {
|
||||
this.popoverShown = false;
|
||||
});
|
||||
|
||||
// Create the observable to notify if an error happened.
|
||||
this.errorObservable = new BehaviorSubject<boolean>(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending auto save.
|
||||
*/
|
||||
cancelAutoSave(): void {
|
||||
clearTimeout(this.autoSaveTimeout);
|
||||
this.autoSaveTimeout = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the answers have changed in a page.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} preflightData Preflight data.
|
||||
* @param {boolean} [offline] Whether the quiz is being attempted in offline mode.
|
||||
*/
|
||||
checkChanges(quiz: any, attempt: any, preflightData: any, offline?: boolean): void {
|
||||
if (this.autoSaveTimeout) {
|
||||
// We already have an auto save pending, no need to check changes.
|
||||
return;
|
||||
}
|
||||
|
||||
const answers = this.getAnswers();
|
||||
|
||||
if (!this.previousAnswers) {
|
||||
// Previous answers isn't set, set it now.
|
||||
this.previousAnswers = answers;
|
||||
} else {
|
||||
// Check if answers have changed.
|
||||
let equal = true;
|
||||
|
||||
for (const name in answers) {
|
||||
if (this.previousAnswers[name] != answers[name]) {
|
||||
equal = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!equal) {
|
||||
this.setAutoSaveTimer(quiz, attempt, preflightData, offline);
|
||||
}
|
||||
|
||||
this.previousAnswers = answers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get answers from a form.
|
||||
*
|
||||
* @return {any} Answers.
|
||||
*/
|
||||
protected getAnswers(): any {
|
||||
return this.questionHelper.getAnswersFromForm(document.forms[this.formName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the auto save error.
|
||||
*/
|
||||
hideAutoSaveError(): void {
|
||||
this.errorObservable.next(false);
|
||||
this.popover.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that will notify when an error happens or stops.
|
||||
* It will send true when there's an error, and false when the error has been ammended.
|
||||
*
|
||||
* @return {BehaviorSubject<boolean>} Observable.
|
||||
*/
|
||||
onError(): BehaviorSubject<boolean> {
|
||||
return this.errorObservable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an auto save process if it's not scheduled already.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} preflightData Preflight data.
|
||||
* @param {boolean} [offline] Whether the quiz is being attempted in offline mode.
|
||||
*/
|
||||
setAutoSaveTimer(quiz: any, attempt: any, preflightData: any, offline?: boolean): void {
|
||||
// Don't schedule if already shceduled or quiz is almost closed.
|
||||
if (quiz.autosaveperiod && !this.autoSaveTimeout && !this.quizProvider.isAttemptTimeNearlyOver(quiz, attempt)) {
|
||||
|
||||
// Schedule save.
|
||||
this.autoSaveTimeout = setTimeout(() => {
|
||||
const answers = this.getAnswers();
|
||||
this.cancelAutoSave();
|
||||
this.previousAnswers = answers; // Update previous answers to match what we're sending to the server.
|
||||
|
||||
this.quizProvider.saveAttempt(quiz, attempt, answers, preflightData, offline).then(() => {
|
||||
// Save successful, we can hide the connection error if it was shown.
|
||||
this.hideAutoSaveError();
|
||||
}).catch((error) => {
|
||||
// Error auto-saving. Show error and set timer again.
|
||||
this.logger.warn('Error auto-saving data.', error);
|
||||
|
||||
// If there was no error already, show the error message.
|
||||
if (!this.errorObservable.getValue()) {
|
||||
this.errorObservable.next(true);
|
||||
this.showAutoSaveError();
|
||||
}
|
||||
|
||||
// Try again.
|
||||
this.setAutoSaveTimer(quiz, attempt, preflightData, offline);
|
||||
});
|
||||
}, quiz.autosaveperiod * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error popover due to an auto save error.
|
||||
*/
|
||||
showAutoSaveError(ev?: Event): void {
|
||||
// Don't show popover if it was already shown.
|
||||
if (!this.popoverShown) {
|
||||
this.popoverShown = true;
|
||||
|
||||
// If no event is provided, simulate it targeting the button.
|
||||
this.popover.present({
|
||||
ev: ev || { target: document.querySelector(this.buttonSelector) }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a process to periodically check changes in answers.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} preflightData Preflight data.
|
||||
* @param {boolean} [offline] Whether the quiz is being attempted in offline mode.
|
||||
*/
|
||||
startCheckChangesProcess(quiz: any, attempt: any, preflightData: any, offline?: boolean): void {
|
||||
if (this.checkChangesInterval || !quiz.autosaveperiod) {
|
||||
// We already have the interval in place or the quiz has autosave disabled.
|
||||
return;
|
||||
}
|
||||
|
||||
this.previousAnswers = undefined;
|
||||
|
||||
// Load initial answers in 2.5 seconds so the first check interval finds them already loaded.
|
||||
this.loadPreviousAnswersTimeout = setTimeout(() => {
|
||||
this.checkChanges(quiz, attempt, preflightData, offline);
|
||||
}, 2500);
|
||||
|
||||
// Check changes every certain time.
|
||||
this.checkChangesInterval = setInterval(() => {
|
||||
this.checkChanges(quiz, attempt, preflightData, offline);
|
||||
}, this.CHECK_CHANGES_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the periodical check for changes.
|
||||
*/
|
||||
stopCheckChangesProcess(): void {
|
||||
clearTimeout(this.loadPreviousAnswersTimeout);
|
||||
clearInterval(this.checkChangesInterval);
|
||||
|
||||
this.loadPreviousAnswersTimeout = undefined;
|
||||
this.checkChangesInterval = undefined;
|
||||
}
|
||||
}
|
|
@ -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 { 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 { AddonModQuizIndexComponent } from './index/index';
|
||||
import { AddonModQuizConnectionErrorComponent } from './connection-error/connection-error';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizIndexComponent,
|
||||
AddonModQuizConnectionErrorComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCourseComponentsModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModQuizIndexComponent,
|
||||
AddonModQuizConnectionErrorComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModQuizIndexComponent,
|
||||
AddonModQuizConnectionErrorComponent
|
||||
]
|
||||
})
|
||||
export class AddonModQuizComponentsModule {}
|
|
@ -0,0 +1,7 @@
|
|||
addon-mod-quiz-connection-error {
|
||||
background-color: $red-light;
|
||||
|
||||
.item {
|
||||
background-color: $red-light;
|
||||
}
|
||||
}
|
|
@ -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 { Component } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Component that displays a quiz entry page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-quiz-connection-error',
|
||||
template: '<ion-item text-wrap>{{ "addon.mod_quiz.connectionerror" | translate }}</ion-item>',
|
||||
})
|
||||
export class AddonModQuizConnectionErrorComponent {
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons end>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
|
||||
|
||||
<!-- Access rules description messages. -->
|
||||
<ion-card *ngIf="(quiz && quiz.gradeMethodReadable) || (accessRules && accessRules.length) || syncTime">
|
||||
<ion-list>
|
||||
<ion-item text-wrap *ngFor="let rule of accessRules">
|
||||
<p>{{ rule }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="quiz && quiz.gradeMethodReadable">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.grademethod' | translate }}</p>
|
||||
<p>{{ quiz.gradeMethodReadable }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="syncTime">
|
||||
<p class="item-heading">{{ 'core.lastsync' | translate }}</p>
|
||||
<p>{{ syncTime }}</p>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
<!-- List of user attempts. -->
|
||||
<ion-card class="addon-mod_quiz-table" *ngIf="attempts && attempts.length">
|
||||
<ion-card-header text-wrap>
|
||||
<h2>{{ 'addon.mod_quiz.summaryofattempts' | translate }}</h2>
|
||||
</ion-card-header>
|
||||
<ion-list>
|
||||
<!-- "Header" of the table -->
|
||||
<ion-item text-wrap class="addon-mod_quiz-table-header" detail-push>
|
||||
<ion-row align-items-center>
|
||||
<ion-col text-center class="hidden-phone" *ngIf="quiz.showAttemptColumn"><b>{{ 'addon.mod_quiz.attemptnumber' | translate }}</b></ion-col>
|
||||
<ion-col text-center class="hidden-tablet" *ngIf="quiz.showAttemptColumn"><b>#</b></ion-col>
|
||||
<ion-col col-7><b>{{ 'addon.mod_quiz.attemptstate' | translate }}</b></ion-col>
|
||||
<ion-col text-center class="hidden-phone" *ngIf="quiz.showMarkColumn"><b>{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }}</b></ion-col>
|
||||
<ion-col text-center *ngIf="quiz.showGradeColumn"><b>{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }}</b></ion-col>
|
||||
</ion-row>
|
||||
</ion-item>
|
||||
<!-- List of attempts. -->
|
||||
<a ion-item text-wrap *ngFor="let attempt of attempts" [ngClass]='{"addon-mod_quiz-highlighted core-white-push-arrow": attempt.highlightGrade}' [navPush]="'AddonModQuizAttemptPage'" [navParams]="{courseId: courseId, quizId: quiz.id, attemptId: attempt.id}" [attr.aria-label]="'core.seemoredetail' | translate">
|
||||
<ion-row align-items-center>
|
||||
<ion-col text-center *ngIf="quiz.showAttemptColumn && attempt.preview">{{ 'addon.mod_quiz.preview' | translate }}</ion-col>
|
||||
<ion-col text-center *ngIf="quiz.showAttemptColumn && !attempt.preview">{{ attempt.attempt }}</ion-col>
|
||||
<ion-col col-7>
|
||||
<p *ngFor="let sentence of attempt.readableState">{{ sentence }}</p>
|
||||
</ion-col>
|
||||
<ion-col text-center class="hidden-phone" *ngIf="quiz.showMarkColumn"><p>{{ attempt.readableMark }}</p></ion-col>
|
||||
<ion-col text-center *ngIf="quiz.showGradeColumn"><p>{{ attempt.readableGrade }}</p></ion-col>
|
||||
</ion-row>
|
||||
</a>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
<!-- Result info. -->
|
||||
<ion-card *ngIf="showResults && (gradeResult || gradeOverridden || gradebookFeedback || (quiz.showFeedbackColumn && overallFeedback))">
|
||||
<ion-list>
|
||||
<ion-item text-wrap *ngIf="gradeResult">{{ gradeResult }}</ion-item>
|
||||
<ion-item text-wrap *ngIf="gradeOverridden">{{ 'core.course.overriddennotice' | translate }}</ion-item>
|
||||
<ion-item text-wrap *ngIf="gradebookFeedback">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.comment' | translate }}</p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="gradebookFeedback"></core-format-text></p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="quiz.showFeedbackColumn && overallFeedback">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.overallfeedback' | translate }}</p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="overallFeedback"></core-format-text></p>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
<!-- More data and button to start/continue. -->
|
||||
<ion-card *ngIf="quiz">
|
||||
<ion-list>
|
||||
<!-- Error messages. -->
|
||||
<ion-item text-wrap class="core-danger-item" *ngFor="let message of preventMessages">
|
||||
<p>{{ message }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap class="core-danger-item" *ngIf="quiz.hasquestions === 0">
|
||||
<p>{{ 'addon.mod_quiz.noquestions' | translate }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap class="core-danger-item" *ngIf="unsupportedQuestions && unsupportedQuestions.length">
|
||||
<p>{{ 'addon.mod_quiz.errorquestionsnotsupported' | translate }}</p>
|
||||
<p *ngFor="let type of unsupportedQuestions">{{ type }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap class="core-danger-item" *ngIf="unsupportedRules && unsupportedRules.length">
|
||||
<p>{{ 'addon.mod_quiz.errorrulesnotsupported' | translate }}</p>
|
||||
<p *ngFor="let name of unsupportedRules">{{ name }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap class="core-danger-item" *ngIf="behaviourSupported === false">
|
||||
<p>{{ 'addon.mod_quiz.errorbehaviournotsupported' | translate }}</p>
|
||||
<p>{{ quiz.preferredbehaviour }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Synchronization warning. -->
|
||||
<div class="core-warning-card" icon-start *ngIf="buttonText && hasOffline && !showStatusSpinner">
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
|
||||
</div>
|
||||
|
||||
<!-- Button to start/continue. -->
|
||||
<ion-item *ngIf="buttonText && !showStatusSpinner">
|
||||
<button ion-button block (click)="attemptQuiz()">
|
||||
{{ buttonText | translate }}
|
||||
</button>
|
||||
</ion-item>
|
||||
|
||||
<!-- Button to open in browser if it cannot be attempted in the app. -->
|
||||
<ion-item *ngIf="!buttonText && ((unsupportedQuestions && unsupportedQuestions.length) || (unsupportedRules && unsupportedRules.length) || behaviourSupported === false)">
|
||||
<a ion-button block [href]="externalUrl" core-link icon-end>
|
||||
{{ 'core.openinbrowser' | translate }}
|
||||
<ion-icon name="open"></ion-icon>
|
||||
</a>
|
||||
</ion-item>
|
||||
|
||||
<!-- Spinner shown while downloading or calculating. -->
|
||||
<ion-item text-center *ngIf="showStatusSpinner">
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
</core-loading>
|
|
@ -0,0 +1,34 @@
|
|||
addon-mod-quiz-index {
|
||||
|
||||
.addon-mod_quiz-table {
|
||||
.addon-mod_quiz-table-header .item-inner {
|
||||
background-image: none;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.item-inner ion-label {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item:nth-child(even) {
|
||||
background-color: $gray-lighter;
|
||||
}
|
||||
|
||||
.addon-mod_quiz-highlighted,
|
||||
.item.addon-mod_quiz-highlighted,
|
||||
.addon-mod_quiz-highlighted p,
|
||||
.item.addon-mod_quiz-highlighted p {
|
||||
background-color: $blue-light;
|
||||
color: $blue-dark;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,607 @@
|
|||
// (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 { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||
import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||
import { AddonModQuizProvider } from '../../providers/quiz';
|
||||
import { AddonModQuizHelperProvider } from '../../providers/helper';
|
||||
import { AddonModQuizOfflineProvider } from '../../providers/quiz-offline';
|
||||
import { AddonModQuizSyncProvider } from '../../providers/quiz-sync';
|
||||
import { AddonModQuizPrefetchHandler } from '../../providers/prefetch-handler';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
|
||||
/**
|
||||
* Component that displays a quiz entry page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-quiz-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComponent {
|
||||
component = AddonModQuizProvider.COMPONENT;
|
||||
moduleName = 'quiz';
|
||||
|
||||
quiz: any; // The quiz.
|
||||
now: number; // Current time.
|
||||
syncTime: string; // Last synchronization time.
|
||||
hasOffline: boolean; // Whether the quiz has offline data.
|
||||
accessRules: string[]; // List of access rules of the quiz.
|
||||
unsupportedRules: string[]; // List of unsupported access rules of the quiz.
|
||||
unsupportedQuestions: string[]; // List of unsupported question types of the quiz.
|
||||
behaviourSupported: boolean; // Whether the quiz behaviour is supported.
|
||||
showResults: boolean; // Whether to show the result of the quiz (grade, etc.).
|
||||
gradeOverridden: boolean; // Whether grade has been overridden.
|
||||
gradebookFeedback: string; // The feedback in the gradebook.
|
||||
gradeResult: string; // Message with the grade.
|
||||
overallFeedback: string; // The feedback for the grade.
|
||||
buttonText: string; // Text to display in the start/continue button.
|
||||
preventMessages: string[]; // List of messages explaining why the quiz cannot be attempted.
|
||||
showStatusSpinner = true; // Whether to show a spinner due to quiz status.
|
||||
|
||||
protected fetchContentDefaultError = 'addon.mod_quiz.errorgetquiz'; // Default error to show when loading contents.
|
||||
protected syncEventName = AddonModQuizSyncProvider.AUTO_SYNCED;
|
||||
|
||||
protected quizData: any; // Quiz instance. This variable will store the quiz instance until it's ready to be shown
|
||||
protected autoReview: any; // Data to auto-review an attempt. It's used to automatically open the review page after finishing.
|
||||
protected quizAccessInfo: any; // Quiz access info.
|
||||
protected attemptAccessInfo: any; // Last attempt access info.
|
||||
protected attempts: any[]; // List of attempts the user has made.
|
||||
protected moreAttempts: boolean; // Whether user can create/continue attempts.
|
||||
protected options: any; // Combined review options.
|
||||
protected bestGrade: any; // Best grade data.
|
||||
protected gradebookData: {grade: number, feedback?: string}; // The gradebook grade and feedback.
|
||||
protected overallStats: boolean; // Equivalent to overallstats in mod_quiz_view_object in Moodle.
|
||||
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,
|
||||
protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider,
|
||||
protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
|
||||
protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController,
|
||||
protected prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
|
||||
this.loadContent(false, true).then(() => {
|
||||
if (!this.quizData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.quizProvider.logViewQuiz(this.quizData.id).then(() => {
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
}).catch((error) => {
|
||||
// Ignore errors.
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for attempt finished events.
|
||||
this.finishedObserver = this.eventsProvider.on(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, (data) => {
|
||||
// Go to review attempt if an attempt in this quiz was finished and synced.
|
||||
if (this.quizData && data.quizId == this.quizData.id) {
|
||||
this.autoReview = data;
|
||||
}
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt the quiz.
|
||||
*/
|
||||
attemptQuiz(): void {
|
||||
if (this.showStatusSpinner) {
|
||||
// Quiz is being downloaded or synchronized, abort.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.quizProvider.isQuizOffline(this.quizData)) {
|
||||
// Quiz supports offline, check if it needs to be downloaded.
|
||||
// If the site doesn't support check updates, always prefetch it because we cannot tell if there's something new.
|
||||
const isDownloaded = this.currentStatus == CoreConstants.DOWNLOADED;
|
||||
|
||||
if (!isDownloaded || !this.prefetchDelegate.canCheckUpdates()) {
|
||||
// Prefetch the quiz.
|
||||
this.showStatusSpinner = true;
|
||||
|
||||
this.prefetchHandler.prefetch(this.module, this.courseId, true).then(() => {
|
||||
// Success downloading, open quiz.
|
||||
this.openQuiz();
|
||||
}).catch((error) => {
|
||||
if (this.hasOffline || (isDownloaded && !this.prefetchDelegate.canCheckUpdates())) {
|
||||
// Error downloading but there is something offline, allow continuing it.
|
||||
// If the site doesn't support check updates, continue too because we cannot tell if there's something new.
|
||||
this.openQuiz();
|
||||
} else {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
}
|
||||
|
||||
}).finally(() => {
|
||||
this.showStatusSpinner = false;
|
||||
});
|
||||
} else {
|
||||
// Already downloaded, open it.
|
||||
this.openQuiz();
|
||||
}
|
||||
} else {
|
||||
// Quiz isn't offline, just open it.
|
||||
this.openQuiz();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the quiz 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> {
|
||||
|
||||
// First get the quiz instance.
|
||||
return this.quizProvider.getQuiz(this.courseId, this.module.id).then((quizData) => {
|
||||
this.quizData = quizData;
|
||||
this.quizData.gradeMethodReadable = this.quizProvider.getQuizGradeMethod(this.quizData.grademethod);
|
||||
|
||||
this.now = new Date().getTime();
|
||||
this.dataRetrieved.emit(this.quizData);
|
||||
this.description = this.quizData.intro || this.description;
|
||||
|
||||
// Try to get warnings from automatic sync.
|
||||
return this.quizSync.getSyncWarnings(this.quizData.id).then((warnings) => {
|
||||
if (warnings && warnings.length) {
|
||||
// Show warnings and delete them so they aren't shown again.
|
||||
this.domUtils.showErrorModal(this.textUtils.buildMessage(warnings));
|
||||
|
||||
return this.quizSync.setSyncWarnings(this.quizData.id, []);
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
if (this.quizProvider.isQuizOffline(this.quizData)) {
|
||||
// Try to sync the quiz.
|
||||
return this.syncActivity(showErrors).catch(() => {
|
||||
// Ignore errors, keep getting data even if sync fails.
|
||||
this.autoReview = undefined;
|
||||
});
|
||||
} else {
|
||||
this.autoReview = undefined;
|
||||
this.showStatusSpinner = false;
|
||||
}
|
||||
}).then(() => {
|
||||
|
||||
if (this.quizProvider.isQuizOffline(this.quizData)) {
|
||||
// Handle status.
|
||||
this.setStatusListener();
|
||||
|
||||
// Get last synchronization time and check if sync button should be seen.
|
||||
// No need to return these promises, they should be faster than the rest.
|
||||
this.quizSync.getReadableSyncTime(this.quizData.id).then((syncTime) => {
|
||||
this.syncTime = syncTime;
|
||||
});
|
||||
|
||||
this.quizSync.hasDataToSync(this.quizData.id).then((hasOffline) => {
|
||||
this.hasOffline = hasOffline;
|
||||
});
|
||||
}
|
||||
|
||||
// Get quiz access info.
|
||||
return this.quizProvider.getQuizAccessInformation(this.quizData.id).then((info) => {
|
||||
this.quizAccessInfo = info;
|
||||
this.quizData.showReviewColumn = info.canreviewmyattempts;
|
||||
this.accessRules = info.accessrules;
|
||||
this.unsupportedRules = this.quizProvider.getUnsupportedRules(info.activerulenames);
|
||||
|
||||
if (this.quizData.preferredbehaviour) {
|
||||
this.behaviourSupported = this.behaviourDelegate.isBehaviourSupported(this.quizData.preferredbehaviour);
|
||||
}
|
||||
|
||||
// Get question types in the quiz.
|
||||
return this.quizProvider.getQuizRequiredQtypes(this.quizData.id).then((types) => {
|
||||
this.unsupportedQuestions = this.quizProvider.getUnsupportedQuestions(types);
|
||||
|
||||
return this.getAttempts();
|
||||
});
|
||||
});
|
||||
|
||||
}).then(() => {
|
||||
// All data obtained, now fill the context menu.
|
||||
this.fillContextMenu(refresh);
|
||||
|
||||
// Quiz is ready to be shown, move it to the variable that is displayed.
|
||||
this.quiz = this.quizData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user attempts in the quiz and the result info.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected getAttempts(): Promise<void> {
|
||||
|
||||
// Get access information of last attempt (it also works if no attempts made).
|
||||
return this.quizProvider.getAttemptAccessInformation(this.quizData.id, 0).then((info) => {
|
||||
this.attemptAccessInfo = info;
|
||||
|
||||
// Get attempts.
|
||||
return this.quizProvider.getUserAttempts(this.quizData.id).then((atts) => {
|
||||
|
||||
return this.treatAttempts(atts).then((atts) => {
|
||||
this.attempts = atts;
|
||||
|
||||
// Check if user can create/continue attempts.
|
||||
if (this.attempts.length) {
|
||||
const last = this.attempts[this.attempts.length - 1];
|
||||
this.moreAttempts = !this.quizProvider.isAttemptFinished(last.state) || !this.attemptAccessInfo.isfinished;
|
||||
} else {
|
||||
this.moreAttempts = !this.attemptAccessInfo.isfinished;
|
||||
}
|
||||
|
||||
this.getButtonText();
|
||||
|
||||
return this.getResultInfo();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text to show in the button. It also sets restriction messages if needed.
|
||||
*/
|
||||
protected getButtonText(): void {
|
||||
this.buttonText = '';
|
||||
|
||||
if (this.quizData.hasquestions !== 0) {
|
||||
if (this.attempts.length && !this.quizProvider.isAttemptFinished(this.attempts[this.attempts.length - 1].state)) {
|
||||
// Last attempt is unfinished.
|
||||
if (this.quizAccessInfo.canattempt) {
|
||||
this.buttonText = 'addon.mod_quiz.continueattemptquiz';
|
||||
} else if (this.quizAccessInfo.canpreview) {
|
||||
this.buttonText = 'addon.mod_quiz.continuepreview';
|
||||
}
|
||||
|
||||
} else {
|
||||
// Last attempt is finished or no attempts.
|
||||
if (this.quizAccessInfo.canattempt) {
|
||||
this.preventMessages = this.attemptAccessInfo.preventnewattemptreasons;
|
||||
if (!this.preventMessages.length) {
|
||||
if (!this.attempts.length) {
|
||||
this.buttonText = 'addon.mod_quiz.attemptquiznow';
|
||||
} else {
|
||||
this.buttonText = 'addon.mod_quiz.reattemptquiz';
|
||||
}
|
||||
}
|
||||
} else if (this.quizAccessInfo.canpreview) {
|
||||
this.buttonText = 'addon.mod_quiz.previewquiznow';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.buttonText) {
|
||||
// So far we think a button should be printed, check if they will be allowed to access it.
|
||||
this.preventMessages = this.quizAccessInfo.preventaccessreasons;
|
||||
|
||||
if (!this.moreAttempts) {
|
||||
this.buttonText = '';
|
||||
} else if (this.quizAccessInfo.canattempt && this.preventMessages.length) {
|
||||
this.buttonText = '';
|
||||
} else if (this.unsupportedQuestions.length || this.unsupportedRules.length || !this.behaviourSupported) {
|
||||
this.buttonText = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result info to show.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected getResultInfo(): Promise<void> {
|
||||
|
||||
if (this.attempts.length && this.quizData.showGradeColumn && this.bestGrade.hasgrade &&
|
||||
typeof this.gradebookData.grade != 'undefined') {
|
||||
|
||||
const formattedGradebookGrade = this.quizProvider.formatGrade(this.gradebookData.grade, this.quizData.decimalpoints),
|
||||
formattedBestGrade = this.quizProvider.formatGrade(this.bestGrade.grade, this.quizData.decimalpoints);
|
||||
let gradeToShow = formattedGradebookGrade; // By default we show the grade in the gradebook.
|
||||
|
||||
this.showResults = true;
|
||||
this.gradeOverridden = formattedGradebookGrade != formattedBestGrade;
|
||||
this.gradebookFeedback = this.gradebookData.feedback;
|
||||
|
||||
if (this.bestGrade.grade > this.gradebookData.grade && this.gradebookData.grade == this.quizData.grade) {
|
||||
// The best grade is higher than the max grade for the quiz.
|
||||
// We'll do like Moodle web and show the best grade instead of the gradebook grade.
|
||||
this.gradeOverridden = false;
|
||||
gradeToShow = formattedBestGrade;
|
||||
}
|
||||
|
||||
if (this.overallStats) {
|
||||
// Show the quiz grade. The message shown is different if the quiz is finished.
|
||||
if (this.moreAttempts) {
|
||||
this.gradeResult = this.translate.instant('addon.mod_quiz.gradesofar', {$a: {
|
||||
method: this.quizData.gradeMethodReadable,
|
||||
mygrade: gradeToShow,
|
||||
quizgrade: this.quizData.gradeFormatted
|
||||
}});
|
||||
} else {
|
||||
const outOfShort = this.translate.instant('addon.mod_quiz.outofshort', {$a: {
|
||||
grade: gradeToShow,
|
||||
maxgrade: this.quizData.gradeFormatted
|
||||
}});
|
||||
|
||||
this.gradeResult = this.translate.instant('addon.mod_quiz.yourfinalgradeis', {$a: outOfShort});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.quizData.showFeedbackColumn) {
|
||||
// Get the quiz overall feedback.
|
||||
return this.quizProvider.getFeedbackForGrade(this.quizData.id, this.gradebookData.grade).then((response) => {
|
||||
this.overallFeedback = response.feedbacktext;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.showResults = false;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to review an attempt that has just been finished.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected goToAutoReview(): Promise<any> {
|
||||
// If we go to auto review it means an attempt was finished. Check completion status.
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
|
||||
// Verify that user can see the review.
|
||||
const attemptId = this.autoReview.attemptId;
|
||||
|
||||
if (this.quizAccessInfo.canreviewmyattempts) {
|
||||
return this.quizProvider.getAttemptReview(attemptId, -1).then(() => {
|
||||
this.navCtrl.push('AddonModQuizReviewPage', {courseId: this.courseId, quizId: this.quizData.id, attemptId});
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (result.attemptFinished) {
|
||||
// An attempt was finished, check completion status.
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
}
|
||||
|
||||
// If the sync call isn't rejected it means the sync was successful.
|
||||
return result.answersSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
super.ionViewDidEnter();
|
||||
|
||||
if (this.hasPlayed) {
|
||||
this.hasPlayed = false;
|
||||
|
||||
// Update data when we come back from the player since the attempt status could have changed.
|
||||
let promise;
|
||||
|
||||
// Check if we need to go to review an attempt automatically.
|
||||
if (this.autoReview && this.autoReview.synced) {
|
||||
promise = this.goToAutoReview();
|
||||
this.autoReview = undefined;
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
// Refresh data.
|
||||
this.loaded = false;
|
||||
this.refreshIcon = 'spinner';
|
||||
this.syncIcon = 'spinner';
|
||||
this.content.scrollToTop();
|
||||
|
||||
promise.then(() => {
|
||||
this.refreshContent().finally(() => {
|
||||
this.loaded = true;
|
||||
this.refreshIcon = 'refresh';
|
||||
this.syncIcon = 'sync';
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.autoReview = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User left the page that contains the component.
|
||||
*/
|
||||
ionViewDidLeave(): void {
|
||||
super.ionViewDidLeave();
|
||||
this.autoReview = undefined;
|
||||
|
||||
if (this.navCtrl.getActive().component.name == 'AddonModQuizPlayerPage') {
|
||||
this.hasPlayed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected invalidateContent(): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.quizProvider.invalidateQuizData(this.courseId));
|
||||
|
||||
if (this.quizData) {
|
||||
promises.push(this.quizProvider.invalidateUserAttemptsForUser(this.quizData.id));
|
||||
promises.push(this.quizProvider.invalidateQuizAccessInformation(this.quizData.id));
|
||||
promises.push(this.quizProvider.invalidateQuizRequiredQtypes(this.quizData.id));
|
||||
promises.push(this.quizProvider.invalidateAttemptAccessInformation(this.quizData.id));
|
||||
promises.push(this.quizProvider.invalidateCombinedReviewOptionsForUser(this.quizData.id));
|
||||
promises.push(this.quizProvider.invalidateUserBestGradeForUser(this.quizData.id));
|
||||
promises.push(this.quizProvider.invalidateGradeFromGradebook(this.courseId));
|
||||
}
|
||||
|
||||
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 (syncEventData.attemptFinished) {
|
||||
// An attempt was finished, check completion status.
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
}
|
||||
|
||||
if (this.quizData && syncEventData.quizId == this.quizData.id) {
|
||||
this.content.scrollToTop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a quiz to attempt it.
|
||||
*/
|
||||
protected openQuiz(): void {
|
||||
this.navCtrl.push('AddonModQuizPlayerPage', {courseId: this.courseId, quizId: this.quiz.id, moduleUrl: this.module.url});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays some data based on the current status.
|
||||
*
|
||||
* @param {string} status The current status.
|
||||
* @param {string} [previousStatus] The previous status. If not defined, there is no previous status.
|
||||
*/
|
||||
protected showStatus(status: string, previousStatus?: string): void {
|
||||
this.showStatusSpinner = status == CoreConstants.DOWNLOADING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected sync(): Promise<any> {
|
||||
return this.quizSync.syncQuiz(this.quizData, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat user attempts.
|
||||
*
|
||||
* @param {any} attempts The attempts to treat.
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected treatAttempts(attempts: any): Promise<any> {
|
||||
if (!attempts || !attempts.length) {
|
||||
// There are no attempts to treat.
|
||||
return Promise.resolve(attempts);
|
||||
}
|
||||
|
||||
const lastFinished = this.quizProvider.getLastFinishedAttemptFromList(attempts),
|
||||
promises = [];
|
||||
|
||||
if (this.autoReview && lastFinished && lastFinished.id >= this.autoReview.attemptId) {
|
||||
// User just finished an attempt in offline and it seems it's been synced, since it's finished in online.
|
||||
// Go to the review of this attempt if the user hasn't left this view.
|
||||
if (!this.isDestroyed && this.isCurrentView) {
|
||||
promises.push(this.goToAutoReview());
|
||||
}
|
||||
this.autoReview = undefined;
|
||||
}
|
||||
|
||||
// Load flag to show if attempts are finished but not synced.
|
||||
promises.push(this.quizProvider.loadFinishedOfflineData(attempts));
|
||||
|
||||
// Get combined review options.
|
||||
promises.push(this.quizProvider.getCombinedReviewOptions(this.quizData.id).then((result) => {
|
||||
this.options = result;
|
||||
}));
|
||||
|
||||
// Get best grade.
|
||||
promises.push(this.quizProvider.getUserBestGrade(this.quizData.id).then((best) => {
|
||||
this.bestGrade = best;
|
||||
|
||||
// Get gradebook grade.
|
||||
return this.quizProvider.getGradeFromGradebook(this.courseId, this.module.id).then((data) => {
|
||||
this.gradebookData = {
|
||||
grade: data.graderaw,
|
||||
feedback: data.feedback
|
||||
};
|
||||
}).catch(() => {
|
||||
// Fallback to quiz best grade if failure or not found.
|
||||
this.gradebookData = {
|
||||
grade: this.bestGrade.grade
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
const grade: number = typeof this.gradebookData.grade != 'undefined' ? this.gradebookData.grade : this.bestGrade.grade,
|
||||
quizGrade = this.quizProvider.formatGrade(grade, this.quizData.decimalpoints);
|
||||
|
||||
// Calculate data to construct the header of the attempts table.
|
||||
this.quizHelper.setQuizCalculatedData(this.quizData, this.options);
|
||||
|
||||
this.overallStats = lastFinished && this.options.alloptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX;
|
||||
|
||||
// Calculate data to show for each attempt.
|
||||
attempts.forEach((attempt) => {
|
||||
// Highlight the highest grade if appropriate.
|
||||
const shouldHighlight = this.overallStats && this.quizData.grademethod == AddonModQuizProvider.GRADEHIGHEST &&
|
||||
attempts.length > 1;
|
||||
|
||||
this.quizHelper.setAttemptCalculatedData(this.quizData, attempt, shouldHighlight, quizGrade);
|
||||
});
|
||||
|
||||
return attempts;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
|
||||
this.finishedObserver && this.finishedObserver.off();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"attemptfirst": "First attempt",
|
||||
"attemptlast": "Last attempt",
|
||||
"attemptnumber": "Attempt",
|
||||
"attemptquiznow": "Attempt quiz now",
|
||||
"attemptstate": "State",
|
||||
"cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:",
|
||||
"comment": "Comment",
|
||||
"completedon": "Completed on",
|
||||
"confirmclose": "Once you submit, you will no longer be able to change your answers for this attempt.",
|
||||
"confirmcontinueoffline": "This attempt has not been synchronised since {{$a}}. If you have continued this attempt in another device since then, you may lose data.",
|
||||
"confirmleavequizonerror": "An error occurred while saving the answers. Are you sure you want to leave the quiz?",
|
||||
"confirmstart": "The quiz has a time limit of {{$a}}. Time will count down from the moment you start your attempt and you must submit before it expires. Are you sure that you wish to start now?",
|
||||
"confirmstartheader": "Timed quiz",
|
||||
"connectionerror": "Network connection lost. (Autosave failed).\n\nMake a note of any responses entered on this page in the last few minutes, then try to re-connect.\n\nOnce connection has been re-established, your responses should be saved and this message will disappear.",
|
||||
"continueattemptquiz": "Continue the last attempt",
|
||||
"continuepreview": "Continue the last preview",
|
||||
"errorbehaviournotsupported": "This quiz can't be attempted in the app because the question behaviour is not supported by the app:",
|
||||
"errordownloading": "Error downloading required data.",
|
||||
"errorgetattempt": "Error getting attempt data.",
|
||||
"errorgetquestions": "Error getting questions.",
|
||||
"errorgetquiz": "Error getting quiz data.",
|
||||
"errorparsequestions": "An error occurred while reading the questions. Please attempt this quiz in a web browser.",
|
||||
"errorquestionsnotsupported": "This quiz can't be attempted in the app because it contains questions not supported by the app:",
|
||||
"errorrulesnotsupported": "This quiz can't be attempted in the app because it has access rules not supported by the app:",
|
||||
"errorsaveattempt": "An error occurred while saving the attempt data.",
|
||||
"feedback": "Feedback",
|
||||
"finishattemptdots": "Finish attempt...",
|
||||
"finishnotsynced": "Finished but not synchronised",
|
||||
"grade": "Grade",
|
||||
"gradeaverage": "Average grade",
|
||||
"gradehighest": "Highest grade",
|
||||
"grademethod": "Grading method",
|
||||
"gradesofar": "{{$a.method}}: {{$a.mygrade}} / {{$a.quizgrade}}.",
|
||||
"hasdatatosync": "This quiz has offline data to be synchronised.",
|
||||
"marks": "Marks",
|
||||
"mustbesubmittedby": "This attempt must be submitted by {{$a}}.",
|
||||
"noquestions": "No questions have been added yet",
|
||||
"noreviewattempt": "You are not allowed to review this attempt.",
|
||||
"notyetgraded": "Not yet graded",
|
||||
"opentoc": "Open navigation popover",
|
||||
"outof": "{{$a.grade}} out of {{$a.maxgrade}}",
|
||||
"outofpercent": "{{$a.grade}} out of {{$a.maxgrade}} ({{$a.percent}}%)",
|
||||
"outofshort": "{{$a.grade}}/{{$a.maxgrade}}",
|
||||
"overallfeedback": "Overall feedback",
|
||||
"overdue": "Overdue",
|
||||
"overduemustbesubmittedby": "This attempt is now overdue. It should already have been submitted. If you would like this quiz to be graded, you must submit it by {{$a}}. If you do not submit it by then, no marks from this attempt will be counted.",
|
||||
"preview": "Preview",
|
||||
"previewquiznow": "Preview quiz now",
|
||||
"question": "Question",
|
||||
"quiznavigation": "Quiz navigation",
|
||||
"quizpassword": "Quiz password",
|
||||
"reattemptquiz": "Re-attempt quiz",
|
||||
"requirepasswordmessage": "To attempt this quiz you need to know the quiz password",
|
||||
"returnattempt": "Return to attempt",
|
||||
"review": "Review",
|
||||
"reviewofattempt": "Review of attempt {{$a}}",
|
||||
"reviewofpreview": "Review of preview",
|
||||
"showall": "Show all questions on one page",
|
||||
"showeachpage": "Show one page at a time",
|
||||
"startattempt": "Start attempt",
|
||||
"startedon": "Started on",
|
||||
"stateabandoned": "Never submitted",
|
||||
"statefinished": "Finished",
|
||||
"statefinisheddetails": "Submitted {{$a}}",
|
||||
"stateinprogress": "In progress",
|
||||
"stateoverdue": "Overdue",
|
||||
"stateoverduedetails": "Must be submitted by {{$a}}",
|
||||
"status": "Status",
|
||||
"submitallandfinish": "Submit all and finish",
|
||||
"summaryofattempt": "Summary of attempt",
|
||||
"summaryofattempts": "Summary of your previous attempts",
|
||||
"timeleft": "Time left",
|
||||
"timetaken": "Time taken",
|
||||
"warningattemptfinished": "Offline attempt discarded as it was finished on the site or not found.",
|
||||
"warningdatadiscarded": "Some offline answers were discarded because the questions were modified online.",
|
||||
"warningdatadiscardedfromfinished": "Attempt unfinished because some offline answers were discarded. Please review your answers then resubmit the attempt.",
|
||||
"yourfinalgradeis": "Your final grade for this quiz is {{$a}}."
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text *ngIf="quiz" [text]="quiz.name"></core-format-text></ion-title>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="loaded" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list *ngIf="attempt">
|
||||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptnumber' | translate }}</p>
|
||||
<p *ngIf="attempt.preview">{{ 'addon.mod_quiz.preview' | translate }}</p>
|
||||
<p *ngIf="!attempt.preview">{{ attempt.attempt }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p>
|
||||
<p *ngFor="let sentence of attempt.readableState">{{ sentence }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="quiz.showMarkColumn && attempt.readableMark !== ''">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }} / {{ quiz.sumGradesFormatted }}</p>
|
||||
<p>{{ attempt.readableMark }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="quiz.showGradeColumn && attempt.readableGrade !== ''">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }} / {{ quiz.gradeFormatted }}</p>
|
||||
<p>{{ attempt.readableGrade }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="quiz.showFeedbackColumn && attempt.feedback">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.feedback' | translate }}</p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="attempt.feedback"></core-format-text></p>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="quiz.showReviewColumn && attempt.finished">
|
||||
<button ion-button block icon-start [navPush]="'AddonModQuizReviewPage'" [navParams]="{courseId: courseId, quizId: quiz.id, attemptId: attempt.id}">
|
||||
<ion-icon name="search"></ion-icon>
|
||||
{{ 'addon.mod_quiz.review' | translate }}
|
||||
</button>
|
||||
</ion-item>
|
||||
<ion-item text-wrap class="core-danger-item" *ngIf="!quiz.showReviewColumn">
|
||||
<p>{{ 'addon.mod_quiz.noreviewattempt' | translate }}</p>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,33 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModQuizAttemptPage } from './attempt';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizAttemptPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
IonicPageModule.forChild(AddonModQuizAttemptPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModQuizAttemptPageModule {}
|
|
@ -0,0 +1,177 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { AddonModQuizProvider } from '../../providers/quiz';
|
||||
import { AddonModQuizHelperProvider } from '../../providers/helper';
|
||||
|
||||
/**
|
||||
* Page that displays some summary data about an attempt.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-quiz-attempt' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-attempt',
|
||||
templateUrl: 'attempt.html',
|
||||
})
|
||||
export class AddonModQuizAttemptPage implements OnInit {
|
||||
courseId: number; // The course ID the quiz belongs to.
|
||||
quiz: any; // The quiz the attempt belongs to.
|
||||
attempt: any; // The attempt to view.
|
||||
component = AddonModQuizProvider.COMPONENT; // Component to link the files to.
|
||||
componentId: number; // Component ID to use in conjunction with the component.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
|
||||
protected attemptId: number; // Attempt to view.
|
||||
protected quizId: number; // ID of the quiz the attempt belongs to.
|
||||
|
||||
constructor(navParams: NavParams, protected domUtils: CoreDomUtilsProvider, protected quizProvider: AddonModQuizProvider,
|
||||
protected quizHelper: AddonModQuizHelperProvider) {
|
||||
|
||||
this.attemptId = navParams.get('attemptId');
|
||||
this.quizId = navParams.get('quizId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchQuizData().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} refresher Refresher.
|
||||
*/
|
||||
doRefresh(refresher: any): void {
|
||||
this.refreshData().finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quiz data and attempt data.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchQuizData(): Promise<void> {
|
||||
return this.quizProvider.getQuizById(this.courseId, this.quizId).then((quizData) => {
|
||||
this.quiz = quizData;
|
||||
this.componentId = this.quiz.coursemodule;
|
||||
|
||||
return this.fetchAttempt();
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'addon.mod_quiz.errorgetattempt', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attempt data.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchAttempt(): Promise<void> {
|
||||
const promises = [];
|
||||
let options,
|
||||
accessInfo;
|
||||
|
||||
// Get all the attempts and search the one we want.
|
||||
promises.push(this.quizProvider.getUserAttempts(this.quizId).then((attempts) => {
|
||||
for (let i = 0; i < attempts.length; i++) {
|
||||
const attempt = attempts[i];
|
||||
if (attempt.id == this.attemptId) {
|
||||
this.attempt = attempt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.attempt) {
|
||||
// Attempt not found, error.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
// Load flag to show if attempt is finished but not synced.
|
||||
return this.quizProvider.loadFinishedOfflineData([this.attempt]);
|
||||
}));
|
||||
|
||||
promises.push(this.quizProvider.getCombinedReviewOptions(this.quiz.id).then((opts) => {
|
||||
options = opts;
|
||||
}));
|
||||
|
||||
// Check if the user can review the attempt.
|
||||
promises.push(this.quizProvider.getQuizAccessInformation(this.quiz.id).then((quizAccessInfo) => {
|
||||
accessInfo = quizAccessInfo;
|
||||
|
||||
if (accessInfo.canreviewmyattempts) {
|
||||
return this.quizProvider.getAttemptReview(this.attemptId, -1).catch(() => {
|
||||
// Error getting the review, assume the user cannot review the attempt.
|
||||
accessInfo.canreviewmyattempts = false;
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
|
||||
// Determine fields to show.
|
||||
this.quizHelper.setQuizCalculatedData(this.quiz, options);
|
||||
this.quiz.showReviewColumn = accessInfo.canreviewmyattempts;
|
||||
|
||||
// Get readable data for the attempt.
|
||||
this.quizHelper.setAttemptCalculatedData(this.quiz, this.attempt, false);
|
||||
|
||||
// Check if the feedback should be displayed.
|
||||
const grade = Number(this.attempt.rescaledGrade);
|
||||
if (this.quiz.showFeedbackColumn && this.quizProvider.isAttemptFinished(this.attempt.state) &&
|
||||
options.someoptions.overallfeedback && !isNaN(grade)) {
|
||||
|
||||
// Feedback should be displayed, get the feedback for the grade.
|
||||
return this.quizProvider.getFeedbackForGrade(this.quiz.id, grade).then((response) => {
|
||||
this.attempt.feedback = response.feedbacktext;
|
||||
});
|
||||
} else {
|
||||
delete this.attempt.feedback;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected refreshData(): Promise<void> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.quizProvider.invalidateQuizData(this.courseId));
|
||||
promises.push(this.quizProvider.invalidateUserAttemptsForUser(this.quizId));
|
||||
promises.push(this.quizProvider.invalidateQuizAccessInformation(this.quizId));
|
||||
promises.push(this.quizProvider.invalidateCombinedReviewOptionsForUser(this.quizId));
|
||||
promises.push(this.quizProvider.invalidateAttemptReview(this.attemptId));
|
||||
|
||||
if (this.attempt && typeof this.attempt.feedback != 'undefined') {
|
||||
promises.push(this.quizProvider.invalidateFeedback(this.quizId));
|
||||
}
|
||||
|
||||
return Promise.all(promises).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.fetchQuizData();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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]="quizComponent.loaded" (ionRefresh)="quizComponent.doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<addon-mod-quiz-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-quiz-index>
|
||||
</ion-content>
|
|
@ -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 { AddonModQuizComponentsModule } from '../../components/components.module';
|
||||
import { AddonModQuizIndexPage } from './index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizIndexPage,
|
||||
],
|
||||
imports: [
|
||||
CoreDirectivesModule,
|
||||
AddonModQuizComponentsModule,
|
||||
IonicPageModule.forChild(AddonModQuizIndexPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModQuizIndexPageModule {}
|
|
@ -0,0 +1,62 @@
|
|||
// (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 { AddonModQuizIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays the quiz entry page.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-quiz-index' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModQuizIndexPage {
|
||||
@ViewChild(AddonModQuizIndexComponent) quizComponent: AddonModQuizIndexComponent;
|
||||
|
||||
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 quiz instance.
|
||||
*
|
||||
* @param {any} quiz Quiz instance.
|
||||
*/
|
||||
updateData(quiz: any): void {
|
||||
this.title = quiz.name || this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
this.quizComponent.ionViewDidEnter();
|
||||
}
|
||||
|
||||
/**
|
||||
* User left the page.
|
||||
*/
|
||||
ionViewDidLeave(): void {
|
||||
this.quizComponent.ionViewDidLeave();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ 'addon.mod_quiz.quiznavigation' | translate }}</ion-title>
|
||||
<ion-buttons end>
|
||||
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content class="addon-mod_quiz-navigation-modal">
|
||||
<nav>
|
||||
<ion-list>
|
||||
<!-- In player, show button to finish attempt. -->
|
||||
<a ion-item text-wrap *ngIf="!isReview" (click)="loadPage(-1)">
|
||||
{{ 'addon.mod_quiz.finishattemptdots' | translate }}
|
||||
</a>
|
||||
|
||||
<!-- In review we can toggle between all questions in same page or one page at a time. -->
|
||||
<a ion-item text-wrap *ngIf="isReview && pageInstance.numPages > 1" (click)="switchMode()">
|
||||
<span *ngIf="!pageInstance.showAll">{{ 'addon.mod_quiz.showall' | translate }}</span>
|
||||
<span *ngIf="pageInstance.showAll">{{ 'addon.mod_quiz.showeachpage' | translate }}</span>
|
||||
</a>
|
||||
<a ion-item text-wrap *ngFor="let question of pageInstance.navigation" class="{{question.stateClass}}" [ngClass]='{"addon-mod_quiz-selected": !pageInstance.showSummary && pageInstance.attempt.currentpage == question.page}' (click)="loadPage(question.page, question.slot)">
|
||||
<span *ngIf="question.number">{{ 'core.question.questionno' | translate:{$a: question.number} }}</span>
|
||||
<span *ngIf="!question.number">{{ 'core.question.information' | translate }}</span>
|
||||
</a>
|
||||
|
||||
<!-- In player, show button to finish attempt. -->
|
||||
<a ion-item text-wrap *ngIf="!isReview" (click)="loadPage(-1)">
|
||||
{{ 'addon.mod_quiz.finishattemptdots' | translate }}
|
||||
</a>
|
||||
|
||||
<!-- In review we can toggle between all questions in same page or one page at a time. -->
|
||||
<a ion-item text-wrap *ngIf="isReview && pageInstance.numPages > 1" (click)="switchMode()">
|
||||
<span *ngIf="!pageInstance.showAll">{{ 'addon.mod_quiz.showall' | translate }}</span>
|
||||
<span *ngIf="pageInstance.showAll">{{ 'addon.mod_quiz.showeachpage' | translate }}</span>
|
||||
</a>
|
||||
</ion-list>
|
||||
</nav>
|
||||
</ion-content>
|
|
@ -0,0 +1,29 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { AddonModQuizNavigationModalPage } from './navigation-modal';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizNavigationModalPage
|
||||
],
|
||||
imports: [
|
||||
IonicPageModule.forChild(AddonModQuizNavigationModalPage),
|
||||
TranslateModule.forChild()
|
||||
]
|
||||
})
|
||||
export class AddonModQuizNavigationModalPageModule {}
|
|
@ -0,0 +1,5 @@
|
|||
page-addon-mod-quiz-navigation-modal {
|
||||
.addon-mod_quiz-selected, .item.addon-mod_quiz-selected {
|
||||
background: $blue-light;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// (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, ViewController, NavParams } from 'ionic-angular';
|
||||
|
||||
/**
|
||||
* Modal that renders the quiz navigation.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-quiz-navigation-modal' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-navigation-modal',
|
||||
templateUrl: 'navigation-modal.html',
|
||||
})
|
||||
export class AddonModQuizNavigationModalPage {
|
||||
|
||||
/**
|
||||
* The instance of the page that opened the modal. We use the instance instead of the needed attributes for these reasons:
|
||||
* - Some attributes can change dynamically, and we don't want to create the modal everytime the user opens it.
|
||||
* - The onDidDismiss function takes a while to be called, making the app seem slow. This way we can directly call
|
||||
* the functions we need without having to wait for the modal to be dismissed.
|
||||
* @type {any}
|
||||
*/
|
||||
pageInstance: any;
|
||||
|
||||
isReview: boolean; // Whether the user is reviewing the attempt.
|
||||
|
||||
constructor(params: NavParams, protected viewCtrl: ViewController) {
|
||||
this.isReview = !!params.get('isReview');
|
||||
this.pageInstance = params.get('page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
this.viewCtrl.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a certain page.
|
||||
*
|
||||
* @param {number} page The page to load.
|
||||
* @param {number} [slot] Slot of the question to scroll to.
|
||||
*/
|
||||
loadPage(page: number, slot: number): void {
|
||||
this.pageInstance.changePage && this.pageInstance.changePage(page, true, slot);
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch mode in review.
|
||||
*/
|
||||
switchMode(): void {
|
||||
this.pageInstance.switchMode && this.pageInstance.switchMode();
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text *ngIf="quiz" [text]="quiz.name"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<button id="addon-mod_quiz-connection-error-button" ion-button icon-only [hidden]="!autoSaveError" (click)="showConnectionError($event)" [attr.aria-label]="'core.error' | translate">
|
||||
<ion-icon name="alert"></ion-icon>
|
||||
</button>
|
||||
<button *ngIf="navigation && navigation.length" ion-button icon-only [attr.aria-label]="'addon.mod_quiz.opentoc' | translate" (click)="navigationModal.present()">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<!-- Navigation arrows and time left. -->
|
||||
<ion-toolbar *ngIf="loaded && endTime && questions && questions.length && !quizAborted && !showSummary" color="light" ion-fixed>
|
||||
<ion-title>
|
||||
<core-timer [endTime]="endTime" (finished)="timeUp()" [timerText]="'addon.mod_quiz.timeleft' | translate" align="center"></core-timer>
|
||||
</ion-title>
|
||||
<ion-buttons end>
|
||||
<a ion-button icon-only *ngIf="previousPage >= 0" (click)="changePage(previousPage)" [title]="'core.previous' | translate">
|
||||
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
|
||||
</a>
|
||||
<a ion-button icon-only *ngIf="nextPage >= -1" (click)="changePage(nextPage)" [title]="'core.next' | translate">
|
||||
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
|
||||
</a>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
<core-loading [hideUntil]="loaded" [class.core-has-fixed-timer]="endTime">
|
||||
<!-- Navigation arrows and time left. -->
|
||||
<ion-toolbar *ngIf="!endTime && questions && questions.length && !quizAborted && !showSummary" color="light">
|
||||
<ion-buttons end>
|
||||
<a ion-button icon-only *ngIf="previousPage >= 0" (click)="changePage(previousPage)" [title]="'core.previous' | translate">
|
||||
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
|
||||
</a>
|
||||
<a ion-button icon-only *ngIf="nextPage >= -1" (click)="changePage(nextPage)" [title]="'core.next' | translate">
|
||||
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
|
||||
</a>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
<!-- Button to start attempting. -->
|
||||
<div padding *ngIf="!attempt">
|
||||
<button ion-button block (click)="start()">{{ 'addon.mod_quiz.startattempt' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Questions -->
|
||||
<form name="addon-mod_quiz-player-form" *ngIf="questions && questions.length && !quizAborted && !showSummary">
|
||||
<div *ngFor="let question of questions">
|
||||
<ion-card id="addon-mod_quiz-question-{{question.slot}}">
|
||||
<!-- "Header" of the question. -->
|
||||
<ion-item-divider color="light">
|
||||
<h2 *ngIf="question.number" class="inline">{{ 'core.question.questionno' | translate:{$a: question.number} }}</h2>
|
||||
<h2 *ngIf="!question.number" class="inline">{{ 'core.question.information' | translate }}</h2>
|
||||
<ion-note text-wrap item-end *ngIf="question.status || question.readableMark">
|
||||
<p *ngIf="question.status" class="block">{{question.status}}</p>
|
||||
<p *ngIf="question.readableMark"><core-format-text [text]="question.readableMark"></core-format-text></p>
|
||||
</ion-note>
|
||||
</ion-item-divider>
|
||||
<!-- Body of the question. -->
|
||||
<core-question text-wrap [question]="question" [component]="component" [componentId]="quiz.coursemodule" [attemptId]="attempt.id" [offlineEnabled]="offline" (onAbort)="abortQuiz()" (buttonClicked)="behaviourButtonClicked($event)"></core-question>
|
||||
</ion-card>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Go to next or previous page. -->
|
||||
<ion-grid text-wrap *ngIf="questions && questions.length && !quizAborted && !showSummary">
|
||||
<ion-row>
|
||||
<ion-col *ngIf="previousPage >= 0" >
|
||||
<button ion-button block icon-start (click)="changePage(previousPage)">
|
||||
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
|
||||
{{ 'core.previous' | translate }}
|
||||
</button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="nextPage >= -1">
|
||||
<button ion-button block icon-end (click)="changePage(nextPage)">
|
||||
{{ 'core.next' | translate }}
|
||||
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
|
||||
</button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- Summary -->
|
||||
<ion-card *ngIf="!quizAborted && showSummary && summaryQuestions && summaryQuestions.length" class="addon-mod_quiz-table">
|
||||
<ion-card-header text-wrap>
|
||||
<h2>{{ 'addon.mod_quiz.summaryofattempt' | translate }}</h2>
|
||||
</ion-card-header>
|
||||
<!-- "Header" of the summary table. -->
|
||||
<ion-item text-wrap>
|
||||
<ion-row align-items-center>
|
||||
<ion-col col-3 text-center><b>{{ 'addon.mod_quiz.question' | translate }}</b></ion-col>
|
||||
<ion-col col-9 text-center><b>{{ 'addon.mod_quiz.status' | translate }}</b></ion-col>
|
||||
</ion-row>
|
||||
</ion-item>
|
||||
<!-- Lift of questions of the summary table. -->
|
||||
<ng-container *ngFor="let question of summaryQuestions">
|
||||
<a ion-item (click)="changePage(question.page, false, question.slot)" *ngIf="question.number" [attr.aria-label]="'core.question.questionno' | translate:{$a: question.number}" [attr.detail-push]="!quiz.isSequential && canReturn ? true : null">
|
||||
<ion-row align-items-center>
|
||||
<ion-col col-3 text-center>{{ question.number }}</ion-col>
|
||||
<ion-col col-9 text-center>{{ question.status }}</ion-col>
|
||||
</ion-row>
|
||||
</a>
|
||||
</ng-container>
|
||||
<!-- Button to return to last page seen. -->
|
||||
<ion-item *ngIf="canReturn">
|
||||
<a ion-button block (click)="changePage(attempt.currentpage)">{{ 'addon.mod_quiz.returnattempt' | translate }}</a>
|
||||
</ion-item>
|
||||
<!-- Due date warning. -->
|
||||
<ion-item text-wrap *ngIf="attempt.dueDateWarning">
|
||||
{{ attempt.dueDateWarning }}
|
||||
</ion-item>
|
||||
<!-- Time left (if quiz is timed). -->
|
||||
<core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()" [timerText]="'addon.mod_quiz.timeleft' | translate"></core-timer>
|
||||
<!-- List of messages explaining why the quiz cannot be submitted. -->
|
||||
<ion-item text-wrap *ngIf="preventSubmitMessages.length">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.cannotsubmitquizdueto' | translate }}</p>
|
||||
<p *ngFor="let message of preventSubmitMessages">{{message}}</p>
|
||||
<a ion-button block icon-end [href]="moduleUrl" core-link>
|
||||
<ion-icon name="open"></ion-icon>
|
||||
{{ 'core.openinbrowser' | translate }}
|
||||
</a>
|
||||
</ion-item>
|
||||
<!-- Button to submit the quiz. -->
|
||||
<ion-item *ngIf="!attempt.finishedOffline && !preventSubmitMessages.length">
|
||||
<a ion-button block (click)="finishAttempt(true)">{{ 'addon.mod_quiz.submitallandfinish' | translate }}</a>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Quiz aborted -->
|
||||
<ion-card *ngIf="attempt && (((!questions || !questions.length) && !showSummary) || quizAborted)">
|
||||
<ion-item text-wrap>
|
||||
<p>{{ 'addon.mod_quiz.errorparsequestions' | translate }}</p>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<a ion-button block icon-end [href]="moduleUrl" core-link>
|
||||
<ion-icon name="open"></ion-icon>
|
||||
{{ 'core.openinbrowser' | translate }}
|
||||
</a>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,35 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreQuestionComponentsModule } from '@core/question/components/components.module';
|
||||
import { AddonModQuizPlayerPage } from './player';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizPlayerPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreQuestionComponentsModule,
|
||||
IonicPageModule.forChild(AddonModQuizPlayerPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModQuizPlayerPageModule {}
|
|
@ -0,0 +1,10 @@
|
|||
page-addon-mod-quiz-player {
|
||||
.toolbar {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.core-has-fixed-timer form {
|
||||
padding-top: 56px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,580 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core';
|
||||
import { IonicPage, NavParams, Content, PopoverController, ModalController, Modal, NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
import { AddonModQuizProvider } from '../../providers/quiz';
|
||||
import { AddonModQuizSyncProvider } from '../../providers/quiz-sync';
|
||||
import { AddonModQuizHelperProvider } from '../../providers/helper';
|
||||
import { AddonModQuizAutoSave } from '../../classes/auto-save';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Page that allows attempting a quiz.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-quiz-player' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-player',
|
||||
templateUrl: 'player.html',
|
||||
})
|
||||
export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
|
||||
@ViewChild(Content) content: Content;
|
||||
|
||||
quiz: any; // The quiz the attempt belongs to.
|
||||
attempt: any; // The attempt being attempted.
|
||||
moduleUrl: string; // URL to the module in the site.
|
||||
component = AddonModQuizProvider.COMPONENT; // Component to link the files to.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
quizAborted: boolean; // Whether the quiz was aborted due to an error.
|
||||
offline: boolean; // Whether the quiz is being attempted in offline mode.
|
||||
navigation: any[]; // List of questions to navigate them.
|
||||
questions: any[]; // Questions of the current page.
|
||||
nextPage: number; // Next page.
|
||||
previousPage: number; // Previous page.
|
||||
showSummary: boolean; // Whether the attempt summary should be displayed.
|
||||
summaryQuestions: any[]; // The questions to display in the summary.
|
||||
canReturn: boolean; // Whether the user can return to a page after seeing the summary.
|
||||
preventSubmitMessages: string[]; // List of messages explaining why the quiz cannot be submitted.
|
||||
endTime: number; // The time when the attempt must be finished.
|
||||
autoSaveError: boolean; // Whether there's been an error in auto-save.
|
||||
navigationModal: Modal; // Modal to navigate through the questions.
|
||||
|
||||
protected courseId: number; // The course ID the quiz belongs to.
|
||||
protected quizId: number; // Quiz ID to attempt.
|
||||
protected preflightData: any = {}; // Preflight data to attempt the quiz.
|
||||
protected quizAccessInfo: any; // Quiz access information.
|
||||
protected attemptAccessInfo: any; // Attempt access info.
|
||||
protected lastAttempt: any; // Last user attempt before a new one is created (if needed).
|
||||
protected newAttempt: boolean; // Whether the user is starting a new attempt.
|
||||
protected quizDataLoaded: boolean; // Whether the quiz data has been loaded.
|
||||
protected timeUpCalled: boolean; // Whether the time up function has been called.
|
||||
protected autoSave: AddonModQuizAutoSave; // Class to auto-save answers every certain time.
|
||||
protected autoSaveErrorSubscription: Subscription; // To be notified when an error happens in auto-save.
|
||||
protected forceLeave = false; // If true, don't perform any check when leaving the view.
|
||||
|
||||
constructor(navParams: NavParams, logger: CoreLoggerProvider, protected translate: TranslateService,
|
||||
protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider,
|
||||
protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController,
|
||||
protected timeUtils: CoreTimeUtilsProvider, protected quizProvider: AddonModQuizProvider,
|
||||
protected quizHelper: AddonModQuizHelperProvider, protected quizSync: AddonModQuizSyncProvider,
|
||||
protected questionHelper: CoreQuestionHelperProvider, protected cdr: ChangeDetectorRef,
|
||||
modalCtrl: ModalController, protected navCtrl: NavController) {
|
||||
|
||||
this.quizId = navParams.get('quizId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.moduleUrl = navParams.get('moduleUrl');
|
||||
|
||||
// Block the quiz so it cannot be synced.
|
||||
this.syncProvider.blockOperation(AddonModQuizProvider.COMPONENT, this.quizId);
|
||||
|
||||
// Create the auto save instance.
|
||||
this.autoSave = new AddonModQuizAutoSave('addon-mod_quiz-player-form', '#addon-mod_quiz-connection-error-button',
|
||||
logger, popoverCtrl, questionHelper, quizProvider);
|
||||
|
||||
// Create the navigation modal.
|
||||
this.navigationModal = modalCtrl.create('AddonModQuizNavigationModalPage', {
|
||||
page: this
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Start the player when the page is loaded.
|
||||
this.start();
|
||||
|
||||
// Listen for errors on auto-save.
|
||||
this.autoSaveErrorSubscription = this.autoSave.onError().subscribe((error) => {
|
||||
this.autoSaveError = error;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
// Stop auto save.
|
||||
this.autoSave.cancelAutoSave();
|
||||
this.autoSave.stopCheckChangesProcess();
|
||||
this.autoSaveErrorSubscription && this.autoSaveErrorSubscription.unsubscribe();
|
||||
|
||||
// Unblock the quiz so it can be synced.
|
||||
this.syncProvider.unblockOperation(AddonModQuizProvider.COMPONENT, this.quizId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
ionViewCanLeave(): boolean | Promise<void> {
|
||||
if (this.forceLeave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.questions && this.questions.length && !this.showSummary) {
|
||||
// Save answers.
|
||||
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
|
||||
return this.processAttempt(false, false).catch(() => {
|
||||
// Save attempt failed. Show confirmation.
|
||||
modal.dismiss();
|
||||
|
||||
return this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmleavequizonerror'));
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the quiz.
|
||||
*/
|
||||
abortQuiz(): void {
|
||||
this.quizAborted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A behaviour button in a question was clicked (Check, Redo, ...).
|
||||
*
|
||||
* @param {any} button Clicked button.
|
||||
*/
|
||||
behaviourButtonClicked(button: any): void {
|
||||
// Confirm that the user really wants to do it.
|
||||
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
|
||||
const modal = this.domUtils.showModalLoading('core.sending', true),
|
||||
answers = this.getAnswers();
|
||||
|
||||
// Add the clicked button data.
|
||||
answers[button.name] = button.value;
|
||||
|
||||
// Behaviour checks are always in online.
|
||||
return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData).then(() => {
|
||||
// Reload the current page.
|
||||
const scrollElement = this.content.getScrollElement(),
|
||||
scrollTop = scrollElement.scrollTop || 0,
|
||||
scrollLeft = scrollElement.scrollLeft || 0;
|
||||
|
||||
this.loaded = false;
|
||||
this.content.scrollToTop(); // Scroll top so the spinner is seen.
|
||||
|
||||
return this.loadPage(this.attempt.currentpage).finally(() => {
|
||||
this.loaded = true;
|
||||
this.content.scrollTo(scrollLeft, scrollTop);
|
||||
});
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error performing action.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current page. If slot is supplied, try to scroll to that question.
|
||||
*
|
||||
* @param {number} page Page to load. -1 means summary.
|
||||
* @param {boolean} [fromModal] Whether the page was selected using the navigation modal.
|
||||
* @param {number} [slot] Slot of the question to scroll to.
|
||||
*/
|
||||
changePage(page: number, fromModal?: boolean, slot?: number): void {
|
||||
if (page != -1 && (this.attempt.state == AddonModQuizProvider.ATTEMPT_OVERDUE || this.attempt.finishedOffline)) {
|
||||
// We can't load a page if overdue or the local attempt is finished.
|
||||
return;
|
||||
} else if (page == this.attempt.currentpage && !this.showSummary && typeof slot != 'undefined') {
|
||||
// Navigating to a question in the current page.
|
||||
this.scrollToQuestion(slot);
|
||||
|
||||
return;
|
||||
} else if ((page == this.attempt.currentpage && !this.showSummary) || (fromModal && this.quiz.isSequential && page != -1)) {
|
||||
// If the user is navigating to the current page we do nothing.
|
||||
// Also, in sequential quizzes we don't allow navigating using the modal except for finishing the quiz (summary).
|
||||
return;
|
||||
} else if (page === -1 && this.showSummary) {
|
||||
// Summary already shown.
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
this.content.scrollToTop();
|
||||
|
||||
// First try to save the attempt data. We only save it if we're not seeing the summary.
|
||||
const promise = this.showSummary ? Promise.resolve() : this.processAttempt(false, false);
|
||||
promise.then(() => {
|
||||
// Attempt data successfully saved, load the page or summary.
|
||||
|
||||
// Attempt data successfully saved, load the page or summary.
|
||||
let subPromise;
|
||||
|
||||
// Stop checking for changes during page change.
|
||||
this.autoSave.stopCheckChangesProcess();
|
||||
|
||||
if (page === -1) {
|
||||
subPromise = this.loadSummary();
|
||||
} else {
|
||||
subPromise = this.loadPage(page);
|
||||
}
|
||||
|
||||
return subPromise.catch((error) => {
|
||||
// If the user isn't seeing the summary, start the check again.
|
||||
if (!this.showSummary) {
|
||||
this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline);
|
||||
}
|
||||
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true);
|
||||
});
|
||||
}, (error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorsaveattempt', true);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
|
||||
if (typeof slot != 'undefined') {
|
||||
// Scroll to the question. Give some time to the questions to render.
|
||||
setTimeout(() => {
|
||||
this.scrollToQuestion(slot);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get the quiz data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchData(): Promise<any> {
|
||||
// Wait for any ongoing sync to finish. We won't sync a quiz while it's being played.
|
||||
return this.quizSync.waitForSync(this.quizId).then(() => {
|
||||
// Sync finished, now get the quiz.
|
||||
return this.quizProvider.getQuizById(this.courseId, this.quizId);
|
||||
}).then((quizData) => {
|
||||
this.quiz = quizData;
|
||||
this.quiz.isSequential = this.quizProvider.isNavigationSequential(this.quiz);
|
||||
|
||||
if (this.quizProvider.isQuizOffline(this.quiz)) {
|
||||
// Quiz supports offline.
|
||||
return true;
|
||||
} else {
|
||||
// Quiz doesn't support offline right now, but maybe it did and then the setting was changed.
|
||||
// If we have an unfinished offline attempt then we'll use offline mode.
|
||||
return this.quizProvider.isLastAttemptOfflineUnfinished(this.quiz);
|
||||
}
|
||||
}).then((offlineMode) => {
|
||||
this.offline = offlineMode;
|
||||
|
||||
if (this.quiz.timelimit > 0) {
|
||||
this.quiz.readableTimeLimit = this.timeUtils.formatTime(this.quiz.timelimit);
|
||||
}
|
||||
|
||||
// Get access information for the quiz.
|
||||
return this.quizProvider.getQuizAccessInformation(this.quiz.id, this.offline, true);
|
||||
}).then((info) => {
|
||||
this.quizAccessInfo = info;
|
||||
|
||||
// Get user attempts to determine last attempt.
|
||||
return this.quizProvider.getUserAttempts(this.quiz.id, 'all', true, this.offline, true);
|
||||
}).then((attempts) => {
|
||||
if (!attempts.length) {
|
||||
// There are no attempts, start a new one.
|
||||
this.newAttempt = true;
|
||||
} else {
|
||||
const promises = [];
|
||||
|
||||
// Get the last attempt. If it's finished, start a new one.
|
||||
this.lastAttempt = attempts[attempts.length - 1];
|
||||
this.newAttempt = this.quizProvider.isAttemptFinished(this.lastAttempt.state);
|
||||
|
||||
// Load quiz last sync time.
|
||||
promises.push(this.quizSync.getSyncTime(this.quiz.id).then((time) => {
|
||||
this.quiz.syncTime = time;
|
||||
this.quiz.syncTimeReadable = this.quizSync.getReadableTimeFromTimestamp(time);
|
||||
}));
|
||||
|
||||
// Load flag to show if attempts are finished but not synced.
|
||||
promises.push(this.quizProvider.loadFinishedOfflineData(attempts));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish an attempt, either by timeup or because the user clicked to finish it.
|
||||
*
|
||||
* @param {boolean} [userFinish] Whether the user clicked to finish the attempt.
|
||||
* @param {boolean} [timeUp] Whether the quiz time is up.
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
finishAttempt(userFinish?: boolean, timeUp?: boolean): Promise<void> {
|
||||
let promise;
|
||||
|
||||
// Show confirm if the user clicked the finish button and the quiz is in progress.
|
||||
if (!timeUp && this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
|
||||
promise = this.domUtils.showConfirm(this.translate.instant('addon.mod_quiz.confirmclose'));
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
|
||||
return this.processAttempt(userFinish, timeUp).then(() => {
|
||||
// Trigger an event to notify the attempt was finished.
|
||||
this.eventsProvider.trigger(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, {
|
||||
quizId: this.quizId,
|
||||
attemptId: this.attempt.id,
|
||||
synced: !this.offline
|
||||
}, this.sitesProvider.getCurrentSiteId());
|
||||
|
||||
// Leave the player.
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorsaveattempt', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the input answers.
|
||||
*
|
||||
* @return {any} Object with the answers.
|
||||
*/
|
||||
protected getAnswers(): any {
|
||||
return this.questionHelper.getAnswersFromForm(document.forms['addon-mod_quiz-player-form']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the timer if enabled.
|
||||
*/
|
||||
protected initTimer(): void {
|
||||
if (this.attemptAccessInfo.endtime > 0) {
|
||||
// Quiz has an end time. Check if time left should be shown.
|
||||
if (this.quizProvider.shouldShowTimeLeft(this.quizAccessInfo.activerulenames, this.attempt,
|
||||
this.attemptAccessInfo.endtime)) {
|
||||
this.endTime = this.attemptAccessInfo.endtime;
|
||||
} else {
|
||||
delete this.endTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a page questions.
|
||||
*
|
||||
* @param {number} page The page to load.
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadPage(page: number): Promise<void> {
|
||||
return this.quizProvider.getAttemptData(this.attempt.id, page, this.preflightData, this.offline, true).then((data) => {
|
||||
// Update attempt, status could change during the execution.
|
||||
this.attempt = data.attempt;
|
||||
this.attempt.currentpage = page;
|
||||
|
||||
this.questions = data.questions;
|
||||
this.nextPage = data.nextpage;
|
||||
this.previousPage = this.quiz.isSequential ? -1 : page - 1;
|
||||
this.showSummary = false;
|
||||
|
||||
this.questions.forEach((question) => {
|
||||
// Get the readable mark for each question.
|
||||
question.readableMark = this.quizHelper.getQuestionMarkFromHtml(question.html);
|
||||
|
||||
// Extract the question info box.
|
||||
this.questionHelper.extractQuestionInfoBox(question, '.info');
|
||||
|
||||
// Set the preferred behaviour.
|
||||
question.preferredBehaviour = this.quiz.preferredbehaviour;
|
||||
|
||||
// Check if the question is blocked. If it is, treat it as a description question.
|
||||
if (this.quizProvider.isQuestionBlocked(question)) {
|
||||
question.type = 'description';
|
||||
}
|
||||
});
|
||||
|
||||
// Mark the page as viewed. We'll ignore errors in this call.
|
||||
this.quizProvider.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline).catch((error) => {
|
||||
// Ignore errors.
|
||||
});
|
||||
|
||||
// Start looking for changes.
|
||||
this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load attempt summary.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadSummary(): Promise<void> {
|
||||
this.summaryQuestions = [];
|
||||
|
||||
return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline, true, true).then((qs) => {
|
||||
this.showSummary = true;
|
||||
this.summaryQuestions = qs;
|
||||
|
||||
this.canReturn = this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS && !this.attempt.finishedOffline;
|
||||
this.preventSubmitMessages = this.quizProvider.getPreventSubmitMessages(this.summaryQuestions);
|
||||
|
||||
this.attempt.dueDateWarning = this.quizProvider.getAttemptDueDateWarning(this.quiz, this.attempt);
|
||||
|
||||
// Log summary as viewed.
|
||||
this.quizProvider.logViewAttemptSummary(this.attempt.id, this.preflightData).catch((error) => {
|
||||
// Ignore errors.
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data to navigate the questions using the navigation modal.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadNavigation(): Promise<void> {
|
||||
// We use the attempt summary to build the navigation because it contains all the questions.
|
||||
return this.quizProvider.getAttemptSummary(this.attempt.id, this.preflightData, this.offline).then((questions) => {
|
||||
this.navigation = questions;
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare the answers to be sent for the attempt.
|
||||
protected prepareAnswers(): Promise<any> {
|
||||
return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process attempt.
|
||||
*
|
||||
* @param {boolean} [userFinish] Whether the user clicked to finish the attempt.
|
||||
* @param {boolean} [timeUp] Whether the quiz time is up.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected processAttempt(userFinish?: boolean, timeUp?: boolean): Promise<any> {
|
||||
// Get the answers to send.
|
||||
return this.prepareAnswers().then((answers) => {
|
||||
// Send the answers.
|
||||
return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData, userFinish, timeUp,
|
||||
this.offline);
|
||||
}).then(() => {
|
||||
// Answers saved, cancel auto save.
|
||||
this.autoSave.cancelAutoSave();
|
||||
this.autoSave.hideAutoSaveError();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a certain question.
|
||||
*
|
||||
* @param {number} slot Slot of the question to scroll to.
|
||||
*/
|
||||
protected scrollToQuestion(slot: number): void {
|
||||
this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_quiz-question-' + slot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show connection error.
|
||||
*
|
||||
* @param {Event} ev Click event.
|
||||
*/
|
||||
showConnectionError(ev: Event): void {
|
||||
this.autoSave.showAutoSaveError(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to start the player.
|
||||
*/
|
||||
start(): void {
|
||||
let promise;
|
||||
this.loaded = false;
|
||||
|
||||
if (this.quizDataLoaded) {
|
||||
// Quiz data has been loaded, try to start or continue.
|
||||
promise = this.startOrContinueAttempt();
|
||||
} else {
|
||||
// Fetch data.
|
||||
promise = this.fetchData().then(() => {
|
||||
this.quizDataLoaded = true;
|
||||
|
||||
return this.startOrContinueAttempt();
|
||||
});
|
||||
}
|
||||
|
||||
promise.finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start or continue an attempt.
|
||||
*
|
||||
* @return {Promise<any>} [description]
|
||||
*/
|
||||
protected startOrContinueAttempt(): Promise<any> {
|
||||
const attempt = this.newAttempt ? undefined : this.lastAttempt;
|
||||
|
||||
// Get the preflight data and start attempt if needed.
|
||||
return this.quizHelper.getAndCheckPreflightData(this.quiz, this.quizAccessInfo, this.preflightData, attempt, this.offline,
|
||||
false, 'addon.mod_quiz.startattempt').then((attempt) => {
|
||||
|
||||
// Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created).
|
||||
return this.quizProvider.getAttemptAccessInformation(this.quiz.id, attempt.id, this.offline, true).then((info) => {
|
||||
this.attemptAccessInfo = info;
|
||||
this.attempt = attempt;
|
||||
|
||||
return this.loadNavigation();
|
||||
}).then(() => {
|
||||
if (this.attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !this.attempt.finishedOffline) {
|
||||
// Attempt not overdue and not finished in offline, load page.
|
||||
return this.loadPage(this.attempt.currentpage).then(() => {
|
||||
this.initTimer();
|
||||
});
|
||||
} else {
|
||||
// Attempt is overdue or finished in offline, we can only load the summary.
|
||||
return this.loadSummary();
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quiz time has finished.
|
||||
*/
|
||||
timeUp(): void {
|
||||
if (this.timeUpCalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.timeUpCalled = true;
|
||||
this.finishAttempt(false, true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ title | translate }}</ion-title>
|
||||
<ion-buttons end>
|
||||
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content padding class="addon-mod_quiz-preflight-modal">
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<form ion-list [formGroup]="preflightForm" (ngSubmit)="sendData()">
|
||||
<!-- Access rules. -->
|
||||
<ng-container *ngFor="let componentClass of accessRulesComponent; let last = last">
|
||||
<core-dynamic-component [component]="componentClass" [data]="data">
|
||||
<p padding>Couldn't find the directive to render this access rule.</p>
|
||||
</core-dynamic-component>
|
||||
<ion-item-divider color="light" *ngIf="!last"></ion-item-divider>
|
||||
</ng-container>
|
||||
|
||||
<button ion-button block type="submit">
|
||||
{{ title | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,31 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { AddonModQuizPreflightModalPage } from './preflight-modal';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizPreflightModalPage
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
IonicPageModule.forChild(AddonModQuizPreflightModalPage),
|
||||
TranslateModule.forChild()
|
||||
]
|
||||
})
|
||||
export class AddonModQuizPreflightModalModule {}
|
|
@ -0,0 +1,122 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Injector, ViewChild } from '@angular/core';
|
||||
import { IonicPage, ViewController, NavParams, Content } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { AddonModQuizAccessRuleDelegate } from '../../providers/access-rules-delegate';
|
||||
|
||||
/**
|
||||
* Modal that renders the access rules for a quiz.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-quiz-preflight-modal' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-preflight-modal',
|
||||
templateUrl: 'preflight-modal.html',
|
||||
})
|
||||
export class AddonModQuizPreflightModalPage implements OnInit {
|
||||
|
||||
@ViewChild(Content) content: Content;
|
||||
|
||||
preflightForm: FormGroup;
|
||||
title: string;
|
||||
accessRulesComponent: any[] = [];
|
||||
data: any;
|
||||
loaded: boolean;
|
||||
|
||||
protected quiz: any;
|
||||
protected attempt: any;
|
||||
protected prefetch: boolean;
|
||||
protected siteId: string;
|
||||
protected rules: string[];
|
||||
protected renderedRules: string[] = [];
|
||||
|
||||
constructor(params: NavParams, fb: FormBuilder, translate: TranslateService, sitesProvider: CoreSitesProvider,
|
||||
protected viewCtrl: ViewController, protected accessRuleDelegate: AddonModQuizAccessRuleDelegate,
|
||||
protected injector: Injector, protected domUtils: CoreDomUtilsProvider) {
|
||||
|
||||
this.title = params.get('title') || translate.instant('addon.mod_quiz.startattempt');
|
||||
this.quiz = params.get('quiz');
|
||||
this.attempt = params.get('attempt');
|
||||
this.prefetch = params.get('prefetch');
|
||||
this.siteId = params.get('siteId') || sitesProvider.getCurrentSiteId();
|
||||
this.rules = params.get('rules') || [];
|
||||
|
||||
// Create an empty form group. The controls will be added by the access rules components.
|
||||
this.preflightForm = fb.group({});
|
||||
|
||||
// Create the data to pass to the access rules components.
|
||||
this.data = {
|
||||
quiz: this.quiz,
|
||||
attempt: this.attempt,
|
||||
prefetch: this.prefetch,
|
||||
form: this.preflightForm,
|
||||
siteId: this.siteId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const promises = [];
|
||||
|
||||
this.rules.forEach((rule) => {
|
||||
// Check if preflight is required for rule and, if so, get the component to render it.
|
||||
promises.push(this.accessRuleDelegate.isPreflightCheckRequiredForRule(rule, this.quiz, this.attempt, this.prefetch,
|
||||
this.siteId).then((required) => {
|
||||
|
||||
if (required) {
|
||||
return this.accessRuleDelegate.getPreflightComponent(rule, this.injector).then((component) => {
|
||||
if (component) {
|
||||
this.renderedRules.push(rule);
|
||||
this.accessRulesComponent.push(component);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Promise.all(promises).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error loading rules');
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the data is valid and send it back.
|
||||
*/
|
||||
sendData(): void {
|
||||
if (!this.preflightForm.valid) {
|
||||
// Form not valid. Scroll to the first element with errors.
|
||||
if (!this.domUtils.scrollToInputError(this.content)) {
|
||||
// Input not found, show an error modal.
|
||||
this.domUtils.showErrorModal('core.errorinvalidform', true);
|
||||
}
|
||||
} else {
|
||||
this.viewCtrl.dismiss(this.preflightForm.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
this.viewCtrl.dismiss();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ 'addon.mod_quiz.review' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<button *ngIf="navigation && navigation.length" ion-button icon-only [attr.aria-label]="'addon.mod_quiz.opentoc' | translate" (click)="navigationModal.present()">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
|
||||
<!-- Review summary -->
|
||||
<ion-card *ngIf="attempt">
|
||||
<ion-card-header text-wrap>
|
||||
<h2 *ngIf="attempt.preview">{{ 'addon.mod_quiz.reviewofpreview' | translate }}</h2>
|
||||
<h2 *ngIf="!attempt.preview">{{ 'addon.mod_quiz.reviewofattempt' | translate:{$a: attempt.attempt} }}</h2>
|
||||
</ion-card-header>
|
||||
<ion-list>
|
||||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.startedon' | translate }}</p>
|
||||
<p>{{ attempt.timestart * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap>
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.attemptstate' | translate }}</p>
|
||||
<p>{{ attempt.readableState }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="showCompleted">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.completedon' | translate }}</p>
|
||||
<p>{{ attempt.timefinish * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.timeTaken">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.timetaken' | translate }}</p>
|
||||
<p>{{ attempt.timeTaken }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.overTime">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.overdue' | translate }}</p>
|
||||
<p>{{ attempt.overTime }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.readableMark">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.marks' | translate }}</p>
|
||||
<p><core-format-text [text]="attempt.readableMark"></core-format-text></p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="attempt.readableGrade">
|
||||
<p class="item-heading">{{ 'addon.mod_quiz.grade' | translate }}</p>
|
||||
<p>{{ attempt.readableGrade }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngFor="let data of additionalData">
|
||||
<p class="item-heading">{{ data.title }}</p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="data.content"></core-format-text>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-card>
|
||||
|
||||
<!-- Questions -->
|
||||
<div *ngIf="attempt && questions.length">
|
||||
<!-- Arrows to go to next/previous. -->
|
||||
<ng-container *ngTemplateOutlet="navArrows"></ng-container>
|
||||
|
||||
<!-- Questions. -->
|
||||
<div *ngFor="let question of questions">
|
||||
<ion-card id="addon-mod_quiz-question-{{question.slot}}">
|
||||
<!-- "Header" of the question. -->
|
||||
<ion-item-divider color="light">
|
||||
<h2 *ngIf="question.number" class="inline">{{ 'core.question.questionno' | translate:{$a: question.number} }}</h2>
|
||||
<h2 *ngIf="!question.number" class="inline">{{ 'core.question.information' | translate }}</h2>
|
||||
<ion-note text-wrap item-end *ngIf="question.status || question.readableMark">
|
||||
<p *ngIf="question.status" class="block">{{question.status}}</p>
|
||||
<p *ngIf="question.readableMark"><core-format-text [text]="question.readableMark"></core-format-text></p>
|
||||
</ion-note>
|
||||
</ion-item-divider>
|
||||
<!-- Body of the question. -->
|
||||
<core-question text-wrap [question]="question" [component]="component" [componentId]="componentId" [attemptId]="attempt.id" [offlineEnabled]="false"></core-question>
|
||||
</ion-card>
|
||||
</div>
|
||||
|
||||
<!-- Arrows to go to next/previous. -->
|
||||
<ng-container *ngTemplateOutlet="navArrows"></ng-container>
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
<!-- Arrows to go to next/previous. -->
|
||||
<ng-template #navArrows>
|
||||
<ion-row align-items-center>
|
||||
<ion-col>
|
||||
<a ion-button icon-only color="light" *ngIf="previousPage >= 0" (click)="changePage(previousPage)" [title]="'core.previous' | translate">
|
||||
<ion-icon name="arrow-back" md="ios-arrow-back"></ion-icon>
|
||||
</a>
|
||||
</ion-col>
|
||||
<ion-col text-right>
|
||||
<a ion-button icon-only color="light" *ngIf="nextPage >= -1" (click)="changePage(nextPage)" [title]="'core.next' | translate">
|
||||
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
|
||||
</a>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ng-template>
|
|
@ -0,0 +1,37 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { CoreQuestionComponentsModule } from '@core/question/components/components.module';
|
||||
import { AddonModQuizReviewPage } from './review';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModQuizReviewPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
CoreQuestionComponentsModule,
|
||||
IonicPageModule.forChild(AddonModQuizReviewPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModQuizReviewPageModule {}
|
|
@ -0,0 +1,11 @@
|
|||
page-addon-mod-quiz-review {
|
||||
.item-radio-disabled,
|
||||
.item-checkbox-disabled,
|
||||
.text-input[disabled] {
|
||||
opacity: 1;
|
||||
|
||||
.label, .radio, .checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { IonicPage, NavParams, Content, ModalController, Modal } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
import { AddonModQuizProvider } from '../../providers/quiz';
|
||||
import { AddonModQuizHelperProvider } from '../../providers/helper';
|
||||
|
||||
/**
|
||||
* Page that allows reviewing a quiz attempt.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-quiz-review' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-quiz-review',
|
||||
templateUrl: 'review.html',
|
||||
})
|
||||
export class AddonModQuizReviewPage implements OnInit {
|
||||
@ViewChild(Content) content: Content;
|
||||
|
||||
attempt: any; // The attempt being reviewed.
|
||||
component = AddonModQuizProvider.COMPONENT; // Component to link the files to.
|
||||
componentId: number; // ID to use in conjunction with the component.
|
||||
showAll: boolean; // Whether to view all questions in the same page.
|
||||
numPages: number; // Number of pages.
|
||||
showCompleted: boolean; // Whether to show completed time.
|
||||
additionalData: any[]; // Additional data to display for the attempt.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
navigation: any[]; // List of questions to navigate them.
|
||||
questions: any[]; // Questions of the current page.
|
||||
nextPage: number; // Next page.
|
||||
previousPage: number; // Previous page.
|
||||
navigationModal: Modal; // Modal to navigate through the questions.
|
||||
|
||||
protected quiz: any; // The quiz the attempt belongs to.
|
||||
protected courseId: number; // The course ID the quiz belongs to.
|
||||
protected quizId: number; // Quiz ID the attempt belongs to.
|
||||
protected attemptId: number; // The attempt being reviewed.
|
||||
protected currentPage: number; // The current page being reviewed.
|
||||
protected options: any; // Review options.
|
||||
|
||||
constructor(navParams: NavParams, modalCtrl: ModalController, protected translate: TranslateService,
|
||||
protected domUtils: CoreDomUtilsProvider, protected timeUtils: CoreTimeUtilsProvider,
|
||||
protected quizProvider: AddonModQuizProvider, protected quizHelper: AddonModQuizHelperProvider,
|
||||
protected questionHelper: CoreQuestionHelperProvider, protected textUtils: CoreTextUtilsProvider) {
|
||||
|
||||
this.quizId = navParams.get('quizId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.attemptId = navParams.get('attemptId');
|
||||
this.currentPage = navParams.get('page') || -1;
|
||||
this.showAll = this.currentPage == -1;
|
||||
|
||||
// Create the navigation modal.
|
||||
this.navigationModal = modalCtrl.create('AddonModQuizNavigationModalPage', {
|
||||
isReview: true,
|
||||
page: this
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchData().then(() => {
|
||||
this.quizProvider.logViewAttemptReview(this.attemptId).catch((error) => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current page. If slot is supplied, try to scroll to that question.
|
||||
*
|
||||
* @param {number} page Page to load. -1 means all questions in same page.
|
||||
* @param {boolean} [fromModal] Whether the page was selected using the navigation modal.
|
||||
* @param {number} [slot] Slot of the question to scroll to.
|
||||
*/
|
||||
changePage(page: number, fromModal?: boolean, slot?: number): void {
|
||||
if (typeof slot != 'undefined' && (this.attempt.currentpage == -1 || page == this.currentPage)) {
|
||||
// Scrol to a certain question in the current page.
|
||||
this.scrollToQuestion(slot);
|
||||
|
||||
return;
|
||||
} else if (page == this.currentPage) {
|
||||
// If the user is navigating to the current page and no question specified, we do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
this.content.scrollToTop();
|
||||
|
||||
this.loadPage(page).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true);
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
|
||||
if (typeof slot != 'undefined') {
|
||||
// Scroll to the question. Give some time to the questions to render.
|
||||
setTimeout(() => {
|
||||
this.scrollToQuestion(slot);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get the quiz data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchData(): Promise<any> {
|
||||
return this.quizProvider.getQuizById(this.courseId, this.quizId).then((quizData) => {
|
||||
this.quiz = quizData;
|
||||
this.componentId = this.quiz.coursemodule;
|
||||
|
||||
return this.quizProvider.getCombinedReviewOptions(this.quizId).then((result) => {
|
||||
this.options = result;
|
||||
|
||||
// Load the navigation data.
|
||||
return this.loadNavigation().then(() => {
|
||||
// Load questions.
|
||||
return this.loadPage(this.currentPage);
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a page questions.
|
||||
*
|
||||
* @param {number} page The page to load.
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadPage(page: number): Promise<void> {
|
||||
return this.quizProvider.getAttemptReview(this.attemptId, page).then((data) => {
|
||||
this.attempt = data.attempt;
|
||||
this.attempt.currentpage = page;
|
||||
this.currentPage = page;
|
||||
|
||||
// Set the summary data.
|
||||
this.setSummaryCalculatedData(data);
|
||||
|
||||
this.questions = data.questions;
|
||||
this.nextPage = page == -1 ? undefined : page + 1;
|
||||
this.previousPage = page - 1;
|
||||
|
||||
this.questions.forEach((question) => {
|
||||
// Get the readable mark for each question.
|
||||
question.readableMark = this.quizHelper.getQuestionMarkFromHtml(question.html);
|
||||
|
||||
// Extract the question info box.
|
||||
this.questionHelper.extractQuestionInfoBox(question, '.info');
|
||||
|
||||
// Set the preferred behaviour.
|
||||
question.preferredBehaviour = this.quiz.preferredbehaviour;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data to navigate the questions using the navigation modal.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved when done.
|
||||
*/
|
||||
protected loadNavigation(): Promise<void> {
|
||||
// Get all questions in single page to retrieve all the questions.
|
||||
return this.quizProvider.getAttemptReview(this.attemptId, -1).then((data) => {
|
||||
const lastQuestion = data.questions[data.questions.length - 1];
|
||||
|
||||
data.questions.forEach((question) => {
|
||||
question.stateClass = this.questionHelper.getQuestionStateClass(question.state);
|
||||
});
|
||||
|
||||
this.navigation = data.questions;
|
||||
this.numPages = lastQuestion ? lastQuestion.page + 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes data.
|
||||
*
|
||||
* @param {any} refresher Refresher
|
||||
*/
|
||||
refreshData(refresher: any): void {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.quizProvider.invalidateQuizData(this.courseId));
|
||||
promises.push(this.quizProvider.invalidateCombinedReviewOptionsForUser(this.quizId));
|
||||
promises.push(this.quizProvider.invalidateAttemptReview(this.attemptId));
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
return this.fetchData();
|
||||
}).finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a certain question.
|
||||
*
|
||||
* @param {number} slot Slot of the question to scroll to.
|
||||
*/
|
||||
protected scrollToQuestion(slot: number): void {
|
||||
this.domUtils.scrollToElementBySelector(this.content, '#addon-mod_quiz-question-' + slot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate review summary data.
|
||||
*
|
||||
* @param {any} data Result of getAttemptReview.
|
||||
*/
|
||||
protected setSummaryCalculatedData(data: any): void {
|
||||
|
||||
this.attempt.readableState = this.quizProvider.getAttemptReadableStateName(this.attempt.state);
|
||||
|
||||
if (this.attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED) {
|
||||
this.showCompleted = true;
|
||||
this.additionalData = data.additionaldata;
|
||||
|
||||
const timeTaken = this.attempt.timefinish - this.attempt.timestart;
|
||||
if (timeTaken) {
|
||||
// Format time taken.
|
||||
this.attempt.timeTaken = this.timeUtils.formatTime(timeTaken);
|
||||
|
||||
// Calculate overdue time.
|
||||
if (this.quiz.timelimit && timeTaken > this.quiz.timelimit + 60) {
|
||||
this.attempt.overTime = this.timeUtils.formatTime(timeTaken - this.quiz.timelimit);
|
||||
}
|
||||
}
|
||||
|
||||
// Treat grade.
|
||||
if (this.options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX &&
|
||||
this.quizProvider.quizHasGrades(this.quiz)) {
|
||||
|
||||
if (data.grade === null || typeof data.grade == 'undefined') {
|
||||
this.attempt.readableGrade = this.quizProvider.formatGrade(data.grade, this.quiz.decimalpoints);
|
||||
} else {
|
||||
// Show raw marks only if they are different from the grade (like on the entry page).
|
||||
if (this.quiz.grade != this.quiz.sumgrades) {
|
||||
this.attempt.readableMark = this.translate.instant('addon.mod_quiz.outofshort', {$a: {
|
||||
grade: this.quizProvider.formatGrade(this.attempt.sumgrades, this.quiz.decimalpoints),
|
||||
maxgrade: this.quizProvider.formatGrade(this.quiz.sumgrades, this.quiz.decimalpoints)
|
||||
}});
|
||||
}
|
||||
|
||||
// Now the scaled grade.
|
||||
const gradeObject: any = {
|
||||
grade: this.quizProvider.formatGrade(data.grade, this.quiz.decimalpoints),
|
||||
maxgrade: this.quizProvider.formatGrade(this.quiz.grade, this.quiz.decimalpoints)
|
||||
};
|
||||
|
||||
if (this.quiz.grade != 100) {
|
||||
gradeObject.percent = this.textUtils.roundToDecimals(this.attempt.sumgrades * 100 / this.quiz.sumgrades, 0);
|
||||
this.attempt.readableGrade = this.translate.instant('addon.mod_quiz.outofpercent', {$a: gradeObject});
|
||||
} else {
|
||||
this.attempt.readableGrade = this.translate.instant('addon.mod_quiz.outof', {$a: gradeObject});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Treat additional data.
|
||||
this.additionalData.forEach((data) => {
|
||||
// Remove help links from additional data.
|
||||
data.content = this.domUtils.removeElementFromHtml(data.content, '.helptooltip');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch mode: all questions in same page OR one page at a time.
|
||||
*/
|
||||
switchMode(): void {
|
||||
this.showAll = !this.showAll;
|
||||
|
||||
// Load all questions or first page, depending on the mode.
|
||||
this.loadPage(this.showAll ? -1 : 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
|
||||
/**
|
||||
* Interface that all access rules handlers must implement.
|
||||
*/
|
||||
export interface AddonModQuizAccessRuleHandler extends CoreDelegateHandler {
|
||||
|
||||
/**
|
||||
* Name of the rule the handler supports. E.g. 'password'.
|
||||
* @type {string}
|
||||
*/
|
||||
ruleName: string;
|
||||
|
||||
/**
|
||||
* Whether the rule requires a preflight check when prefetch/start/continue an attempt.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Whether the rule requires a preflight check.
|
||||
*/
|
||||
isPreflightCheckRequired(quiz: any, attempt?: any, prefetch?: boolean, siteId?: string): boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Add preflight data that doesn't require user interaction. The data should be added to the preflightData param.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} preflightData Object where to add the preflight data.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} Promise resolved when done if async, void if it's synchronous.
|
||||
*/
|
||||
getFixedPreflightData?(quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise<any>;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the access rule preflight.
|
||||
* Implement this if your access rule requires a preflight check with user interaction.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getPreflightComponent?(injector: Injector): any | Promise<any>;
|
||||
|
||||
/**
|
||||
* Function called when the preflight check has passed. This is a chance to record that fact in some way.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} attempt The attempt started/continued.
|
||||
* @param {any} preflightData Preflight data gathered.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} Promise resolved when done if async, void if it's synchronous.
|
||||
*/
|
||||
notifyPreflightCheckPassed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string)
|
||||
: void | Promise<any>;
|
||||
|
||||
/**
|
||||
* Function called when the preflight check fails. This is a chance to record that fact in some way.
|
||||
*
|
||||
* @param {any} quiz The quiz the rule belongs to.
|
||||
* @param {any} attempt The attempt started/continued.
|
||||
* @param {any} preflightData Preflight data gathered.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} Promise resolved when done if async, void if it's synchronous.
|
||||
*/
|
||||
notifyPreflightCheckFailed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string)
|
||||
: void | Promise<any>;
|
||||
|
||||
/**
|
||||
* Whether or not the time left of an attempt should be displayed.
|
||||
*
|
||||
* @param {any} attempt The attempt.
|
||||
* @param {number} endTime The attempt end time (in seconds).
|
||||
* @param {number} timeNow The current time in seconds.
|
||||
* @return {boolean} Whether it should be displayed.
|
||||
*/
|
||||
shouldShowTimeLeft?(attempt: any, endTime: number, timeNow: number): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to register access rules for quiz module.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizAccessRuleDelegate extends CoreDelegate {
|
||||
|
||||
protected handlerNameProperty = 'ruleName';
|
||||
|
||||
constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
|
||||
protected utils: CoreUtilsProvider) {
|
||||
super('AddonModQuizAccessRulesDelegate', logger, sitesProvider, eventsProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the handler for a certain rule.
|
||||
*
|
||||
* @param {string} ruleName Name of the access rule.
|
||||
* @return {AddonModQuizAccessRuleHandler} Handler. Undefined if no handler found for the rule.
|
||||
*/
|
||||
getAccessRuleHandler(ruleName: string): AddonModQuizAccessRuleHandler {
|
||||
return <AddonModQuizAccessRuleHandler> this.getHandler(ruleName, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of rules, get some fixed preflight data (data that doesn't require user interaction).
|
||||
*
|
||||
* @param {string[]} rules List of active rules names.
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} preflightData Object where to store the preflight data.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when all the data has been gathered.
|
||||
*/
|
||||
getFixedPreflightData(rules: string[], quiz: any, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string)
|
||||
: Promise<any> {
|
||||
rules = rules || [];
|
||||
|
||||
const promises = [];
|
||||
rules.forEach((rule) => {
|
||||
promises.push(Promise.resolve(
|
||||
this.executeFunctionOnEnabled(rule, 'getFixedPreflightData', [quiz, preflightData, attempt, prefetch, siteId])
|
||||
));
|
||||
});
|
||||
|
||||
return this.utils.allPromises(promises).catch(() => {
|
||||
// Never reject.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Component to use to display the access rule preflight.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @return {Promise<any>} Promise resolved with the component to use, undefined if not found.
|
||||
*/
|
||||
getPreflightComponent(rule: string, injector: Injector): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(rule, 'getPreflightComponent', [injector]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an access rule is supported.
|
||||
*
|
||||
* @param {string} ruleName Name of the rule.
|
||||
* @return {boolean} Whether it's supported.
|
||||
*/
|
||||
isAccessRuleSupported(ruleName: string): boolean {
|
||||
return this.hasHandler(ruleName, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of rules, check if preflight check is required.
|
||||
*
|
||||
* @param {string[]} rules List of active rules names.
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether it's required.
|
||||
*/
|
||||
isPreflightCheckRequired(rules: string[], quiz: any, attempt: any, prefetch?: boolean, siteId?: string): Promise<boolean> {
|
||||
rules = rules || [];
|
||||
|
||||
const promises = [];
|
||||
let isRequired = false;
|
||||
|
||||
rules.forEach((rule) => {
|
||||
promises.push(this.isPreflightCheckRequiredForRule(rule, quiz, attempt, prefetch, siteId).then((required) => {
|
||||
if (required) {
|
||||
isRequired = true;
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return this.utils.allPromises(promises).then(() => {
|
||||
return isRequired;
|
||||
}).catch(() => {
|
||||
// Never reject.
|
||||
return isRequired;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if preflight check is required for a certain rule.
|
||||
*
|
||||
* @param {string} rule Rule name.
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether it's required.
|
||||
*/
|
||||
isPreflightCheckRequiredForRule(rule: string, quiz: any, attempt: any, prefetch?: boolean, siteId?: string): Promise<boolean> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(rule, 'isPreflightCheckRequired', [quiz, attempt, prefetch, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all rules that the preflight check has passed.
|
||||
*
|
||||
* @param {string[]} rules List of active rules names.
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} preflightData Preflight data gathered.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
notifyPreflightCheckPassed(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string)
|
||||
: Promise<any> {
|
||||
rules = rules || [];
|
||||
|
||||
const promises = [];
|
||||
rules.forEach((rule) => {
|
||||
promises.push(Promise.resolve(
|
||||
this.executeFunctionOnEnabled(rule, 'notifyPreflightCheckPassed', [quiz, attempt, preflightData, prefetch, siteId])
|
||||
));
|
||||
});
|
||||
|
||||
return this.utils.allPromises(promises).catch(() => {
|
||||
// Never reject.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all rules that the preflight check has failed.
|
||||
*
|
||||
* @param {string[]} rules List of active rules names.
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} preflightData Preflight data gathered.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
notifyPreflightCheckFailed(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string)
|
||||
: Promise<any> {
|
||||
rules = rules || [];
|
||||
|
||||
const promises = [];
|
||||
rules.forEach((rule) => {
|
||||
promises.push(Promise.resolve(
|
||||
this.executeFunctionOnEnabled(rule, 'notifyPreflightCheckFailed', [quiz, attempt, preflightData, prefetch, siteId])
|
||||
));
|
||||
});
|
||||
|
||||
return this.utils.allPromises(promises).catch(() => {
|
||||
// Never reject.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the time left of an attempt should be displayed.
|
||||
*
|
||||
* @param {string[]} rules List of active rules names.
|
||||
* @param {any} attempt The attempt.
|
||||
* @param {number} endTime The attempt end time (in seconds).
|
||||
* @param {number} timeNow The current time in seconds.
|
||||
* @return {boolean} Whether it should be displayed.
|
||||
*/
|
||||
shouldShowTimeLeft(rules: string[], attempt: any, endTime: number, timeNow: number): boolean {
|
||||
rules = rules || [];
|
||||
|
||||
for (const i in rules) {
|
||||
const rule = rules[i];
|
||||
|
||||
if (this.executeFunctionOnEnabled(rule, 'shouldShowTimeLeft', [attempt, endTime, timeNow])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -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 { Injectable } from '@angular/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreContentLinksModuleGradeHandler } from '@core/contentlinks/classes/module-grade-handler';
|
||||
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||
import { AddonModQuizProvider } from './quiz';
|
||||
|
||||
/**
|
||||
* Handler to treat links to quiz grade.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizGradeLinkHandler extends CoreContentLinksModuleGradeHandler {
|
||||
name = 'AddonModQuizGradeLinkHandler';
|
||||
canReview = false;
|
||||
|
||||
constructor(courseHelper: CoreCourseHelperProvider, domUtils: CoreDomUtilsProvider, sitesProvider: CoreSitesProvider,
|
||||
protected quizProvider: AddonModQuizProvider) {
|
||||
super(courseHelper, domUtils, sitesProvider, 'AddonModQuiz', 'quiz');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||
* If not defined, defaults to true.
|
||||
*
|
||||
* @param {string} siteId The site ID.
|
||||
* @param {string} url The URL to treat.
|
||||
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
|
||||
*/
|
||||
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
|
||||
return this.quizProvider.isPluginEnabled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
// (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 { ModalController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { AddonModQuizProvider } from './quiz';
|
||||
import { AddonModQuizOfflineProvider } from './quiz-offline';
|
||||
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
|
||||
|
||||
/**
|
||||
* Helper service that provides some features for quiz.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizHelperProvider {
|
||||
|
||||
protected div = document.createElement('div'); // A div element to search in HTML code.
|
||||
|
||||
constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
|
||||
private accessRuleDelegate: AddonModQuizAccessRuleDelegate, private quizProvider: AddonModQuizProvider,
|
||||
private modalCtrl: ModalController, private quizOfflineProvider: AddonModQuizOfflineProvider) { }
|
||||
|
||||
/**
|
||||
* Validate a preflight data or show a modal to input the preflight data if required.
|
||||
* It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
|
||||
* @param {any} preflightData Object where to store the preflight data.
|
||||
* @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt.
|
||||
* @param {boolean} [offline] Whether the attempt is offline.
|
||||
* @param {boolean} [prefetch] Whether user is prefetching.
|
||||
* @param {string} [title] The title to display in the modal and in the submit button.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {boolean} [retrying] Whether we're retrying after a failure.
|
||||
* @return {Promise<any>} Promise resolved when the preflight data is validated. The resolve param is the attempt.
|
||||
*/
|
||||
getAndCheckPreflightData(quiz: any, accessInfo: any, preflightData: any, attempt: any, offline?: boolean, prefetch?: boolean,
|
||||
title?: string, siteId?: string, retrying?: boolean): Promise<any> {
|
||||
|
||||
const rules = accessInfo.activerulenames;
|
||||
let isPreflightCheckRequired = false;
|
||||
|
||||
// Check if the user needs to input preflight data.
|
||||
return this.accessRuleDelegate.isPreflightCheckRequired(rules, quiz, attempt, prefetch, siteId).then((required) => {
|
||||
isPreflightCheckRequired = required;
|
||||
|
||||
if (required) {
|
||||
// Preflight check is required but no preflightData has been sent. Show a modal with the preflight form.
|
||||
return this.getPreflightData(quiz, accessInfo, attempt, prefetch, title, siteId).then((data) => {
|
||||
// Data entered by the user, add it to preflight data and check it again.
|
||||
Object.assign(preflightData, data);
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
// Get some fixed preflight data from access rules (data that doesn't require user interaction).
|
||||
return this.accessRuleDelegate.getFixedPreflightData(rules, quiz, preflightData, attempt, prefetch, siteId);
|
||||
}).then(() => {
|
||||
|
||||
// All the preflight data is gathered, now validate it.
|
||||
return this.validatePreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch, siteId)
|
||||
.catch((error) => {
|
||||
|
||||
if (prefetch) {
|
||||
return Promise.reject(error);
|
||||
} else if (retrying && !isPreflightCheckRequired) {
|
||||
// We're retrying after a failure, but the preflight check wasn't required.
|
||||
// This means there's something wrong with some access rule or user is offline and data isn't cached.
|
||||
// Don't retry again because it would lead to an infinite loop.
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
// Show error and ask for the preflight again.
|
||||
// Wait to show the error because we want it to be shown over the preflight modal.
|
||||
setTimeout(() => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.error', true);
|
||||
}, 100);
|
||||
|
||||
return this.getAndCheckPreflightData(quiz, accessInfo, preflightData, attempt, offline, prefetch,
|
||||
title, siteId, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preflight data from the user using a modal.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
|
||||
* @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt.
|
||||
* @param {boolean} [prefetch] Whether the user is prefetching the quiz.
|
||||
* @param {string} [title] The title to display in the modal and in the submit button.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with the preflight data. Rejected if user cancels.
|
||||
*/
|
||||
getPreflightData(quiz: any, accessInfo: any, attempt: any, prefetch?: boolean, title?: string, siteId?: string): Promise<any> {
|
||||
const notSupported: string[] = [];
|
||||
|
||||
// Check if there is any unsupported rule.
|
||||
accessInfo.activerulenames.forEach((rule) => {
|
||||
if (!this.accessRuleDelegate.isAccessRuleSupported(rule)) {
|
||||
notSupported.push(rule);
|
||||
}
|
||||
});
|
||||
|
||||
if (notSupported.length) {
|
||||
return Promise.reject(this.translate.instant('addon.mod_quiz.errorrulesnotsupported') + ' ' +
|
||||
JSON.stringify(notSupported));
|
||||
}
|
||||
|
||||
// Create and show the modal.
|
||||
const modal = this.modalCtrl.create('AddonModQuizPreflightModalPage', {
|
||||
title: title,
|
||||
quiz: quiz,
|
||||
attempt: attempt,
|
||||
prefetch: !!prefetch,
|
||||
siteId: siteId,
|
||||
rules: accessInfo.activerulenames
|
||||
});
|
||||
|
||||
modal.present();
|
||||
|
||||
// Wait for modal to be dismissed.
|
||||
return new Promise((resolve, reject): void => {
|
||||
modal.onDidDismiss((data) => {
|
||||
if (typeof data != 'undefined') {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(this.domUtils.createCanceledError());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mark string from a question HTML.
|
||||
* Example result: "Marked out of 1.00".
|
||||
*
|
||||
* @param {string} html Question's HTML.
|
||||
* @return {string} Question's mark.
|
||||
*/
|
||||
getQuestionMarkFromHtml(html: string): string {
|
||||
this.div.innerHTML = html;
|
||||
|
||||
return this.domUtils.getContentsOfElement(this.div, '.grade');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some calculated data to the attempt.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {boolean} highlight Whether we should check if attempt should be highlighted.
|
||||
* @param {number} [bestGrade] Quiz's best grade (formatted). Required if highlight=true.
|
||||
*/
|
||||
setAttemptCalculatedData(quiz: any, attempt: any, highlight?: boolean, bestGrade?: string): void {
|
||||
|
||||
attempt.rescaledGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false);
|
||||
attempt.finished = this.quizProvider.isAttemptFinished(attempt.state);
|
||||
attempt.readableState = this.quizProvider.getAttemptReadableState(quiz, attempt);
|
||||
|
||||
if (quiz.showMarkColumn && attempt.finished) {
|
||||
attempt.readableMark = this.quizProvider.formatGrade(attempt.sumgrades, quiz.decimalpoints);
|
||||
} else {
|
||||
attempt.readableMark = '';
|
||||
}
|
||||
|
||||
if (quiz.showGradeColumn && attempt.finished) {
|
||||
attempt.readableGrade = this.quizProvider.formatGrade(attempt.rescaledGrade, quiz.decimalpoints);
|
||||
|
||||
// Highlight the highest grade if appropriate.
|
||||
attempt.highlightGrade = highlight && !attempt.preview && attempt.state == AddonModQuizProvider.ATTEMPT_FINISHED &&
|
||||
attempt.readableGrade == bestGrade;
|
||||
} else {
|
||||
attempt.readableGrade = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some calculated data to the quiz.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} options Options returned by AddonModQuizProvider.getCombinedReviewOptions.
|
||||
*/
|
||||
setQuizCalculatedData(quiz: any, options: any): void {
|
||||
quiz.sumGradesFormatted = this.quizProvider.formatGrade(quiz.sumgrades, quiz.decimalpoints);
|
||||
quiz.gradeFormatted = this.quizProvider.formatGrade(quiz.grade, quiz.decimalpoints);
|
||||
|
||||
quiz.showAttemptColumn = quiz.attempts != 1;
|
||||
quiz.showGradeColumn = options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX &&
|
||||
this.quizProvider.quizHasGrades(quiz);
|
||||
quiz.showMarkColumn = quiz.showGradeColumn && quiz.grade != quiz.sumgrades;
|
||||
quiz.showFeedbackColumn = quiz.hasfeedback && options.alloptions.overallfeedback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the preflight data. It calls AddonModQuizProvider.startAttempt if a new attempt is needed.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
|
||||
* @param {any} preflightData Object where to store the preflight data.
|
||||
* @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt.
|
||||
* @param {boolean} [offline] Whether the attempt is offline.
|
||||
* @param {boolean} [sent] Whether preflight data has been entered by the user.
|
||||
* @param {boolean} [prefetch] Whether user is prefetching.
|
||||
* @param {string} [title] The title to display in the modal and in the submit button.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the preflight data is validated.
|
||||
*/
|
||||
validatePreflightData(quiz: any, accessInfo: any, preflightData: any, attempt: any, offline?: boolean, prefetch?: boolean,
|
||||
siteId?: string): Promise<any> {
|
||||
|
||||
const rules = accessInfo.activerulenames;
|
||||
let promise;
|
||||
|
||||
if (attempt) {
|
||||
if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) {
|
||||
// We're continuing an attempt. Call getAttemptData to validate the preflight data.
|
||||
const page = attempt.currentpage;
|
||||
|
||||
promise = this.quizProvider.getAttemptData(attempt.id, page, preflightData, offline, true, siteId).then(() => {
|
||||
if (offline) {
|
||||
// Get current page stored in local.
|
||||
return this.quizOfflineProvider.getAttemptById(attempt.id).then((localAttempt) => {
|
||||
attempt.currentpage = localAttempt.currentpage;
|
||||
}).catch(() => {
|
||||
// No local data.
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Attempt is overdue or finished in offline, we can only see the summary.
|
||||
// Call getAttemptSummary to validate the preflight data.
|
||||
promise = this.quizProvider.getAttemptSummary(attempt.id, preflightData, offline, true, false, siteId);
|
||||
}
|
||||
} else {
|
||||
// We're starting a new attempt, call startAttempt.
|
||||
promise = this.quizProvider.startAttempt(quiz.id, preflightData, false, siteId).then((att) => {
|
||||
attempt = att;
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
// Preflight data validated.
|
||||
this.accessRuleDelegate.notifyPreflightCheckPassed(rules, quiz, attempt, preflightData, prefetch, siteId);
|
||||
|
||||
return attempt;
|
||||
}).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService returned an error, assume the preflight failed.
|
||||
this.accessRuleDelegate.notifyPreflightCheckFailed(rules, quiz, attempt, preflightData, prefetch, siteId);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// (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 { AddonModQuizProvider } from './quiz';
|
||||
|
||||
/**
|
||||
* Handler to treat links to quiz index.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizIndexLinkHandler extends CoreContentLinksModuleIndexHandler {
|
||||
name = 'AddonModQuizIndexLinkHandler';
|
||||
|
||||
constructor(courseHelper: CoreCourseHelperProvider, protected quizProvider: AddonModQuizProvider) {
|
||||
super(courseHelper, 'AddonModQuiz', 'quiz');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||
* If not defined, defaults to true.
|
||||
*
|
||||
* @param {string} siteId The site ID.
|
||||
* @param {string} url The URL to treat.
|
||||
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
|
||||
*/
|
||||
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
|
||||
return this.quizProvider.isPluginEnabled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// (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 { AddonModQuizIndexComponent } from '../components/index/index';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Handler to support quiz modules.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizModuleHandler implements CoreCourseModuleHandler {
|
||||
name = 'AddonModQuiz';
|
||||
modName = 'quiz';
|
||||
|
||||
constructor(private courseProvider: CoreCourseProvider) { }
|
||||
|
||||
/**
|
||||
* 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 true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('quiz'),
|
||||
title: module.name,
|
||||
class: 'addon-mod_quiz-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||
navCtrl.push('AddonModQuizIndexPage', {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 AddonModQuizIndexComponent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,451 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
|
||||
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
|
||||
import { AddonModQuizProvider } from './quiz';
|
||||
import { AddonModQuizHelperProvider } from './helper';
|
||||
import { AddonModQuizAccessRuleDelegate } from './access-rules-delegate';
|
||||
import { AddonModQuizSyncProvider } from './quiz-sync';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
|
||||
/**
|
||||
* Handler to prefetch quizzes.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
|
||||
name = 'AddonModQuiz';
|
||||
modName = 'quiz';
|
||||
component = AddonModQuizProvider.COMPONENT;
|
||||
updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^questions$|^attempts$/;
|
||||
|
||||
protected syncProvider: AddonModQuizSyncProvider; // It will be injected later to prevent circular dependencies.
|
||||
|
||||
constructor(protected injector: Injector, protected quizProvider: AddonModQuizProvider,
|
||||
protected textUtils: CoreTextUtilsProvider, protected quizHelper: AddonModQuizHelperProvider,
|
||||
protected accessRuleDelegate: AddonModQuizAccessRuleDelegate, protected questionHelper: CoreQuestionHelperProvider) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the module.
|
||||
*
|
||||
* @param {any} module The module object returned by WS.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||
* @return {Promise<any>} Promise resolved when all content is downloaded.
|
||||
*/
|
||||
download(module: any, courseId: number, dirPath?: string): Promise<any> {
|
||||
// Same implementation for download or prefetch.
|
||||
return this.prefetch(module, courseId, false, dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download size of a module.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {Number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @return {Promise<{size: number, total: boolean}>} Promise resolved with the size and a boolean indicating if it was able
|
||||
* to calculate the total size.
|
||||
*/
|
||||
getDownloadSize(module: any, courseId: any, single?: boolean): Promise<{ size: number, total: boolean }> {
|
||||
return Promise.resolve({
|
||||
size: -1,
|
||||
total: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of files. If not defined, we'll assume they're in module.contents.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {Number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @return {Promise<any[]>} Promise resolved with the list of files.
|
||||
*/
|
||||
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather some preflight data for an attempt. This function will start a new attempt if needed.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} accessInfo Quiz access info returned by AddonModQuizProvider.getQuizAccessInformation.
|
||||
* @param {any} [attempt] Attempt to continue. Don't pass any value if the user needs to start a new attempt.
|
||||
* @param {boolean} [askPreflight] Whether it should ask for preflight data if needed.
|
||||
* @param {string} [modalTitle] Lang key of the title to set to preflight modal (e.g. 'addon.mod_quiz.startattempt').
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with the preflight data.
|
||||
*/
|
||||
getPreflightData(quiz: any, accessInfo: any, attempt?: any, askPreflight?: boolean, title?: string, siteId?: string)
|
||||
: Promise<any> {
|
||||
const preflightData = {};
|
||||
let promise;
|
||||
|
||||
if (askPreflight) {
|
||||
// We can ask preflight, check if it's needed and get the data.
|
||||
promise = this.quizHelper.getAndCheckPreflightData(
|
||||
quiz, accessInfo, preflightData, attempt, false, true, title, siteId);
|
||||
} else {
|
||||
// Get some fixed preflight data from access rules (data that doesn't require user interaction).
|
||||
const rules = accessInfo.activerulenames;
|
||||
|
||||
promise = this.accessRuleDelegate.getFixedPreflightData(rules, quiz, preflightData, attempt, true, siteId).then(() => {
|
||||
if (!attempt) {
|
||||
// We need to create a new attempt.
|
||||
return this.quizProvider.startAttempt(quiz.id, preflightData, false, siteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
return preflightData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId The course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||
return this.quizProvider.invalidateContent(moduleId, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate WS calls needed to determine module status.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when invalidated.
|
||||
*/
|
||||
invalidateModule(module: any, courseId: number): Promise<any> {
|
||||
// Invalidate the calls required to check if a quiz is downloadable.
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.quizProvider.invalidateQuizData(courseId));
|
||||
promises.push(this.quizProvider.invalidateUserAttemptsForUser(module.instance));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {boolean|Promise<boolean>} Whether the module can be downloaded. The promise should never be rejected.
|
||||
*/
|
||||
isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
|
||||
const siteId = this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quiz) => {
|
||||
if (quiz.allowofflineattempts !== 1 || quiz.hasquestions === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Not downloadable if we reached max attempts or the quiz has an unfinished attempt.
|
||||
return this.quizProvider.getUserAttempts(quiz.id, undefined, true, false, false, siteId).then((attempts) => {
|
||||
const isLastFinished = !attempts.length || this.quizProvider.isAttemptFinished(attempts[attempts.length - 1].state);
|
||||
|
||||
return quiz.attempts === 0 || quiz.attempts > attempts.length || !isLastFinished;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return this.quizProvider.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a module.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
|
||||
return this.prefetchPackage(module, courseId, single, this.prefetchQuiz.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a quiz.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param {String} siteId Site ID.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected prefetchQuiz(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
let attempts: any[],
|
||||
startAttempt = false,
|
||||
quiz,
|
||||
quizAccessInfo,
|
||||
attemptAccessInfo,
|
||||
preflightData;
|
||||
|
||||
// Get quiz.
|
||||
return this.quizProvider.getQuiz(courseId, module.id, false, siteId).then((quizData) => {
|
||||
quiz = quizData;
|
||||
|
||||
const promises = [],
|
||||
introFiles = this.getIntroFilesFromInstance(module, quiz);
|
||||
|
||||
// Prefetch some quiz data.
|
||||
promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => {
|
||||
quizAccessInfo = info;
|
||||
}));
|
||||
promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId));
|
||||
promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => {
|
||||
attempts = atts;
|
||||
}));
|
||||
promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId).then((info) => {
|
||||
attemptAccessInfo = info;
|
||||
}));
|
||||
|
||||
promises.push(this.filepoolProvider.addFilesToQueue(siteId, introFiles, AddonModQuizProvider.COMPONENT, module.id));
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
// Check if we need to start a new attempt.
|
||||
let attempt = attempts[attempts.length - 1];
|
||||
if (!attempt || this.quizProvider.isAttemptFinished(attempt.state)) {
|
||||
// Check if the user can attempt the quiz.
|
||||
if (attemptAccessInfo.preventnewattemptreasons.length) {
|
||||
return Promise.reject(this.textUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons));
|
||||
}
|
||||
|
||||
startAttempt = true;
|
||||
attempt = undefined;
|
||||
}
|
||||
|
||||
// Get the preflight data. This function will also start a new attempt if needed.
|
||||
return this.getPreflightData(quiz, quizAccessInfo, attempt, single, 'core.download', siteId);
|
||||
|
||||
}).then((data) => {
|
||||
preflightData = data;
|
||||
|
||||
const promises = [];
|
||||
|
||||
if (startAttempt) {
|
||||
// Re-fetch user attempts since we created a new one.
|
||||
promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => {
|
||||
attempts = atts;
|
||||
}));
|
||||
|
||||
// Update the download time to prevent detecting the new attempt as an update.
|
||||
promises.push(this.filepoolProvider.updatePackageDownloadTime(siteId, AddonModQuizProvider.COMPONENT, module.id)
|
||||
.catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
}
|
||||
|
||||
// Fetch attempt related data.
|
||||
promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId));
|
||||
promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId));
|
||||
promises.push(this.quizProvider.getGradeFromGradebook(courseId, module.id, true, siteId).then((gradebookData) => {
|
||||
if (typeof gradebookData.graderaw != 'undefined') {
|
||||
return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId);
|
||||
}
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt.
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
// We have quiz data, now we'll get specific data for each attempt.
|
||||
const promises = [];
|
||||
|
||||
attempts.forEach((attempt) => {
|
||||
promises.push(this.prefetchAttempt(quiz, attempt, preflightData, siteId));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
// If there's nothing to send, mark the quiz as synchronized.
|
||||
// We don't return the promises because it should be fast and we don't want to block the user for this.
|
||||
if (!this.syncProvider) {
|
||||
this.syncProvider = this.injector.get(AddonModQuizSyncProvider);
|
||||
}
|
||||
|
||||
this.syncProvider.hasDataToSync(quiz.id, siteId).then((hasData) => {
|
||||
if (!hasData) {
|
||||
this.syncProvider.setSyncTime(quiz.id, siteId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all WS data for an attempt.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} preflightData Preflight required data (like password).
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the prefetch is finished. Data returned is not reliable.
|
||||
*/
|
||||
prefetchAttempt(quiz: any, attempt: any, preflightData: any, siteId?: string): Promise<any> {
|
||||
const pages = this.quizProvider.getPagesFromLayout(attempt.layout),
|
||||
promises = [],
|
||||
isSequential = this.quizProvider.isNavigationSequential(quiz);
|
||||
|
||||
if (this.quizProvider.isAttemptFinished(attempt.state)) {
|
||||
// Attempt is finished, get feedback and review data.
|
||||
|
||||
const attemptGrade = this.quizProvider.rescaleGrade(attempt.sumgrades, quiz, false);
|
||||
if (typeof attemptGrade != 'undefined') {
|
||||
promises.push(this.quizProvider.getFeedbackForGrade(quiz.id, Number(attemptGrade), true, siteId));
|
||||
}
|
||||
|
||||
// Get the review for each page.
|
||||
pages.forEach((page) => {
|
||||
promises.push(this.quizProvider.getAttemptReview(attempt.id, page, true, siteId).catch(() => {
|
||||
// Ignore failures, maybe the user can't review the attempt.
|
||||
}));
|
||||
});
|
||||
|
||||
// Get the review for all questions in same page.
|
||||
promises.push(this.quizProvider.getAttemptReview(attempt.id, -1, true, siteId).then((data) => {
|
||||
// Download the files inside the questions.
|
||||
const questionPromises = [];
|
||||
|
||||
data.questions.forEach((question) => {
|
||||
questionPromises.push(this.questionHelper.prefetchQuestionFiles(
|
||||
question, this.component, quiz.coursemodule, siteId));
|
||||
});
|
||||
|
||||
return Promise.all(questionPromises);
|
||||
}, () => {
|
||||
// Ignore failures, maybe the user can't review the attempt.
|
||||
}));
|
||||
} else {
|
||||
|
||||
// Attempt not finished, get data needed to continue the attempt.
|
||||
promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, attempt.id, false, true, siteId));
|
||||
promises.push(this.quizProvider.getAttemptSummary(attempt.id, preflightData, false, true, false, siteId));
|
||||
|
||||
if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
|
||||
// Get data for each page.
|
||||
pages.forEach((page) => {
|
||||
if (isSequential && page < attempt.currentpage) {
|
||||
// Sequential quiz, cannot get pages before the current one.
|
||||
return;
|
||||
}
|
||||
|
||||
promises.push(this.quizProvider.getAttemptData(attempt.id, page, preflightData, false, true, siteId)
|
||||
.then((data) => {
|
||||
// Download the files inside the questions.
|
||||
const questionPromises = [];
|
||||
|
||||
data.questions.forEach((question) => {
|
||||
questionPromises.push(this.questionHelper.prefetchQuestionFiles(
|
||||
question, this.component, quiz.coursemodule, siteId));
|
||||
});
|
||||
|
||||
return Promise.all(questionPromises);
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetches some data for a quiz and its last attempt.
|
||||
* This function will NOT start a new attempt, it only reads data for the quiz and the last attempt.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {boolean} [askPreflight] Whether it should ask for preflight data if needed.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetchQuizAndLastAttempt(quiz: any, askPreflight?: boolean, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises = [];
|
||||
let attempts,
|
||||
quizAccessInfo,
|
||||
preflightData,
|
||||
lastAttempt;
|
||||
|
||||
// Get quiz data.
|
||||
promises.push(this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => {
|
||||
quizAccessInfo = info;
|
||||
}));
|
||||
promises.push(this.quizProvider.getQuizRequiredQtypes(quiz.id, true, siteId));
|
||||
promises.push(this.quizProvider.getCombinedReviewOptions(quiz.id, true, siteId));
|
||||
promises.push(this.quizProvider.getUserBestGrade(quiz.id, true, siteId));
|
||||
promises.push(this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((atts) => {
|
||||
attempts = atts;
|
||||
}));
|
||||
promises.push(this.quizProvider.getGradeFromGradebook(quiz.course, quiz.coursemodule, true, siteId)
|
||||
.then((gradebookData) => {
|
||||
if (typeof gradebookData.graderaw != 'undefined') {
|
||||
return this.quizProvider.getFeedbackForGrade(quiz.id, gradebookData.graderaw, true, siteId);
|
||||
}
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
promises.push(this.quizProvider.getAttemptAccessInformation(quiz.id, 0, false, true, siteId)); // Last attempt.
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
lastAttempt = attempts[attempts.length - 1];
|
||||
if (!lastAttempt) {
|
||||
// No need to get attempt data, we don't need preflight data.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the preflight data.
|
||||
return this.getPreflightData(quiz, quizAccessInfo, lastAttempt, askPreflight, 'core.download', siteId);
|
||||
|
||||
}).then((data) => {
|
||||
preflightData = data;
|
||||
|
||||
if (lastAttempt) {
|
||||
// Get data for last attempt.
|
||||
return this.prefetchAttempt(quiz, lastAttempt, preflightData, siteId);
|
||||
}
|
||||
}).then(() => {
|
||||
// Prefetch finished, get current status to determine if we need to change it.
|
||||
|
||||
return this.filepoolProvider.getPackageStatus(siteId, this.component, quiz.coursemodule);
|
||||
}).then((status) => {
|
||||
if (status !== CoreConstants.NOT_DOWNLOADED) {
|
||||
// Quiz was downloaded, set the new status.
|
||||
// If no attempts or last is finished we'll mark it as not downloaded to show download icon.
|
||||
const isLastFinished = !lastAttempt || this.quizProvider.isAttemptFinished(lastAttempt.state),
|
||||
newStatus = isLastFinished ? CoreConstants.NOT_DOWNLOADED : CoreConstants.DOWNLOADED;
|
||||
|
||||
return this.filepoolProvider.storePackageStatus(siteId, newStatus, this.component, quiz.coursemodule);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionProvider } from '@core/question/providers/question';
|
||||
import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate';
|
||||
import { AddonModQuizProvider } from './quiz';
|
||||
import { SQLiteDB } from '@classes/sqlitedb';
|
||||
|
||||
/**
|
||||
* Service to handle offline quiz.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizOfflineProvider {
|
||||
|
||||
protected logger;
|
||||
|
||||
// Variables for database.
|
||||
protected ATTEMPTS_TABLE = 'addon_mod_quiz_attempts';
|
||||
protected tablesSchema = [
|
||||
{
|
||||
name: this.ATTEMPTS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'id', // Attempt ID.
|
||||
type: 'INTEGER',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'attempt', // Attempt number.
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'userid',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'quizid',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'currentpage',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'timecreated',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'timemodified',
|
||||
type: 'INTEGER'
|
||||
},
|
||||
{
|
||||
name: 'finished',
|
||||
type: 'INTEGER'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider,
|
||||
private questionProvider: CoreQuestionProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
|
||||
private behaviourDelegate: CoreQuestionBehaviourDelegate) {
|
||||
this.logger = logger.getInstance('AddonModQuizOfflineProvider');
|
||||
|
||||
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify the answers in questions.
|
||||
*
|
||||
* @param {any} answers List of answers.
|
||||
* @return {any} Object with the questions, the keys are the slot. Each question contains its answers.
|
||||
*/
|
||||
classifyAnswersInQuestions(answers: any): any {
|
||||
const questionsWithAnswers = {};
|
||||
|
||||
// Classify the answers in each question.
|
||||
for (const name in answers) {
|
||||
const slot = this.questionProvider.getQuestionSlotFromName(name),
|
||||
nameWithoutPrefix = this.questionProvider.removeQuestionPrefix(name);
|
||||
|
||||
if (!questionsWithAnswers[slot]) {
|
||||
questionsWithAnswers[slot] = {
|
||||
answers: {},
|
||||
prefix: name.substr(0, name.indexOf(nameWithoutPrefix))
|
||||
};
|
||||
}
|
||||
questionsWithAnswers[slot].answers[nameWithoutPrefix] = answers[name];
|
||||
}
|
||||
|
||||
return questionsWithAnswers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of questions with answers classified in it (@see AddonModQuizOfflineProvider.classifyAnswersInQuestions),
|
||||
* returns a list of answers (including prefix in the name).
|
||||
*
|
||||
* @param {any} questions Questions.
|
||||
* @return {any} Answers.
|
||||
*/
|
||||
extractAnswersFromQuestions(questions: any): any {
|
||||
const answers = {};
|
||||
|
||||
for (const slot in questions) {
|
||||
const question = questions[slot];
|
||||
|
||||
for (const name in question.answers) {
|
||||
answers[question.prefix + name] = question.answers[name];
|
||||
}
|
||||
}
|
||||
|
||||
return answers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the offline attempts in a certain site.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with the offline attempts.
|
||||
*/
|
||||
getAllAttempts(siteId?: string): Promise<any[]> {
|
||||
return this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.getAllRecords(this.ATTEMPTS_TABLE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an attempt answers from site DB.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with the answers.
|
||||
*/
|
||||
getAttemptAnswers(attemptId: number, siteId?: string): Promise<any[]> {
|
||||
return this.questionProvider.getAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an attempt from site DB.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with the attempt.
|
||||
*/
|
||||
getAttemptById(attemptId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.getRecord(this.ATTEMPTS_TABLE, {id: attemptId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an attempt from site DB.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {number} [userId] User ID. If not defined, user current site's user.
|
||||
* @return {Promise<any[]>} Promise resolved with the attempts.
|
||||
*/
|
||||
getQuizAttempts(quizId: number, siteId?: string, userId?: number): Promise<any[]> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.getDb().getRecords(this.ATTEMPTS_TABLE, {quizid: quizId, userid: userId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load local state in the questions.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {any[]} questions List of questions.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
loadQuestionsLocalStates(attemptId: number, questions: any[], siteId?: string): Promise<any[]> {
|
||||
const promises = [];
|
||||
|
||||
questions.forEach((question) => {
|
||||
promises.push(this.questionProvider.getQuestion(AddonModQuizProvider.COMPONENT, attemptId, question.slot, siteId)
|
||||
.then((q) => {
|
||||
|
||||
const state = this.questionProvider.getState(q.state);
|
||||
question.state = q.state;
|
||||
question.status = this.translate.instant('core.question.' + state.status);
|
||||
}).catch(() => {
|
||||
// Question not found.
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return questions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an attempt, saving its data.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} questions Object with the questions of the quiz. The keys should be the question slot.
|
||||
* @param {any} data Data to save.
|
||||
* @param {boolean} [finish] Whether to finish the quiz.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
|
||||
*/
|
||||
processAttempt(quiz: any, attempt: any, questions: any, data: any, finish?: boolean, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const now = this.timeUtils.timestamp();
|
||||
let db: SQLiteDB;
|
||||
|
||||
return this.sitesProvider.getSiteDb(siteId).then((siteDb) => {
|
||||
db = siteDb;
|
||||
|
||||
// Check if an attempt already exists.
|
||||
return this.getAttemptById(attempt.id, siteId).catch(() => {
|
||||
// Attempt doesn't exist, create a new entry.
|
||||
return {
|
||||
quizid: quiz.id,
|
||||
userid: attempt.userid,
|
||||
id: attempt.id,
|
||||
courseid: quiz.course,
|
||||
timecreated: now,
|
||||
attempt: attempt.attempt,
|
||||
currentpage: attempt.currentpage
|
||||
};
|
||||
});
|
||||
}).then((entry) => {
|
||||
// Save attempt in DB.
|
||||
entry.timemodified = now;
|
||||
entry.finished = finish ? 1 : 0;
|
||||
|
||||
return db.insertRecord(this.ATTEMPTS_TABLE, entry);
|
||||
}).then(() => {
|
||||
// Attempt has been saved, now we need to save the answers.
|
||||
return this.saveAnswers(quiz, attempt, questions, data, now, siteId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an attempt and its answers from local DB.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
removeAttemptAndAnswers(attemptId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises = [];
|
||||
|
||||
// Remove stored answers and questions.
|
||||
promises.push(this.questionProvider.removeAttemptAnswers(AddonModQuizProvider.COMPONENT, attemptId, siteId));
|
||||
promises.push(this.questionProvider.removeAttemptQuestions(AddonModQuizProvider.COMPONENT, attemptId, siteId));
|
||||
|
||||
// Remove the attempt.
|
||||
promises.push(this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.deleteRecords(this.ATTEMPTS_TABLE, {id: attemptId});
|
||||
}));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a question and its answers from local DB.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {number} slot Question slot.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when finished.
|
||||
*/
|
||||
removeQuestionAndAnswers(attemptId: number, slot: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.questionProvider.removeQuestion(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId));
|
||||
promises.push(this.questionProvider.removeQuestionAnswers(AddonModQuizProvider.COMPONENT, attemptId, slot, siteId));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an attempt's answers and calculate state for questions modified.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {any} attempt Attempt.
|
||||
* @param {any} questions Object with the questions of the quiz. The keys should be the question slot.
|
||||
* @param {any} answers Answers to save.
|
||||
* @param {number} [timeMod] Time modified to set in the answers. If not defined, current time.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
saveAnswers(quiz: any, attempt: any, questions: any, answers: any, timeMod?: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
timeMod = timeMod || this.timeUtils.timestamp();
|
||||
|
||||
const questionsWithAnswers = {},
|
||||
newStates = {};
|
||||
let promises = [];
|
||||
|
||||
// Classify the answers in each question.
|
||||
for (const name in answers) {
|
||||
const slot = this.questionProvider.getQuestionSlotFromName(name),
|
||||
nameWithoutPrefix = this.questionProvider.removeQuestionPrefix(name);
|
||||
|
||||
if (questions[slot]) {
|
||||
if (!questionsWithAnswers[slot]) {
|
||||
questionsWithAnswers[slot] = questions[slot];
|
||||
questionsWithAnswers[slot].answers = {};
|
||||
}
|
||||
questionsWithAnswers[slot].answers[nameWithoutPrefix] = answers[name];
|
||||
}
|
||||
}
|
||||
|
||||
// First determine the new state of each question. We won't save the new state yet.
|
||||
for (const slot in questionsWithAnswers) {
|
||||
const question = questionsWithAnswers[slot];
|
||||
|
||||
promises.push(this.behaviourDelegate.determineNewState(
|
||||
quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, attempt.id, question, siteId).then((state) => {
|
||||
// Check if state has changed.
|
||||
if (state && state.name != question.state) {
|
||||
newStates[question.slot] = state.name;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
// Now save the answers.
|
||||
return this.questionProvider.saveAnswers(AddonModQuizProvider.COMPONENT, quiz.id, attempt.id, attempt.userid,
|
||||
answers, timeMod, siteId);
|
||||
}).then(() => {
|
||||
// Answers have been saved, now we can save the questions with the states.
|
||||
promises = [];
|
||||
|
||||
for (const slot in newStates) {
|
||||
const question = questionsWithAnswers[slot];
|
||||
|
||||
promises.push(this.questionProvider.saveQuestion(AddonModQuizProvider.COMPONENT, quiz.id, attempt.id,
|
||||
attempt.userid, question, newStates[slot], siteId));
|
||||
}
|
||||
|
||||
return this.utils.allPromises(promises).catch((err) => {
|
||||
// Ignore errors when saving question state.
|
||||
this.logger.error('Error saving question state', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attempt's current page.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {number} page Page to set.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved in success, rejected otherwise.
|
||||
*/
|
||||
setAttemptCurrentPage(attemptId: number, page: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.updateRecords(this.ATTEMPTS_TABLE, {currentpage: page}, {id: attemptId});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,404 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreQuestionProvider } from '@core/question/providers/question';
|
||||
import { CoreQuestionDelegate } from '@core/question/providers/delegate';
|
||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||
import { AddonModQuizProvider } from './quiz';
|
||||
import { AddonModQuizOfflineProvider } from './quiz-offline';
|
||||
import { AddonModQuizPrefetchHandler } from './prefetch-handler';
|
||||
|
||||
/**
|
||||
* Data returned by a quiz sync.
|
||||
*/
|
||||
export interface AddonModQuizSyncResult {
|
||||
/**
|
||||
* List of warnings.
|
||||
* @type {string[]}
|
||||
*/
|
||||
warnings: string[];
|
||||
|
||||
/**
|
||||
* Whether an attempt was finished in the site due to the sync,
|
||||
* @type {boolean}
|
||||
*/
|
||||
attemptFinished: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to sync quizzes.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizSyncProvider extends CoreSyncBaseProvider {
|
||||
|
||||
static AUTO_SYNCED = 'addon_mod_quiz_autom_synced';
|
||||
static SYNC_TIME = 300000;
|
||||
|
||||
protected componentTranslate: string;
|
||||
|
||||
constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider,
|
||||
syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService,
|
||||
courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider,
|
||||
private quizProvider: AddonModQuizProvider, private quizOfflineProvider: AddonModQuizOfflineProvider,
|
||||
private prefetchHandler: AddonModQuizPrefetchHandler, private questionProvider: CoreQuestionProvider,
|
||||
private questionDelegate: CoreQuestionDelegate) {
|
||||
|
||||
super('AddonModQuizSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||
|
||||
this.componentTranslate = courseProvider.translateModuleName('quiz');
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish a sync process: remove offline data if needed, prefetch quiz data, set sync time and return the result.
|
||||
*
|
||||
* @param {string} siteId Site ID.
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string[]} warnings List of warnings generated by the sync.
|
||||
* @param {number} [attemptId] Last attempt ID.
|
||||
* @param {any} [offlineAttempt] Offline attempt synchronized, if any.
|
||||
* @param {any} [onlineAttempt] Online data for the offline attempt.
|
||||
* @param {boolean} [removeAttempt] Whether the offline data should be removed.
|
||||
* @param {boolean} [updated] Whether some data was sent to the site.
|
||||
* @return {Promise<AddonModQuizSyncResult>} Promise resolved on success.
|
||||
*/
|
||||
protected finishSync(siteId: string, quiz: any, courseId: number, warnings: string[], attemptId?: number, offlineAttempt?: any,
|
||||
onlineAttempt?: any, removeAttempt?: boolean, updated?: boolean): Promise<AddonModQuizSyncResult> {
|
||||
|
||||
// Invalidate the data for the quiz and attempt.
|
||||
return this.quizProvider.invalidateAllQuizData(quiz.id, courseId, attemptId, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
if (removeAttempt && attemptId) {
|
||||
return this.quizOfflineProvider.removeAttemptAndAnswers(attemptId, siteId);
|
||||
}
|
||||
}).then(() => {
|
||||
if (updated) {
|
||||
// Data has been sent. Update prefetched data.
|
||||
return this.prefetchHandler.prefetchQuizAndLastAttempt(quiz, false, siteId);
|
||||
}
|
||||
}).then(() => {
|
||||
return this.setSyncTime(quiz.id, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}).then(() => {
|
||||
// Check if online attempt was finished because of the sync.
|
||||
if (onlineAttempt && !this.quizProvider.isAttemptFinished(onlineAttempt.state)) {
|
||||
// Attempt wasn't finished at start. Check if it's finished now.
|
||||
return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, false, siteId).then((attempts) => {
|
||||
// Search the attempt.
|
||||
for (const i in attempts) {
|
||||
const attempt = attempts[i];
|
||||
|
||||
if (attempt.id == onlineAttempt.id) {
|
||||
return this.quizProvider.isAttemptFinished(attempt.state);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}).then((attemptFinished) => {
|
||||
return {
|
||||
warnings: warnings,
|
||||
attemptFinished: attemptFinished
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a quiz has data to synchronize.
|
||||
*
|
||||
* @param {number} quizId Quiz ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether it has data to sync.
|
||||
*/
|
||||
hasDataToSync(quizId: number, siteId?: string): Promise<boolean> {
|
||||
return this.quizOfflineProvider.getQuizAttempts(quizId, siteId).then((attempts) => {
|
||||
return !!attempts.length;
|
||||
}).catch(() => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all the quizzes in a certain site or in all sites.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
syncAllQuizzes(siteId?: string): Promise<any> {
|
||||
return this.syncOnSites('all quizzes', this.syncAllQuizzesFunc.bind(this), [], siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all quizzes on a site.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected syncAllQuizzesFunc(siteId?: string): Promise<any> {
|
||||
// Get all offline attempts.
|
||||
return this.quizOfflineProvider.getAllAttempts(siteId).then((attempts) => {
|
||||
const quizzes = [],
|
||||
ids = [], // To prevent duplicates.
|
||||
promises = [];
|
||||
|
||||
// Get the IDs of all the quizzes that have something to be synced.
|
||||
attempts.forEach((attempt) => {
|
||||
if (ids.indexOf(attempt.quizid) == -1) {
|
||||
ids.push(attempt.quizid);
|
||||
|
||||
quizzes.push({
|
||||
id: attempt.quizid,
|
||||
courseid: attempt.courseid
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sync all quizzes that haven't been synced for a while and that aren't attempted right now.
|
||||
quizzes.forEach((quiz) => {
|
||||
if (!this.syncProvider.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) {
|
||||
|
||||
// Quiz not blocked, try to synchronize it.
|
||||
promises.push(this.quizProvider.getQuizById(quiz.courseid, quiz.id, false, siteId).then((quiz) => {
|
||||
return this.syncQuizIfNeeded(quiz, false, siteId).then((data) => {
|
||||
if (data && data.warnings && data.warnings.length) {
|
||||
// Store the warnings to show them when the user opens the quiz.
|
||||
return this.setSyncWarnings(quiz.id, data.warnings, siteId).then(() => {
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}).then((data) => {
|
||||
if (typeof data != 'undefined') {
|
||||
// Sync successful. Send event.
|
||||
this.eventsProvider.trigger(AddonModQuizSyncProvider.AUTO_SYNCED, {
|
||||
quizId: quiz.id,
|
||||
attemptFinished: data.attemptFinished,
|
||||
warnings: data.warnings
|
||||
}, siteId);
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a quiz only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {boolean} [askPreflight] Whether we should ask for preflight data if needed.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the quiz is synced or if it doesn't need to be synced.
|
||||
*/
|
||||
syncQuizIfNeeded(quiz: any, askPreflight?: boolean, siteId?: string): Promise<any> {
|
||||
return this.isSyncNeeded(quiz.id, siteId).then((needed) => {
|
||||
if (needed) {
|
||||
return this.syncQuiz(quiz, askPreflight, siteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize a quiz.
|
||||
* The promise returned will be resolved with an array with warnings if the synchronization is successful.
|
||||
*
|
||||
* @param {any} quiz Quiz.
|
||||
* @param {boolean} [askPreflight] Whether we should ask for preflight data if needed.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<AddonModQuizSyncResult>} Promise resolved in success.
|
||||
*/
|
||||
syncQuiz(quiz: any, askPreflight?: boolean, siteId?: string): Promise<AddonModQuizSyncResult> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const warnings = [],
|
||||
courseId = quiz.course;
|
||||
let syncPromise,
|
||||
preflightData;
|
||||
|
||||
if (this.isSyncing(quiz.id, siteId)) {
|
||||
// There's already a sync ongoing for this quiz, return the promise.
|
||||
return this.getOngoingSync(quiz.id, siteId);
|
||||
}
|
||||
|
||||
// Verify that quiz isn't blocked.
|
||||
if (this.syncProvider.isBlocked(AddonModQuizProvider.COMPONENT, quiz.id, siteId)) {
|
||||
this.logger.debug('Cannot sync quiz ' + quiz.id + ' because it is blocked.');
|
||||
|
||||
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||
}
|
||||
|
||||
this.logger.debug('Try to sync quiz ' + quiz.id + ' in site ' + siteId);
|
||||
|
||||
// Get all the offline attempts for the quiz.
|
||||
syncPromise = this.quizOfflineProvider.getQuizAttempts(quiz.id, siteId).then((attempts) => {
|
||||
// Should return 0 or 1 attempt.
|
||||
if (!attempts.length) {
|
||||
return this.finishSync(siteId, quiz, courseId, warnings);
|
||||
}
|
||||
|
||||
const offlineAttempt = attempts.pop();
|
||||
|
||||
// Now get the list of online attempts to make sure this attempt exists and isn't finished.
|
||||
return this.quizProvider.getUserAttempts(quiz.id, 'all', true, false, true, siteId).then((attempts) => {
|
||||
const lastAttemptId = attempts.length ? attempts[attempts.length - 1].id : undefined;
|
||||
let onlineAttempt;
|
||||
|
||||
// Search the attempt we retrieved from offline.
|
||||
for (const i in attempts) {
|
||||
const attempt = attempts[i];
|
||||
|
||||
if (attempt.id == offlineAttempt.id) {
|
||||
onlineAttempt = attempt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!onlineAttempt || this.quizProvider.isAttemptFinished(onlineAttempt.state)) {
|
||||
// Attempt not found or it's finished in online. Discard it.
|
||||
warnings.push(this.translate.instant('addon.mod_quiz.warningattemptfinished'));
|
||||
|
||||
return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt, true);
|
||||
}
|
||||
|
||||
// Get the data stored in offline.
|
||||
return this.quizOfflineProvider.getAttemptAnswers(offlineAttempt.id, siteId).then((answersList) => {
|
||||
|
||||
if (!answersList.length) {
|
||||
// No answers stored, finish.
|
||||
return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt,
|
||||
true);
|
||||
}
|
||||
|
||||
const answers = this.questionProvider.convertAnswersArrayToObject(answersList),
|
||||
offlineQuestions = this.quizOfflineProvider.classifyAnswersInQuestions(answers);
|
||||
let finish;
|
||||
|
||||
// We're going to need preflightData, get it.
|
||||
return this.quizProvider.getQuizAccessInformation(quiz.id, false, true, siteId).then((info) => {
|
||||
|
||||
return this.prefetchHandler.getPreflightData(quiz, info, onlineAttempt, askPreflight,
|
||||
'core.settings.synchronization', siteId);
|
||||
}).then((data) => {
|
||||
preflightData = data;
|
||||
|
||||
// Now get the online questions data.
|
||||
const pages = this.quizProvider.getPagesFromLayoutAndQuestions(onlineAttempt.layout, offlineQuestions);
|
||||
|
||||
return this.quizProvider.getAllQuestionsData(quiz, onlineAttempt, preflightData, pages, false, true,
|
||||
siteId);
|
||||
}).then((onlineQuestions) => {
|
||||
|
||||
// Validate questions, discarding the offline answers that can't be synchronized.
|
||||
return this.validateQuestions(onlineAttempt.id, onlineQuestions, offlineQuestions, siteId);
|
||||
}).then((discardedData) => {
|
||||
|
||||
// Get the answers to send.
|
||||
const answers = this.quizOfflineProvider.extractAnswersFromQuestions(offlineQuestions);
|
||||
finish = offlineAttempt.finished && !discardedData;
|
||||
|
||||
if (discardedData) {
|
||||
if (offlineAttempt.finished) {
|
||||
warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscardedfromfinished'));
|
||||
} else {
|
||||
warnings.push(this.translate.instant('addon.mod_quiz.warningdatadiscarded'));
|
||||
}
|
||||
}
|
||||
|
||||
return this.quizProvider.processAttempt(quiz, onlineAttempt, answers, preflightData, finish, false, false,
|
||||
siteId);
|
||||
}).then(() => {
|
||||
|
||||
// Answers sent, now set the current page if the attempt isn't finished.
|
||||
if (!finish) {
|
||||
return this.quizProvider.logViewAttempt(onlineAttempt.id, offlineAttempt.currentpage, preflightData,
|
||||
false).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
|
||||
// Data sent. Finish the sync.
|
||||
return this.finishSync(siteId, quiz, courseId, warnings, lastAttemptId, offlineAttempt, onlineAttempt,
|
||||
true, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return this.addOngoingSync(quiz.id, syncPromise, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate questions, discarding the offline answers that can't be synchronized.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @param {any} onlineQuestions Online questions
|
||||
* @param {any} offlineQuestions Offline questions.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: true if some offline data was discarded, false otherwise.
|
||||
*/
|
||||
validateQuestions(attemptId: number, onlineQuestions: any, offlineQuestions: any, siteId?: string): Promise<boolean> {
|
||||
const promises = [];
|
||||
let discardedData = false;
|
||||
|
||||
for (const slot in offlineQuestions) {
|
||||
const offlineQuestion = offlineQuestions[slot],
|
||||
onlineQuestion = onlineQuestions[slot],
|
||||
offlineSequenceCheck = offlineQuestion.answers[':sequencecheck'];
|
||||
|
||||
if (onlineQuestion) {
|
||||
|
||||
// We found the online data for the question, validate that the sequence check is ok.
|
||||
if (!this.questionDelegate.validateSequenceCheck(onlineQuestion, offlineSequenceCheck)) {
|
||||
// Sequence check is not valid, remove the offline data.
|
||||
discardedData = true;
|
||||
promises.push(this.quizOfflineProvider.removeQuestionAndAnswers(attemptId, Number(slot), siteId));
|
||||
delete offlineQuestions[slot];
|
||||
} else {
|
||||
// Sequence check is valid. Use the online one to prevent synchronization errors.
|
||||
offlineQuestion.answers[':sequencecheck'] = onlineQuestion.sequencecheck;
|
||||
}
|
||||
} else {
|
||||
// Online question not found, it can happen for 2 reasons:
|
||||
// 1- It's a sequential quiz and the question is in a page already passed.
|
||||
// 2- Quiz layout has changed (shouldn't happen since it's blocked if there are attempts).
|
||||
discardedData = true;
|
||||
promises.push(this.quizOfflineProvider.removeQuestionAndAnswers(attemptId, Number(slot), siteId));
|
||||
delete offlineQuestions[slot];
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return discardedData;
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,117 @@
|
|||
// (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 { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
|
||||
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||
import { AddonModQuizProvider } from './quiz';
|
||||
|
||||
/**
|
||||
* Handler to treat links to quiz review.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizReviewLinkHandler extends CoreContentLinksHandlerBase {
|
||||
name = 'AddonModQuizReviewLinkHandler';
|
||||
featureName = 'CoreCourseModuleDelegate_AddonModQuiz';
|
||||
pattern = /\/mod\/quiz\/review\.php.*([\&\?]attempt=\d+)/;
|
||||
|
||||
constructor(protected domUtils: CoreDomUtilsProvider, protected quizProvider: AddonModQuizProvider,
|
||||
protected courseHelper: CoreCourseHelperProvider, protected linkHelper: CoreContentLinksHelperProvider) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of actions for a link (url).
|
||||
*
|
||||
* @param {string[]} siteIds List of sites the URL belongs to.
|
||||
* @param {string} url The URL to treat.
|
||||
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||
* @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
|
||||
*/
|
||||
getActions(siteIds: string[], url: string, params: any, courseId?: number):
|
||||
CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
|
||||
courseId = courseId || params.courseid || params.cid;
|
||||
|
||||
return [{
|
||||
action: (siteId, navCtrl?): void => {
|
||||
// Retrieve the quiz ID using the attempt ID.
|
||||
const modal = this.domUtils.showModalLoading(),
|
||||
attemptId = parseInt(params.attempt, 10),
|
||||
page = parseInt(params.page, 10);
|
||||
let quizId;
|
||||
|
||||
this.getQuizIdByAttemptId(attemptId).then((id) => {
|
||||
quizId = id;
|
||||
|
||||
// Get the courseId if we don't have it.
|
||||
if (courseId) {
|
||||
return courseId;
|
||||
} else {
|
||||
return this.courseHelper.getModuleCourseIdByInstance(quizId, 'quiz', siteId);
|
||||
}
|
||||
}).then((courseId) => {
|
||||
// Go to the review page.
|
||||
const pageParams = {
|
||||
quizId: quizId,
|
||||
attemptId: attemptId,
|
||||
courseId: courseId,
|
||||
page: params.showall ? -1 : (isNaN(page) ? -1 : page)
|
||||
};
|
||||
|
||||
this.linkHelper.goInSite(navCtrl, 'AddonModQuizReviewPage', pageParams, siteId);
|
||||
}).catch((error) => {
|
||||
|
||||
this.domUtils.showErrorModalDefault(error, 'An error occurred while loading the required data.');
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
/**
|
||||
* Get a quiz ID by attempt ID.
|
||||
*
|
||||
* @param {number} attemptId Attempt ID.
|
||||
* @return {Promise<number>} Promise resolved with the quiz ID.
|
||||
*/
|
||||
protected getQuizIdByAttemptId(attemptId: number): Promise<number> {
|
||||
// Use getAttemptReview to retrieve the quiz ID.
|
||||
return this.quizProvider.getAttemptReview(attemptId).then((reviewData) => {
|
||||
if (reviewData.attempt && reviewData.attempt.quiz) {
|
||||
return reviewData.attempt.quiz;
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||
* If not defined, defaults to true.
|
||||
*
|
||||
* @param {string} siteId The site ID.
|
||||
* @param {string} url The URL to treat.
|
||||
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
|
||||
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
|
||||
*/
|
||||
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
|
||||
return this.quizProvider.isPluginEnabled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreCronHandler } from '@providers/cron';
|
||||
import { AddonModQuizSyncProvider } from './quiz-sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModQuizSyncCronHandler implements CoreCronHandler {
|
||||
name = 'AddonModQuizSyncCronHandler';
|
||||
|
||||
constructor(private quizSync: AddonModQuizSyncProvider) {}
|
||||
|
||||
/**
|
||||
* Execute the process.
|
||||
* Receives the ID of the site affected, undefined for all sites.
|
||||
*
|
||||
* @param {string} [siteId] ID of the site affected, undefined for all sites.
|
||||
* @return {Promise<any>} Promise resolved when done, rejected if failure.
|
||||
*/
|
||||
execute(siteId?: string): Promise<any> {
|
||||
return this.quizSync.syncAllQuizzes(siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time between consecutive executions.
|
||||
*
|
||||
* @return {number} Time between consecutive executions (in ms).
|
||||
*/
|
||||
getInterval(): number {
|
||||
return AddonModQuizSyncProvider.SYNC_TIME;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CoreCronDelegate } from '@providers/cron';
|
||||
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||
import { AddonModQuizAccessRuleDelegate } from './providers/access-rules-delegate';
|
||||
import { AddonModQuizProvider } from './providers/quiz';
|
||||
import { AddonModQuizOfflineProvider } from './providers/quiz-offline';
|
||||
import { AddonModQuizHelperProvider } from './providers/helper';
|
||||
import { AddonModQuizSyncProvider } from './providers/quiz-sync';
|
||||
import { AddonModQuizModuleHandler } from './providers/module-handler';
|
||||
import { AddonModQuizPrefetchHandler } from './providers/prefetch-handler';
|
||||
import { AddonModQuizSyncCronHandler } from './providers/sync-cron-handler';
|
||||
import { AddonModQuizIndexLinkHandler } from './providers/index-link-handler';
|
||||
import { AddonModQuizGradeLinkHandler } from './providers/grade-link-handler';
|
||||
import { AddonModQuizReviewLinkHandler } from './providers/review-link-handler';
|
||||
import { AddonModQuizComponentsModule } from './components/components.module';
|
||||
|
||||
// Access rules.
|
||||
import { AddonModQuizAccessDelayBetweenAttemptsModule } from './accessrules/delaybetweenattempts/delaybetweenattempts.module';
|
||||
import { AddonModQuizAccessIpAddressModule } from './accessrules/ipaddress/ipaddress.module';
|
||||
import { AddonModQuizAccessNumAttemptsModule } from './accessrules/numattempts/numattempts.module';
|
||||
import { AddonModQuizAccessOfflineAttemptsModule } from './accessrules/offlineattempts/offlineattempts.module';
|
||||
import { AddonModQuizAccessOpenCloseDateModule } from './accessrules/openclosedate/openclosedate.module';
|
||||
import { AddonModQuizAccessPasswordModule } from './accessrules/password/password.module';
|
||||
import { AddonModQuizAccessSafeBrowserModule } from './accessrules/safebrowser/safebrowser.module';
|
||||
import { AddonModQuizAccessSecureWindowModule } from './accessrules/securewindow/securewindow.module';
|
||||
import { AddonModQuizAccessTimeLimitModule } from './accessrules/timelimit/timelimit.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
AddonModQuizComponentsModule,
|
||||
AddonModQuizAccessDelayBetweenAttemptsModule,
|
||||
AddonModQuizAccessIpAddressModule,
|
||||
AddonModQuizAccessNumAttemptsModule,
|
||||
AddonModQuizAccessOfflineAttemptsModule,
|
||||
AddonModQuizAccessOpenCloseDateModule,
|
||||
AddonModQuizAccessPasswordModule,
|
||||
AddonModQuizAccessSafeBrowserModule,
|
||||
AddonModQuizAccessSecureWindowModule,
|
||||
AddonModQuizAccessTimeLimitModule
|
||||
],
|
||||
providers: [
|
||||
AddonModQuizAccessRuleDelegate,
|
||||
AddonModQuizProvider,
|
||||
AddonModQuizOfflineProvider,
|
||||
AddonModQuizHelperProvider,
|
||||
AddonModQuizSyncProvider,
|
||||
AddonModQuizModuleHandler,
|
||||
AddonModQuizPrefetchHandler,
|
||||
AddonModQuizSyncCronHandler,
|
||||
AddonModQuizIndexLinkHandler,
|
||||
AddonModQuizGradeLinkHandler,
|
||||
AddonModQuizReviewLinkHandler
|
||||
]
|
||||
})
|
||||
export class AddonModQuizModule {
|
||||
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModQuizModuleHandler,
|
||||
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModQuizPrefetchHandler,
|
||||
cronDelegate: CoreCronDelegate, syncHandler: AddonModQuizSyncCronHandler, linksDelegate: CoreContentLinksDelegate,
|
||||
indexHandler: AddonModQuizIndexLinkHandler, gradeHandler: AddonModQuizGradeLinkHandler,
|
||||
reviewHandler: AddonModQuizReviewLinkHandler) {
|
||||
|
||||
moduleDelegate.registerHandler(moduleHandler);
|
||||
prefetchDelegate.registerHandler(prefetchHandler);
|
||||
cronDelegate.register(syncHandler);
|
||||
linksDelegate.registerHandler(indexHandler);
|
||||
linksDelegate.registerHandler(gradeHandler);
|
||||
linksDelegate.registerHandler(reviewHandler);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ import { AddonModResourceIndexComponent } from '../components/index/index';
|
|||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
|
||||
/**
|
||||
* Handler to support resource modules.
|
||||
|
@ -29,8 +31,12 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler {
|
|||
name = 'AddonModResource';
|
||||
modName = 'resource';
|
||||
|
||||
protected statusObserver;
|
||||
|
||||
constructor(protected resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider,
|
||||
protected mimetypeUtils: CoreMimetypeUtilsProvider, private resourceHelper: AddonModResourceHelperProvider) { }
|
||||
protected mimetypeUtils: CoreMimetypeUtilsProvider, private resourceHelper: AddonModResourceHelperProvider,
|
||||
protected prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
|
@ -50,6 +56,11 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler {
|
|||
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||
*/
|
||||
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||
const updateStatus = (status: string): void => {
|
||||
handlerData.buttons[0].hidden = status !== CoreConstants.DOWNLOADED ||
|
||||
this.resourceHelper.isDisplayedInIframe(module);
|
||||
};
|
||||
|
||||
const handlerData = {
|
||||
icon: this.courseProvider.getModuleIconSrc('resource'),
|
||||
title: module.name,
|
||||
|
@ -58,8 +69,9 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler {
|
|||
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||
navCtrl.push('AddonModResourceIndexPage', {module: module, courseId: courseId}, options);
|
||||
},
|
||||
updateStatus: updateStatus.bind(this),
|
||||
buttons: [ {
|
||||
hidden: !this.resourceHelper.isDisplayedInIframe(module),
|
||||
hidden: true,
|
||||
icon: 'document',
|
||||
label: 'addon.mod_resource.openthefile',
|
||||
action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => {
|
||||
|
@ -92,7 +104,9 @@ export class AddonModResourceModuleHandler implements CoreCourseModuleHandler {
|
|||
*/
|
||||
protected hideOpenButton(module: any, courseId: number): Promise<boolean> {
|
||||
return this.courseProvider.loadModuleContents(module, courseId).then(() => {
|
||||
return this.resourceHelper.isDisplayedInIframe(module);
|
||||
return this.prefetchDelegate.getModuleStatus(module, courseId).then((status) => {
|
||||
return status !== CoreConstants.DOWNLOADED || this.resourceHelper.isDisplayedInIframe(module);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -13,15 +13,6 @@ addon-mod-survey-index {
|
|||
background-color: $white;
|
||||
}
|
||||
|
||||
ion-select {
|
||||
float: right;
|
||||
max-width: none;
|
||||
.select-text {
|
||||
white-space: normal;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.even {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
|
|
@ -188,15 +188,15 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
});
|
||||
}
|
||||
|
||||
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.refreshContent(false);
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'addon.mod_survey.cannotsubmitsurvey', true);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'addon.mod_survey.cannotsubmitsurvey', true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -35,12 +35,14 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider {
|
|||
static AUTO_SYNCED = 'addon_mod_survey_autom_synced';
|
||||
protected componentTranslate: string;
|
||||
|
||||
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
|
||||
protected appProvider: CoreAppProvider, private surveyOffline: AddonModSurveyOfflineProvider,
|
||||
constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider,
|
||||
syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService,
|
||||
courseProvider: CoreCourseProvider, private surveyOffline: AddonModSurveyOfflineProvider,
|
||||
private eventsProvider: CoreEventsProvider, private surveyProvider: AddonModSurveyProvider,
|
||||
private translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider,
|
||||
courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider) {
|
||||
super('AddonModSurveySyncProvider', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils);
|
||||
private utils: CoreUtilsProvider) {
|
||||
|
||||
super('AddonModSurveySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||
|
||||
this.componentTranslate = courseProvider.translateModuleName('survey');
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<form name="itemEdit" (ngSubmit)="addNote()">
|
||||
<ion-item>
|
||||
<ion-label>{{ 'addon.notes.publishstate' | translate }}</ion-label>
|
||||
<ion-select [(ngModel)]="publishState" name="publishState">
|
||||
<ion-select [(ngModel)]="publishState" name="publishState" interface="popover">
|
||||
<ion-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-option>
|
||||
<ion-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-option>
|
||||
<ion-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-option>
|
||||
|
|
|
@ -34,12 +34,13 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
|
|||
|
||||
static AUTO_SYNCED = 'addon_notes_autom_synced';
|
||||
|
||||
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
|
||||
protected appProvider: CoreAppProvider, private notesOffline: AddonNotesOfflineProvider,
|
||||
constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider,
|
||||
syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService,
|
||||
private notesOffline: AddonNotesOfflineProvider, private utils: CoreUtilsProvider,
|
||||
private eventsProvider: CoreEventsProvider, private notesProvider: AddonNotesProvider,
|
||||
private coursesProvider: CoreCoursesProvider, private translate: TranslateService, private utils: CoreUtilsProvider,
|
||||
syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) {
|
||||
super('AddonNotesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils);
|
||||
private coursesProvider: CoreCoursesProvider) {
|
||||
|
||||
super('AddonNotesSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
<ion-item text-wrap class="addon-qbehaviour-deferredcbm-certainty-title" *ngIf="question.behaviourCertaintyOptions && question.behaviourCertaintyOptions.length">
|
||||
<p>{{ 'core.question.certainty' | translate }}</p>
|
||||
</ion-item>
|
||||
<ion-radio *ngFor="let option of question.behaviourCertaintyOptions" id="{{option.id}}" name="{{option.name}}" [ngModel]="question.behaviourCertaintySelected" [value]="option.value" [disabled]="option.disabled">
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text></p>
|
||||
</ion-radio>
|
||||
<div *ngIf="question.behaviourCertaintyOptions && question.behaviourCertaintyOptions.length">
|
||||
<ion-item text-wrap class="addon-qbehaviour-deferredcbm-certainty-title" >
|
||||
<p>{{ 'core.question.certainty' | translate }}</p>
|
||||
</ion-item>
|
||||
<div radio-group [(ngModel)]="question.behaviourCertaintySelected" [name]="question.behaviourCertaintyOptions[0].name">
|
||||
<ion-item text-wrap *ngFor="let option of question.behaviourCertaintyOptions">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-radio id="{{option.id}}" [value]="option.value" [disabled]="option.disabled"></ion-radio>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
|
||||
<input type="hidden" [ngModel]="question.behaviourCertaintySelected" [attr.name]="question.behaviourCertaintyOptions[0].name">
|
||||
</div>
|
||||
|
|
|
@ -8,24 +8,26 @@
|
|||
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ion-item text-wrap>
|
||||
<ion-row>
|
||||
<!-- Display unit select before the answer input. -->
|
||||
<ng-container *ngIf="question.select && question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
<ion-item text-wrap ion-grid>
|
||||
<ion-grid item-content>
|
||||
<ion-row>
|
||||
<!-- Display unit select before the answer input. -->
|
||||
<ng-container *ngIf="question.select && question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Input to enter the answer. -->
|
||||
<ion-col>
|
||||
<ion-input type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}' autocorrect="off">
|
||||
</ion-input>
|
||||
</ion-col>
|
||||
<!-- Input to enter the answer. -->
|
||||
<ion-col>
|
||||
<ion-input padding-left type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}' autocorrect="off">
|
||||
</ion-input>
|
||||
</ion-col>
|
||||
|
||||
<!-- Display unit select after the answer input. -->
|
||||
<ng-container *ngIf="question.select && !question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
</ion-row>
|
||||
<!-- Display unit select after the answer input. -->
|
||||
<ng-container *ngIf="question.select && !question.selectFirst">
|
||||
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
|
||||
</ng-container>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display unit options after the answer input. -->
|
||||
|
@ -38,18 +40,26 @@
|
|||
<ng-template #selectUnits>
|
||||
<ion-col>
|
||||
<label *ngIf="question.select.accessibilityLabel" class="accesshide" for="{{question.select.id}}">{{ question.select.accessibilityLabel }}</label>
|
||||
<ion-select id="{{question.select.id}}" [name]="question.select.name" [ngModel]="question.select.selected">
|
||||
<ion-select id="{{question.select.id}}" [name]="question.select.name" [(ngModel)]="question.select.selected" interface="popover">
|
||||
<ion-option *ngFor="let option of question.select.options" [value]="option.value">{{option.label}}</ion-option>
|
||||
</ion-select>
|
||||
<!-- @todo: select fix? -->
|
||||
|
||||
<!-- ion-select doesn't use a select. Create a hidden input to hold the selected value. -->
|
||||
<input type="hidden" [ngModel]="question.select.selected" [attr.name]="question.select.name">
|
||||
</ion-col>
|
||||
</ng-template>
|
||||
|
||||
<!-- Template for units entered using radio buttons. -->
|
||||
<ng-template #radioUnits>
|
||||
<div radio-group [ngModel]="question.unit" [name]="question.optionsName">
|
||||
<ion-radio *ngFor="let option of question.options" [value]="option.value" [disabled]="option.disabled">
|
||||
<p>{{option.text}}</p>
|
||||
</ion-radio>
|
||||
<div radio-group [(ngModel)]="question.unit" [name]="question.optionsName">
|
||||
<ion-item text-wrap *ngFor="let option of question.options">
|
||||
<ion-label>
|
||||
<p>{{option.text}}</p>
|
||||
</ion-label>
|
||||
<ion-radio [value]="option.value" [disabled]="option.disabled"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
|
||||
<input type="hidden" [ngModel]="question.unit" [attr.name]="question.optionsName">
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -45,6 +45,7 @@ export class AddonQtypeDdImageOrTextQuestion {
|
|||
protected topNode: HTMLElement;
|
||||
protected proportion = 1;
|
||||
protected selected: HTMLElement; // Selected element (being "dragged").
|
||||
protected resizeFunction;
|
||||
|
||||
/**
|
||||
* Create the this.
|
||||
|
@ -182,7 +183,10 @@ export class AddonQtypeDdImageOrTextQuestion {
|
|||
*/
|
||||
destroy(): void {
|
||||
this.stopPolling();
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
|
||||
if (this.resizeFunction) {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,7 +196,7 @@ export class AddonQtypeDdImageOrTextQuestion {
|
|||
* @return {AddonQtypeDdImageOrTextQuestionDocStructure} The object.
|
||||
*/
|
||||
docStructure(slot: number): AddonQtypeDdImageOrTextQuestionDocStructure {
|
||||
const topNode = <HTMLElement> this.container.querySelector(`#core-question-${slot} .addon-qtype-ddimageortext-container`),
|
||||
const topNode = <HTMLElement> this.container.querySelector('.addon-qtype-ddimageortext-container'),
|
||||
dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems'),
|
||||
doc: AddonQtypeDdImageOrTextQuestionDocStructure = {};
|
||||
|
||||
|
@ -456,6 +460,7 @@ export class AddonQtypeDdImageOrTextQuestion {
|
|||
this.pollForImageLoad();
|
||||
});
|
||||
|
||||
this.resizeFunction = this.repositionDragsForQuestion.bind(this);
|
||||
window.addEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
|
||||
|
@ -637,13 +642,6 @@ export class AddonQtypeDdImageOrTextQuestion {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call when the window is resized.
|
||||
*/
|
||||
resizeFunction(): void {
|
||||
this.repositionDragsForQuestion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a draggable element as selected.
|
||||
*
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<core-loading [hideUntil]="question.loaded"></core-loading>
|
||||
|
||||
<ion-item text-wrap [ngClass]="{invisible: !question.loaded}">
|
||||
<p *ngIf="!question.readOnly" class="core-info-card-icon">
|
||||
<ion-icon name="information" item-start></ion-icon>
|
||||
<p *ngIf="!question.readOnly" class="core-info-card" icon-start>
|
||||
<ion-icon name="information"></ion-icon>
|
||||
{{ 'core.question.howtodraganddrop' | translate }}
|
||||
</p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea"></core-format-text>
|
||||
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea" (afterRender)="questionRendered()"></core-format-text>
|
||||
</ion-item>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
// Style ddimageortext content a bit. Almost all these styles are copied from Moodle.
|
||||
addon-qtype-ddimageortext {
|
||||
.qtext {
|
||||
margin-bottom: 0.5em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.droparea img {
|
||||
border: 1px solid $gray-darker;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.draghome {
|
||||
vertical-align: top;
|
||||
margin: 5px;
|
||||
visibility : hidden;
|
||||
}
|
||||
.draghome img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.draghome {
|
||||
border: 1px solid $gray-darker;
|
||||
cursor: pointer;
|
||||
background-color: #B0C4DE;
|
||||
display:inline-block;
|
||||
height: auto;
|
||||
width: auto;
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.group1 {
|
||||
background-color: $white;
|
||||
}
|
||||
.group2 {
|
||||
background-color: $blue-light;
|
||||
}
|
||||
.group3 {
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
.group4 {
|
||||
background-color: #D8BFD8;
|
||||
}
|
||||
.group5 {
|
||||
background-color: #87CEFA;
|
||||
}
|
||||
.group6 {
|
||||
background-color: #DAA520;
|
||||
}
|
||||
.group7 {
|
||||
background-color: #FFD700;
|
||||
}
|
||||
.group8 {
|
||||
background-color: #F0E68C;
|
||||
}
|
||||
.drag {
|
||||
border: 1px solid $gray-darker;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
.dragitems.readonly .drag {
|
||||
cursor: auto;
|
||||
}
|
||||
.dragitems>div {
|
||||
clear: both;
|
||||
}
|
||||
.dragitems {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dragitems.readonly {
|
||||
cursor: auto;
|
||||
}
|
||||
.drag img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.ddarea {
|
||||
text-align : center;
|
||||
position: relative;
|
||||
}
|
||||
.dropbackground {
|
||||
margin:0 auto;
|
||||
}
|
||||
.dropzone {
|
||||
border: 1px solid $gray-darker;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.readonly .dropzone {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
div.dragitems div.draghome, div.dragitems div.drag {
|
||||
font:13px/1.231 arial,helvetica,clean,sans-serif;
|
||||
}
|
||||
.drag.beingdragged {
|
||||
z-index: 3;
|
||||
box-shadow: 3px 3px 4px $gray-darker;
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, AfterViewInit, Injector, ElementRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
|
||||
|
@ -24,14 +24,15 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
|
|||
selector: 'addon-qtype-ddimageortext',
|
||||
templateUrl: 'ddimageortext.html'
|
||||
})
|
||||
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected questionInstance: AddonQtypeDdImageOrTextQuestion;
|
||||
protected drops: any[]; // The drop zones received in the init object of the question.
|
||||
protected destroyed = false;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(logger, 'AddonQtypeDdImageOrTextComponent', injector);
|
||||
constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(loggerProvider, 'AddonQtypeDdImageOrTextComponent', injector);
|
||||
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
@ -76,18 +77,21 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
|
|||
}
|
||||
|
||||
/**
|
||||
* View has been initialized.
|
||||
* The question has been rendered.
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdImageOrTextQuestion(this.logger, this.domUtils, this.element,
|
||||
this.question, this.question.readOnly, this.drops);
|
||||
questionRendered(): void {
|
||||
if (!this.destroyed) {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdImageOrTextQuestion(this.loggerProvider, this.domUtils, this.element,
|
||||
this.question, this.question.readOnly, this.drops);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
this.questionInstance && this.questionInstance.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
protected proportion = 1;
|
||||
protected selected: HTMLElement; // Selected element (being "dragged").
|
||||
protected graphics: AddonQtypeDdMarkerGraphicsApi;
|
||||
protected resizeFunction;
|
||||
|
||||
doc: AddonQtypeDdMarkerQuestionDocStructure;
|
||||
shapes = [];
|
||||
|
@ -157,7 +158,9 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
* Function to call when the instance is no longer needed.
|
||||
*/
|
||||
destroy(): void {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
if (this.resizeFunction) {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,7 +170,7 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
* @return {AddonQtypeDdMarkerQuestionDocStructure} The object.
|
||||
*/
|
||||
docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure {
|
||||
const topNode = <HTMLElement> this.container.querySelector('#core-question-' + slot + ' .addon-qtype-ddmarker-container'),
|
||||
const topNode = <HTMLElement> this.container.querySelector('.addon-qtype-ddmarker-container'),
|
||||
dragItemsArea = <HTMLElement> topNode.querySelector('div.dragitems');
|
||||
|
||||
return {
|
||||
|
@ -293,9 +296,9 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
const markerTexts = this.doc.markerTexts();
|
||||
// Check if there is already a marker text for this drop zone.
|
||||
if (link) {
|
||||
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo + ' a');
|
||||
existingMarkerText = <HTMLElement> markerTexts.querySelector('span.markertext' + dropZoneNo + ' a');
|
||||
} else {
|
||||
existingMarkerText = markerTexts.querySelector('span.markertext' + dropZoneNo);
|
||||
existingMarkerText = <HTMLElement> markerTexts.querySelector('span.markertext' + dropZoneNo);
|
||||
}
|
||||
|
||||
if (existingMarkerText) {
|
||||
|
@ -538,7 +541,7 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
dragging = (this.doc.dragItemBeingDragged(choiceNo) !== null),
|
||||
coords: number[][] = [];
|
||||
|
||||
if (fv !== '' && typeof fv != 'undefined') {
|
||||
if (fv !== '' && typeof fv != 'undefined' && fv !== null) {
|
||||
// Get all the coordinates in the input and add them to the coords list.
|
||||
const coordsStrings = fv.split(';');
|
||||
|
||||
|
@ -645,6 +648,7 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
this.pollForImageLoad();
|
||||
});
|
||||
|
||||
this.resizeFunction = this.redrawDragsAndDrops.bind(this);
|
||||
window.addEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
|
||||
|
@ -731,6 +735,9 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraw all draggables and drop zones.
|
||||
*/
|
||||
redrawDragsAndDrops(): void {
|
||||
// Mark all the draggable items as not placed.
|
||||
const drags = this.doc.dragItems();
|
||||
|
@ -789,7 +796,7 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
dropZone = this.dropZones[dropZoneNo],
|
||||
dzNo = Number(dropZoneNo);
|
||||
|
||||
this.drawDropZone(dzNo, dropZone.markerText, dropZone.shape, dropZone.coords, colourForDropZone, true);
|
||||
this.drawDropZone(dzNo, dropZone.markertext, dropZone.shape, dropZone.coords, colourForDropZone, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -803,13 +810,6 @@ export class AddonQtypeDdMarkerQuestion {
|
|||
this.setFormValue(choiceNo, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call when the window is resized.
|
||||
*/
|
||||
resizeFunction(): void {
|
||||
this.redrawDragsAndDrops();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the colour index.
|
||||
*/
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<core-loading [hideUntil]="question.loaded"></core-loading>
|
||||
|
||||
<ion-item text-wrap [ngClass]="{invisible: !question.loaded}">
|
||||
<p *ngIf="!question.readOnly" class="core-info-card-icon">
|
||||
<ion-icon name="information" item-start></ion-icon>
|
||||
<p *ngIf="!question.readOnly" class="core-info-card" icon-start>
|
||||
<ion-icon name="information"></ion-icon>
|
||||
{{ 'core.question.howtodraganddrop' | translate }}
|
||||
</p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea"></core-format-text>
|
||||
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId" [text]="question.ddArea" (afterRender)="questionRendered()"></core-format-text>
|
||||
</ion-item>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
// Style ddmarker content a bit. Almost all these styles are copied from Moodle.
|
||||
addon-qtype-ddmarker {
|
||||
.qtext {
|
||||
margin-bottom: 0.5em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.droparea img {
|
||||
border: 1px solid $gray-darker;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.draghome img, .draghome span {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dragitems .dragitem {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dropzones {
|
||||
position: absolute;
|
||||
}
|
||||
.dropzones svg {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.dragitem.beingdragged .markertext {
|
||||
z-index: 5;
|
||||
box-shadow: 3px 3px 4px $gray-darker;
|
||||
}
|
||||
.dragitems .draghome {
|
||||
margin: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dragitems.readonly .dragitem {
|
||||
cursor: auto;
|
||||
}
|
||||
div.ddarea {
|
||||
text-align: center;
|
||||
}
|
||||
div.ddarea .markertexts {
|
||||
min-height: 80px;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
}
|
||||
.dropbackground {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
div.dragitems div.draghome, div.dragitems div.dragitem,
|
||||
div.draghome, div.drag {
|
||||
font: 13px/1.231 arial,helvetica,clean,sans-serif;
|
||||
}
|
||||
div.dragitems span.markertext,
|
||||
div.markertexts span.markertext {
|
||||
margin: 0 5px;
|
||||
z-index: 2;
|
||||
background-color: $white;
|
||||
border: 2px solid $gray-darker;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
div.markertexts span.markertext {
|
||||
z-index: 3;
|
||||
background-color: $yellow-light;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: $yellow;
|
||||
position: absolute;
|
||||
}
|
||||
span.wrongpart {
|
||||
background-color: $yellow-light;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: $yellow;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
filter: alpha(opacity=60);
|
||||
opacity: 0.6;
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
div.dragitems img.target {
|
||||
position: absolute;
|
||||
left: -7px; /* This must be half the size of the target image, minus 0.5. */
|
||||
top: -7px; /* In other words, this works for a 15x15 cross-hair. */
|
||||
}
|
||||
div.dragitems div.draghome img.target {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, AfterViewInit, Injector, ElementRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
|
||||
|
@ -24,14 +24,15 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
|
|||
selector: 'addon-qtype-ddmarker',
|
||||
templateUrl: 'ddmarker.html'
|
||||
})
|
||||
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected questionInstance: AddonQtypeDdMarkerQuestion;
|
||||
protected dropZones: any[]; // The drop zones received in the init object of the question.
|
||||
protected destroyed = false;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(logger, 'AddonQtypeDdMarkerComponent', injector);
|
||||
constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(loggerProvider, 'AddonQtypeDdMarkerComponent', injector);
|
||||
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
@ -83,18 +84,21 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
|
|||
}
|
||||
|
||||
/**
|
||||
* View has been initialized.
|
||||
* The question has been rendered.
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdMarkerQuestion(this.logger, this.domUtils, this.textUtils, this.element,
|
||||
this.question, this.question.readOnly, this.dropZones);
|
||||
questionRendered(): void {
|
||||
if (!this.destroyed) {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdMarkerQuestion(this.loggerProvider, this.domUtils, this.textUtils, this.element,
|
||||
this.question, this.question.readOnly, this.dropZones);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
this.questionInstance && this.questionInstance.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ export class AddonQtypeDdwtosQuestion {
|
|||
protected selectors: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors.
|
||||
protected placed: {[no: number]: number}; // Map that relates drag elements numbers with drop zones numbers.
|
||||
protected selected: HTMLElement; // Selected element (being "dragged").
|
||||
protected resizeFunction;
|
||||
|
||||
/**
|
||||
* Create the instance.
|
||||
|
@ -125,7 +126,7 @@ export class AddonQtypeDdwtosQuestion {
|
|||
* @return {AddonQtypeDdwtosQuestionCSSSelectors} Object with the functions to get the selectors.
|
||||
*/
|
||||
cssSelectors(slot: number): AddonQtypeDdwtosQuestionCSSSelectors {
|
||||
const topNode = '#core-question-' + slot + ' .addon-qtype-ddwtos-container',
|
||||
const topNode = '.addon-qtype-ddwtos-container',
|
||||
selectors: AddonQtypeDdwtosQuestionCSSSelectors = {};
|
||||
|
||||
selectors.topNode = (): string => {
|
||||
|
@ -193,7 +194,9 @@ export class AddonQtypeDdwtosQuestion {
|
|||
* Function to call when the instance is no longer needed.
|
||||
*/
|
||||
destroy(): void {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
if (this.resizeFunction) {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -285,6 +288,7 @@ export class AddonQtypeDdwtosQuestion {
|
|||
this.positionDragItems();
|
||||
});
|
||||
|
||||
this.resizeFunction = this.positionDragItems.bind(this);
|
||||
window.addEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
|
||||
|
@ -488,13 +492,6 @@ export class AddonQtypeDdwtosQuestion {
|
|||
this.placeDragInDrop(null, drop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call when the window is resized.
|
||||
*/
|
||||
resizeFunction(): void {
|
||||
this.positionDragItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a certain element as being "dragged".
|
||||
*
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<section ion-list *ngIf="question.text || question.text === ''" class="addon-qtype-ddwtos-container">
|
||||
<ion-item text-wrap>
|
||||
<p *ngIf="!question.readOnly" class="core-info-card-icon">
|
||||
<ion-icon name="information" item-start></ion-icon>
|
||||
<section ion-list *ngIf="question.text || question.text === ''">
|
||||
<ion-item text-wrap class="addon-qtype-ddwtos-container">
|
||||
<p *ngIf="!question.readOnly" class="core-info-card" icon-start>
|
||||
<ion-icon name="information"></ion-icon>
|
||||
{{ 'core.question.howtodraganddrop' | translate }}
|
||||
</p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
<core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers"></core-format-text>
|
||||
<core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers" (afterRender)="questionRendered()"></core-format-text>
|
||||
<div class="drags"></div>
|
||||
</ion-item>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
// Style ddwtos content a bit. Almost all these styles are copied from Moodle.
|
||||
addon-qtype-ddwtos {
|
||||
.qtext {
|
||||
margin-bottom: 0.5em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.draghome {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.answertext {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.drop {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: 1px solid $gray-darker;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.draghome, .drag {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.draghome, .drag.unplaced{
|
||||
border: 1px solid $gray-darker;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.draghome {
|
||||
visibility: hidden;
|
||||
}
|
||||
.drag {
|
||||
z-index: 2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.drag.selected {
|
||||
z-index: 3;
|
||||
box-shadow: 3px 3px 4px $gray-darker;
|
||||
}
|
||||
|
||||
.drop.selected {
|
||||
border-color: $yellow-light;
|
||||
box-shadow: 0 0 5px 5px $yellow-light;
|
||||
}
|
||||
|
||||
&.notreadonly .drag,
|
||||
&.notreadonly .draghome,
|
||||
&.notreadonly .drop,
|
||||
&.notreadonly .answercontainer {
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&.readonly .drag,
|
||||
&.readonly .draghome,
|
||||
&.readonly .drop,
|
||||
&.readonly .answercontainer {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
span.incorrect {
|
||||
background-color: $red-light;
|
||||
}
|
||||
span.correct {
|
||||
background-color: $green-light;
|
||||
}
|
||||
|
||||
.group1 {
|
||||
background-color: $white;
|
||||
}
|
||||
.group2 {
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
.group3 {
|
||||
background-color: $blue-light;
|
||||
}
|
||||
.group4 {
|
||||
background-color: #D8BFD8;
|
||||
}
|
||||
.group5 {
|
||||
background-color: #87CEFA;
|
||||
}
|
||||
.group6 {
|
||||
background-color: #DAA520;
|
||||
}
|
||||
.group7 {
|
||||
background-color: #FFD700;
|
||||
}
|
||||
.group8 {
|
||||
background-color: #F0E68C;
|
||||
}
|
||||
|
||||
sub, sup {
|
||||
font-size: 80%;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sup {
|
||||
top: -0.4em;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.2em;
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, AfterViewInit, Injector, ElementRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Injector, ElementRef } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
|
||||
|
@ -24,14 +24,15 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
|
|||
selector: 'addon-qtype-ddwtos',
|
||||
templateUrl: 'ddwtos.html'
|
||||
})
|
||||
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected questionInstance: AddonQtypeDdwtosQuestion;
|
||||
protected inputIds: string[]; // Ids of the inputs of the question (where the answers will be stored).
|
||||
protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored).
|
||||
protected destroyed = false;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(logger, 'AddonQtypeDdwtosComponent', injector);
|
||||
constructor(protected loggerProvider: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(loggerProvider, 'AddonQtypeDdwtosComponent', injector);
|
||||
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
@ -54,7 +55,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
|
|||
this.questionHelper.replaceFeedbackClasses(div);
|
||||
|
||||
// Treat the correct/incorrect icons.
|
||||
this.questionHelper.treatCorrectnessIcons(div, this.component, this.componentId);
|
||||
this.questionHelper.treatCorrectnessIcons(div);
|
||||
|
||||
const answerContainer = div.querySelector('.answercontainer');
|
||||
if (!answerContainer) {
|
||||
|
@ -74,28 +75,32 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
|
|||
}
|
||||
|
||||
// Get the inputs where the answers will be stored and add them to the question text.
|
||||
const inputEls = <HTMLElement[]> Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])')),
|
||||
inputIds = [];
|
||||
const inputEls = <HTMLElement[]> Array.from(div.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
|
||||
|
||||
inputEls.forEach((inputEl) => {
|
||||
this.question.text += inputEl.outerHTML;
|
||||
inputIds.push(inputEl.getAttribute('id'));
|
||||
this.inputIds.push(inputEl.getAttribute('id'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* View has been initialized.
|
||||
* The question has been rendered.
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdwtosQuestion(this.logger, this.domUtils, this.element, this.question,
|
||||
this.question.readOnly, this.inputIds);
|
||||
questionRendered(): void {
|
||||
if (!this.destroyed) {
|
||||
// Create the instance.
|
||||
this.questionInstance = new AddonQtypeDdwtosQuestion(this.loggerProvider, this.domUtils, this.element, this.question,
|
||||
this.question.readOnly, this.inputIds);
|
||||
|
||||
this.questionHelper.treatCorrectnessIconsClicks(this.element, this.component, this.componentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
this.questionInstance && this.questionInstance.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,18 @@
|
|||
<!-- Textarea. -->
|
||||
<ion-item *ngIf="question.textarea && !question.hasDraftFiles">
|
||||
<!-- "Format" hidden input -->
|
||||
<input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value" >
|
||||
<input item-content *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value" >
|
||||
<!-- Plain text textarea. -->
|
||||
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true">{{question.textarea.text}}</ion-textarea>
|
||||
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name" aria-multiline="true" [ngModel]="question.textarea.text"></ion-textarea>
|
||||
<!-- Rich text editor. -->
|
||||
<core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"></core-rich-text-editor>
|
||||
<core-rich-text-editor item-content *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}" [control]="formControl" [name]="question.textarea.name"></core-rich-text-editor>
|
||||
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
|
||||
model="textarea" [name]="textarea.name" [component]="component" [componentId]="componentId" -->
|
||||
[component]="component" [componentId]="componentId" -->
|
||||
</ion-item>
|
||||
|
||||
<!-- Draft files not supported. -->
|
||||
<ng-container *ngIf="question.textarea && question.hasDraftFiles">
|
||||
<ion-item text-wrap class="core-error-item">
|
||||
<ion-item text-wrap class="core-danger-item">
|
||||
<p class="core-question-warning">{{ 'core.question.errorinlinefilesnotsupported' | translate }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap>
|
||||
|
@ -27,7 +27,7 @@
|
|||
</ng-container>
|
||||
|
||||
<!-- Attachments not supported in the app yet. -->
|
||||
<ion-item text-wrap *ngIf="question.allowsAttachments" class="core-error-item">
|
||||
<ion-item text-wrap *ngIf="question.allowsAttachments" class="core-danger-item">
|
||||
<p class="core-question-warning">{{ 'core.question.errorattachmentsnotsupported' | translate }}</p>
|
||||
</ion-item>
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import { Component, OnInit, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
import { FormControl, FormBuilder } from '@angular/forms';
|
||||
|
||||
/**
|
||||
* Component to render an essay question.
|
||||
|
@ -25,7 +26,9 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-
|
|||
})
|
||||
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
protected formControl: FormControl;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector, protected fb: FormBuilder) {
|
||||
super(logger, 'AddonQtypeEssayComponent', injector);
|
||||
}
|
||||
|
||||
|
@ -34,5 +37,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
|
|||
*/
|
||||
ngOnInit(): void {
|
||||
this.initEssayComponent();
|
||||
|
||||
this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<section ion-list class="addon-qtype-gapselect-container" *ngIf="question.text || question.text === ''">
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text" (afterRender)="questionRendered()"></core-format-text></p>
|
||||
</ion-item>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// Style gapselect content a bit. All these styles are copied from Moodle.
|
||||
addon-qtype-gapselect {
|
||||
p {
|
||||
margin: 0 0 .5em;
|
||||
}
|
||||
|
||||
select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
display: inline-block;
|
||||
border: 1px solid $gray-dark;
|
||||
padding: 4px 6px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
background: $gray-lighter;
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Injector } from '@angular/core';
|
||||
import { Component, OnInit, Injector, ElementRef } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
|
@ -25,8 +25,12 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-
|
|||
})
|
||||
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(logger, 'AddonQtypeGapSelectComponent', injector);
|
||||
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,4 +39,11 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent impl
|
|||
ngOnInit(): void {
|
||||
this.initOriginalTextComponent('.qtext');
|
||||
}
|
||||
|
||||
/**
|
||||
* The question has been rendered.
|
||||
*/
|
||||
questionRendered(): void {
|
||||
this.questionHelper.treatCorrectnessIconsClicks(this.element, this.component, this.componentId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,21 @@
|
|||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngFor="let row of question.rows">
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<p><core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId" [text]="row.text"></core-format-text></p>
|
||||
</ion-col>
|
||||
<ion-col [ngClass]='{"core-question-answer-correct": row.isCorrect === 1, "core-question-answer-incorrect": row.isCorrect === 0}'>
|
||||
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">{{ row.accessibilityLabel }}</label>
|
||||
<ion-select id="{{row.id}}" [name]="row.name" [attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [ngModel]="row.selected">
|
||||
<ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option>
|
||||
</ion-select>
|
||||
<!-- @todo: select fix? -->
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-grid item-content>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<p><core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId" [text]="row.text"></core-format-text></p>
|
||||
</ion-col>
|
||||
<ion-col [ngClass]='{"core-question-answer-correct": row.isCorrect === 1, "core-question-answer-incorrect": row.isCorrect === 0}'>
|
||||
<label class="accesshide" for="{{row.id}}" *ngIf="row.accessibilityLabel">{{ row.accessibilityLabel }}</label>
|
||||
<ion-select id="{{row.id}}" [name]="row.name" [attr.aria-labelledby]="'addon-qtype-match-question-' + row.id" [(ngModel)]="row.selected" interface="popover">
|
||||
<ion-option *ngFor="let option of row.options" [value]="option.value">{{option.label}}</ion-option>
|
||||
</ion-select>
|
||||
|
||||
<!-- ion-select doesn't use a select. Create a hidden input to hold the selected value. -->
|
||||
<input type="hidden" [ngModel]="row.selected" [attr.name]="row.name">
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item>
|
||||
</section>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<section ion-list class="addon-qtype-multianswer-container" *ngIf="question.text || question.text === ''">
|
||||
<ion-item text-wrap>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text" (afterRender)="questionRendered()"></core-format-text></p>
|
||||
</ion-item>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Style multianswer content a bit. All these styles are copied from Moodle.
|
||||
addon-qtype-multianswer {
|
||||
p {
|
||||
margin: 0 0 .5em;
|
||||
}
|
||||
|
||||
.answer div.r0, .answer div.r1, .answer td.r0, .answer td.r1 {
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
display: table;
|
||||
}
|
||||
|
||||
tr {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
td {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
input, select {
|
||||
display: inline-block;
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px 6px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
input[type="radio"], input[type="checkbox"] {
|
||||
margin-top: -4px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Injector } from '@angular/core';
|
||||
import { Component, OnInit, Injector, ElementRef } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component';
|
||||
|
||||
|
@ -25,8 +25,12 @@ import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-
|
|||
})
|
||||
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector) {
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, injector: Injector, element: ElementRef) {
|
||||
super(logger, 'AddonQtypeMultiAnswerComponent', injector);
|
||||
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,4 +39,11 @@ export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent im
|
|||
ngOnInit(): void {
|
||||
this.initOriginalTextComponent('.formulation');
|
||||
}
|
||||
|
||||
/**
|
||||
* The question has been rendered.
|
||||
*/
|
||||
questionRendered(): void {
|
||||
this.questionHelper.treatCorrectnessIconsClicks(this.element, this.component, this.componentId);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue