diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 0ccbb17cf..fe33b00d9 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -583,7 +583,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { */ protected processAttempt(userFinish?: boolean, timeUp?: boolean, retrying?: boolean): Promise { // Get the answers to send. - return this.prepareAnswers().then((answers) => { + let promise = Promise.resolve({}); + + if (!this.showSummary) { + promise = this.prepareAnswers(); + } + + return promise.then((answers) => { // Send the answers. return this.quizProvider.processAttempt(this.quiz, this.attempt, answers, this.preflightData, userFinish, timeUp, this.offline).catch((error) => { diff --git a/src/addon/qtype/ddmarker/providers/handler.ts b/src/addon/qtype/ddmarker/providers/handler.ts index 3971da112..a3c2bd01e 100644 --- a/src/addon/qtype/ddmarker/providers/handler.ts +++ b/src/addon/qtype/ddmarker/providers/handler.ts @@ -18,6 +18,7 @@ import { CoreQuestionProvider } from '@core/question/providers/question'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { AddonQtypeDdMarkerComponent } from '../component/ddmarker'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to support drag-and-drop markers question type. @@ -119,9 +120,9 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: any, usageId: number): string[] { + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { this.questionHelper.extractQuestionScripts(question, usageId); if (question.amdArgs && typeof question.amdArgs[1] == 'string') { diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts index 527b8505f..834850ca4 100644 --- a/src/addon/qtype/essay/providers/handler.ts +++ b/src/addon/qtype/essay/providers/handler.ts @@ -24,6 +24,7 @@ import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { CoreQuestion } from '@core/question/providers/question'; import { AddonQtypeEssayComponent } from '../component/essay'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to support essay question type. @@ -67,6 +68,23 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { return this.questionHelper.deleteStoredQuestionFiles(question, component, componentId, siteId); } + /** + * Get the list of files that needs to be downloaded in addition to the files embedded in the HTML. + * + * @param question Question. + * @param usageId Usage ID. + * @return List of files or URLs. + */ + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { + if (!question.responsefileareas) { + return []; + } + + return question.responsefileareas.reduce((urlsList, area) => { + return urlsList.concat(area.files || []); + }, []); + } + /** * Check whether the question allows text and/or attachments. * @@ -165,8 +183,8 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { return attachments && attachments.length >= Number(question.settings.attachmentsrequired) ? 1 : 0; } - return (hasTextAnswer || question.settings.responserequired == '0') && - (attachments && attachments.length > Number(question.settings.attachmentsrequired)) ? 1 : 0; + return ((hasTextAnswer || question.settings.responserequired == '0') && + (attachments && attachments.length >= Number(question.settings.attachmentsrequired))) ? 1 : 0; } /** diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index c40ad2703..d8993b267 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, ElementRef } from '@angular/core'; +import { Component, ViewChild, ElementRef, OnDestroy } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; @@ -25,6 +25,7 @@ import { CoreLoginHelperProvider } from '../../providers/helper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreCustomURLSchemes } from '@providers/urlschemes'; +import { Subscription } from 'rxjs'; /** * Page to enter the user credentials. @@ -34,7 +35,7 @@ import { CoreCustomURLSchemes } from '@providers/urlschemes'; selector: 'page-core-login-credentials', templateUrl: 'credentials.html', }) -export class CoreLoginCredentialsPage { +export class CoreLoginCredentialsPage implements OnDestroy { @ViewChild('credentialsForm') formElement: ElementRef; @@ -57,6 +58,7 @@ export class CoreLoginCredentialsPage { protected viewLeft = false; protected siteId: string; protected urlToOpen: string; + protected valueChangeSubscription: Subscription; constructor(private navCtrl: NavController, navParams: NavParams, @@ -89,6 +91,28 @@ export class CoreLoginCredentialsPage { } else { this.showScanQR = false; } + + if (appProvider.isIOS()) { + // Make iOS auto-fill work. The field that isn't focused doesn't get updated, do it manually. + // Debounce it to prevent triggering this function too often when the user is typing. + this.valueChangeSubscription = this.credForm.valueChanges.debounceTime(1000).subscribe((changes) => { + if (!this.formElement || !this.formElement.nativeElement) { + return; + } + + const usernameInput = this.formElement.nativeElement.querySelector('input[name="username"]'); + const passwordInput = this.formElement.nativeElement.querySelector('input[name="password"]'); + const usernameValue = usernameInput && usernameInput.value; + const passwordValue = passwordInput && passwordInput.value; + + if (typeof usernameValue != 'undefined' && usernameValue != changes.username) { + this.credForm.get('username').setValue(usernameValue); + } + if (typeof passwordValue != 'undefined' && passwordValue != changes.password) { + this.credForm.get('password').setValue(passwordValue); + } + }); + } } /** @@ -336,4 +360,11 @@ export class CoreLoginCredentialsPage { } } } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe(); + } } diff --git a/src/core/question/providers/delegate.ts b/src/core/question/providers/delegate.ts index 6caa7eb2b..7a0cd7675 100644 --- a/src/core/question/providers/delegate.ts +++ b/src/core/question/providers/delegate.ts @@ -18,6 +18,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreQuestionDefaultHandler } from './default-question-handler'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Interface that all question type handlers must implement. @@ -119,9 +120,9 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles?(question: any, usageId: number): string[]; + getAdditionalDownloadableFiles?(question: any, usageId: number): (string | CoreWSExternalFile)[]; /** * Clear temporary data after the data has been saved. @@ -324,9 +325,9 @@ export class CoreQuestionDelegate extends CoreDelegate { * * @param question Question. * @param usageId Usage ID. - * @return List of URLs. + * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: any, usageId: number): string[] { + getAdditionalDownloadableFiles(question: any, usageId: number): (string | CoreWSExternalFile)[] { const type = this.getTypeName(question); return this.executeFunctionOnEnabled(type, 'getAdditionalDownloadableFiles', [question, usageId]) || []; diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index 9c6570858..b90274929 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -590,29 +590,39 @@ export class CoreQuestionHelperProvider { */ prefetchQuestionFiles(question: any, component?: string, componentId?: string | number, siteId?: string, usageId?: number) : Promise { - const urls = this.filepoolProvider.extractDownloadableFilesFromHtml(question.html); if (!component) { component = CoreQuestionProvider.COMPONENT; componentId = question.number; } - urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question, usageId)); + const files = this.questionDelegate.getAdditionalDownloadableFiles(question, usageId) || []; + + files.push(...this.filepoolProvider.extractDownloadableFilesFromHtml(question.html)); return this.sitesProvider.getSite(siteId).then((site) => { const promises = []; + const treated = {}; - urls.forEach((url) => { - if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(url)) { + files.forEach((file) => { + const fileUrl = typeof file == 'string' ? file : file.fileurl; + const timemodified = (typeof file != 'string' && file.timemodified) || 0; + + if (treated[fileUrl]) { + return; + } + treated[fileUrl] = true; + + if (!site.canDownloadFiles() && this.urlUtils.isPluginFileUrl(fileUrl)) { return; } - if (url.indexOf('theme/image.php') > -1 && url.indexOf('flagged') > -1) { + if (fileUrl.indexOf('theme/image.php') > -1 && fileUrl.indexOf('flagged') > -1) { // Ignore flag images. return; } - promises.push(this.filepoolProvider.addToQueueByUrl(siteId, url, component, componentId)); + promises.push(this.filepoolProvider.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified)); }); return Promise.all(promises);