diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts
new file mode 100644
index 000000000..4350c88e3
--- /dev/null
+++ b/src/core/features/course/components/components.module.ts
@@ -0,0 +1,42 @@
+// (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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CoreSharedModule } from '@/core/shared.module';
+import { CoreBlockComponentsModule } from '@features/block/components/components.module';
+import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
+import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
+
+@NgModule({
+ declarations: [
+ CoreCourseModuleDescriptionComponent,
+ CoreCourseUnsupportedModuleComponent,
+ ],
+ imports: [
+ CoreBlockComponentsModule,
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreSharedModule,
+ ],
+ exports: [
+ CoreCourseModuleDescriptionComponent,
+ CoreCourseUnsupportedModuleComponent,
+ ],
+})
+export class CoreCourseComponentsModule {}
diff --git a/src/core/features/course/components/module-description/core-course-module-description.html b/src/core/features/course/components/module-description/core-course-module-description.html
new file mode 100644
index 000000000..bd8df6e14
--- /dev/null
+++ b/src/core/features/course/components/module-description/core-course-module-description.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ {{ note }}
+
+
\ No newline at end of file
diff --git a/src/core/features/course/components/module-description/course-module-description.scss b/src/core/features/course/components/module-description/course-module-description.scss
new file mode 100644
index 000000000..ca2d02d73
--- /dev/null
+++ b/src/core/features/course/components/module-description/course-module-description.scss
@@ -0,0 +1,16 @@
+// ion-app.app-root {
+// .safe-area-page,
+// .safe-padding-horizontal {
+// core-course-module-description {
+// padding-left: 0 !important;
+// padding-right: 0 !important;
+// .item-ios.item-block {
+// @include safe-area-padding-horizontal($item-ios-padding-end / 2, null);
+
+// .item-inner {
+// @include safe-area-padding-horizontal(null, $item-ios-padding-end / 2);
+// }
+// }
+// }
+// }
+// }
\ No newline at end of file
diff --git a/src/core/features/course/components/module-description/module-description.ts b/src/core/features/course/components/module-description/module-description.ts
new file mode 100644
index 000000000..d5fcf1139
--- /dev/null
+++ b/src/core/features/course/components/module-description/module-description.ts
@@ -0,0 +1,48 @@
+// (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 } from '@angular/core';
+
+/**
+ * Component to display the description of a module.
+ *
+ * This directive is meant to display a module description in a similar way throughout all the app.
+ *
+ * You can add a note at the right side of the description by using the 'note' attribute.
+ *
+ * You can also pass a component and componentId to be used in format-text.
+ *
+ * Module descriptions are shortened by default, allowing the user to see the full description by clicking in it.
+ * If you want the whole description to be shown you can use the 'showFull' attribute.
+ *
+ * Example usage:
+ *
+ *
+
+
+
+
{{ 'core.whoops' | translate }}
+ {{ 'core.uhoh' | translate }}
+
+ {{ 'core.course.activitydisabled' | translate }}
+
+ {{ 'core.course.activitynotyetviewablesiteupgradeneeded' | translate }}
+
+
+ {{ 'core.course.activitynotyetviewableremoteaddon' | translate }}
+
+ {{ 'core.course.askadmintosupport' | translate }}
+
+
+
{{ 'core.course.useactivityonbrowser' | translate }}
+
+ {{ 'core.openinbrowser' | translate }}
+
+
+
+
\ No newline at end of file
diff --git a/src/core/features/course/components/unsupported-module/unsupported-module.ts b/src/core/features/course/components/unsupported-module/unsupported-module.ts
new file mode 100644
index 000000000..46f2136cb
--- /dev/null
+++ b/src/core/features/course/components/unsupported-module/unsupported-module.ts
@@ -0,0 +1,49 @@
+// (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, OnInit } from '@angular/core';
+
+import { CoreCourse, CoreCourseModuleData } from '@features/course/services/course';
+import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
+
+/**
+ * Component that displays info about an unsupported module.
+ */
+@Component({
+ selector: 'core-course-unsupported-module',
+ templateUrl: 'core-course-unsupported-module.html',
+})
+export class CoreCourseUnsupportedModuleComponent implements OnInit {
+
+ @Input() courseId?: number; // The course to module belongs to.
+ @Input() module?: CoreCourseModuleData; // The module to render.
+
+ isDisabledInSite?: boolean;
+ isSupportedByTheApp?: boolean;
+ moduleName?: string;
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ if (!this.module) {
+ return;
+ }
+
+ this.isDisabledInSite = CoreCourseModuleDelegate.instance.isModuleDisabledInSite(this.module.modname);
+ this.isSupportedByTheApp = CoreCourseModuleDelegate.instance.hasHandler(this.module.modname);
+ this.moduleName = CoreCourse.instance.translateModuleName(this.module.modname);
+ }
+
+}
diff --git a/src/core/features/course/course.module.ts b/src/core/features/course/course.module.ts
index a064ab67e..6960585bc 100644
--- a/src/core/features/course/course.module.ts
+++ b/src/core/features/course/course.module.ts
@@ -15,11 +15,16 @@
import { NgModule } from '@angular/core';
import { CORE_SITE_SCHEMAS } from '@services/sites';
-
+import { CoreCourseComponentsModule } from './components/components.module';
+import { CoreCourseFormatModule } from './format/formats.module';
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course';
import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log';
@NgModule({
+ imports: [
+ CoreCourseFormatModule,
+ CoreCourseComponentsModule,
+ ],
providers: [
{
provide: CORE_SITE_SCHEMAS,
diff --git a/src/core/features/course/format/formats.module.ts b/src/core/features/course/format/formats.module.ts
new file mode 100644
index 000000000..7da1f733a
--- /dev/null
+++ b/src/core/features/course/format/formats.module.ts
@@ -0,0 +1,33 @@
+// (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 { NgModule } from '@angular/core';
+
+import { CoreCourseFormatSingleActivityModule } from './singleactivity/singleactivity.module';
+import { CoreCourseFormatSocialModule } from './social/social.module';
+import { CoreCourseFormatTopicsModule } from './topics/topics.module';
+import { CoreCourseFormatWeeksModule } from './weeks/weeks.module';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CoreCourseFormatSingleActivityModule,
+ CoreCourseFormatSocialModule,
+ CoreCourseFormatTopicsModule,
+ CoreCourseFormatWeeksModule,
+ ],
+ providers: [],
+ exports: [],
+})
+export class CoreCourseFormatModule { }
diff --git a/src/core/features/course/format/singleactivity/components/core-course-format-single-activity.html b/src/core/features/course/format/singleactivity/components/core-course-format-single-activity.html
new file mode 100644
index 000000000..1f3a37007
--- /dev/null
+++ b/src/core/features/course/format/singleactivity/components/core-course-format-single-activity.html
@@ -0,0 +1 @@
+
diff --git a/src/core/features/course/format/singleactivity/components/singleactivity.ts b/src/core/features/course/format/singleactivity/components/singleactivity.ts
new file mode 100644
index 000000000..379cff7ef
--- /dev/null
+++ b/src/core/features/course/format/singleactivity/components/singleactivity.ts
@@ -0,0 +1,104 @@
+// (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, SimpleChange, ViewChild, Output, EventEmitter, Type } from '@angular/core';
+
+import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
+import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module';
+import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
+import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
+import { CoreCourseSection } from '@features/course/services/course';
+import { IonRefresher } from '@ionic/angular';
+
+/**
+ * Component to display single activity format. It will determine the right component to use and instantiate it.
+ *
+ * The instantiated component will receive the course and the module as inputs.
+ */
+@Component({
+ selector: 'core-course-format-single-activity',
+ templateUrl: 'core-course-format-single-activity.html',
+})
+export class CoreCourseFormatSingleActivityComponent implements OnChanges {
+
+ @Input() course?: CoreCourseAnyCourseData; // The course to render.
+ @Input() sections?: CoreCourseSection[]; // List of course sections.
+ @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
+ @Input() initialSectionId?: number; // The section to load first (by ID).
+ @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.
+ @Output() completionChanged?: EventEmitter; // Will emit an event when any module completion changes.
+
+ @ViewChild(CoreDynamicComponent) dynamicComponent?: CoreDynamicComponent;
+
+ componentClass?: Type; // The class of the component to render.
+ data: Record = {}; // Data to pass to the component.
+
+ /**
+ * Detect changes on input properties.
+ */
+ async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise {
+ if (!changes.course || !changes.sections) {
+ return;
+ }
+
+ if (!this.course || !this.sections || !this.sections.length) {
+ return;
+ }
+
+ // In single activity the module should only have 1 section and 1 module. Get the module.
+ const module = this.sections?.[0].modules?.[0];
+
+ this.data.courseId = this.course.id;
+ this.data.module = module;
+
+ if (module && !this.componentClass) {
+ // We haven't obtained the class yet. Get it now.
+ const component = await CoreCourseModuleDelegate.instance.getMainComponent(this.course, module);
+ this.componentClass = component || CoreCourseUnsupportedModuleComponent;
+ }
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param refresher Refresher.
+ * @param done Function to call when done.
+ * @param afterCompletionChange Whether the refresh is due to a completion change.
+ * @return Promise resolved when done.
+ */
+ async doRefresh(refresher?: CustomEvent, done?: () => void, afterCompletionChange?: boolean): Promise {
+ if (afterCompletionChange) {
+ // Don't refresh the view after a completion change since completion isn't displayed.
+ return;
+ }
+
+ await this.dynamicComponent?.callComponentFunction('doRefresh', [refresher, done]);
+ }
+
+ /**
+ * User entered the page that contains the component.
+ */
+ ionViewDidEnter(): void {
+ this.dynamicComponent?.callComponentFunction('ionViewDidEnter');
+ }
+
+ /**
+ * User left the page that contains the component.
+ */
+ ionViewDidLeave(): void {
+ this.dynamicComponent?.callComponentFunction('ionViewDidLeave');
+ }
+
+}
diff --git a/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts
new file mode 100644
index 000000000..203d4910f
--- /dev/null
+++ b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts
@@ -0,0 +1,153 @@
+// (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 { Injectable, Type } from '@angular/core';
+
+import { CoreCourseSection } from '@features/course/services/course';
+import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
+import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
+import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
+import { CoreCourseFormatSingleActivityComponent } from '../../components/singleactivity';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Handler to support singleactivity course format.
+ */
+@Injectable({ providedIn: 'root' })
+export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseFormatHandler {
+
+ name = 'CoreCourseFormatSingleActivity';
+ format = 'singleactivity';
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return True or promise resolved with true if enabled.
+ */
+ async isEnabled(): Promise {
+ return true;
+ }
+
+ /**
+ * Whether it allows seeing all sections at the same time. Defaults to true.
+ *
+ * @param course The course to check.
+ * @return Whether it can view all sections.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ canViewAllSections(course: CoreCourseAnyCourseData): boolean {
+ return false;
+ }
+
+ /**
+ * Whether the option blocks should be displayed. Defaults to true.
+ *
+ * @param course The course to check.
+ * @return Whether it can display blocks.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ displayBlocks(course: CoreCourseAnyCourseData): boolean {
+ return false;
+ }
+
+ /**
+ * Get the title to use in course page. If not defined, course displayname or fullname.
+ * This function will be called without sections first, and then call it again when the sections are retrieved.
+ *
+ * @param course The course.
+ * @param sections List of sections.
+ * @return Title.
+ */
+ getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseSection[]): string {
+ if (sections?.[0]?.modules?.[0]) {
+ return sections[0].modules[0].name;
+ }
+
+ if (course.displayname) {
+ return course.displayname;
+ } else if (course.fullname) {
+ return course.fullname;
+ }
+
+ return '';
+ }
+
+ /**
+ * Whether the option to enable section/module download should be displayed. Defaults to true.
+ *
+ * @param course The course to check.
+ * @return Whether the option to enable section/module download should be displayed
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ displayEnableDownload(course: CoreCourseAnyCourseData): boolean {
+ return false;
+ }
+
+ /**
+ * Whether the default section selector should be displayed. Defaults to true.
+ *
+ * @param course The course to check.
+ * @return Whether the default section selector should be displayed.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ displaySectionSelector(course: CoreCourseAnyCourseData): boolean {
+ return false;
+ }
+
+ /**
+ * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
+ * and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true.
+ *
+ * @param course The course to check.
+ * @param sections List of course sections.
+ * @return Whether the refresher should be displayed.
+ */
+ displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): boolean {
+ if (sections?.[0]?.modules?.[0]) {
+ return CoreCourseModuleDelegate.instance.displayRefresherInSingleActivity(sections[0].modules[0].modname);
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Return the Component to use to display the course format instead of using the default one.
+ * Use it if you want to display a format completely different from the default one.
+ * If you want to customize the default format there are several methods to customize parts of it.
+ * It's recommended to return the class of the component, but you can also return an instance of the component.
+ *
+ * @param injector Injector.
+ * @param course The course to render.
+ * @return The component (or promise resolved with component) to use, undefined if not found.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async getCourseFormatComponent(course: CoreCourseAnyCourseData): Promise> {
+ return CoreCourseFormatSingleActivityComponent;
+ }
+
+ /**
+ * Whether the view should be refreshed when completion changes. If your course format doesn't display
+ * activity completion then you should return false.
+ *
+ * @param course The course.
+ * @return Whether course view should be refreshed when an activity completion changes.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise {
+ return false;
+ }
+
+}
+
+export class CoreCourseFormatSingleActivityHandler extends makeSingleton(CoreCourseFormatSingleActivityHandlerService) {}
diff --git a/src/core/features/course/format/singleactivity/singleactivity.module.ts b/src/core/features/course/format/singleactivity/singleactivity.module.ts
new file mode 100644
index 000000000..be619a5ab
--- /dev/null
+++ b/src/core/features/course/format/singleactivity/singleactivity.module.ts
@@ -0,0 +1,43 @@
+// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
+
+import { CoreSharedModule } from '@/core/shared.module';
+import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
+import { CoreCourseFormatSingleActivityComponent } from './components/singleactivity';
+import { CoreCourseFormatSingleActivityHandler } from './services/handlers/singleactivity-format';
+
+@NgModule({
+ declarations: [
+ CoreCourseFormatSingleActivityComponent,
+ ],
+ imports: [
+ CoreSharedModule,
+ ],
+ providers: [
+ {
+ provide: APP_INITIALIZER,
+ multi: true,
+ deps: [],
+ useFactory: () => () => {
+ CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatSingleActivityHandler.instance);
+ },
+ },
+ ],
+ exports: [
+ CoreCourseFormatSingleActivityComponent,
+ ],
+})
+export class CoreCourseFormatSingleActivityModule {}
diff --git a/src/core/features/course/format/social/services/handlers/social-format.ts b/src/core/features/course/format/social/services/handlers/social-format.ts
new file mode 100644
index 000000000..dde6c8711
--- /dev/null
+++ b/src/core/features/course/format/social/services/handlers/social-format.ts
@@ -0,0 +1,32 @@
+// (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 { Injectable } from '@angular/core';
+import { makeSingleton } from '@singletons';
+
+import { CoreCourseFormatSingleActivityHandlerService } from '../../../singleactivity/services/handlers/singleactivity-format';
+
+/**
+ * Handler to support social course format.
+ * This format is like singleactivity in the mobile app, so we'll use the same implementation for both.
+ */
+@Injectable({ providedIn: 'root' })
+export class CoreCourseFormatSocialHandlerService extends CoreCourseFormatSingleActivityHandlerService {
+
+ name = 'CoreCourseFormatSocial';
+ format = 'social';
+
+}
+
+export class CoreCourseFormatSocialHandler extends makeSingleton(CoreCourseFormatSocialHandlerService) {}
diff --git a/src/core/features/course/format/social/social.module.ts b/src/core/features/course/format/social/social.module.ts
new file mode 100644
index 000000000..7455034b7
--- /dev/null
+++ b/src/core/features/course/format/social/social.module.ts
@@ -0,0 +1,34 @@
+// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
+
+import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
+import { CoreCourseFormatSocialHandler } from './services/handlers/social-format';
+
+@NgModule({
+ declarations: [],
+ imports: [],
+ providers: [
+ {
+ provide: APP_INITIALIZER,
+ multi: true,
+ deps: [],
+ useFactory: () => () => {
+ CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatSocialHandler.instance);
+ },
+ },
+ ],
+})
+export class CoreCourseFormatSocialModule {}
diff --git a/src/core/features/course/format/topics/services/handlers/topics-format.ts b/src/core/features/course/format/topics/services/handlers/topics-format.ts
new file mode 100644
index 000000000..78d305273
--- /dev/null
+++ b/src/core/features/course/format/topics/services/handlers/topics-format.ts
@@ -0,0 +1,40 @@
+// (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 { Injectable } from '@angular/core';
+
+import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Handler to support topics course format.
+ */
+@Injectable({ providedIn: 'root' })
+export class CoreCourseFormatTopicsHandlerService implements CoreCourseFormatHandler {
+
+ name = 'CoreCourseFormatTopics';
+ format = 'topics';
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return True or promise resolved with true if enabled.
+ */
+ async isEnabled(): Promise {
+ return true;
+ }
+
+}
+
+export class CoreCourseFormatTopicsHandler extends makeSingleton(CoreCourseFormatTopicsHandlerService) {}
diff --git a/src/core/features/course/format/topics/topics.module.ts b/src/core/features/course/format/topics/topics.module.ts
new file mode 100644
index 000000000..01c8518c4
--- /dev/null
+++ b/src/core/features/course/format/topics/topics.module.ts
@@ -0,0 +1,34 @@
+// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
+
+import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
+import { CoreCourseFormatTopicsHandler } from './services/handlers/topics-format';
+
+@NgModule({
+ declarations: [],
+ imports: [],
+ providers: [
+ {
+ provide: APP_INITIALIZER,
+ multi: true,
+ deps: [],
+ useFactory: () => () => {
+ CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatTopicsHandler.instance);
+ },
+ },
+ ],
+})
+export class CoreCourseFormatTopicsModule {}
diff --git a/src/core/features/course/format/weeks/services/handlers/weeks-format.ts b/src/core/features/course/format/weeks/services/handlers/weeks-format.ts
new file mode 100644
index 000000000..e668a96d8
--- /dev/null
+++ b/src/core/features/course/format/weeks/services/handlers/weeks-format.ts
@@ -0,0 +1,95 @@
+// (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 { Injectable } from '@angular/core';
+
+import { CoreTimeUtils } from '@services/utils/time';
+import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
+import { makeSingleton } from '@singletons';
+import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
+import { CoreCourseSection } from '@features/course/services/course';
+import { CoreConstants } from '@/core/constants';
+
+/**
+ * Handler to support weeks course format.
+ */
+@Injectable({ providedIn: 'root' })
+export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHandler {
+
+ name = 'CoreCourseFormatWeeks';
+ format = 'weeks';
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return True or promise resolved with true if enabled.
+ */
+ async isEnabled(): Promise {
+ return true;
+ }
+
+ /**
+ * Given a list of sections, get the "current" section that should be displayed first.
+ *
+ * @param course The course to get the title.
+ * @param sections List of sections.
+ * @return Current section (or promise resolved with current section).
+ */
+ async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise {
+ const now = CoreTimeUtils.instance.timestamp();
+
+ if ((course.startdate && now < course.startdate) || (course.enddate && now > course.enddate)) {
+ // Course hasn't started yet or it has ended already. Return all sections.
+ return sections[0];
+ }
+
+ for (let i = 0; i < sections.length; i++) {
+ const section = sections[i];
+ if (typeof section.section == 'undefined' || section.section < 1) {
+ continue;
+ }
+
+ const dates = this.getSectionDates(section, course.startdate || 0);
+ if (now >= dates.start && now < dates.end) {
+ return section;
+ }
+ }
+
+ // The section wasn't found, return all sections.
+ return sections[0];
+ }
+
+ /**
+ * Return the start and end date of a section.
+ *
+ * @param section The section to treat.
+ * @param startDate The course start date (in seconds).
+ * @return An object with the start and end date of the section.
+ */
+ protected getSectionDates(section: CoreCourseSection, startDate: number): { start: number; end: number } {
+ // Hack alert. We add 2 hours to avoid possible DST problems. (e.g. we go into daylight savings and the date changes).
+ startDate = startDate + 7200;
+
+ const dates = {
+ start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)),
+ end: 0,
+ };
+ dates.end = dates.start + CoreConstants.SECONDS_WEEK;
+
+ return dates;
+ }
+
+}
+
+export class CoreCourseFormatWeeksHandler extends makeSingleton(CoreCourseFormatWeeksHandlerService) {}
diff --git a/src/core/features/course/format/weeks/weeks.module.ts b/src/core/features/course/format/weeks/weeks.module.ts
new file mode 100644
index 000000000..3ad1b0f19
--- /dev/null
+++ b/src/core/features/course/format/weeks/weeks.module.ts
@@ -0,0 +1,34 @@
+// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
+
+import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
+import { CoreCourseFormatWeeksHandler } from './services/handlers/weeks-format';
+
+@NgModule({
+ declarations: [],
+ imports: [],
+ providers: [
+ {
+ provide: APP_INITIALIZER,
+ multi: true,
+ deps: [],
+ useFactory: () => () => {
+ CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatWeeksHandler.instance);
+ },
+ },
+ ],
+})
+export class CoreCourseFormatWeeksModule {}
diff --git a/src/core/features/course/services/database/log.ts b/src/core/features/course/services/database/log.ts
index 1031ff3d4..20eac30c2 100644
--- a/src/core/features/course/services/database/log.ts
+++ b/src/core/features/course/services/database/log.ts
@@ -44,7 +44,7 @@ export const SITE_SCHEMA: CoreSiteSchema = {
{
name: 'time',
type: 'INTEGER',
- }
+ },
],
primaryKeys: ['component', 'componentid', 'ws', 'time'],
},