Merge pull request #3536 from crazyserver/linting

[4.2] Linting and small improvements release
main
Dani Palou 2023-03-14 16:30:32 +01:00 committed by GitHub
commit a812ee3625
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
124 changed files with 1151 additions and 1400 deletions

View File

@ -27,7 +27,7 @@ import {
import { AddonCalendarOffline } from './calendar-offline';
import { AddonCalendarHelper } from './calendar-helper';
import { makeSingleton, Translate } from '@singletons';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreNetworkError } from '@classes/errors/network-error';
import moment from 'moment-timezone';
@ -301,13 +301,11 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
export const AddonCalendarSync = makeSingleton(AddonCalendarSyncProvider);
export type AddonCalendarSyncEvents = {
warnings: string[];
export type AddonCalendarSyncEvents = CoreSyncResult & {
events: AddonCalendarEvent[];
offlineIdMap: Record<number, number>; // Map offline ID with online ID for created events.
deleted: number[];
toinvalidate: AddonCalendarSyncInvalidateEvent[];
updated: boolean;
source?: string; // Added on pages.
moment?: moment.Moment; // Added on day page.
};

View File

@ -270,9 +270,10 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
}
if (this.userId) {
const userId = this.userId;
// Get the member info. Invalidate first to make sure we get the latest status.
promises.push(AddonMessages.invalidateMemberInfo(this.userId).then(async () => {
this.otherMember = await AddonMessages.getMemberInfo(this.userId!);
this.otherMember = await AddonMessages.getMemberInfo(userId);
if (!exists && this.otherMember) {
this.conversationImage = this.otherMember.profileimageurl;
@ -288,6 +289,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
} else {
if (this.userId) {
const userId = this.userId;
// Fake the user member info.
promises.push(CoreUser.getProfile(this.userId).then(async (user) => {
this.otherMember = {
@ -305,8 +308,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
canmessage: true,
requirescontact: false,
};
this.otherMember.isblocked = await AddonMessages.isBlocked(this.userId!);
this.otherMember.iscontact = await AddonMessages.isContact(this.userId!);
this.otherMember.isblocked = await AddonMessages.isBlocked(userId);
this.otherMember.iscontact = await AddonMessages.isContact(userId);
this.blockIcon = this.otherMember.isblocked ? 'fas-user-check' : 'fas-user-lock';
return;

View File

@ -80,17 +80,17 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
AddonMessagesProvider.NEW_MESSAGE_EVENT,
(data) => {
if (data.userId && this.discussions) {
const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
const discussion = this.discussions.find((disc) => disc.message?.user === data.userId);
if (discussion === undefined) {
this.loaded = false;
this.refreshData().finally(() => {
this.loaded = true;
});
} else {
// An existing discussion has a new message, update the last message.
discussion.message!.message = data.message;
discussion.message!.timecreated = data.timecreated;
} else if (discussion.message) {
// An existing discussion has a new message, update the last message.
discussion.message.message = data.message;
discussion.message.timecreated = data.timecreated;
}
}
},
@ -102,10 +102,10 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
AddonMessagesProvider.READ_CHANGED_EVENT,
(data) => {
if (data.userId && this.discussions) {
const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
const discussion = this.discussions.find((disc) => disc.message?.user === data.userId);
if (discussion !== undefined) {
// A discussion has been read reset counter.
// A discussion has been read reset counter.
discussion.unread = false;
// Conversations changed, invalidate them and refresh unread counts.
@ -138,7 +138,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
}
/**
* Component loaded.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.route.queryParams.subscribe(async (params) => {
@ -150,9 +150,9 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
await this.fetchData();
if (!this.discussionUserId && this.discussions.length > 0 && CoreScreen.isTablet) {
if (!this.discussionUserId && this.discussions.length > 0 && CoreScreen.isTablet && this.discussions[0].message) {
// Take first and load it.
await this.gotoDiscussion(this.discussions[0].message!.user);
await this.gotoDiscussion(this.discussions[0].message.user);
}
// Treat deep link now that the conversation route has been loaded if needed.
@ -287,7 +287,7 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
}
/**
* Component destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.newMessagesObserver?.off();

View File

@ -14,6 +14,7 @@
import { Component, Optional, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { CoreError } from '@classes/errors/error';
import { CoreSite } from '@classes/site';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
@ -314,11 +315,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
/**
* @inheritdoc
*/
protected hasSyncSucceed(result?: AddonModAssignSyncResult): boolean {
if (!result) {
return false;
}
protected hasSyncSucceed(result: AddonModAssignSyncResult): boolean {
if (result.updated) {
this.submissionComponent?.invalidateAndRefresh(false);
}
@ -384,9 +381,9 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
/**
* @inheritdoc
*/
protected async sync(): Promise<AddonModAssignSyncResult | void> {
protected async sync(): Promise<AddonModAssignSyncResult> {
if (!this.assign) {
return;
throw new CoreError('Cannot sync without a assign.');
}
return AddonModAssignSync.syncAssign(this.assign.id);

View File

@ -32,7 +32,7 @@ import {
AddonModAssignSubmissionsDBRecordFormatted,
AddonModAssignSubmissionsGradingDBRecordFormatted,
} from './assign-offline';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreUtils } from '@services/utils/utils';
import { CoreNetwork } from '@services/network';
@ -530,9 +530,7 @@ export const AddonModAssignSync = makeSingleton(AddonModAssignSyncProvider);
/**
* Data returned by a assign sync.
*/
export type AddonModAssignSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
export type AddonModAssignSyncResult = CoreSyncResult & {
courseId?: number; // Course the assign belongs to (if known).
gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
};

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, Optional, OnInit } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { IonContent } from '@ionic/angular';
@ -454,22 +455,14 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected sync(): Promise<AddonModChoiceSyncResult> {
return AddonModChoiceSync.syncChoice(this.choice!.id, this.userId);
}
if (!this.choice) {
throw new CoreError('Cannot sync without a choice.');
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns Whether it succeed or not.
*/
protected hasSyncSucceed(result: AddonModChoiceSyncResult): boolean {
return result.updated;
return AddonModChoiceSync.syncChoice(this.choice.id, this.userId);
}
}

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
import { CoreSyncResult } from '@services/sync';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreNetwork } from '@services/network';
@ -217,10 +218,7 @@ export const AddonModChoiceSync = makeSingleton(AddonModChoiceSyncProvider);
/**
* Data returned by a choice sync.
*/
export type AddonModChoiceSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};
export type AddonModChoiceSyncResult = CoreSyncResult;
/**
* Data passed to AUTO_SYNCED event.

View File

@ -77,15 +77,14 @@ export class AddonModChoiceProvider {
choiceId: number,
name: string,
courseId: number,
responses?: number[],
responses: number[] = [],
siteId?: string,
): Promise<boolean> {
siteId = siteId || CoreSites.getCurrentSiteId();
responses = responses || [];
// Convenience function to store a message to be synchronized later.
const storeOffline = async (): Promise<boolean> => {
await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses!, true, siteId);
await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses, true, siteId);
return false;
};

View File

@ -519,24 +519,12 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected sync(): Promise<AddonModDataSyncResult> {
return AddonModDataPrefetchHandler.sync(this.module, this.courseId);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
*/
protected hasSyncSucceed(result: AddonModDataSyncResult): boolean {
return result.updated;
}
/**
* @inheritdoc
*/

View File

@ -24,7 +24,7 @@ import { CoreRatingSync } from '@features/rating/services/rating-sync';
import { CoreNetwork } from '@services/network';
import { CoreFileEntry } from '@services/file-helper';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { Translate, makeSingleton } from '@singletons';
@ -477,10 +477,7 @@ export type AddonModDataSyncEntryResult = {
/**
* Data returned by a database sync.
*/
export type AddonModDataSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};
export type AddonModDataSyncResult = CoreSyncResult;
export type AddonModDataAutoSyncData = {
dataId: number;

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, Input, Optional, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreTabsComponent } from '@components/tabs/tabs';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
@ -477,14 +478,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
* @inheritdoc
*/
protected sync(): Promise<AddonModFeedbackSyncResult> {
return AddonModFeedbackSync.syncFeedback(this.feedback!.id);
}
if (!this.feedback) {
throw new CoreError('Cannot sync without a feedback.');
}
/**
* @inheritdoc
*/
protected hasSyncSucceed(result: AddonModFeedbackSyncResult): boolean {
return result.updated;
return AddonModFeedbackSync.syncFeedback(this.feedback.id);
}
/**

View File

@ -20,7 +20,7 @@ import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/c
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreNetwork } from '@services/network';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
@ -292,10 +292,7 @@ export const AddonModFeedbackSync = makeSingleton(AddonModFeedbackSyncProvider);
/**
* Data returned by a feedback sync.
*/
export type AddonModFeedbackSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};
export type AddonModFeedbackSyncResult = CoreSyncResult;
/**
* Data passed to AUTO_SYNCED event.

View File

@ -500,24 +500,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected sync(): Promise<AddonModForumSyncResult> {
return AddonModForumPrefetchHandler.sync(this.module, this.courseId);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns Whether it succeed or not.
*/
protected hasSyncSucceed(result: AddonModForumSyncResult): boolean {
return result.updated;
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*

View File

@ -62,7 +62,7 @@ export class AddonModForumHelperProvider {
message: string,
attachments?: CoreFileEntry[],
options?: AddonModForumDiscussionOptions,
groupIds?: number[],
groupIds: number[] = [],
timeCreated?: number,
siteId?: string,
): Promise<number[] | null> {
@ -76,7 +76,7 @@ export class AddonModForumHelperProvider {
// Convenience function to store a message to be synchronized later.
const storeOffline = async (): Promise<void> => {
// Multiple groups, the discussion is being posted to all groups.
const groupId = groupIds!.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds![0];
const groupId = groupIds.length > 1 ? AddonModForumProvider.ALL_GROUPS : groupIds[0];
if (offlineAttachments && options) {
options.attachmentsid = offlineAttachments;
@ -182,7 +182,7 @@ export class AddonModForumHelperProvider {
* @param siteId Site ID. If not defined, current site.
* @returns Promise resolved with the object converted to Online.
*/
convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
async convertOfflineReplyToOnline(offlineReply: AddonModForumOfflineReply, siteId?: string): Promise<AddonModForumPost> {
const reply: AddonModForumPost = {
id: -offlineReply.timecreated,
discussionid: offlineReply.discussionid,
@ -236,11 +236,11 @@ export class AddonModForumHelperProvider {
),
);
return Promise.all(promises).then(() => {
reply.attachment = reply.attachments!.length > 0 ? 1 : 0;
await Promise.all(promises);
return reply;
});
reply.attachment = reply.attachments?.length ? 1 : 0;
return reply;
}
/**

View File

@ -21,7 +21,7 @@ import { CoreRatingSync } from '@features/rating/services/rating-sync';
import { CoreNetwork } from '@services/network';
import { CoreGroups } from '@services/groups';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
@ -221,7 +221,7 @@ export class AddonModForumSyncProvider extends CoreCourseActivitySyncBaseProvide
};
// Sync offline logs.
const syncDiscussions = async (): Promise<{ warnings: string[]; updated: boolean }> => {
const syncDiscussions = async (): Promise<AddonModForumSyncResult> => {
await CoreUtils.ignoreErrors(
CoreCourseLogHelper.syncActivity(AddonModForumProvider.COMPONENT, forumId, siteId),
);
@ -643,10 +643,7 @@ export const AddonModForumSync = makeSingleton(AddonModForumSyncProvider);
/**
* Result of forum sync.
*/
export type AddonModForumSyncResult = {
updated: boolean;
warnings: string[];
};
export type AddonModForumSyncResult = CoreSyncResult;
/**
* Data passed to AUTO_SYNCED event.

View File

@ -22,7 +22,7 @@ import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } fro
import { CoreUser } from '@features/user/services/user';
import { CoreGroups, CoreGroupsProvider } from '@services/groups';
import { CoreUtils } from '@services/utils/utils';
import { AddonModForumSync } from '../forum-sync';
import { AddonModForumSync, AddonModForumSyncResult } from '../forum-sync';
import { makeSingleton } from '@singletons';
import { CoreCourses } from '@features/courses/services/courses';
@ -341,11 +341,3 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe
}
export const AddonModForumPrefetchHandler = makeSingleton(AddonModForumPrefetchHandlerService);
/**
* Data returned by a forum sync.
*/
export type AddonModForumSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};

View File

@ -215,24 +215,12 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected sync(): Promise<AddonModGlossarySyncResult> {
return AddonModGlossaryPrefetchHandler.sync(this.module, this.courseId);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns Whether it succeed or not.
*/
protected hasSyncSucceed(result: AddonModGlossarySyncResult): boolean {
return result.updated;
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*

View File

@ -21,7 +21,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreRatingSync } from '@features/rating/services/rating-sync';
import { CoreNetwork } from '@services/network';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
@ -344,10 +344,7 @@ export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
/**
* Data returned by a glossary sync.
*/
export type AddonModGlossarySyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};
export type AddonModGlossarySyncResult = CoreSyncResult;
/**
* Data passed to AUTO_SYNCED event.

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
import { CoreSyncResult } from '@services/sync';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreXAPIOffline } from '@features/xapi/services/offline';
import { CoreXAPI } from '@features/xapi/services/xapi';
@ -197,10 +198,7 @@ export const AddonModH5PActivitySync = makeSingleton(AddonModH5PActivitySyncProv
/**
* Sync result.
*/
export type AddonModH5PActivitySyncResult = {
updated: boolean;
warnings: string[];
};
export type AddonModH5PActivitySyncResult = CoreSyncResult;
/**
* Data passed to AUTO_SYNC event.

View File

@ -47,6 +47,7 @@ import {
} from '../../services/lesson-sync';
import { AddonModLessonModuleHandlerService } from '../../services/handlers/module';
import { CoreTime } from '@singletons/time';
import { CoreError } from '@classes/errors/error';
/**
* Component that displays a lesson entry page.
@ -270,10 +271,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
* @inheritdoc
*/
protected hasSyncSucceed(result: AddonModLessonSyncResult): boolean {
if (result.updated || this.dataSent) {
@ -637,12 +635,14 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected async sync(): Promise<AddonModLessonSyncResult> {
const result = await AddonModLessonSync.syncLesson(this.lesson!.id, true);
if (!this.lesson) {
throw new CoreError('Cannot sync without a lesson.');
}
const result = await AddonModLessonSync.syncLesson(this.lesson.id, true);
if (!result.updated && this.dataSent && this.isPrefetched()) {
// The user sent data to server, but not in the sync process. Check if we need to fetch data.

View File

@ -21,7 +21,7 @@ import { CoreCourse } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreNetwork } from '@services/network';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
@ -495,9 +495,7 @@ export const AddonModLessonSync = makeSingleton(AddonModLessonSyncProvider);
/**
* Data returned by a lesson sync.
*/
export type AddonModLessonSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
export type AddonModLessonSyncResult = CoreSyncResult & {
courseId?: number; // Course the lesson belongs to (if known).
};

View File

@ -418,10 +418,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
* @inheritdoc
*/
protected hasSyncSucceed(result: AddonModQuizSyncResult): boolean {
if (result.attemptFinished) {
@ -553,9 +550,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected async sync(): Promise<AddonModQuizSyncResult> {
if (!this.candidateQuiz) {

View File

@ -23,7 +23,7 @@ import { CoreQuestion, CoreQuestionQuestionParsed } from '@features/question/ser
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
import { CoreNetwork } from '@services/network';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
@ -482,10 +482,8 @@ export const AddonModQuizSync = makeSingleton(AddonModQuizSyncProvider);
/**
* Data returned by a quiz sync.
*/
export type AddonModQuizSyncResult = {
warnings: string[]; // List of warnings.
export type AddonModQuizSyncResult = CoreSyncResult & {
attemptFinished: boolean; // Whether an attempt was finished in the site due to the sync.
updated: boolean;
};
/**

View File

@ -99,7 +99,7 @@ export class AddonModQuizProvider {
* @returns Grade to display.
*/
formatGrade(grade?: number | null, decimals?: number): string {
if (grade === undefined || grade == -1 || grade === null || isNaN(grade)) {
if (grade === undefined || grade === -1 || grade === null || isNaN(grade)) {
return Translate.instant('addon.mod_quiz.notyetgraded');
}
@ -1800,7 +1800,7 @@ export class AddonModQuizProvider {
): string | undefined {
let grade: number | undefined;
const rawGradeNum = typeof rawGrade == 'string' ? parseFloat(rawGrade) : rawGrade;
const rawGradeNum = typeof rawGrade === 'string' ? parseFloat(rawGrade) : rawGrade;
if (rawGradeNum !== undefined && rawGradeNum !== null && !isNaN(rawGradeNum)) {
if (quiz.sumgrades && quiz.sumgrades >= 0.000005) {
grade = rawGradeNum * (quiz.grade ?? 0) / quiz.sumgrades;

View File

@ -143,7 +143,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
this.displayDescription = false;
this.warning = downloadResult.failed
? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!)
? this.getErrorDownloadingSomeFilesMessage(downloadResult.error ?? '')
: '';
return;

View File

@ -61,8 +61,8 @@ export class AddonModResourcePrefetchHandlerService extends CoreCourseResourcePr
async downloadOrPrefetch(module: CoreCourseModuleData, courseId: number, prefetch?: boolean): Promise<void> {
let dirPath: string | undefined;
if (AddonModResourceHelper.isDisplayedInIframe(module)) {
dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url!);
if (AddonModResourceHelper.isDisplayedInIframe(module) && module.url !== undefined) {
dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url);
}
const promises: Promise<unknown>[] = [];

View File

@ -62,7 +62,7 @@ export class AddonModResourceHelperProvider {
* @returns Promise resolved with the iframe src.
*/
async getIframeSrc(module: CoreCourseModuleData): Promise<string> {
if (!module.contents?.length) {
if (!module.contents?.length || module.url === undefined) {
throw new CoreError('No contents available in module');
}
@ -74,7 +74,7 @@ export class AddonModResourceHelperProvider {
}
try {
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(CoreSites.getCurrentSiteId(), module.url!);
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(CoreSites.getCurrentSiteId(), module.url);
// This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser.
return CorePath.concatenatePaths(dirPath, mainFilePath);

View File

@ -14,6 +14,7 @@
import { CoreConstants } from '@/core/constants';
import { Component, Input, OnInit, Optional } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
@ -59,7 +60,10 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
moduleName = 'scorm';
scorm?: AddonModScormScorm; // The SCORM object.
currentOrganization: Partial<AddonModScormOrganization> = {}; // Selected organization.
currentOrganization: Partial<AddonModScormOrganization> & { identifier: string} = {
identifier: '',
}; // Selected organization.
startNewAttempt = false;
errorMessage?: string; // Error message.
syncTime?: string; // Last sync time.
@ -70,7 +74,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
percentage?: string; // Download/unzip percentage.
showPercentage = false; // Whether to show the percentage.
progressMessage?: string; // Message about download/unzip.
organizations?: AddonModScormOrganization[]; // List of organizations.
organizations: AddonModScormOrganization[] = []; // List of organizations.
loadingToc = false; // Whether the TOC is being loaded.
toc?: AddonModScormTOCScoWithIcon[]; // Table of contents (structure).
accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
@ -128,7 +132,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
try {
await AddonModScormPrefetchHandler.download(this.module, this.courseId, undefined, (data) => {
if (!data) {
if (!data || !this.scorm) {
return;
}
@ -137,8 +141,8 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
if (data.downloading) {
// Downloading package.
if (this.scorm!.packagesize && data.progress) {
const percentageNumber = Number(data.progress.loaded / this.scorm!.packagesize) * 100;
if (this.scorm.packagesize && data.progress) {
const percentageNumber = Number(data.progress.loaded / this.scorm.packagesize) * 100;
this.percentage = percentageNumber.toFixed(1);
this.showPercentage = percentageNumber >= 0 && percentageNumber <= 100;
}
@ -198,7 +202,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
(
this.accessInfo.canskipview && !this.accessInfo.canviewreport &&
(this.scorm.skipview ?? 0) >= AddonModScormProvider.SKIPVIEW_FIRST &&
(this.scorm.skipview == AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt == 0)
(this.scorm.skipview === AddonModScormProvider.SKIPVIEW_ALWAYS || this.lastAttempt === 0)
)
);
}
@ -221,7 +225,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
this.lastAttempt = attempt.num;
this.lastIsOffline = attempt.offline;
if (this.lastAttempt != this.attempts.lastAttempt.num) {
if (this.lastAttempt !== this.attempts.lastAttempt.num) {
this.attemptToContinue = this.lastAttempt;
} else {
this.attemptToContinue = undefined;
@ -237,7 +241,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
this.gradeMethodReadable = AddonModScorm.getScormGradeMethod(scorm);
this.attemptsLeft = AddonModScorm.countAttemptsLeft(scorm, this.attempts.lastAttempt.num);
if (scorm.forcenewattempt == AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS ||
if (scorm.forcenewattempt === AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS ||
(scorm.forcenewattempt && !this.incomplete)) {
this.startNewAttempt = true;
}
@ -272,13 +276,9 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
protected async fetchStructure(scorm: AddonModScormScorm): Promise<void> {
this.organizations = await AddonModScorm.getOrganizations(scorm.id, { cmId: this.module.id });
if (!this.currentOrganization.identifier) {
if (this.currentOrganization.identifier === '' && this.organizations[0]?.identifier) {
// Load first organization (if any).
if (this.organizations.length) {
this.currentOrganization.identifier = this.organizations[0].identifier;
} else {
this.currentOrganization.identifier = '';
}
this.currentOrganization.identifier = this.organizations[0].identifier;
}
return this.loadOrganizationToc(scorm, this.currentOrganization.identifier);
@ -297,7 +297,11 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
offline: boolean,
attempts: Record<number, AddonModScormAttemptGrade>,
): Promise<void> {
const grade = await AddonModScorm.getAttemptGrade(this.scorm!, attempt, offline);
if (!this.scorm) {
return;
}
const grade = await AddonModScorm.getAttemptGrade(this.scorm, attempt, offline);
attempts[attempt] = {
num: attempt,
@ -361,10 +365,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
* @inheritdoc
*/
protected hasSyncSucceed(result: AddonModScormSyncResult): boolean {
if (result.updated || this.dataSent) {
@ -374,7 +375,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
this.dataSent = false;
return true;
return result.updated;
}
/**
@ -438,8 +439,12 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
* Load a organization's TOC.
*/
async loadOrganization(): Promise<void> {
if (!this.scorm) {
return;
}
try {
await this.loadOrganizationToc(this.scorm!, this.currentOrganization.identifier!);
await this.loadOrganizationToc(this.scorm, this.currentOrganization.identifier);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
}
@ -453,7 +458,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
* @returns Promise resolved when done.
*/
protected async loadOrganizationToc(scorm: AddonModScormScorm, organizationId: string): Promise<void> {
if (!scorm.displaycoursestructure) {
if (!scorm.displaycoursestructure || this.lastAttempt === undefined) {
// TOC is not displayed, no need to load it.
return;
}
@ -461,18 +466,17 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
this.loadingToc = true;
try {
this.toc = await AddonModScormHelper.getToc(scorm.id, this.lastAttempt!, this.incomplete, {
this.toc = await AddonModScormHelper.getToc(scorm.id, this.lastAttempt, this.incomplete, {
organization: organizationId,
offline: this.lastIsOffline,
cmId: this.module.id,
});
// Search organization title.
this.organizations!.forEach((org) => {
if (org.identifier == organizationId) {
this.currentOrganization.title = org.title;
}
});
const organization = this.organizations.find((org) => org.identifier === organizationId);
if (organization) {
this.currentOrganization.title = organization.title;
}
} finally {
this.loadingToc = false;
}
@ -486,20 +490,18 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
* @param scoId SCO that needs to be loaded when the SCORM is opened. If not defined, load first SCO.
*/
async open(event?: Event, preview: boolean = false, scoId?: number): Promise<void> {
if (event) {
event.preventDefault();
event.stopPropagation();
}
event?.preventDefault();
event?.stopPropagation();
if (this.downloading) {
if (this.downloading || !this.scorm) {
// Scope is being downloaded, abort.
return;
}
const isOutdated = this.currentStatus == CoreConstants.OUTDATED;
const scorm = this.scorm!;
const isOutdated = this.currentStatus === CoreConstants.OUTDATED;
const scorm = this.scorm;
if (!isOutdated && this.currentStatus != CoreConstants.NOT_DOWNLOADED) {
if (!isOutdated && this.currentStatus !== CoreConstants.NOT_DOWNLOADED) {
// Already downloaded, open it.
this.openScorm(scoId, preview);
@ -552,7 +554,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
this.dataSentObserver?.off();
this.dataSentObserver = CoreEvents.on(AddonModScormProvider.DATA_SENT_EVENT, (data) => {
if (data.scormId === this.scorm!.id) {
if (data.scormId === this.scorm?.id) {
this.dataSent = true;
if (this.module.completiondata && CoreCourse.isIncompleteAutomaticCompletion(this.module.completiondata)) {
@ -581,14 +583,14 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
*/
protected async showStatus(status: string): Promise<void> {
if (status == CoreConstants.OUTDATED && this.scorm) {
if (status === CoreConstants.OUTDATED && this.scorm) {
// Only show the outdated message if the file should be downloaded.
const download = await AddonModScorm.shouldDownloadMainFile(this.scorm, true);
this.statusMessage = download ? 'addon.mod_scorm.scormstatusoutdated' : '';
} else if (status == CoreConstants.NOT_DOWNLOADED) {
} else if (status === CoreConstants.NOT_DOWNLOADED) {
this.statusMessage = 'addon.mod_scorm.scormstatusnotdownloaded';
} else if (status == CoreConstants.DOWNLOADING) {
} else if (status === CoreConstants.DOWNLOADING) {
if (!this.downloading) {
// It's being downloaded right now but the view isn't tracking it. "Restore" the download.
this.downloadScormPackage();
@ -605,14 +607,18 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
* @returns Promise resolved when done.
*/
protected async sync(retries = 0): Promise<AddonModScormSyncResult> {
if (CoreSync.isBlocked(AddonModScormProvider.COMPONENT, this.scorm!.id) && retries < 5) {
if (!this.scorm) {
throw new CoreError('Cannot sync without a scorm.');
}
if (CoreSync.isBlocked(AddonModScormProvider.COMPONENT, this.scorm.id) && retries < 5) {
// Sync is currently blocked, this can happen when SCORM player is left. Retry in a bit.
await CoreUtils.wait(400);
return this.sync(retries + 1);
}
const result = await AddonModScormSync.syncScorm(this.scorm!);
const result = await AddonModScormSync.syncScorm(this.scorm);
if (!result.updated && this.dataSent) {
// The user sent data to server, but not in the sync process. Check if we need to fetch data.

View File

@ -18,7 +18,7 @@ import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/act
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
@ -841,18 +841,14 @@ export const AddonModScormSync = makeSingleton(AddonModScormSyncProvider);
/**
* Data returned by a SCORM sync.
*/
export type AddonModScormSyncResult = {
warnings: string[]; // List of warnings.
export type AddonModScormSyncResult = CoreSyncResult & {
attemptFinished: boolean; // Whether an attempt was finished in the site due to the sync,
updated: boolean; // Whether some data was sent to the site.
};
/**
* Auto sync event data.
*/
export type AddonModScormAutoSyncEventData = {
export type AddonModScormAutoSyncEventData = CoreSyncResult & {
scormId: number;
attemptFinished: boolean;
warnings: string[];
updated: boolean;
};

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, OnInit, Optional } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
@ -117,8 +118,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
if (sync) {
// Try to synchronize the survey.
const answersSent = await this.syncActivity(showErrors);
if (answersSent) {
const updated = await this.syncActivity(showErrors);
if (updated) {
// Answers were sent, update the survey.
this.survey = await AddonModSurvey.getSurvey(this.courseId, this.module.id);
}
@ -130,17 +131,18 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
: await AddonModSurveyOffline.hasAnswers(this.survey.id);
if (!this.survey.surveydone && !this.hasOffline) {
await this.fetchQuestions();
await this.fetchQuestions(this.survey.id);
}
}
/**
* Convenience function to get survey questions.
*
* @param surveyId Survey Id.
* @returns Promise resolved when done.
*/
protected async fetchQuestions(): Promise<void> {
const questions = await AddonModSurvey.getQuestions(this.survey!.id, { cmId: this.module.id });
protected async fetchQuestions(surveyId: number): Promise<void> {
const questions = await AddonModSurvey.getQuestions(surveyId, { cmId: this.module.id });
this.questions = AddonModSurveyHelper.formatQuestions(questions);
@ -183,6 +185,10 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
* Save options selected.
*/
async submit(): Promise<void> {
if (!this.survey) {
return;
}
let modal: CoreIonLoadingElement | undefined;
try {
@ -198,7 +204,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
});
}
const online = await AddonModSurvey.submitAnswers(this.survey!.id, this.survey!.name, this.courseId, answers);
const online = await AddonModSurvey.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers);
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: this.moduleName });
@ -231,22 +237,14 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected sync(): Promise<AddonModSurveySyncResult> {
return AddonModSurveySync.syncSurvey(this.survey!.id, this.currentUserId);
}
protected async sync(): Promise<AddonModSurveySyncResult> {
if (!this.survey) {
throw new CoreError('Cannot sync without a survey.');
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
*/
protected hasSyncSucceed(result: AddonModSurveySyncResult): boolean {
return result.answersSent;
return AddonModSurveySync.syncSurvey(this.survey.id, this.currentUserId);
}
}

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
import { CoreSyncResult } from '@services/sync';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreNetwork } from '@services/network';
@ -80,7 +81,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
? this.syncSurvey(entry.surveyid, entry.userid, siteId)
: this.syncSurveyIfNeeded(entry.surveyid, entry.userid, siteId));
if (result && result.answersSent) {
if (result && result.updated) {
// Sync successful, send event.
CoreEvents.trigger(AddonModSurveySyncProvider.AUTO_SYNCED, {
surveyId: entry.surveyid,
@ -150,7 +151,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
protected async performSyncSurvey(surveyId: number, userId: number, siteId: string): Promise<AddonModSurveySyncResult> {
const result: AddonModSurveySyncResult = {
warnings: [],
answersSent: false,
updated: false,
};
// Sync offline logs.
@ -179,7 +180,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
try {
await AddonModSurvey.submitAnswersOnline(surveyId, data.answers, siteId);
result.answersSent = true;
result.updated = true;
// Answers sent, delete them.
await AddonModSurveyOffline.deleteSurveyAnswers(surveyId, siteId, userId);
@ -190,7 +191,7 @@ export class AddonModSurveySyncProvider extends CoreCourseActivitySyncBaseProvid
}
// The WebService has thrown an error, this means that answers cannot be submitted. Delete them.
result.answersSent = true;
result.updated = true;
await AddonModSurveyOffline.deleteSurveyAnswers(surveyId, siteId, userId);
@ -236,9 +237,7 @@ declare module '@singletons/events' {
/**
* Data returned by a assign sync.
*/
export type AddonModSurveySyncResult = {
warnings: string[]; // List of warnings.
answersSent: boolean; // Whether some data was sent to the server or offline data was updated.
export type AddonModSurveySyncResult = CoreSyncResult & {
courseId?: number; // Course the survey belongs to (if known).
};

View File

@ -189,7 +189,11 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
*/
go(): void {
this.logView();
AddonModUrlHelper.open(this.url!);
if (!this.url) {
return;
}
AddonModUrlHelper.open(this.url);
}
}

View File

@ -707,12 +707,9 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
* @inheritdoc
*/
protected hasSyncSucceed(result: AddonModWikiSyncWikiResult | undefined): boolean {
protected hasSyncSucceed(result: AddonModWikiSyncWikiResult): boolean {
if (!result) {
return false;
}
@ -843,13 +840,11 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected async sync(): Promise<AddonModWikiSyncWikiResult | undefined> {
protected async sync(): Promise<AddonModWikiSyncWikiResult> {
if (!this.wiki) {
return;
throw new CoreError('Cannot sync without a wiki.');
}
return AddonModWikiSync.syncWiki(this.wiki.id, this.courseId, this.wiki.coursemodule);

View File

@ -45,8 +45,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
cmId?: number; // Course module ID.
courseId?: number; // Course the wiki belongs to.
title?: string; // Title to display.
pageForm?: FormGroup; // The form group.
contentControl?: FormControl; // The FormControl for the page content.
pageForm: FormGroup; // The form group.
contentControl: FormControl; // The FormControl for the page content.
canEditTitle = false; // Whether title can be edited.
loaded = false; // Whether the data has been loaded.
component = AddonModWikiProvider.COMPONENT; // Component to link the files to.
@ -71,7 +71,10 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
constructor(
protected formBuilder: FormBuilder,
) { }
) {
this.contentControl = this.formBuilder.control('');
this.pageForm = this.formBuilder.group({});
}
/**
* @inheritdoc
@ -96,10 +99,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
this.blockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId);
// Create the form group and its controls.
this.contentControl = this.formBuilder.control('');
this.pageForm = this.formBuilder.group({
title: pageTitle,
});
this.pageForm.addControl('title', this.formBuilder.control(pageTitle));
this.pageForm.addControl('text', this.contentControl);
// Block the wiki so it cannot be synced.
@ -121,7 +121,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
if (success && !this.isDestroyed) {
// Block the subwiki now that we have blockId for sure.
const newBlockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId);
if (newBlockId != this.blockId) {
if (newBlockId !== this.blockId) {
CoreSync.unblockOperation(this.component, this.blockId);
this.blockId = newBlockId;
CoreSync.blockOperation(this.component, this.blockId);
@ -143,7 +143,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
try {
// Wait for sync to be over (if any).
const syncResult = await AddonModWikiSync.waitForSync(this.blockId!);
const syncResult = this.blockId ? await AddonModWikiSync.waitForSync(this.blockId) : undefined;
if (this.pageId) {
// Editing a page that already exists.
@ -154,7 +154,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
// Get page contents to obtain title and editing permission
const pageContents = await AddonModWiki.getPageContents(this.pageId, { cmId: this.cmId });
this.pageForm!.controls.title.setValue(pageContents.title); // Set the title in the form group.
this.pageForm.controls.title.setValue(pageContents.title); // Set the title in the form group.
this.wikiId = pageContents.wikiid;
this.subwikiId = pageContents.subwikiid;
this.title = Translate.instant('addon.mod_wiki.editingpage', { $a: pageContents.title });
@ -177,7 +177,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
// Get the original page contents, treating file URLs if needed.
const content = CoreTextUtils.replacePluginfileUrls(editContents.content || '', this.subwikiFiles);
this.contentControl!.setValue(content);
this.contentControl.setValue(content);
this.originalContent = content;
this.version = editContents.version;
@ -188,7 +188,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
}, AddonModWikiProvider.RENEW_LOCK_TIME);
}
} else {
const pageTitle = this.pageForm!.controls.title.value;
const pageTitle = this.pageForm.controls.title.value;
this.editing = false;
canEdit = !!this.blockId; // If no blockId, the user cannot edit the page.
@ -222,7 +222,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
if (page) {
// Load offline content.
this.contentControl!.setValue(page.cachedcontent);
this.contentControl.setValue(page.cachedcontent);
this.originalContent = page.cachedcontent;
this.editOffline = true;
} else {
@ -286,13 +286,17 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
* @param title Page title.
*/
protected goToPage(title: string): void {
if (!this.wikiId) {
return;
}
// Not the firstpage.
AddonModWiki.setEditedPageData({
cmId: this.cmId,
courseId: this.courseId,
pageId: this.pageId,
pageTitle: title,
wikiId: this.wikiId!,
wikiId: this.wikiId,
subwikiId: this.subwikiId,
userId: this.userId,
groupId: this.groupId,
@ -307,7 +311,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
* @returns Whether data has changed.
*/
protected hasDataChanged(): boolean {
const values = this.pageForm!.value;
const values = this.pageForm.value;
return !(this.originalContent == values.text || (!this.editing && !values.text && !values.title));
}
@ -348,7 +352,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
* @returns Promise resolved when done.
*/
async save(): Promise<void> {
const values = this.pageForm!.value;
const values = this.pageForm.value;
const title = values.title;
let text = values.text;
@ -358,14 +362,14 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
text = CoreTextUtils.formatHtmlLines(text);
try {
if (this.editing) {
if (this.editing && this.pageId) {
// Edit existing page.
await AddonModWiki.editPage(this.pageId!, text, this.section);
await AddonModWiki.editPage(this.pageId, text, this.section);
CoreForms.triggerFormSubmittedEvent(this.formElement, true, CoreSites.getCurrentSiteId());
// Invalidate page since it changed.
await AddonModWiki.invalidatePage(this.pageId!);
await AddonModWiki.invalidatePage(this.pageId);
return this.goToPage(title);
}
@ -451,7 +455,11 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave {
* Renew lock and control versions.
*/
protected async renewLock(): Promise<void> {
const response = await AddonModWiki.getPageForEditing(this.pageId!, this.section, true);
if (!this.pageId) {
return;
}
const response = await AddonModWiki.getPageForEditing(this.pageId, this.section, true);
if (response.version && this.version != response.version) {
this.wrongVersionLock = true;

View File

@ -103,6 +103,10 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
try {
const route = CoreNavigator.getCurrentRoute({ pageComponent: AddonModWikiIndexPage });
if (!route) {
// Current view isn't wiki index.
return;
}
const subwikiId = parseInt(params.swid, 10);
const wikiId = parseInt(params.wid, 10);
let path = AddonModWikiModuleHandlerService.PAGE_NAME;
@ -112,7 +116,7 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
if (isSameWiki) {
// User is seeing the wiki, we can get the module from the wiki params.
path = path + `/${route!.snapshot.params.courseId}/${route!.snapshot.params.cmId}/edit`;
path = path + `/${route.snapshot.params.courseId}/${route.snapshot.params.cmId}/edit`;
} else if (wikiId) {
// The URL specifies which wiki it belongs to. Get the module.
const module = await CoreCourse.getModuleBasicInfoByInstance(

View File

@ -19,7 +19,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreNetwork } from '@services/network';
import { CoreGroups } from '@services/groups';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
@ -345,9 +345,7 @@ export const AddonModWikiSync = makeSingleton(AddonModWikiSyncProvider);
/**
* Data returned by a subwiki sync.
*/
export type AddonModWikiSyncSubwikiResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether data was updated in the site.
export type AddonModWikiSyncSubwikiResult = CoreSyncResult & {
created: AddonModWikiCreatedPage[]; // List of created pages.
discarded: AddonModWikiDiscardedPage[]; // List of discarded pages.
};
@ -355,9 +353,7 @@ export type AddonModWikiSyncSubwikiResult = {
/**
* Data returned by a wiki sync.
*/
export type AddonModWikiSyncWikiResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether data was updated in the site.
export type AddonModWikiSyncWikiResult = CoreSyncResult & {
subwikis: {
[subwikiId: number]: { // List of subwikis.
created: AddonModWikiCreatedPage[];

View File

@ -162,6 +162,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
cmId: this.workshop.coursemodule,
});
if (!this.data.assessment.form) {
return;
}
if (this.edit) {
try {
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop.id, this.assessmentId);
@ -176,7 +180,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
}
// Override assessment plugins values.
this.data.assessment.form!.current = AddonModWorkshop.parseFields(
this.data.assessment.form.current = AddonModWorkshop.parseFields(
CoreUtils.objectToArrayOfObjects(offlineData, 'name', 'value'),
);
@ -221,7 +225,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
try {
this.data.selectedValues = await AddonWorkshopAssessmentStrategyDelegate.getOriginalValues(
this.strategy,
this.data.assessment.form!,
this.data.assessment.form,
this.workshop.id,
);
} finally {
@ -245,7 +249,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
* @returns True if data has changed.
*/
hasDataChanged(): boolean {
if (!this.assessmentStrategyLoaded) {
if (!this.assessmentStrategyLoaded || !this.workshop.strategy) {
return false;
}
@ -269,7 +273,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
}
return AddonWorkshopAssessmentStrategyDelegate.hasDataChanged(
this.workshop.strategy!,
this.workshop.strategy,
this.originalData.selectedValues,
this.data.selectedValues,
);
@ -281,6 +285,10 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
* @returns Promise resolved when done, rejected if assessment could not be saved.
*/
async saveAssessment(): Promise<void> {
if (!this.data.assessment?.form) {
return;
}
const files = CoreFileSession.getFiles(
AddonModWorkshopProvider.COMPONENT,
this.workshop.id + '_' + this.assessmentId,
@ -328,7 +336,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe
this.workshop,
this.data.selectedValues,
text,
this.data.assessment!.form!,
this.data.assessment.form,
attachmentsId,
);
} catch (errors) {

View File

@ -14,6 +14,7 @@
import { Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
import { Params } from '@angular/router';
import { CoreError } from '@classes/errors/error';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { IonContent } from '@ionic/angular';
@ -264,7 +265,12 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
* @returns Resolved when done.
*/
async gotoSubmissionsPage(page: number): Promise<void> {
const report = await AddonModWorkshop.getGradesReport(this.workshop!.id, {
if (!this.workshop) {
return;
}
const workshop = this.workshop;
const report = await AddonModWorkshop.getGradesReport(workshop.id, {
groupId: this.group,
page,
cmId: this.module.id,
@ -284,7 +290,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
await Promise.all(grades.map(async (grade) => {
const submission: AddonModWorkshopSubmissionDataWithOfflineData = {
id: grade.submissionid,
workshopid: this.workshop!.id,
workshopid: workshop.id,
example: false,
authorid: grade.userid,
timecreated: grade.submissionmodified,
@ -303,7 +309,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
reviewerof: this.parseReviewer(grade.reviewerof),
};
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
if (workshop.phase == AddonModWorkshopPhase.PHASE_ASSESSMENT) {
submission.reviewedbydone = grade.reviewedby?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
submission.reviewerofdone = grade.reviewerof?.reduce((a, b) => a + (b.grade ? 1 : 0), 0) || 0;
submission.reviewedbycount = grade.reviewedby?.length || 0;
@ -313,7 +319,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
const offlineData = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
if (offlineData !== undefined) {
this.grades!.push(offlineData);
this.grades.push(offlineData);
}
}));
}
@ -358,8 +364,12 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
* Go to submit page.
*/
gotoSubmit(): void {
if (this.canSubmit && ((this.access!.creatingsubmissionallowed && !this.submission) ||
(this.access!.modifyingsubmissionallowed && this.submission))) {
if (!this.canSubmit || !this.access) {
return;
}
if ((this.access.creatingsubmissionallowed && !this.submission) ||
(this.access.modifyingsubmissionallowed && this.submission)) {
const params: Params = {
module: this.module,
access: this.access,
@ -378,20 +388,22 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
* View Phase info.
*/
async viewPhaseInfo(): Promise<void> {
if (this.phases) {
const modalData = await CoreDomUtils.openModal<boolean>({
component: AddonModWorkshopPhaseInfoComponent,
componentProps: {
phases: CoreUtils.objectToArray(this.phases),
workshopPhase: this.workshop!.phase,
externalUrl: this.module.url,
showSubmit: this.showSubmit,
},
});
if (!this.phases || !this.workshop) {
return;
}
if (modalData === true) {
this.gotoSubmit();
}
const modalData = await CoreDomUtils.openModal<boolean>({
component: AddonModWorkshopPhaseInfoComponent,
componentProps: {
phases: CoreUtils.objectToArray(this.phases),
workshopPhase: this.workshop.phase,
externalUrl: this.module.url,
showSubmit: this.showSubmit,
},
});
if (modalData === true) {
this.gotoSubmit();
}
}
@ -413,26 +425,32 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
* @returns Promise resolved when done.
*/
protected async setPhaseInfo(): Promise<void> {
if (!this.phases || !this.workshop || !this.access) {
return;
}
this.submission = undefined;
this.canAssess = false;
this.assessments = [];
this.userGrades = undefined;
this.publishedSubmissions = [];
const workshop = this.workshop;
this.canSubmit = AddonModWorkshopHelper.canSubmit(
this.workshop!,
this.access!,
this.phases![AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
this.workshop,
this.access,
this.phases[AddonModWorkshopPhase.PHASE_SUBMISSION].tasks,
);
this.showSubmit = this.canSubmit &&
((this.access!.creatingsubmissionallowed && !this.submission) ||
(this.access!.modifyingsubmissionallowed && !!this.submission));
((this.access.creatingsubmissionallowed && !this.submission) ||
(this.access.modifyingsubmissionallowed && !!this.submission));
const promises: Promise<void>[] = [];
if (this.canSubmit) {
promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop!.id, { cmId: this.module.id })
promises.push(AddonModWorkshopHelper.getUserSubmission(this.workshop.id, { cmId: this.module.id })
.then(async (submission) => {
this.submission = await AddonModWorkshopHelper.applyOfflineData(submission, this.offlineSubmissions);
@ -440,27 +458,27 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
}));
}
if (this.access!.canviewallsubmissions && this.workshop!.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
if (this.access.canviewallsubmissions && this.workshop.phase >= AddonModWorkshopPhase.PHASE_SUBMISSION) {
promises.push(this.gotoSubmissionsPage(this.page));
}
let assessPromise = Promise.resolve();
if (this.workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop!, this.access!);
if (this.workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT) {
this.canAssess = AddonModWorkshopHelper.canAssess(this.workshop, this.access);
if (this.canAssess) {
assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop!.id, {
assessPromise = AddonModWorkshopHelper.getReviewerAssessments(this.workshop.id, {
cmId: this.module.id,
}).then(async (assessments) => {
await Promise.all(assessments.map(async (assessment) => {
assessment.strategy = this.workshop!.strategy;
assessment.strategy = workshop.strategy;
if (!this.hasOffline) {
return;
}
try {
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(this.workshop!.id, assessment.id);
const offlineAssessment = await AddonModWorkshopOffline.getAssessment(workshop.id, assessment.id);
assessment.offline = true;
assessment.timemodified = Math.floor(offlineAssessment.timemodified / 1000);
@ -477,27 +495,23 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
}
}
if (this.workshop!.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
promises.push(AddonModWorkshop.getGrades(this.workshop!.id, { cmId: this.module.id }).then((grades) => {
if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED) {
promises.push(AddonModWorkshop.getGrades(this.workshop.id, { cmId: this.module.id }).then((grades) => {
this.userGrades = grades.submissionlongstrgrade || grades.assessmentlongstrgrade ? grades : undefined;
return;
}));
if (this.access!.canviewpublishedsubmissions) {
if (this.access.canviewpublishedsubmissions) {
promises.push(assessPromise.then(async () => {
const submissions: AddonModWorkshopSubmissionDataWithOfflineData[] =
await AddonModWorkshop.getSubmissions(this.workshop!.id, { cmId: this.module.id });
await AddonModWorkshop.getSubmissions(workshop.id, { cmId: this.module.id });
this.publishedSubmissions = submissions.filter((submission) => {
if (submission.published) {
submission.reviewedby = [];
this.assessments.forEach((assessment) => {
if (assessment.submissionid == submission.id) {
submission.reviewedby!.push(AddonModWorkshopHelper.realGradeValue(this.workshop!, assessment));
}
});
submission.reviewedby =
this.assessments.filter((assessment) => assessment.submissionid === submission.id)
.map((assessment => AddonModWorkshopHelper.realGradeValue(workshop, assessment)));
return true;
}
@ -514,22 +528,14 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
}
/**
* Performs the sync of the activity.
*
* @returns Promise resolved when done.
* @inheritdoc
*/
protected sync(): Promise<AddonModWorkshopSyncResult> {
return AddonModWorkshopSync.syncWorkshop(this.workshop!.id);
}
if (!this.workshop) {
throw new CoreError('Cannot sync without a workshop.');
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
*/
protected hasSyncSucceed(result: AddonModWorkshopSyncResult): boolean {
return result.updated;
return AddonModWorkshopSync.syncWorkshop(this.workshop.id);
}
/**

View File

@ -385,11 +385,14 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca
);
newSubmissionId = false;
} else {
if (!submissionId) {
throw new CoreError('Submission cannot be updated without a submissionId');
}
// Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine.
newSubmissionId = await AddonModWorkshop.updateSubmission(
this.workshopId,
submissionId!,
submissionId,
this.courseId,
inputData.title,
inputData.content,

View File

@ -254,7 +254,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
return;
}));
} else if (this.currentUserId == this.userId && this.assessmentId) {
} else if (this.currentUserId === this.userId && this.assessmentId) {
// Get new data, different that came from stateParams.
promises.push(AddonModWorkshop.getAssessment(this.workshopId, this.assessmentId, {
cmId: this.module.id,
@ -268,7 +268,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
return;
}));
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.userId == this.currentUserId) {
} else if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED && this.userId === this.currentUserId) {
const assessments = await AddonModWorkshop.getSubmissionAssessments(this.workshopId, this.submissionId, {
cmId: this.module.id,
});
@ -276,7 +276,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
this.submissionInfo.reviewedby = assessments.map((assessment) => this.parseAssessment(assessment));
}
if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED) {
if (this.canAddFeedback || this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED) {
this.evaluate = {
published: this.submission.published,
text: this.submission.feedbackauthor || '',
@ -284,54 +284,13 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
}
if (this.canAddFeedback) {
if (!this.isDestroyed) {
// Block the workshop.
CoreSync.blockOperation(this.component, this.workshopId);
}
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
promises.push(CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1)
.then(async (grades) => {
this.evaluationGrades = grades;
this.evaluate!.grade = {
label: CoreGradesHelper.getGradeLabelFromValue(grades, this.submissionInfo.gradeover) ||
defaultGrade,
value: this.submissionInfo.gradeover || -1,
};
try {
const offlineSubmission =
await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
this.hasOffline = true;
this.evaluate!.published = offlineSubmission.published;
this.evaluate!.text = offlineSubmission.feedbacktext;
this.evaluate!.grade = {
label: CoreGradesHelper.getGradeLabelFromValue(
grades,
parseInt(offlineSubmission.gradeover, 10),
) || defaultGrade,
value: offlineSubmission.gradeover || -1,
};
} catch {
// Ignore errors.
this.hasOffline = false;
} finally {
this.originalEvaluation.published = this.evaluate!.published;
this.originalEvaluation.text = this.evaluate!.text;
this.originalEvaluation.grade = this.evaluate!.grade.value;
this.feedbackForm.controls['published'].setValue(this.evaluate!.published);
this.feedbackForm.controls['grade'].setValue(this.evaluate!.grade.value);
this.feedbackForm.controls['text'].setValue(this.evaluate!.text);
}
return;
}));
} else if (this.workshop.phase == AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
promises.push(this.fillEvaluationsGrades());
} else if (this.workshop.phase === AddonModWorkshopPhase.PHASE_CLOSED && this.submission.gradeoverby &&
this.evaluate && this.evaluate.text) {
promises.push(CoreUser.getProfile(this.submission.gradeoverby, this.courseId, true).then((profile) => {
this.evaluateByProfile = profile;
@ -362,6 +321,49 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
}
}
/**
* Fill evaluation grade info.
*/
protected async fillEvaluationsGrades(): Promise<void> {
const defaultGrade = Translate.instant('addon.mod_workshop.notoverridden');
this.evaluationGrades = await CoreGradesHelper.makeGradesMenu(this.workshop.grade || 0, undefined, defaultGrade, -1);
if (!this.evaluate) {
// Should not happen.
return;
}
this.evaluate.grade = {
label: CoreGradesHelper.getGradeLabelFromValue(this.evaluationGrades, this.submissionInfo.gradeover) || defaultGrade,
value: this.submissionInfo.gradeover || -1,
};
try {
const offlineSubmission = await AddonModWorkshopOffline.getEvaluateSubmission(this.workshopId, this.submissionId);
this.hasOffline = true;
this.evaluate.published = offlineSubmission.published;
this.evaluate.text = offlineSubmission.feedbacktext;
this.evaluate.grade = {
label: CoreGradesHelper.getGradeLabelFromValue(this.evaluationGrades, parseInt(offlineSubmission.gradeover, 10)) ||
defaultGrade,
value: offlineSubmission.gradeover || -1,
};
} catch {
// Ignore errors.
this.hasOffline = false;
} finally {
this.originalEvaluation.published = this.evaluate.published;
this.originalEvaluation.text = this.evaluate.text;
this.originalEvaluation.grade = this.evaluate.grade.value;
this.feedbackForm.controls['published'].setValue(this.evaluate.published);
this.feedbackForm.controls['grade'].setValue(this.evaluate.grade.value);
this.feedbackForm.controls['text'].setValue(this.evaluate.text);
}
}
/**
* Parse assessment to be shown.
*

View File

@ -71,7 +71,7 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
): Promise<{ workshop?: AddonModWorkshopData; groups: CoreGroup[]; files: CoreWSFile[]}> {
let groups: CoreGroup[] = [];
let files: CoreWSFile[] = [];
let workshop: AddonModWorkshopData | undefined;
let workshop: AddonModWorkshopData;
let access: AddonModWorkshopGetWorkshopAccessInformationWSResponse | undefined;
const modOptions = {
@ -79,11 +79,25 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
...options, // Include all options.
};
try {
const site = await CoreSites.getSite(options.siteId);
const userId = site.getUserId();
const workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
const site = await CoreSites.getSite(options.siteId);
options.siteId = options.siteId ?? site.getId();
const userId = site.getUserId();
try {
workshop = await AddonModWorkshop.getWorkshop(courseId, module.id, options);
} catch (error) {
if (options.omitFail) {
// Any error, return the info we have.
return {
groups: [],
files: [],
};
}
throw error;
}
try {
files = this.getIntroFilesFromInstance(module, workshop);
files = files.concat(workshop.instructauthorsfiles || []).concat(workshop.instructreviewersfiles || []);
@ -124,7 +138,7 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
await Promise.all(submissions.map(async (submission) => {
files = files.concat(submission.contentfiles || []).concat(submission.attachmentfiles || []);
const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop!.id, submission.id, {
const assessments = await AddonModWorkshop.getSubmissionAssessments(workshop.id, submission.id, {
cmId: module.id,
});
@ -133,9 +147,9 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
.concat(assessment.feedbackcontentfiles);
});
if (workshop!.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
if (workshop.phase >= AddonModWorkshopPhase.PHASE_ASSESSMENT && canAssess) {
await Promise.all(assessments.map((assessment) =>
AddonModWorkshopHelper.getReviewerAssessmentById(workshop!.id, assessment.id)));
AddonModWorkshopHelper.getReviewerAssessmentById(workshop.id, assessment.id)));
}
}));
@ -267,7 +281,12 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr
// Prefetch the workshop data.
const info = await this.getWorkshopInfoHelper(module, courseId, commonOptions);
const workshop = info.workshop!;
if (!info.workshop) {
// It would throw an exception so it would not happen.
return;
}
const workshop = info.workshop;
const promises: Promise<unknown>[] = [];
const assessmentIds: number[] = [];

View File

@ -181,7 +181,7 @@ export class AddonModWorkshopHelperProvider {
assessment = await AddonModWorkshop.getAssessment(workshopId, assessmentId, options);
} catch (error) {
const assessments = await AddonModWorkshop.getReviewerAssessments(workshopId, options);
assessment = assessments.find((assessment_1) => assessment_1.id == assessmentId);
assessment = assessments.find((ass) => ass.id === assessmentId);
if (!assessment) {
throw error;
@ -266,9 +266,7 @@ export class AddonModWorkshopHelperProvider {
* Upload or store some files for a submission, depending if the user is offline or not.
*
* @param workshopId Workshop ID.
* @param submissionId If not editing, it will refer to timecreated.
* @param files List of files.
* @param editing If the submission is being edited or added otherwise.
* @param offline True if files sould be stored for offline, false to upload them.
* @param siteId Site ID. If not defined, current site.
* @returns Promise resolved if success.
@ -451,30 +449,26 @@ export class AddonModWorkshopHelperProvider {
* @returns Promise resolved with the files.
*/
async applyOfflineData(
submission?: AddonModWorkshopSubmissionDataWithOfflineData,
submission: AddonModWorkshopSubmissionDataWithOfflineData = {
id: 0,
workshopid: 0,
title: '',
content: '',
timemodified: 0,
example: false,
authorid: 0,
timecreated: 0,
contenttrust: 0,
attachment: 0,
published: false,
late: 0,
},
actions: AddonModWorkshopOfflineSubmission[] = [],
): Promise<AddonModWorkshopSubmissionDataWithOfflineData | undefined> {
if (actions.length == 0) {
if (actions.length === 0) {
return submission;
}
if (submission === undefined) {
submission = {
id: 0,
workshopid: 0,
title: '',
content: '',
timemodified: 0,
example: false,
authorid: 0,
timecreated: 0,
contenttrust: 0,
attachment: 0,
published: false,
late: 0,
};
}
let attachmentsId: CoreFileUploaderStoreFilesResult | undefined;
const workshopId = actions[0].workshopid;
@ -482,17 +476,17 @@ export class AddonModWorkshopHelperProvider {
switch (action.action) {
case AddonModWorkshopAction.ADD:
case AddonModWorkshopAction.UPDATE:
submission!.title = action.title;
submission!.content = action.content;
submission!.title = action.title;
submission!.courseid = action.courseid;
submission!.submissionmodified = action.timemodified / 1000;
submission!.offline = true;
submission.title = action.title;
submission.content = action.content;
submission.title = action.title;
submission.courseid = action.courseid;
submission.submissionmodified = action.timemodified / 1000;
submission.offline = true;
attachmentsId = action.attachmentsid as CoreFileUploaderStoreFilesResult;
break;
case AddonModWorkshopAction.DELETE:
submission!.deleted = true;
submission!.submissionmodified = action.timemodified / 1000;
submission.deleted = true;
submission.submissionmodified = action.timemodified / 1000;
break;
default:
}
@ -534,7 +528,8 @@ export class AddonModWorkshopHelperProvider {
}
const data =
(await AddonWorkshopAssessmentStrategyDelegate.prepareAssessmentData(workshop.strategy!, selectedValues, form)) || {};
(await AddonWorkshopAssessmentStrategyDelegate.prepareAssessmentData(workshop.strategy ?? '', selectedValues, form)) ||
{};
data.feedbackauthor = feedbackText;
data.feedbackauthorattachmentsid = attachmentsId;
data.nodims = form.dimenssionscount;
@ -551,16 +546,16 @@ export class AddonModWorkshopHelperProvider {
* @returns Real grade formatted.
*/
protected realGradeValueHelper(value?: number | string, max = 0, decimals = 0): string | undefined {
if (typeof value == 'string') {
if (typeof value === 'string') {
// Already treated.
return value;
}
if (value == null || value === undefined) {
if (value === null || value === undefined) {
return undefined;
}
if (max == 0) {
if (max === 0) {
return '0';
}

View File

@ -20,7 +20,7 @@ import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/service
import { CoreNetwork } from '@services/network';
import { CoreFileEntry } from '@services/file-helper';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreSync, CoreSyncResult } from '@services/sync';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { Translate, makeSingleton } from '@singletons';
@ -639,7 +639,4 @@ export type AddonModWorkshopAutoSyncData = {
warnings: string[];
};
export type AddonModWorkshopSyncResult = {
warnings: string[];
updated: boolean;
};
export type AddonModWorkshopSyncResult = CoreSyncResult;

View File

@ -610,13 +610,16 @@ export class AddonModWorkshopProvider {
grades: AddonModWorkshopGradesData[],
options: AddonModWorkshopGetGradesReportOptions = {},
): Promise<AddonModWorkshopGradesData[]> {
options.page = options.page ?? 0;
options.perPage = options.perPage ?? AddonModWorkshopProvider.PER_PAGE;
const report = await this.getGradesReport(workshopId, options);
Array.prototype.push.apply(grades, report.grades);
const canLoadMore = ((options.page! + 1) * options.perPage!) < report.totalcount;
const canLoadMore = ((options.page + 1) * options.perPage) < report.totalcount;
if (canLoadMore) {
options.page!++;
options.page++;
return this.fetchGradeReportsRecursive(workshopId, grades, options);
}
@ -778,7 +781,11 @@ export class AddonModWorkshopProvider {
// Other errors ocurring.
CoreWS.throwOnFailedStatus(response, 'Add submission failed');
return response.submissionid!;
if (!response.submissionid) {
throw new CoreError('Add submission failed, no submission id was returned');
}
return response.submissionid;
}
/**

View File

@ -1,57 +1,57 @@
<ion-list class="addon-qtype-calculated-container" *ngIf="calcQuestion && (calcQuestion.text || calcQuestion.text === '')">
<ion-list class="addon-qtype-calculated-container" *ngIf="question && (question.text || question.text === '')">
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="calcQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- Display unit options before the answer input. -->
<ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && calcQuestion.optionsFirst">
<ng-container *ngIf="question.options && question.options.length && question.optionsFirst">
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
</ng-container>
<ion-item *ngIf="calcQuestion.input" class="ion-text-wrap core-{{calcQuestion.input.correctIconColor}}-item">
<ion-item *ngIf="question.input" class="ion-text-wrap core-{{question.input.correctIconColor}}-item">
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
<div class="flex-row">
<!-- Display unit select before the answer input. -->
<ng-container *ngIf="calcQuestion.select && calcQuestion.selectFirst">
<ng-container *ngIf="question.select && question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
<!-- Input to enter the answer. -->
<ion-input type="text" [attr.name]="calcQuestion.input.name"
[placeholder]="calcQuestion.input.readOnly ? '' : 'core.question.answer' | translate" [value]="calcQuestion.input.value"
[disabled]="calcQuestion.input.readOnly" autocorrect="off">
<ion-input type="text" [attr.name]="question.input.name"
[placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate" [value]="question.input.value"
[disabled]="question.input.readOnly" autocorrect="off">
</ion-input>
<!-- Display unit select after the answer input. -->
<ng-container *ngIf="calcQuestion.select && !calcQuestion.selectFirst">
<ng-container *ngIf="question.select && !question.selectFirst">
<ng-container *ngTemplateOutlet="selectUnits"></ng-container>
</ng-container>
</div>
<ion-icon *ngIf="calcQuestion.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
[name]="calcQuestion.input.correctIcon" [color]="[calcQuestion.input.correctIconColor]">
<ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon ion-align-self-center" slot="end"
[name]="question.input.correctIcon" [color]="[question.input.correctIconColor]">
</ion-icon>
</ion-item>
<!-- Display unit options after the answer input. -->
<ng-container *ngIf="calcQuestion.options && calcQuestion.options.length && !calcQuestion.optionsFirst">
<ng-container *ngIf="question.options && question.options.length && !question.optionsFirst">
<ng-container *ngTemplateOutlet="radioUnits"></ng-container>
</ng-container>
</ion-list>
<!-- Template for units entered using a select. -->
<ng-template #selectUnits>
<label *ngIf="calcQuestion!.select!.accessibilityLabel" class="accesshide" for="{{calcQuestion!.select!.id}}">
{{ calcQuestion!.select!.accessibilityLabel }}
<label *ngIf="question!.select!.accessibilityLabel" class="accesshide" for="{{question!.select!.id}}">
{{ question!.select!.accessibilityLabel }}
</label>
<ion-select id="{{calcQuestion!.select!.id}}" [name]="calcQuestion!.select!.name" [(ngModel)]="calcQuestion!.select!.selected"
interface="action-sheet" [disabled]="calcQuestion!.select!.disabled" [slot]="calcQuestion?.selectFirst ? 'start' : 'end'"
<ion-select id="{{question!.select!.id}}" [name]="question!.select!.name" [(ngModel)]="question!.select!.selected"
interface="action-sheet" [disabled]="question!.select!.disabled" [slot]="question?.selectFirst ? 'start' : 'end'"
[interfaceOptions]="{header: 'addon.mod_quiz.unit' | translate}">
<ion-select-option *ngFor="let option of calcQuestion!.select!.options" [value]="option.value">
<ion-select-option *ngFor="let option of question!.select!.options" [value]="option.value">
{{option.label}}
</ion-select-option>
</ion-select>
@ -59,15 +59,15 @@
<!-- Template for units entered using radio buttons. -->
<ng-template #radioUnits>
<ion-radio-group [(ngModel)]="calcQuestion!.unit" [name]="calcQuestion!.optionsName">
<ion-item class="ion-text-wrap" *ngFor="let option of calcQuestion!.options">
<ion-radio-group [(ngModel)]="question!.unit" [name]="question!.optionsName">
<ion-item class="ion-text-wrap" *ngFor="let option of question!.options">
<ion-label>{{ option.text }}</ion-label>
<ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || calcQuestion!.input?.readOnly"
[color]="calcQuestion!.input?.correctIconColor">
<ion-radio slot="end" [value]="option.value" [disabled]="option.disabled || question!.input?.readOnly"
[color]="question!.input?.correctIconColor">
</ion-radio>
</ion-item>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="calcQuestion!.unit" [attr.name]="calcQuestion!.optionsName">
<input type="hidden" [ngModel]="question!.unit" [attr.name]="question!.optionsName">
</ion-radio-group>
</ng-template>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -23,9 +23,7 @@ import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@feat
selector: 'addon-qtype-calculated',
templateUrl: 'addon-qtype-calculated.html',
})
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent implements OnInit {
calcQuestion?: AddonModQuizCalculatedQuestion;
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent<AddonModQuizCalculatedQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeCalculatedComponent', elementRef);
@ -34,10 +32,8 @@ export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent imp
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initCalculatedComponent();
this.calcQuestion = this.question;
}
}

View File

@ -708,9 +708,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
this.topNode = this.container.querySelector<HTMLElement>('.addon-qtype-ddimageortext-container');
this.dragItemsArea = this.topNode?.querySelector<HTMLElement>('div.draghomes') || null;
if (!this.topNode) {
this.logger.error('ddimageortext container not found');
return;
}
if (this.dragItemsArea) {
// On 3.9+ dragitems were removed.
const dragItems = this.topNode!.querySelector('div.dragitems');
const dragItems = this.topNode.querySelector('div.dragitems');
if (dragItems) {
// Remove empty div.dragitems.
@ -718,10 +724,10 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
}
// 3.6+ site, transform HTML so it has the same structure as in Moodle 3.5.
const ddArea = this.topNode!.querySelector('div.ddarea');
const ddArea = this.topNode.querySelector('div.ddarea');
if (ddArea) {
// Move div.dropzones to div.ddarea.
const dropZones = this.topNode!.querySelector('div.dropzones');
const dropZones = this.topNode.querySelector('div.dropzones');
if (dropZones) {
ddArea.appendChild(dropZones);
}
@ -738,7 +744,7 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
draghome.classList.add(`dragitemhomes${index}`);
});
} else {
this.dragItemsArea = this.topNode!.querySelector<HTMLElement>('div.dragitems');
this.dragItemsArea = this.topNode.querySelector<HTMLElement>('div.dragitems');
}
}
@ -797,14 +803,15 @@ export class AddonQtypeDdImageOrTextQuestionDocStructure {
getClassnameNumericSuffix(node: HTMLElement, prefix: string): number | undefined {
if (node.classList && node.classList.length) {
const patt1 = new RegExp(`^${prefix}([0-9])+$`);
const patt2 = new RegExp('([0-9])+$');
for (let index = 0; index < node.classList.length; index++) {
if (patt1.test(node.classList[index])) {
const match = patt2.exec(node.classList[index]);
const classFound = Array.from(node.classList)
.find((className) => patt1.test(className));
return Number(match![0]);
}
if (classFound) {
const patt2 = new RegExp('([0-9])+$');
const match = patt2.exec(classFound);
return Number(match?.[0]);
}
}

View File

@ -1,24 +1,24 @@
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddimageortext-container">
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddimageortext-container">
<!-- Content is outside the core-loading to let the script calculate drag items position -->
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
<core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
<ion-item class="ion-text-wrap" [hidden]="!question.loaded">
<ion-label>
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
<ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item>
</ion-card>
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
</core-format-text>
</ion-label>
</ion-item>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
</core-format-text>
</div>
</div>

View File

@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { Component, OnDestroy, ElementRef } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
/**
@ -27,9 +26,9 @@ import { AddonQtypeDdImageOrTextQuestion } from '../classes/ddimageortext';
templateUrl: 'addon-qtype-ddimageortext.html',
styleUrls: ['ddimageortext.scss'],
})
export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
ddQuestion?: AddonModQuizDdImageOrTextQuestionData;
export class AddonQtypeDdImageOrTextComponent
extends CoreQuestionBaseComponent<AddonModQuizDdImageOrTextQuestionData>
implements OnDestroy {
protected questionInstance?: AddonQtypeDdImageOrTextQuestion;
protected drops?: unknown[]; // The drop zones received in the init object of the question.
@ -44,50 +43,47 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
return;
}
this.ddQuestion = this.question;
const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
const questionElement = this.initComponent();
if (!questionElement) {
return;
}
// Get D&D area and question text.
const ddArea = element.querySelector('.ddarea');
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
if (!ddArea || this.ddQuestion.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
const ddArea = questionElement.querySelector('.ddarea');
if (!ddArea) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
// Set the D&D area HTML.
this.ddQuestion.ddArea = ddArea.outerHTML;
this.ddQuestion.readOnly = false;
this.question.ddArea = ddArea.outerHTML;
this.question.readOnly = false;
if (this.ddQuestion.initObjects) {
if (this.question.initObjects) {
// Moodle version = 3.5.
if (this.ddQuestion.initObjects.drops !== undefined) {
this.drops = <unknown[]> this.ddQuestion.initObjects.drops;
if (this.question.initObjects.drops !== undefined) {
this.drops = <unknown[]> this.question.initObjects.drops;
}
if (this.ddQuestion.initObjects.readonly !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
if (this.question.initObjects.readonly !== undefined) {
this.question.readOnly = !!this.question.initObjects.readonly;
}
} else if (this.ddQuestion.amdArgs) {
} else if (this.question.amdArgs) {
// Moodle version >= 3.6.
if (this.ddQuestion.amdArgs[1] !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[1];
if (this.question.amdArgs[1] !== undefined) {
this.question.readOnly = !!this.question.amdArgs[1];
}
if (this.ddQuestion.amdArgs[2] !== undefined) {
this.drops = <unknown[]> this.ddQuestion.amdArgs[2];
if (this.question.amdArgs[2] !== undefined) {
this.drops = <unknown[]> this.question.amdArgs[2];
}
}
this.ddQuestion.loaded = false;
this.question.loaded = false;
}
/**
@ -114,12 +110,12 @@ export class AddonQtypeDdImageOrTextComponent extends CoreQuestionBaseComponent
* The question has been rendered.
*/
protected questionRendered(): void {
if (!this.destroyed && this.ddQuestion) {
if (!this.destroyed && this.question) {
// Create the instance.
this.questionInstance = new AddonQtypeDdImageOrTextQuestion(
this.hostElement,
this.ddQuestion,
!!this.ddQuestion.readOnly,
this.question,
!!this.question.readOnly,
this.drops,
);
}

View File

@ -1,23 +1,23 @@
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')" class="addon-qtype-ddmarker-container">
<div *ngIf="question && (question.text || question.text === '')" class="addon-qtype-ddmarker-container">
<!-- Content is outside the core-loading to let the script calculate drag items position -->
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
<core-loading [hideUntil]="question.loaded"></core-loading>
<ion-item class="ion-text-wrap" [hidden]="!ddQuestion.loaded">
<ion-item class="ion-text-wrap" [hidden]="!question.loaded">
<ion-label>
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
<ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item>
</ion-card>
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" #questiontext
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" #questiontext
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" (afterRender)="textRendered()">
</core-format-text>
</ion-label>
</ion-item>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
<core-format-text *ngIf="ddQuestion.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="ddQuestion.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<core-format-text *ngIf="question.ddArea" [adaptImg]="false" [component]="component" [componentId]="componentId"
[text]="question.ddArea" [filter]="false" (afterRender)="ddAreaRendered()">
</core-format-text>
</div>
</div>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -29,12 +29,12 @@ import { AddonQtypeDdMarkerQuestion } from '../classes/ddmarker';
templateUrl: 'addon-qtype-ddmarker.html',
styleUrls: ['ddmarker.scss'],
})
export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
export class AddonQtypeDdMarkerComponent
extends CoreQuestionBaseComponent<AddonQtypeDdMarkerQuestionData>
implements OnDestroy {
@ViewChild('questiontext') questionTextEl?: ElementRef;
ddQuestion?: AddonQtypeDdMarkerQuestionData;
protected questionInstance?: AddonQtypeDdMarkerQuestion;
protected dropZones: unknown[] = []; // The drop zones received in the init object of the question.
protected imgSrc?: string; // Background image URL.
@ -49,65 +49,64 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
return;
}
this.ddQuestion = this.question;
const element = CoreDomUtils.convertToElement(this.question.html);
const questionElement = this.initComponent();
if (!questionElement) {
return;
}
// Get D&D area, form and question text.
const ddArea = element.querySelector('.ddarea');
const ddForm = element.querySelector('.ddform');
const ddArea = questionElement.querySelector('.ddarea');
const ddForm = questionElement.querySelector('.ddform');
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
if (!ddArea || !ddForm || this.ddQuestion.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
if (!ddArea || !ddForm) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
// Build the D&D area HTML.
this.ddQuestion.ddArea = ddArea.outerHTML;
this.question.ddArea = ddArea.outerHTML;
const wrongParts = element.querySelector('.wrongparts');
const wrongParts = questionElement.querySelector('.wrongparts');
if (wrongParts) {
this.ddQuestion.ddArea += wrongParts.outerHTML;
this.question.ddArea += wrongParts.outerHTML;
}
this.ddQuestion.ddArea += ddForm.outerHTML;
this.ddQuestion.readOnly = false;
this.question.ddArea += ddForm.outerHTML;
this.question.readOnly = false;
if (this.ddQuestion.initObjects) {
if (this.question.initObjects) {
// Moodle version = 3.5.
if (this.ddQuestion.initObjects.dropzones !== undefined) {
this.dropZones = <unknown[]> this.ddQuestion.initObjects.dropzones;
if (this.question.initObjects.dropzones !== undefined) {
this.dropZones = <unknown[]> this.question.initObjects.dropzones;
}
if (this.ddQuestion.initObjects.readonly !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.initObjects.readonly;
if (this.question.initObjects.readonly !== undefined) {
this.question.readOnly = !!this.question.initObjects.readonly;
}
} else if (this.ddQuestion.amdArgs) {
} else if (this.question.amdArgs) {
// Moodle version >= 3.6.
let nextIndex = 1;
// Moodle version >= 3.9, imgSrc is not specified, do not advance index.
if (this.ddQuestion.amdArgs[nextIndex] !== undefined && typeof this.ddQuestion.amdArgs[nextIndex] != 'boolean') {
this.imgSrc = <string> this.ddQuestion.amdArgs[nextIndex];
if (this.question.amdArgs[nextIndex] !== undefined && typeof this.question.amdArgs[nextIndex] !== 'boolean') {
this.imgSrc = <string> this.question.amdArgs[nextIndex];
nextIndex++;
}
if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
this.ddQuestion.readOnly = !!this.ddQuestion.amdArgs[nextIndex];
if (this.question.amdArgs[nextIndex] !== undefined) {
this.question.readOnly = !!this.question.amdArgs[nextIndex];
}
nextIndex++;
if (this.ddQuestion.amdArgs[nextIndex] !== undefined) {
this.dropZones = <unknown[]> this.ddQuestion.amdArgs[nextIndex];
if (this.question.amdArgs[nextIndex] !== undefined) {
this.dropZones = <unknown[]> this.question.amdArgs[nextIndex];
}
}
this.ddQuestion.loaded = false;
this.question.loaded = false;
}
/**
@ -134,9 +133,10 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
* The question has been rendered.
*/
protected async questionRendered(): Promise<void> {
if (this.destroyed) {
if (this.destroyed || !this.question) {
return;
}
// Download background image (3.6+ sites).
let imgSrc = this.imgSrc;
const site = CoreSites.getCurrentSite();
@ -160,8 +160,8 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple
// Create the instance.
this.questionInstance = new AddonQtypeDdMarkerQuestion(
this.hostElement,
this.ddQuestion!,
!!this.ddQuestion!.readOnly,
this.question,
!!this.question.readOnly,
this.dropZones,
imgSrc,
);

View File

@ -1,20 +1,20 @@
<div *ngIf="ddQuestion && (ddQuestion.text || ddQuestion.text === '')">
<div *ngIf="question && (question.text || question.text === '')">
<!-- Content is outside the core-loading to let the script calculate drag items position -->
<core-loading [hideUntil]="ddQuestion.loaded"></core-loading>
<core-loading [hideUntil]="question.loaded"></core-loading>
<div class="fake-ion-item ion-text-wrap" [hidden]="!ddQuestion.loaded">
<ion-card *ngIf="!ddQuestion.readOnly" class="core-info-card">
<div class="fake-ion-item ion-text-wrap" [hidden]="!question.loaded">
<ion-card *ngIf="!question.readOnly" class="core-info-card">
<ion-item>
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.question.howtodraganddrop' | translate }}</ion-label>
</ion-item>
</ion-card>
<div class="addon-qtype-ddwtos-container">
<core-format-text [component]="component" [componentId]="componentId" [text]="ddQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId" #questiontext (afterRender)="textRendered()">
</core-format-text>
<core-format-text *ngIf="ddQuestion.answers" [component]="component" [componentId]="componentId" [text]="ddQuestion.answers"
<core-format-text *ngIf="question.answers" [component]="component" [componentId]="componentId" [text]="question.answers"
[filter]="false" (afterRender)="answersRendered()">
</core-format-text>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { Component, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { AddonModQuizQuestionBasicData, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -27,12 +27,10 @@ import { AddonQtypeDdwtosQuestion } from '../classes/ddwtos';
templateUrl: 'addon-qtype-ddwtos.html',
styleUrls: ['ddwtos.scss'],
})
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent implements OnInit, OnDestroy {
export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent<AddonModQuizDdwtosQuestionData> implements OnDestroy {
@ViewChild('questiontext') questionTextEl?: ElementRef;
ddQuestion?: AddonModQuizDdwtosQuestionData;
protected questionInstance?: AddonQtypeDdwtosQuestion;
protected inputIds: string[] = []; // Ids of the inputs of the question (where the answers will be stored).
protected destroyed = false;
@ -46,52 +44,50 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
if (!this.question) {
this.logger.warn('Aborting because of no question received.');
return CoreQuestionHelper.showComponentError(this.onAbort);
return;
}
this.ddQuestion = this.question;
const element = CoreDomUtils.convertToElement(this.ddQuestion.html);
const questionElement = this.initComponent();
if (!questionElement) {
return;
}
// Replace Moodle's correct/incorrect and feedback classes with our own.
CoreQuestionHelper.replaceCorrectnessClasses(element);
CoreQuestionHelper.replaceFeedbackClasses(element);
CoreQuestionHelper.replaceCorrectnessClasses(questionElement);
CoreQuestionHelper.replaceFeedbackClasses(questionElement);
// Treat the correct/incorrect icons.
CoreQuestionHelper.treatCorrectnessIcons(element);
CoreQuestionHelper.treatCorrectnessIcons(questionElement);
const answerContainer = element.querySelector('.answercontainer');
const answerContainer = questionElement.querySelector('.answercontainer');
if (!answerContainer) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
this.ddQuestion.readOnly = answerContainer.classList.contains('readonly');
this.ddQuestion.answers = answerContainer.outerHTML;
this.ddQuestion.text = CoreDomUtils.getContentsOfElement(element, '.qtext');
if (this.ddQuestion.text === undefined) {
this.logger.warn('Aborting because of an error parsing question.', this.ddQuestion.slot);
return CoreQuestionHelper.showComponentError(this.onAbort);
}
this.question.readOnly = answerContainer.classList.contains('readonly');
this.question.answers = answerContainer.outerHTML;
// Get the inputs where the answers will be stored and add them to the question text.
const inputEls = <HTMLElement[]> Array.from(element.querySelectorAll('input[type="hidden"]:not([name*=sequencecheck])'));
const inputEls = Array.from(
questionElement.querySelectorAll<HTMLInputElement>('input[type="hidden"]:not([name*=sequencecheck])'),
);
let questionText = this.question.text;
inputEls.forEach((inputEl) => {
this.ddQuestion!.text += inputEl.outerHTML;
questionText += inputEl.outerHTML;
const id = inputEl.getAttribute('id');
if (id) {
this.inputIds.push(id);
}
});
this.ddQuestion.loaded = false;
this.question.text = questionText;
this.question.loaded = false;
}
/**
@ -118,7 +114,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
* The question has been rendered.
*/
protected async questionRendered(): Promise<void> {
if (this.destroyed) {
if (this.destroyed || !this.question) {
return;
}
@ -129,8 +125,8 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
// Create the instance.
this.questionInstance = new AddonQtypeDdwtosQuestion(
this.hostElement,
this.ddQuestion!,
!!this.ddQuestion!.readOnly,
this.question,
!!this.question.readOnly,
this.inputIds,
);
@ -143,7 +139,7 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent impleme
this.courseId,
);
this.ddQuestion!.loaded = true;
this.question.loaded = true;
}

View File

@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
/**
@ -23,7 +22,7 @@ import { CoreQuestionBaseComponent } from '@features/question/classes/base-quest
selector: 'addon-qtype-description',
templateUrl: 'addon-qtype-description.html',
})
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent {
seenInput?: { name: string; value: string };
@ -34,20 +33,22 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
const questionEl = this.initComponent();
if (!questionEl) {
return;
}
// Get the "seen" hidden input.
const input = <HTMLInputElement> questionEl.querySelector('input[type="hidden"][name*=seen]');
if (input) {
this.seenInput = {
name: input.name,
value: input.value,
};
const input = questionEl.querySelector<HTMLInputElement>('input[type="hidden"][name*=seen]');
if (!input) {
return;
}
this.seenInput = {
name: input.name,
value: input.value,
};
}
}

View File

@ -1,8 +1,8 @@
<ion-list *ngIf="essayQuestion && (essayQuestion.text || essayQuestion.text === '')">
<ion-list *ngIf="question && (question.text || question.text === '')">
<!-- Question text. -->
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
@ -11,27 +11,26 @@
<!-- Editing the question. -->
<ng-container *ngIf="!review">
<!-- Textarea. -->
<ion-item *ngIf="essayQuestion.textarea && (!essayQuestion.hasDraftFiles || uploadFilesSupported)">
<ion-item *ngIf="question.textarea && (!question.hasDraftFiles || uploadFilesSupported)">
<ion-label class="sr-only">{{ 'core.question.answer' | translate }}</ion-label>
<!-- "Format" and draftid hidden inputs -->
<input *ngIf="essayQuestion.formatInput" type="hidden" [name]="essayQuestion.formatInput.name"
[value]="essayQuestion.formatInput.value">
<input *ngIf="essayQuestion.answerDraftIdInput" type="hidden" [name]="essayQuestion.answerDraftIdInput.name"
[value]="essayQuestion.answerDraftIdInput.value">
<input *ngIf="question.formatInput" type="hidden" [name]="question.formatInput.name" [value]="question.formatInput.value">
<input *ngIf="question.answerDraftIdInput" type="hidden" [name]="question.answerDraftIdInput.name"
[value]="question.answerDraftIdInput.value">
<!-- Plain text textarea. -->
<ion-textarea *ngIf="essayQuestion.isPlainText" class="core-question-textarea"
[ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' placeholder="{{ 'core.question.answer' | translate }}"
[attr.name]="essayQuestion.textarea.name" [ngModel]="essayQuestion.textarea.text">
<ion-textarea *ngIf="question.isPlainText" class="core-question-textarea" [ngClass]='{"core-monospaced": question.isMonospaced}'
placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.textarea.name"
[ngModel]="question.textarea.text">
</ion-textarea>
<!-- Rich text editor. -->
<core-rich-text-editor *ngIf="!essayQuestion.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
[control]="formControl" [name]="essayQuestion.textarea.name" [component]="component" [componentId]="componentId"
<core-rich-text-editor *ngIf="!question.isPlainText" placeholder="{{ 'core.question.answer' | translate }}"
[control]="formControl" [name]="question.textarea.name" [component]="component" [componentId]="componentId"
[autoSave]="false">
</core-rich-text-editor>
</ion-item>
<!-- Draft files not supported. -->
<ng-container *ngIf="essayQuestion.textarea && essayQuestion.hasDraftFiles && !uploadFilesSupported">
<ng-container *ngIf="question.textarea && question.hasDraftFiles && !uploadFilesSupported">
<ion-item class="ion-text-wrap core-danger-item">
<ion-label class="core-question-warning">
{{ 'core.question.errorembeddedfilesnotsupportedinsite' | translate }}
@ -39,7 +38,7 @@
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.textarea.text"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.textarea.text"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
@ -47,15 +46,14 @@
</ng-container>
<!-- Attachments. -->
<ng-container *ngIf="essayQuestion.allowsAttachments">
<core-attachments *ngIf="uploadFilesSupported && essayQuestion.attachmentsDraftIdInput" [files]="attachments"
[component]="component" [componentId]="componentId" [maxSize]="essayQuestion.attachmentsMaxBytes"
[maxSubmissions]="essayQuestion.attachmentsMaxFiles" [allowOffline]="offlineEnabled"
[acceptedTypes]="essayQuestion.attachmentsAcceptedTypes" [courseId]="courseId">
<ng-container *ngIf="question.allowsAttachments">
<core-attachments *ngIf="uploadFilesSupported && question.attachmentsDraftIdInput" [files]="attachments" [component]="component"
[componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles"
[allowOffline]="offlineEnabled" [acceptedTypes]="question.attachmentsAcceptedTypes" [courseId]="courseId">
</core-attachments>
<input *ngIf="essayQuestion.attachmentsDraftIdInput" type="hidden" [name]="essayQuestion.attachmentsDraftIdInput.name"
[value]="essayQuestion.attachmentsDraftIdInput.value">
<input *ngIf="question.attachmentsDraftIdInput" type="hidden" [name]="question.attachmentsDraftIdInput.name"
[value]="question.attachmentsDraftIdInput.value">
<!-- Attachments not supported in this site. -->
<ion-item *ngIf="!uploadFilesSupported" class="ion-text-wrap core-danger-item">
@ -69,36 +67,35 @@
<!-- Reviewing the question. -->
<ng-container *ngIf="review">
<!-- Answer to the question and attachments (reviewing). -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.answer || essayQuestion.answer == ''">
<ion-item class="ion-text-wrap" *ngIf="question.answer || question.answer == ''">
<ion-label>
<core-format-text [ngClass]='{"core-monospaced": essayQuestion.isMonospaced}' [component]="component"
[componentId]="componentId" [text]="essayQuestion.answer" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
<core-format-text [ngClass]='{"core-monospaced": question.isMonospaced}' [component]="component" [componentId]="componentId"
[text]="question.answer" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- Word count info. -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.wordCountInfo">
<ion-item class="ion-text-wrap" *ngIf="question.wordCountInfo">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.wordCountInfo"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.wordCountInfo"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- Answer plagiarism. -->
<ion-item class="ion-text-wrap" *ngIf="essayQuestion.answerPlagiarism">
<ion-item class="ion-text-wrap" *ngIf="question.answerPlagiarism">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.answerPlagiarism"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.answerPlagiarism"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<!-- List of attachments when reviewing. -->
<core-files *ngIf="essayQuestion.attachments" [files]="essayQuestion.attachments" [component]="component"
[componentId]="componentId" [extraHtml]="essayQuestion.attachmentsPlagiarisms">
<core-files *ngIf="question.attachments" [files]="question.attachments" [component]="component" [componentId]="componentId"
[extraHtml]="question.attachmentsPlagiarisms">
</core-files>
</ng-container>
</ion-list>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { FileEntry } from '@ionic-native/file/ngx';
@ -30,12 +30,11 @@ import { CoreFileEntry } from '@services/file-helper';
selector: 'addon-qtype-essay',
templateUrl: 'addon-qtype-essay.html',
})
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent<AddonModQuizEssayQuestion> {
formControl?: FormControl;
attachments?: CoreFileEntry[];
uploadFilesSupported = false;
essayQuestion?: AddonModQuizEssayQuestion;
constructor(elementRef: ElementRef, protected fb: FormBuilder) {
super('AddonQtypeEssayComponent', elementRef);
@ -44,14 +43,18 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
/**
* @inheritdoc
*/
ngOnInit(): void {
this.uploadFilesSupported = this.question?.responsefileareas !== undefined;
init(): void {
if (!this.question) {
return;
}
this.uploadFilesSupported = this.question.responsefileareas !== undefined;
this.initEssayComponent(this.review);
this.essayQuestion = this.question;
this.formControl = this.fb.control(this.essayQuestion?.textarea?.text);
this.formControl = this.fb.control(this.question?.textarea?.text);
if (this.essayQuestion?.allowsAttachments && this.uploadFilesSupported && !this.review) {
if (this.question?.allowsAttachments && this.uploadFilesSupported && !this.review) {
this.loadAttachments();
}
}
@ -62,10 +65,14 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
* @returns Promise resolved when done.
*/
async loadAttachments(): Promise<void> {
if (this.offlineEnabled && this.essayQuestion?.localAnswers?.attachments_offline) {
if (!this.question) {
return;
}
if (this.offlineEnabled && this.question.localAnswers?.attachments_offline) {
const attachmentsData: CoreFileUploaderStoreFilesResult = CoreTextUtils.parseJSON(
this.essayQuestion.localAnswers.attachments_offline,
this.question.localAnswers.attachments_offline,
{
online: [],
offline: 0,
@ -75,7 +82,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
if (attachmentsData.offline) {
offlineFiles = <FileEntry[]> await CoreQuestionHelper.getStoredQuestionFiles(
this.essayQuestion,
this.question,
this.component || '',
this.componentId || -1,
);
@ -83,12 +90,12 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen
this.attachments = [...attachmentsData.online, ...offlineFiles];
} else {
this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question!, 'attachments'));
this.attachments = Array.from(CoreQuestionHelper.getResponseFileAreaFiles(this.question, 'attachments'));
}
CoreFileSession.setFiles(
this.component || '',
CoreQuestion.getQuestionComponentId(this.question!, this.componentId || -1),
CoreQuestion.getQuestionComponentId(this.question, this.componentId || -1),
this.attachments,
);
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -25,7 +25,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
templateUrl: 'addon-qtype-gapselect.html',
styleUrls: ['gapselect.scss'],
})
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent {
constructor(elementRef: ElementRef) {
super('AddonQtypeGapSelectComponent', elementRef);
@ -34,7 +34,7 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent impl
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initOriginalTextComponent('.qtext');
}

View File

@ -1,12 +1,12 @@
<section class="addon-qtype-match-container" *ngIf="matchQuestion && matchQuestion.loaded">
<section class="addon-qtype-match-container" *ngIf="question && question.loaded">
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="matchQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let row of matchQuestion.rows">
<ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
<ion-label>
<core-format-text id="addon-qtype-match-question-{{row.id}}" [component]="component" [componentId]="componentId"
[text]="row.text" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizMatchQuestion, CoreQuestionBaseComponent } from '@features/
templateUrl: 'addon-qtype-match.html',
styleUrls: ['match.scss'],
})
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implements OnInit {
matchQuestion?: AddonModQuizMatchQuestion;
export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent<AddonModQuizMatchQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeMatchComponent', elementRef);
@ -35,9 +33,8 @@ export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent implemen
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initMatchComponent();
this.matchQuestion = this.question;
}
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
import { CoreQuestionHelper } from '@features/question/services/question-helper';
@ -24,7 +24,7 @@ import { CoreQuestionHelper } from '@features/question/services/question-helper'
templateUrl: 'addon-qtype-multianswer.html',
styleUrls: ['multianswer.scss'],
})
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent {
constructor(elementRef: ElementRef) {
super('AddonQtypeMultiAnswerComponent', elementRef);
@ -33,7 +33,7 @@ export class AddonQtypeMultiAnswerComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initOriginalTextComponent('.formulation');
}

View File

@ -1,23 +1,23 @@
<ion-list *ngIf="multiQuestion && (multiQuestion.text || multiQuestion.text === '')">
<ion-list *ngIf="question && (question.text || question.text === '')">
<!-- Question text first. -->
<ion-item class="ion-text-wrap">
<ion-label>
<p>
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.text"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</p>
<p *ngIf="multiQuestion.prompt">
<core-format-text [component]="component" [componentId]="componentId" [text]="multiQuestion.prompt"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId">
<p *ngIf="question.prompt">
<core-format-text [component]="component" [componentId]="componentId" [text]="question.prompt" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
<!-- Checkbox for multiple choice. -->
<ng-container *ngIf="multiQuestion.multi">
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
<ng-container *ngIf="question.multi">
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
<ion-label [color]='(option.isCorrect === 1 ? "success": "") + (option.isCorrect === 0 ? "danger": "")' [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
@ -44,8 +44,8 @@
</ng-container>
<!-- Radio buttons for single choice. -->
<ion-radio-group *ngIf="!multiQuestion.multi" [(ngModel)]="multiQuestion.singleChoiceModel" [name]="multiQuestion.optionsName">
<ion-item class="ion-text-wrap answer" *ngFor="let option of multiQuestion.options">
<ion-radio-group *ngIf="!question.multi" [(ngModel)]="question.singleChoiceModel" [name]="question.optionsName">
<ion-item class="ion-text-wrap answer" *ngFor="let option of question.options">
<ion-label [class]="option.class">
<core-format-text [component]="component" [componentId]="componentId" [text]="option.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
@ -66,12 +66,12 @@
<ion-icon *ngIf="option.isCorrect === 0" class="core-correct-icon" name="fas-times" color="danger"
[attr.aria-label]="'core.question.incorrect' | translate"></ion-icon>
</ion-item>
<ion-button *ngIf="!multiQuestion.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
[disabled]="!multiQuestion.singleChoiceModel" (click)="clear()" type="button">
<ion-button *ngIf="!question.disabled" class="ion-text-wrap ion-margin-top" expand="block" fill="outline"
[disabled]="!question.singleChoiceModel" (click)="clear()" type="button">
{{ 'addon.mod_quiz.clearchoice' | translate }}
</ion-button>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->
<input type="hidden" [ngModel]="multiQuestion.singleChoiceModel" [attr.name]="multiQuestion.optionsName">
<input type="hidden" [ngModel]="question.singleChoiceModel" [attr.name]="question.optionsName">
</ion-radio-group>
</ion-list>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizMultichoiceQuestion, CoreQuestionBaseComponent } from '@fea
templateUrl: 'addon-qtype-multichoice.html',
styleUrls: ['multichoice.scss'],
})
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit {
multiQuestion?: AddonModQuizMultichoiceQuestion;
export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent<AddonModQuizMultichoiceQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeMultichoiceComponent', elementRef);
@ -35,16 +33,19 @@ export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initMultichoiceComponent();
this.multiQuestion = this.question;
}
/**
* Clear selected choices.
*/
clear(): void {
this.multiQuestion!.singleChoiceModel = undefined;
if (!this.question) {
return;
}
this.question.singleChoiceModel = undefined;
}
}

View File

@ -117,7 +117,7 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
// To know if it's single or multi answer we need to search for answers with "choice" in the name.
for (const name in newAnswers) {
if (name.indexOf('choice') != -1) {
if (name.indexOf('choice') !== -1) {
isSingle = false;
if (!CoreUtils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) {
isMultiSame = false;
@ -128,9 +128,9 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
if (isSingle) {
return this.isSameResponseSingle(prevAnswers, newAnswers);
} else {
return isMultiSame;
}
return isMultiSame;
}
/**
@ -151,10 +151,11 @@ export class AddonQtypeMultichoiceHandlerService implements CoreQuestionHandler
question: AddonModQuizMultichoiceQuestion,
answers: CoreQuestionsAnswers,
): void {
if (question && !question.multi && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
if (question && !question.multi &&
question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
/* It's a single choice and the user hasn't answered. Delete the answer because
sending an empty string (default value) will mark the first option as selected. */
delete answers[question.optionsName!];
delete answers[question.optionsName];
}
}

View File

@ -1,20 +1,19 @@
<ion-list *ngIf="textQuestion && (textQuestion.text || textQuestion.text === '')">
<ion-list *ngIf="question && (question.text || question.text === '')">
<ion-item class="ion-text-wrap addon-qtype-shortanswer-text">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="textQuestion.text" [contextLevel]="contextLevel"
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<ion-item *ngIf="textQuestion.input && !textQuestion.input.isInline"
class="ion-text-wrap addon-qtype-shortanswer-input core-{{textQuestion.input.correctIconColor}}-item">
<ion-item *ngIf="question.input && !question.input.isInline"
class="ion-text-wrap addon-qtype-shortanswer-input core-{{question.input.correctIconColor}}-item">
<ion-label position="stacked">{{ 'addon.mod_quiz.answercolon' | translate }}</ion-label>
<ion-input type="text" [placeholder]="textQuestion.input.readOnly ? '' : 'core.question.answer' | translate"
[attr.name]="textQuestion.input.name" [value]="textQuestion.input.value" autocorrect="off"
[disabled]="textQuestion.input.readOnly">
<ion-input type="text" [placeholder]="question.input.readOnly ? '' : 'core.question.answer' | translate"
[attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly">
</ion-input>
<ion-icon *ngIf="textQuestion.input.correctIcon" class="core-correct-icon" slot="end" [name]="textQuestion.input.correctIcon"
[color]="[textQuestion.input.correctIconColor]">
<ion-icon *ngIf="question.input.correctIcon" class="core-correct-icon" slot="end" [name]="question.input.correctIcon"
[color]="[question.input.correctIconColor]">
</ion-icon>
</ion-item>
</ion-list>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ElementRef } from '@angular/core';
import { Component, ElementRef } from '@angular/core';
import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
@ -24,9 +24,7 @@ import { AddonModQuizTextQuestion, CoreQuestionBaseComponent } from '@features/q
templateUrl: 'addon-qtype-shortanswer.html',
styleUrls: ['shortanswer.scss'],
})
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent implements OnInit {
textQuestion?: AddonModQuizTextQuestion;
export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent<AddonModQuizTextQuestion> {
constructor(elementRef: ElementRef) {
super('AddonQtypeShortAnswerComponent', elementRef);
@ -35,9 +33,8 @@ export class AddonQtypeShortAnswerComponent extends CoreQuestionBaseComponent im
/**
* @inheritdoc
*/
ngOnInit(): void {
init(): void {
this.initInputTextComponent();
this.textQuestion = this.question;
}
}

View File

@ -83,9 +83,9 @@ export class AddonQtypeTrueFalseHandlerService implements CoreQuestionHandler {
question: AddonModQuizMultichoiceQuestion,
answers: CoreQuestionsAnswers,
): void | Promise<void> {
if (question && answers[question.optionsName!] !== undefined && !answers[question.optionsName!]) {
if (question && question.optionsName && answers[question.optionsName] !== undefined && !answers[question.optionsName]) {
// The user hasn't answered. Delete the answer to prevent marking one of the answers automatically.
delete answers[question.optionsName!];
delete answers[question.optionsName];
}
}

View File

@ -145,7 +145,7 @@ export function conditionalRoutes(routes: Routes, condition: () => boolean): Rou
return {
...newRoute,
matcher: buildConditionalUrlMatcher(matcher || path!, condition),
matcher: buildConditionalUrlMatcher(matcher || path || '', condition),
};
});
}

View File

@ -91,7 +91,12 @@ export class CoreQueueRunner {
return;
}
const item = this.orderedQueue.shift()!;
const item = this.orderedQueue.shift();
if (!item) {
// No item found.
return;
}
this.numberRunning++;
try {

View File

@ -174,7 +174,8 @@ export class SQLiteDB {
sql = equal ? '= ?' : '<> ?';
params = Array.isArray(items) ? items : [items];
} else {
sql = (equal ? '' : 'NOT ') + 'IN (' + ',?'.repeat(items.length).substring(1) + ')';
const questionMarks = ',?'.repeat(items.length).substring(1);
sql = (equal ? '' : 'NOT ') + `IN (${questionMarks})`;
params = items;
}
@ -237,7 +238,7 @@ export class SQLiteDB {
tableCheck?: string,
): string {
const columnsSql: string[] = [];
let sql = `CREATE TABLE IF NOT EXISTS ${name} (`;
let tableStructureSQL = '';
// First define all the columns.
for (const index in columns) {
@ -245,7 +246,7 @@ export class SQLiteDB {
let columnSql: string = column.name || '';
if (column.type) {
columnSql += ' ' + column.type;
columnSql += ` ${column.type}`;
}
if (column.primaryKey) {
@ -273,25 +274,25 @@ export class SQLiteDB {
columnsSql.push(columnSql);
}
sql += columnsSql.join(', ');
tableStructureSQL += columnsSql.join(', ');
// Now add the table constraints.
if (primaryKeys && primaryKeys.length) {
sql += `, PRIMARY KEY (${primaryKeys.join(', ')})`;
tableStructureSQL += `, PRIMARY KEY (${primaryKeys.join(', ')})`;
}
if (uniqueKeys && uniqueKeys.length) {
for (const index in uniqueKeys) {
const setOfKeys = uniqueKeys[index];
if (setOfKeys && setOfKeys.length) {
sql += `, UNIQUE (${setOfKeys.join(', ')})`;
tableStructureSQL += `, UNIQUE (${setOfKeys.join(', ')})`;
}
}
}
if (tableCheck) {
sql += `, CHECK (${tableCheck})`;
tableStructureSQL += `, CHECK (${tableCheck})`;
}
for (const index in foreignKeys) {
@ -301,18 +302,18 @@ export class SQLiteDB {
continue;
}
sql += `, FOREIGN KEY (${foreignKey.columns.join(', ')}) REFERENCES ${foreignKey.table} `;
tableStructureSQL += `, FOREIGN KEY (${foreignKey.columns.join(', ')}) REFERENCES ${foreignKey.table} `;
if (foreignKey.foreignColumns && foreignKey.foreignColumns.length) {
sql += `(${foreignKey.foreignColumns.join(', ')})`;
tableStructureSQL += `(${foreignKey.foreignColumns.join(', ')})`;
}
if (foreignKey.actions) {
sql += ` ${foreignKey.actions}`;
tableStructureSQL += ` ${foreignKey.actions}`;
}
}
return sql + ')';
return `CREATE TABLE IF NOT EXISTS ${name} (${tableStructureSQL})`;
}
/**
@ -323,7 +324,7 @@ export class SQLiteDB {
async close(): Promise<void> {
await this.ready();
await this.db!.close();
await this.db?.close();
}
/**
@ -355,7 +356,7 @@ export class SQLiteDB {
countItem: string = 'COUNT(\'x\')',
): Promise<number> {
if (select) {
select = 'WHERE ' + select;
select = `WHERE ${select}`;
}
return this.countRecordsSql(`SELECT ${countItem} FROM ${table} ${select}`, params);
@ -470,7 +471,7 @@ export class SQLiteDB {
*/
async deleteRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[]): Promise<number> {
if (select) {
select = 'WHERE ' + select;
select = `WHERE ${select}`;
}
const result = await this.execute(`DELETE FROM ${table} ${select}`, params);
@ -501,7 +502,7 @@ export class SQLiteDB {
async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<any> {
await this.ready();
return this.db!.executeSql(sql, params);
return this.db?.executeSql(sql, params);
}
/**
@ -516,7 +517,7 @@ export class SQLiteDB {
async executeBatch(sqlStatements: (string | string[] | any)[]): Promise<void> {
await this.ready();
await this.db!.sqlBatch(sqlStatements);
await this.db?.sqlBatch(sqlStatements);
}
/**
@ -529,22 +530,8 @@ export class SQLiteDB {
return;
}
// Remove undefined entries and convert null to "NULL".
for (const name in data) {
if (data[name] === undefined) {
delete data[name];
}
}
}
/**
* Format the data to where params.
*
* @param data Object data.
* @returns List of params.
*/
protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] {
return Object.keys(data).map((key) => data[key]!);
// Remove undefined entries.
Object.keys(data).forEach(key => data[key] === undefined && delete data[key]);
}
/**
@ -587,7 +574,7 @@ export class SQLiteDB {
params?: SQLiteDBRecordValue[],
): Promise<SQLiteDBRecordValue> {
if (select) {
select = 'WHERE ' + select;
select = `WHERE ${select}`;
}
return this.getFieldSql(`SELECT ${field} FROM ${table} ${select}`, params);
@ -648,7 +635,7 @@ export class SQLiteDB {
fields: string = '*',
): Promise<T> {
if (select) {
select = ' WHERE ' + select;
select = ` WHERE ${select}`;
}
return this.getRecordSql<T>(`SELECT ${fields} FROM ${table} ${select}`, params);
@ -746,10 +733,10 @@ export class SQLiteDB {
limitNum: number = 0,
): Promise<T[]> {
if (select) {
select = ' WHERE ' + select;
select = ` WHERE ${select}`;
}
if (sort) {
sort = ' ORDER BY ' + sort;
sort = ` ORDER BY ${sort}`;
}
const sql = `SELECT ${fields} FROM ${table} ${select} ${sort}`;
@ -778,7 +765,7 @@ export class SQLiteDB {
if (limits[1] < 1) {
limits[1] = Number.MAX_VALUE;
}
sql += ' LIMIT ' + limits[0] + ', ' + limits[1];
sql += ` LIMIT ${limits[0]}, ${limits[1]}`;
}
const result = await this.execute(sql, params);
@ -807,7 +794,7 @@ export class SQLiteDB {
return {
sql: `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`,
params: this.formatDataToSQLParams(data),
params: Object.values(data),
};
}
@ -897,7 +884,7 @@ export class SQLiteDB {
): Promise<void> {
try {
await this.tableExists(oldTable);
} catch (error) {
} catch {
// Old table does not exist, ignore.
return;
}
@ -919,7 +906,7 @@ export class SQLiteDB {
try {
await this.dropTable(oldTable);
} catch (error) {
} catch {
// Error deleting old table, ignore.
}
}
@ -958,7 +945,7 @@ export class SQLiteDB {
async open(): Promise<void> {
await this.ready();
await this.db!.open();
await this.db?.open();
}
/**
@ -1066,7 +1053,7 @@ export class SQLiteDB {
}
// Create the list of params using the "data" object and the params for the where clause.
let params = this.formatDataToSQLParams(data);
let params = Object.values(data);
if (where && whereParams) {
params = params.concat(whereParams);
}
@ -1090,19 +1077,19 @@ export class SQLiteDB {
};
}
const where: string[] = [];
const params: SQLiteDBRecordValue[] = [];
for (const key in conditions) {
const value = conditions[key];
const where = Object.keys(conditions).map((field) => {
const value = conditions[field];
if (value === undefined || value === null) {
where.push(key + ' IS NULL');
} else {
where.push(key + ' = ?');
params.push(value);
return `${field} IS NULL`;
}
}
params.push(value);
return `${field} = ?`;
});
return {
sql: where.join(' AND '),
@ -1130,7 +1117,7 @@ export class SQLiteDB {
values.forEach((value) => {
if (value === undefined || value === null) {
sql = field + ' IS NULL';
sql = `${field} IS NULL`;
} else {
params.push(value);
}
@ -1138,14 +1125,14 @@ export class SQLiteDB {
if (params && params.length) {
if (sql !== '') {
sql = sql + ' OR ';
sql += ' OR ';
}
if (params.length == 1) {
sql = sql + field + ' = ?';
sql += `${field} = ?`;
} else {
const questionMarks = ',?'.repeat(params.length).substring(1);
sql = sql + field + ' IN (' + questionMarks + ')';
sql += ` ${field} IN (${questionMarks})`;
}
}
@ -1232,7 +1219,7 @@ export class SQLiteDB {
}
export type SQLiteDBRecordValues = {
[key: string]: SQLiteDBRecordValue | undefined | null;
[key: string]: SQLiteDBRecordValue;
};
export type SQLiteDBQueryParams = {
@ -1240,4 +1227,4 @@ export type SQLiteDBQueryParams = {
params: SQLiteDBRecordValue[];
};
export type SQLiteDBRecordValue = number | string;
export type SQLiteDBRecordValue = number | string | undefined | null;

View File

@ -24,7 +24,7 @@ describe('CoreError', () => {
// Arrange
const message = Faker.lorem.sentence();
let error: CoreError | null = null;
let error: CoreError;
// Act
try {
@ -37,10 +37,10 @@ describe('CoreError', () => {
expect(error).not.toBeNull();
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(CoreError);
expect(error!.name).toEqual('CoreError');
expect(error!.message).toEqual(message);
expect(error!.stack).not.toBeNull();
expect(error!.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
expect(error.name).toEqual('CoreError');
expect(error.message).toEqual(message);
expect(error.stack).not.toBeNull();
expect(error.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
});
it('can be subclassed', () => {
@ -55,7 +55,7 @@ describe('CoreError', () => {
const message = Faker.lorem.sentence();
let error: CustomCoreError | null = null;
let error: CustomCoreError;
// Act
try {
@ -69,10 +69,10 @@ describe('CoreError', () => {
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(CoreError);
expect(error).toBeInstanceOf(CustomCoreError);
expect(error!.name).toEqual('CustomCoreError');
expect(error!.message).toEqual(`Custom message: ${message}`);
expect(error!.stack).not.toBeNull();
expect(error!.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
expect(error.name).toEqual('CustomCoreError');
expect(error.message).toEqual(`Custom message: ${message}`);
expect(error.stack).not.toBeNull();
expect(error.stack).toContain(agnosticPath('src/core/classes/tests/error.test.ts'));
});
});

View File

@ -46,7 +46,7 @@ import { CoreUtils } from '@services/utils/utils';
})
export class CoreAttachmentsComponent implements OnInit {
@Input() files?: CoreFileEntry[]; // List of attachments. New attachments will be added to this array.
@Input() files: CoreFileEntry[] = []; // List of attachments. New attachments will be added to this array.
@Input() maxSize?: number; // Max size. -1 means unlimited, 0 means course/user max size, not defined means unknown.
@Input() maxSubmissions?: number; // Max number of attachments. -1 means unlimited, not defined means unknown limit.
@Input() component?: string; // Component the downloaded files will be linked to.
@ -177,7 +177,7 @@ export class CoreAttachmentsComponent implements OnInit {
* @param data The data received.
*/
renamed(index: number, data: { file: FileEntry }): void {
this.files![index] = data.file;
this.files[index] = data.file;
}
}

View File

@ -71,11 +71,11 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
generateLabels: (chart: Chart): ChartLegendLabelItem[] => {
const data = chart.data;
if (data.labels?.length) {
const datasets = data.datasets![0];
const datasets = data.datasets?.[0];
return data.labels.map((label, i) => ({
text: label + ': ' + datasets.data![i],
fillStyle: datasets.backgroundColor![i],
return data.labels.map<ChartLegendLabelItem>((label, i) => ({
text: label + ': ' + datasets?.data?.[i],
fillStyle: datasets?.backgroundColor?.[i],
}));
}
@ -87,14 +87,18 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
legend = Object.assign({}, this.legend);
}
if (this.type == 'bar' && this.data.length >= 5) {
if (this.type === 'bar' && this.data.length >= 5) {
this.type = 'horizontalBar';
}
// Format labels if needed.
await this.formatLabels();
const context = this.canvas!.nativeElement.getContext('2d')!;
const context = this.canvas?.nativeElement.getContext('2d');
if (!context) {
return;
}
this.chart = new Chart(context, {
type: this.type,
data: {
@ -123,7 +127,11 @@ export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
await this.formatLabels();
}
this.chart.data.datasets![0] = {
if (!this.chart.data.datasets) {
this.chart.data.datasets = [];
}
this.chart.data.datasets[0] = {
data: this.data,
backgroundColor: this.getRandomColors(this.data.length),
};

View File

@ -33,7 +33,6 @@ import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
import { CoreFileComponent } from './file/file';
import { CoreFilesComponent } from './files/files';
import { CoreIconComponent } from './icon/icon';
import { CoreIframeComponent } from './iframe/iframe';
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
import { CoreInputErrorsComponent } from './input-errors/input-errors';
@ -82,7 +81,6 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
CoreFileComponent,
CoreFilesComponent,
CoreGroupSelectorComponent,
CoreIconComponent,
CoreIframeComponent,
CoreInfiniteLoadingComponent,
CoreInputErrorsComponent,
@ -136,7 +134,6 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
CoreFileComponent,
CoreFilesComponent,
CoreGroupSelectorComponent,
CoreIconComponent,
CoreIframeComponent,
CoreInfiniteLoadingComponent,
CoreInputErrorsComponent,

View File

@ -149,24 +149,30 @@ export class CoreFileComponent implements OnInit, OnDestroy {
* @param isOpenButton Whether the open button was clicked.
* @returns Promise resolved when file is opened.
*/
openFile(ev?: Event, isOpenButton = false): Promise<void> {
async openFile(ev?: Event, isOpenButton = false): Promise<void> {
ev?.preventDefault();
ev?.stopPropagation();
if (!this.file) {
return;
}
const options: CoreUtilsOpenFileOptions = {};
if (isOpenButton) {
// Use the non-default method.
options.iOSOpenFileAction = this.defaultIsOpenWithPicker ? OpenFileAction.OPEN : OpenFileAction.OPEN_WITH;
}
return CoreFileHelper.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => {
if (event && 'calculating' in event && event.calculating) {
// The process is calculating some data required for the download, show the spinner.
this.isDownloading = true;
}
}, undefined, options).catch((error) => {
try {
return await CoreFileHelper.downloadAndOpenFile(this.file, this.component, this.componentId, this.state, (event) => {
if (event && 'calculating' in event && event.calculating) {
// The process is calculating some data required for the download, show the spinner.
this.isDownloading = true;
}
}, undefined, options);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
});
}
}
/**
@ -264,7 +270,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
}
/**
* Component destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.observer?.off();

View File

@ -30,7 +30,7 @@ import { CoreUtils } from '@services/utils/utils';
})
export class CoreFilesComponent implements OnInit, DoCheck {
@Input() files?: CoreFileEntry[]; // List of files.
@Input() files: CoreFileEntry[] = []; // List of files.
@Input() component?: string; // Component the downloaded files will be linked to.
@Input() componentId?: string | number; // Component ID.
@Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded.
@ -75,7 +75,7 @@ export class CoreFilesComponent implements OnInit, DoCheck {
* Calculate contentText based on fils that can be rendered inline.
*/
protected renderInlineFiles(): void {
this.contentText = this.files!.reduce((previous, file) => {
this.contentText = this.files.reduce((previous, file) => {
const text = CoreMimetypeUtils.getEmbeddedHtml(file);
return text ? previous + '<br>' + text : previous;

View File

@ -1,128 +0,0 @@
:host {
margin: 0;
}
:host-context([dir=rtl]).icon-flip-rtl {
transform: scaleX(-1);
}
:host-context(ion-item.md) ion-icon {
&[slot] {
color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.54);
font-size: 24px;
margin-top: 12px;
margin-bottom: 12px;
}
&[slot=start] {
margin-right: 32px;
}
&[slot=end] {
margin-left: 16px;
}
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
&[slot=start] {
margin-right: unset;
-webkit-margin-end: 32px;
margin-inline-end: 32px;
}
}
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
&[slot=end] {
margin-left: unset;
-webkit-margin-start: 16px;
margin-inline-start: 16px;
}
}
}
:host-context(ion-item.ios) ion-icon {
&[slot] {
font-size: 1.6em;
}
&[slot=start] {
margin-top: 7px;
margin-bottom: 7px;
margin-left: 0;
margin-right: 20px;
}
&[slot=end] {
margin-top: 7px;
margin-bottom: 7px;
margin-left: 10px;
margin-right: 10px;
}
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
&[slot=start] {
margin-left: unset;
margin-right: unset;
-webkit-margin-start: 0;
margin-inline-start: 0;
-webkit-margin-end: 20px;
margin-inline-end: 20px;
}
}
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
&[slot=end] {
margin-left: unset;
margin-right: unset;
-webkit-margin-start: 10px;
margin-inline-start: 10px;
-webkit-margin-end: 10px;
margin-inline-end: 10px;
}
}
}
:host-context(ion-item.ion-color) {
color: var(--ion-color-contrast);
}
:host-context(ion-button.md) ion-icon,
:host-context(ion-button.ios) ion-icon {
&[slot] {
font-size: 1.4em;
pointer-events: none;
}
&[slot=start] {
margin-left: -0.3em;
margin-right: 0.3em;
margin-top: 0;
margin-bottom: 0;
}
&[slot=end] {
margin-left: 0.3em;
margin-right: -0.2em;
margin-top: 0;
margin-bottom: 0;
}
&[slot=icon-only] {
font-size: 1.8em;
}
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
&[slot=start] {
margin-left: unset;
margin-right: unset;
-webkit-margin-start: -0.3em;
margin-inline-start: -0.3em;
-webkit-margin-end: 0.3em;
margin-inline-end: 0.3em;
}
}
@supports (margin-inline-start: 0) or (-webkit-margin-start: 0) {
&[slot=end] {
margin-left: unset;
margin-right: unset;
-webkit-margin-start: 0.3em;
margin-inline-start: 0.3em;
-webkit-margin-end: -0.2em;
margin-inline-end: -0.2em;
}
}
}

View File

@ -1,116 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, OnChanges, ElementRef, SimpleChange } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
/**
* Core Icon is a component that enables the posibility to add fontawesome icon to the html. It
* To use fontawesome just place the full icon name with the fa- prefix and
* the component will detect it.
*
* Check available icons at https://fontawesome.com/icons?d=gallery&m=free
*
* @deprecated since 3.9.3. Please use <ion-icon name="fas-icon"> instead.
*/
@Component({
selector: 'core-icon',
template: '<ion-icon [name]="name"><ng-content></ng-content></ion-icon>',
styleUrls: ['icon.scss'],
})
export class CoreIconComponent implements OnChanges {
// Common params.
@Input() name = '';
@Input() color?: string;
@Input() slash?: boolean; // Display a red slash over the icon.
// FontAwesome params.
@Input('fixed-width') fixedWidth?: boolean; // eslint-disable-line @angular-eslint/no-input-rename
@Input() label?: string;
@Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false.
protected element: HTMLElement;
constructor(
el: ElementRef,
) {
this.element = el.nativeElement;
CoreLogger.getInstance('CoreIconComponent').error('CoreIconComponent is deprecated. Please use ion-icon instead.');
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
if (!changes.name || !this.name) {
return;
}
setTimeout(() => {
this.updateIcon(this.element.children[0]);
});
}
protected updateIcon(iconElement: Element): void {
!this.label && iconElement.setAttribute('aria-hidden', 'true');
!this.label && iconElement.setAttribute('role', 'presentation');
this.label && iconElement.setAttribute('aria-label', this.label);
this.label && iconElement.setAttribute('title', this.label);
const attrs = this.element.attributes;
for (let i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name != 'name') {
iconElement.setAttribute(attrs[i].name, attrs[i].value);
}
}
if (this.isTrueProperty(this.slash)) {
iconElement.classList.add('icon-slash');
} else {
iconElement.classList.remove('icon-slash');
}
if (this.isTrueProperty(this.flipRtl)) {
iconElement.classList.add('icon-flip-rtl');
} else {
iconElement.classList.remove('icon-flip-rtl');
}
if (this.isTrueProperty(this.fixedWidth)) {
iconElement.classList.add('fa-fw');
} else {
iconElement.classList.remove('fa-fw');
}
}
/**
* Check if the value is true or on.
*
* @param val Value to be checked.
* @returns If has a value equivalent to true.
*/
isTrueProperty(val: unknown): boolean {
if (typeof val === 'string') {
val = val.toLowerCase().trim();
return (val === 'true' || val === 'on' || val === '');
}
return !!val;
}
}

View File

@ -28,10 +28,10 @@ import { CorePath } from '@singletons/path';
})
export class CoreRecaptchaComponent implements OnInit {
@Input() model?: Record<string, string>; // The model where to store the recaptcha response.
@Input() model: Record<string, string> = {}; // The model where to store the recaptcha response.
@Input() publicKey?: string; // The site public key.
@Input() modelValueName = 'recaptcharesponse'; // Name of the model property where to store the response.
@Input() siteUrl?: string; // The site URL. If not defined, current site.
@Input() siteUrl = ''; // The site URL. If not defined, current site.
expired = false;
@ -45,7 +45,7 @@ export class CoreRecaptchaComponent implements OnInit {
* @inheritdoc
*/
ngOnInit(): void {
this.siteUrl = this.siteUrl || CoreSites.getCurrentSite()?.getURL();
this.siteUrl = this.siteUrl || CoreSites.getRequiredCurrentSite().getURL();
}
/**
@ -62,7 +62,7 @@ export class CoreRecaptchaComponent implements OnInit {
// Open the recaptcha challenge in an InAppBrowser.
// The app used to use an iframe for this, but the app can no longer access the iframe to create the required callbacks.
// The app cannot render the recaptcha directly because it has problems with the local protocols and domains.
const src = CorePath.concatenatePaths(this.siteUrl!, 'webservice/recaptcha.php?lang=' + this.lang);
const src = CorePath.concatenatePaths(this.siteUrl, 'webservice/recaptcha.php?lang=' + this.lang);
const inAppBrowserWindow = CoreUtils.openInApp(src);
if (!inAppBrowserWindow) {
@ -90,7 +90,7 @@ export class CoreRecaptchaComponent implements OnInit {
this.expireRecaptchaAnswer();
} else if (event.data.action == 'callback') {
this.expired = false;
this.model![this.modelValueName] = event.data.value;
this.model[this.modelValueName] = event.data.value;
// Close the InAppBrowser now.
inAppBrowserWindow.close();
@ -105,7 +105,7 @@ export class CoreRecaptchaComponent implements OnInit {
*/
expireRecaptchaAnswer(): void {
this.expired = true;
this.model![this.modelValueName] = '';
this.model[this.modelValueName] = '';
}
}

View File

@ -1,36 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreIconComponent } from '@components/icon/icon';
import { renderWrapperComponent } from '@/testing/utils';
describe('CoreIconComponent', () => {
it('should render', async () => {
// Act
const fixture = await renderWrapperComponent(CoreIconComponent, 'core-icon', { name: 'fa-thumbs-up' });
// Assert
expect(fixture.nativeElement.innerHTML.trim()).not.toHaveLength(0);
const icon = fixture.nativeElement.querySelector('ion-icon');
const name = icon?.getAttribute('name') || icon?.getAttribute('ng-reflect-name') || '';
expect(icon).not.toBeNull();
expect(name).toEqual('fa-thumbs-up');
expect(icon?.getAttribute('role')).toEqual('presentation');
});
});

View File

@ -54,7 +54,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
let iconName = this.name;
let font = 'ionicons';
const parts = iconName.split('-', 2);
if (parts.length == 2) {
if (parts.length === 2) {
switch (parts[0]) {
case 'far':
library = 'regular';
@ -82,7 +82,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
}
}
if (font == 'ionicons') {
if (font === 'ionicons') {
this.element.removeAttribute('src');
this.logger.warn(`Ionic icon ${this.name} detected`);
@ -103,7 +103,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
ngAfterViewInit(): void {
if (!this.element.getAttribute('aria-label') &&
!this.element.getAttribute('aria-labelledby') &&
this.element.getAttribute('aria-hidden') != 'true') {
this.element.getAttribute('aria-hidden') !== 'true') {
this.logger.warn('Aria label not set on icon ' + this.name, this.element);
this.element.setAttribute('aria-hidden', 'true');
@ -111,7 +111,7 @@ export class CoreFaIconDirective implements AfterViewInit, OnChanges {
}
/**
* Detect changes on input properties.
* @inheritdoc
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (!changes.name || !this.name) {

View File

@ -36,19 +36,23 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implemen
async ngOnInit(): Promise<void> {
await super.ngOnInit();
this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.';
this.fetchContentDefaultError = `Error getting ${this.block.contents?.title} data.`;
}
/**
* Go to the block page.
*/
gotoBlock(): void {
if (!this.link) {
return;
}
const navOptions = this.navOptions || {};
if (this.linkParams) {
navOptions.params = this.linkParams;
}
CoreNavigator.navigateToSitePath(this.link!, navOptions);
CoreNavigator.navigateToSitePath(this.link, navOptions);
}
}

View File

@ -119,7 +119,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
}
/**
* View loaded.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
try {
@ -445,9 +445,13 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
* @returns Promise resolved with modified comment when done.
*/
protected async loadCommentProfile(comment: CoreCommentsDataToDisplay): Promise<CoreCommentsDataToDisplay> {
// Get the user profile image.
if (!comment.userid) {
return comment;
}
try {
const user = await CoreUser.getProfile(comment.userid!, undefined, true);
// Get the user profile image.
const user = await CoreUser.getProfile(comment.userid, undefined, true);
comment.profileimageurl = user.profileimageurl;
comment.fullname = user.fullname;
} catch {
@ -599,7 +603,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
}
/**
* Page destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.syncObserver?.off();

View File

@ -23,6 +23,7 @@ import { CoreNetwork } from '@services/network';
import { CoreUtils } from '@services/utils/utils';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCommentsDBRecord, CoreCommentsDeletedDBRecord } from './database/comments';
import { CoreSyncResult } from '@services/sync';
/**
* Service to sync omments.
@ -318,10 +319,7 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider<CoreCommentsS
}
export const CoreCommentsSync = makeSingleton(CoreCommentsSyncProvider);
export type CoreCommentsSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};
export type CoreCommentsSyncResult = CoreSyncResult;
/**
* Data passed to AUTO_SYNCED event.

View File

@ -152,7 +152,7 @@ export class CoreCommentsProvider {
this.invalidateCommentsData(contextLevel, instanceId, component, itemId, area, siteId),
);
return commentsResponse![0];
return commentsResponse[0];
}
/**
@ -166,9 +166,9 @@ export class CoreCommentsProvider {
async addCommentsOnline(
comments: CoreCommentsCommentBasicData[],
siteId?: string,
): Promise<CoreCommentsAddCommentsWSResponse | undefined> {
): Promise<CoreCommentsAddCommentsWSResponse> {
if (!comments || !comments.length) {
return;
return [];
}
const site = await CoreSites.getSite(siteId);
@ -231,8 +231,12 @@ export class CoreCommentsProvider {
// Convenience function to store the action to be synchronized later.
const storeOffline = async (): Promise<boolean> => {
if (!comment.id) {
return false;
}
await CoreCommentsOffline.deleteComment(
comment.id!,
comment.id,
comment.contextlevel,
comment.instanceid,
comment.component,

View File

@ -67,7 +67,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB
courseId?: number,
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
courseId = Number(courseId || params.courseid || params.cid);
const courseIdentifier = Number(courseId || params.courseid || params.cid);
return [{
action: async (siteId): Promise<void> => {
@ -79,14 +79,14 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB
CoreCourseHelper.navigateToModule(
Number(params.id),
{
courseId,
courseId: courseIdentifier,
modName: this.useModNameToGetModule ? this.modName : undefined,
siteId,
},
);
} else if (this.canReview) {
// Use the goToReview function.
this.goToReview(url, params, courseId!, siteId);
this.goToReview(url, params, courseIdentifier, siteId);
} else {
// Not current user and cannot review it in the app, open it in browser.
site.openInBrowserWithAutoLogin(url);

View File

@ -20,9 +20,9 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourse } from '../services/course';
import { CoreUtils } from '@services/utils/utils';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreWSExternalWarning } from '@services/ws';
import { CoreCourseContentsPage } from '../pages/contents/contents';
import { CoreSites } from '@services/sites';
import { CoreSyncResult } from '@services/sync';
/**
* Template class to easily create CoreCourseModuleMainComponent of activities.
@ -188,32 +188,34 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
*
* @returns Promise resolved when done.
*/
protected async sync(): Promise<unknown> {
return {};
protected async sync(): Promise<CoreSyncResult> {
return {
updated: false,
warnings: [],
};
}
/**
* Checks if sync has succeed from result sync data.
* Checks if sync has updated data on the server.
*
* @param result Data returned on the sync function.
* @returns If suceed or not.
* @returns If data has been updated or not.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected hasSyncSucceed(result: unknown): boolean {
return true;
protected hasSyncSucceed(result: CoreSyncResult): boolean {
return result.updated;
}
/**
* Tries to synchronize the activity.
*
* @param showErrors If show errors to the user of hide them.
* @returns Promise resolved with true if sync succeed, or false if failed.
* @returns Promise resolved with true if sync hast updated data to the server, false otherwise.
*/
protected async syncActivity(showErrors: boolean = false): Promise<boolean> {
try {
const result = <{warnings?: CoreWSExternalWarning[]}> await this.sync();
const result = await this.sync();
if (result?.warnings?.length) {
if (result.warnings.length) {
CoreDomUtils.showErrorModal(result.warnings[0]);
}

View File

@ -76,6 +76,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
@Input() initialSectionNumber?: number; // The section to load first (by number).
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent<any>>;
// All the possible component classes.

View File

@ -58,16 +58,19 @@ export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
ev.stopPropagation();
const modal = await CoreDomUtils.showModalLoading();
const courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId;
const courseId = this.courseId ? Number(this.courseId) : undefined;
try {
if (!this.module) {
// Try to get the module from cache.
this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId;
this.module = await CoreCourse.getModule(this.moduleId!, courseId);
const moduleId = Number(this.moduleId);
if (!moduleId) {
return;
}
this.module = await CoreCourse.getModule(moduleId, courseId);
}
const componentId = this.componentId || module.id;
const componentId = this.componentId ? Number(this.componentId) : this.module.id;
await CoreCourseHelper.downloadModuleAndOpenFile(
this.module,

View File

@ -448,7 +448,11 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
: (handler as CoreCourseOptionsHandler).getDisplayData;
promises.push(Promise.resolve(getFunction!.call(handler, courseWithOptions)).then((data) => {
if (!getFunction) {
return;
}
promises.push(Promise.resolve(getFunction.call(handler, courseWithOptions)).then((data) => {
handlersToDisplay.push({
data: data,
priority: handler.priority || 0,
@ -468,7 +472,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
handlersToDisplay.sort((
a: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
b: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay,
) => b.priority! - a.priority!);
) => (b.priority || 0) - (a.priority || 0));
return handlersToDisplay;
}

View File

@ -1274,9 +1274,9 @@ export class CoreCourseProvider {
if (!result.status) {
if (result.warnings && result.warnings.length) {
throw new CoreWSError(result.warnings[0]);
} else {
throw new CoreError('Cannot change completion.');
}
throw new CoreError('Cannot change completion.');
}
return result;

View File

@ -141,8 +141,12 @@ ion-card.core-course-list-item {
// Card layout.
ion-card.core-course-list-card {
display: flex;
flex-direction: column;
&::part(native) {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
}
height: calc(100% - var(--card-vertical-margin) - var(--card-vertical-margin));
margin-top: var(--card-vertical-margin);

View File

@ -119,24 +119,20 @@ export class CoreEmulatorCaptureHelperProvider {
if (mimetypes?.length) {
// Search for a supported mimetype.
for (let i = 0; i < mimetypes.length; i++) {
const mimetype = mimetypes[i];
result.mimetype = mimetypes.find((mimetype) => {
const matches = mimetype.match(new RegExp('^' + type + '/'));
if (matches?.length && window.MediaRecorder.isTypeSupported(mimetype)) {
result.mimetype = mimetype;
break;
}
}
return matches?.length && window.MediaRecorder.isTypeSupported(mimetype);
});
}
if (result.mimetype) {
// Found a supported mimetype in the mimetypes array, get the extension.
result.extension = CoreMimetypeUtils.getExtension(result.mimetype);
} else if (type == 'video') {
} else if (type === 'video' && this.videoMimeType) {
// No mimetype found, use default extension.
result.mimetype = this.videoMimeType;
result.extension = this.possibleVideoMimeTypes[result.mimetype!];
result.extension = this.possibleVideoMimeTypes[result.mimetype];
}
return result;

View File

@ -76,7 +76,9 @@ export class FileTransferObjectMock extends FileTransferObject {
abort(): void {
if (this.xhr) {
this.xhr.abort();
this.reject!(new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source!, this.target!, 0, '', ''));
this.reject?.(
new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source || '', this.target || '', 0, '', ''),
);
}
}

View File

@ -189,7 +189,7 @@ export class CoreFileUploaderDelegateService extends CoreDelegate<CoreFileUpload
}
// Sort them by priority.
handlers.sort((a, b) => a.priority! <= b.priority! ? 1 : -1);
handlers.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
return handlers;
}

View File

@ -806,8 +806,8 @@ export class CoreFileUploaderHelperProvider {
}
}
if (maxSize != -1 && size > maxSize) {
throw this.createMaxBytesError(maxSize, file!.name);
if (maxSize != -1 && size > maxSize && file) {
throw this.createMaxBytesError(maxSize, file.name);
}
if (size > 0) {
@ -849,12 +849,12 @@ export class CoreFileUploaderHelperProvider {
stringKey: string,
progress: ProgressEvent | CoreFileProgressEvent,
): void {
if (!progress || !progress.lengthComputable) {
if (!progress || !progress.lengthComputable || progress.loaded === undefined || !progress.total) {
return;
}
// Calculate the progress percentage.
const perc = Math.min((progress.loaded! / progress.total!) * 100, 100);
const perc = Math.min((progress.loaded / progress.total) * 100, 100);
if (isNaN(perc) || perc < 0) {
return;

View File

@ -286,8 +286,8 @@ export class CoreFileUploaderProvider {
if (!stillInList) {
filesToDelete.push({
filepath: file.filepath!,
filename: file.filename!,
filepath: file.filepath || '',
filename: file.filename || '',
});
}
});
@ -643,7 +643,7 @@ export class CoreFileUploaderProvider {
filesToUpload.push(<FileEntry> file);
} else {
// It's an online file.
usedNames[file.filename!.toLowerCase()] = file;
usedNames[(file.filename || '').toLowerCase()] = file;
}
});
@ -681,7 +681,7 @@ export class CoreFileUploaderProvider {
): Promise<number> {
siteId = siteId || CoreSites.getCurrentSiteId();
let fileName: string | undefined;
let fileName = '';
let fileEntry: FileEntry | undefined;
const isOnline = !CoreUtils.isFileEntry(file);
@ -692,7 +692,7 @@ export class CoreFileUploaderProvider {
fileEntry = file;
} else {
// It's an online file. We need to download it and re-upload it.
fileName = file.filename;
fileName = file.filename || '';
const path = await CoreFilepool.downloadUrl(
siteId,
@ -710,9 +710,9 @@ export class CoreFileUploaderProvider {
}
// Now upload the file.
const extension = CoreMimetypeUtils.getFileExtension(fileName!);
const extension = CoreMimetypeUtils.getFileExtension(fileName);
const mimetype = extension ? CoreMimetypeUtils.getMimeType(extension) : undefined;
const options = this.getFileUploadOptions(fileEntry.toURL(), fileName!, mimetype, isOnline, 'draft', itemId);
const options = this.getFileUploadOptions(fileEntry.toURL(), fileName, mimetype, isOnline, 'draft', itemId);
const result = await this.uploadFile(fileEntry.toURL(), options, undefined, siteId);

View File

@ -39,7 +39,7 @@ export class CoreGradesReportLinkHandlerService extends CoreContentLinksHandlerB
courseId?: number,
data?: { cmid?: string },
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
courseId = courseId || Number(params.id);
const courseIdentifier = courseId || Number(params.id);
data = data || {};
return [{
@ -47,7 +47,7 @@ export class CoreGradesReportLinkHandlerService extends CoreContentLinksHandlerB
const userId = params.userid ? parseInt(params.userid, 10) : undefined;
const moduleId = data?.cmid && parseInt(data.cmid, 10) || undefined;
CoreGradesHelper.goToGrades(courseId!, userId, moduleId, siteId);
CoreGradesHelper.goToGrades(courseIdentifier, userId, moduleId, siteId);
},
}];
}

View File

@ -39,7 +39,7 @@ export class CoreGradesUserLinkHandlerService extends CoreContentLinksHandlerBas
courseId?: number,
data?: { cmid?: string },
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
courseId = courseId || Number(params.id);
const courseIdentifier = courseId || Number(params.id);
data = data || {};
return [{
@ -47,7 +47,7 @@ export class CoreGradesUserLinkHandlerService extends CoreContentLinksHandlerBas
const userId = params.user ? parseInt(params.user, 10) : undefined;
const moduleId = data?.cmid && parseInt(data.cmid, 10) || undefined;
CoreGradesHelper.goToGrades(courseId!, userId, moduleId, siteId);
CoreGradesHelper.goToGrades(courseIdentifier, userId, moduleId, siteId);
},
}];
}

View File

@ -166,16 +166,16 @@ export class CoreH5PPlayer {
* @returns Promise resolved when done.
*/
async deleteAllContentIndexesForSite(siteId?: string): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
const siteIdentifier = siteId || CoreSites.getCurrentSiteId();
if (!siteId) {
if (!siteIdentifier) {
return;
}
const records = await this.h5pCore.h5pFramework.getAllContentData(siteId);
const records = await this.h5pCore.h5pFramework.getAllContentData(siteIdentifier);
await Promise.all(records.map(async (record) => {
await CoreUtils.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId!));
await CoreUtils.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteIdentifier));
}));
}

View File

@ -217,12 +217,13 @@ export class CoreLoginEmailSignupPage implements OnInit {
this.countryControl.setValue(this.settings.country || '');
}
this.namefieldsErrors = {};
const namefieldsErrors = {};
if (this.settings.namefields) {
this.settings.namefields.forEach((field) => {
this.namefieldsErrors![field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
namefieldsErrors[field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
});
}
this.namefieldsErrors = namefieldsErrors;
this.countries = await CoreUtils.getCountryListSorted();
}

Some files were not shown because too many files have changed in this diff Show More