diff --git a/src/classes/delegate.ts b/src/classes/delegate.ts index 61fd14b76..db6726933 100644 --- a/src/classes/delegate.ts +++ b/src/classes/delegate.ts @@ -15,6 +15,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreEventsProvider } from '@providers/events'; +import { CoreSite } from '@classes/site'; export interface CoreDelegateHandler { /** @@ -272,10 +273,10 @@ export class CoreDelegate { * Check if feature is enabled or disabled in the site, depending on the feature prefix and the handler name. * * @param {CoreDelegateHandler} handler Handler to check. - * @param {any} site Site to check. - * @return {boolean} Whether is enabled or disabled in site. + * @param {CoreSite} site Site to check. + * @return {boolean} Whether is enabled or disabled in site. */ - protected isFeatureDisabled(handler: CoreDelegateHandler, site: any): boolean { + protected isFeatureDisabled(handler: CoreDelegateHandler, site: CoreSite): boolean { return typeof this.featurePrefix != 'undefined' && site.isFeatureDisabled(this.featurePrefix + handler.name); } diff --git a/src/core/block/providers/delegate.ts b/src/core/block/providers/delegate.ts index 363dff591..0275350be 100644 --- a/src/core/block/providers/delegate.ts +++ b/src/core/block/providers/delegate.ts @@ -18,6 +18,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreBlockDefaultHandler } from './default-block-handler'; +import { CoreSite } from '@classes/site'; /** * Interface that all blocks must implement. @@ -87,6 +88,30 @@ export class CoreBlockDelegate extends CoreDelegate { super('CoreBlockDelegate', logger, sitesProvider, eventsProvider); } + /** + * Check if blocks are disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + areBlocksDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('NoDelegate_SiteBlocks'); + } + + /** + * Check if blocks are disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + areBlocksDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.areBlocksDisabledInSite(site); + }); + } + /** * Get the display data for a certain block. * @@ -121,4 +146,15 @@ export class CoreBlockDelegate extends CoreDelegate { isBlockSupported(name: string): boolean { return this.hasHandler(name, true); } + + /** + * Check if feature is enabled or disabled in the site, depending on the feature prefix and the handler name. + * + * @param {CoreDelegateHandler} handler Handler to check. + * @param {CoreSite} site Site to check. + * @return {boolean} Whether is enabled or disabled in site. + */ + protected isFeatureDisabled(handler: CoreDelegateHandler, site: CoreSite): boolean { + return this.areBlocksDisabledInSite(site) || super.isFeatureDisabled(handler, site); + } } diff --git a/src/core/courses/components/components.module.ts b/src/core/courses/components/components.module.ts index 45b2671cb..ccb458973 100644 --- a/src/core/courses/components/components.module.ts +++ b/src/core/courses/components/components.module.ts @@ -19,15 +19,17 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; -import { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress'; -import { CoreCoursesCourseListItemComponent } from '../components/course-list-item/course-list-item'; -import { CoreCoursesCourseOptionsMenuComponent } from '../components/course-options-menu/course-options-menu'; +import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress'; +import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item'; +import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu'; +import { CoreCoursesMyCoursesComponent } from './my-courses/my-courses'; @NgModule({ declarations: [ CoreCoursesCourseProgressComponent, CoreCoursesCourseListItemComponent, - CoreCoursesCourseOptionsMenuComponent + CoreCoursesCourseOptionsMenuComponent, + CoreCoursesMyCoursesComponent ], imports: [ CommonModule, @@ -42,7 +44,8 @@ import { CoreCoursesCourseOptionsMenuComponent } from '../components/course-opti exports: [ CoreCoursesCourseProgressComponent, CoreCoursesCourseListItemComponent, - CoreCoursesCourseOptionsMenuComponent + CoreCoursesCourseOptionsMenuComponent, + CoreCoursesMyCoursesComponent ], entryComponents: [ CoreCoursesCourseOptionsMenuComponent diff --git a/src/core/courses/components/my-courses/my-courses.html b/src/core/courses/components/my-courses/my-courses.html new file mode 100644 index 000000000..06ca202b8 --- /dev/null +++ b/src/core/courses/components/my-courses/my-courses.html @@ -0,0 +1,14 @@ + + + + + + + + + + + +

{{ 'core.courses.searchcoursesadvice' | translate }}

+
+
diff --git a/src/core/courses/components/my-courses/my-courses.ts b/src/core/courses/components/my-courses/my-courses.ts new file mode 100644 index 000000000..fc0817350 --- /dev/null +++ b/src/core/courses/components/my-courses/my-courses.ts @@ -0,0 +1,242 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Searchbar } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreCoursesProvider } from '../../providers/courses'; +import { CoreCoursesHelperProvider } from '../../providers/helper'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; + +/** + * Component that displays the list of courses the user is enrolled in. + */ +@Component({ + selector: 'core-courses-my-courses', + templateUrl: 'my-courses.html', +}) +export class CoreCoursesMyCoursesComponent implements OnInit, OnDestroy { + @ViewChild('searchbar') searchbar: Searchbar; + + courses: any[]; + filteredCourses: any[]; + searchEnabled: boolean; + filter = ''; + showFilter = false; + coursesLoaded = false; + prefetchCoursesData: any = {}; + downloadAllCoursesEnabled: boolean; + + protected prefetchIconInitialized = false; + protected myCoursesObserver; + protected siteUpdatedObserver; + protected isDestroyed = false; + protected courseIds = ''; + + constructor(private coursesProvider: CoreCoursesProvider, + private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider, + private sitesProvider: CoreSitesProvider, private courseHelper: CoreCourseHelperProvider, + private courseOptionsDelegate: CoreCourseOptionsDelegate, private coursesHelper: CoreCoursesHelperProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); + + this.fetchCourses().finally(() => { + this.coursesLoaded = true; + }); + + this.myCoursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => { + this.fetchCourses(); + }, this.sitesProvider.getCurrentSiteId()); + + // Refresh the enabled flags if site is updated. + this.siteUpdatedObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { + const wasEnabled = this.downloadAllCoursesEnabled; + + this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); + + if (!wasEnabled && this.downloadAllCoursesEnabled && this.coursesLoaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcon(); + } + }, this.sitesProvider.getCurrentSiteId()); + } + + /** + * Fetch the user courses. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchCourses(): Promise { + return this.coursesProvider.getUserCourses().then((courses) => { + const promises = [], + courseIds = courses.map((course) => { + return course.id; + }); + + this.courseIds = courseIds.join(','); + + promises.push(this.coursesHelper.loadCoursesExtraInfo(courses)); + + if (this.coursesProvider.canGetAdminAndNavOptions()) { + promises.push(this.coursesProvider.getCoursesAdminAndNavOptions(courseIds).then((options) => { + courses.forEach((course) => { + course.navOptions = options.navOptions[course.id]; + course.admOptions = options.admOptions[course.id]; + }); + })); + } + + return Promise.all(promises).then(() => { + this.courses = courses; + this.filteredCourses = this.courses; + this.filter = ''; + + this.initPrefetchCoursesIcon(); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true); + }); + } + + /** + * Refresh the courses. + * + * @param {any} refresher Refresher. + */ + refreshCourses(refresher: any): void { + const promises = []; + + promises.push(this.coursesProvider.invalidateUserCourses()); + promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); + if (this.courseIds) { + promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds)); + } + + Promise.all(promises).finally(() => { + + this.prefetchIconInitialized = false; + this.fetchCourses().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Show or hide the filter. + */ + switchFilter(): void { + this.filter = ''; + this.showFilter = !this.showFilter; + this.filteredCourses = this.courses; + if (this.showFilter) { + setTimeout(() => { + this.searchbar.setFocus(); + }, 500); + } + } + + /** + * The filter has changed. + * + * @param {any} Received Event. + */ + filterChanged(event: any): void { + const newValue = event.target.value && event.target.value.trim().toLowerCase(); + if (!newValue || !this.courses) { + this.filteredCourses = this.courses; + } else { + // Use displayname if avalaible, or fullname if not. + if (this.courses.length > 0 && typeof this.courses[0].displayname != 'undefined') { + this.filteredCourses = this.courses.filter((course) => { + return course.displayname.toLowerCase().indexOf(newValue) > -1; + }); + } else { + this.filteredCourses = this.courses.filter((course) => { + return course.fullname.toLowerCase().indexOf(newValue) > -1; + }); + } + } + } + + /** + * Prefetch all the courses. + * + * @return {Promise} Promise resolved when done. + */ + prefetchCourses(): Promise { + const initialIcon = this.prefetchCoursesData.icon; + + this.prefetchCoursesData.icon = 'spinner'; + this.prefetchCoursesData.badge = ''; + + return this.courseHelper.confirmAndPrefetchCourses(this.courses, (progress) => { + this.prefetchCoursesData.badge = progress.count + ' / ' + progress.total; + }).then(() => { + this.prefetchCoursesData.icon = 'ion-android-refresh'; + }).catch((error) => { + if (!this.isDestroyed) { + this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + this.prefetchCoursesData.icon = initialIcon; + } + }).finally(() => { + this.prefetchCoursesData.badge = ''; + }); + } + + /** + * Initialize the prefetch icon for the list of courses. + */ + protected initPrefetchCoursesIcon(): void { + if (this.prefetchIconInitialized || !this.downloadAllCoursesEnabled) { + // Already initialized. + return; + } + + this.prefetchIconInitialized = true; + + if (!this.courses || this.courses.length < 2) { + // Not enough courses. + this.prefetchCoursesData.icon = ''; + + return; + } + + this.courseHelper.determineCoursesStatus(this.courses).then((status) => { + let icon = this.courseHelper.getCourseStatusIconAndTitleFromStatus(status).icon; + if (icon == 'spinner') { + // It seems all courses are being downloaded, show a download button instead. + icon = 'cloud-download'; + } + this.prefetchCoursesData.icon = icon; + }); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.myCoursesObserver && this.myCoursesObserver.off(); + this.siteUpdatedObserver && this.siteUpdatedObserver.off(); + } +} diff --git a/src/core/courses/pages/dashboard/dashboard.html b/src/core/courses/pages/dashboard/dashboard.html index d648f0cbe..f9eff8228 100644 --- a/src/core/courses/pages/dashboard/dashboard.html +++ b/src/core/courses/pages/dashboard/dashboard.html @@ -7,7 +7,12 @@ - + + + + + + @@ -46,5 +51,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/courses/pages/dashboard/dashboard.ts b/src/core/courses/pages/dashboard/dashboard.ts index 6171a18ed..cff6155b2 100644 --- a/src/core/courses/pages/dashboard/dashboard.ts +++ b/src/core/courses/pages/dashboard/dashboard.ts @@ -24,6 +24,7 @@ import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome'; import { CoreSiteHomeIndexComponent } from '@core/sitehome/components/index/index'; import { CoreCoursesProvider } from '../../providers/courses'; import { CoreCoursesDashboardProvider } from '../../providers/dashboard'; +import { CoreCoursesMyCoursesComponent } from '../../components/my-courses/my-courses'; /** * Page that displays the dashboard. @@ -37,6 +38,7 @@ export class CoreCoursesDashboardPage implements OnDestroy { @ViewChild(CoreTabsComponent) tabsComponent: CoreTabsComponent; @ViewChild(CoreSiteHomeIndexComponent) siteHomeComponent: CoreSiteHomeIndexComponent; @ViewChildren(CoreBlockComponent) blocksComponents: QueryList; + @ViewChild(CoreCoursesMyCoursesComponent) mcComponent: CoreCoursesMyCoursesComponent; firstSelectedTab: number; siteHomeEnabled = false; @@ -186,6 +188,26 @@ export class CoreCoursesDashboardPage implements OnDestroy { }); } + /** + * Refresh the dashboard data and My Courses. + * + * @param {any} refresher Refresher. + */ + refreshMyCourses(refresher: any): void { + // First of all, refresh dashboard blocks, maybe a new block was added and now we can display the dashboard. + this.dashboardProvider.invalidateDashboardBlocks().finally(() => { + return this.loadDashboardContent(); + }).finally(() => { + if (!this.dashboardEnabled) { + // Dashboard still not enabled. Refresh my courses. + this.mcComponent && this.mcComponent.refreshCourses(refresher); + } else { + this.tabsComponent.selectTab(1); + refresher.complete(); + } + }); + } + /** * Toggle download enabled. */ diff --git a/src/core/courses/pages/my-courses/my-courses.html b/src/core/courses/pages/my-courses/my-courses.html index 2129adec0..c23f5cc59 100644 --- a/src/core/courses/pages/my-courses/my-courses.html +++ b/src/core/courses/pages/my-courses/my-courses.html @@ -3,33 +3,20 @@ {{ 'core.courses.mycourses' | translate }} - - - - + + + - + - - - - - - - - - - - -

{{ 'core.courses.searchcoursesadvice' | translate }}

-
-
+
diff --git a/src/core/courses/pages/my-courses/my-courses.ts b/src/core/courses/pages/my-courses/my-courses.ts index bf874e962..3ca35b8a9 100644 --- a/src/core/courses/pages/my-courses/my-courses.ts +++ b/src/core/courses/pages/my-courses/my-courses.ts @@ -12,15 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, ViewChild } from '@angular/core'; -import { IonicPage, Searchbar, NavController } from 'ionic-angular'; -import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreCoursesProvider } from '../../providers/courses'; -import { CoreCoursesHelperProvider } from '../../providers/helper'; -import { CoreCourseHelperProvider } from '@core/course/providers/helper'; -import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; +import { Component, ViewChild } from '@angular/core'; +import { IonicPage, NavController } from 'ionic-angular'; +import { CoreCoursesMyCoursesComponent } from '../../components/my-courses/my-courses'; /** * Page that displays the list of courses the user is enrolled in. @@ -30,131 +24,10 @@ import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delega selector: 'page-core-courses-my-courses', templateUrl: 'my-courses.html', }) -export class CoreCoursesMyCoursesPage implements OnDestroy { - @ViewChild('searchbar') searchbar: Searchbar; +export class CoreCoursesMyCoursesPage { + @ViewChild(CoreCoursesMyCoursesComponent) mcComponent: CoreCoursesMyCoursesComponent; - courses: any[]; - filteredCourses: any[]; - searchEnabled: boolean; - filter = ''; - showFilter = false; - coursesLoaded = false; - prefetchCoursesData: any = {}; - downloadAllCoursesEnabled: boolean; - - protected prefetchIconInitialized = false; - protected myCoursesObserver; - protected siteUpdatedObserver; - protected isDestroyed = false; - protected courseIds = ''; - - constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider, - private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider, - private sitesProvider: CoreSitesProvider, private courseHelper: CoreCourseHelperProvider, - private courseOptionsDelegate: CoreCourseOptionsDelegate, private coursesHelper: CoreCoursesHelperProvider) { } - - /** - * View loaded. - */ - ionViewDidLoad(): void { - this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); - this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); - - this.fetchCourses().finally(() => { - this.coursesLoaded = true; - }); - - this.myCoursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => { - this.fetchCourses(); - }, this.sitesProvider.getCurrentSiteId()); - - // Refresh the enabled flags if site is updated. - this.siteUpdatedObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { - const wasEnabled = this.downloadAllCoursesEnabled; - - this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); - this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); - - if (!wasEnabled && this.downloadAllCoursesEnabled && this.coursesLoaded) { - // Download all courses is enabled now, initialize it. - this.initPrefetchCoursesIcon(); - } - }, this.sitesProvider.getCurrentSiteId()); - } - - /** - * Fetch the user courses. - * - * @return {Promise} Promise resolved when done. - */ - protected fetchCourses(): Promise { - return this.coursesProvider.getUserCourses().then((courses) => { - const promises = [], - courseIds = courses.map((course) => { - return course.id; - }); - - this.courseIds = courseIds.join(','); - - promises.push(this.coursesHelper.loadCoursesExtraInfo(courses)); - - if (this.coursesProvider.canGetAdminAndNavOptions()) { - promises.push(this.coursesProvider.getCoursesAdminAndNavOptions(courseIds).then((options) => { - courses.forEach((course) => { - course.navOptions = options.navOptions[course.id]; - course.admOptions = options.admOptions[course.id]; - }); - })); - } - - return Promise.all(promises).then(() => { - this.courses = courses; - this.filteredCourses = this.courses; - this.filter = ''; - - this.initPrefetchCoursesIcon(); - }); - }).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true); - }); - } - - /** - * Refresh the courses. - * - * @param {any} refresher Refresher. - */ - refreshCourses(refresher: any): void { - const promises = []; - - promises.push(this.coursesProvider.invalidateUserCourses()); - promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); - if (this.courseIds) { - promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds)); - } - - Promise.all(promises).finally(() => { - - this.prefetchIconInitialized = false; - this.fetchCourses().finally(() => { - refresher.complete(); - }); - }); - } - - /** - * Show or hide the filter. - */ - switchFilter(): void { - this.filter = ''; - this.showFilter = !this.showFilter; - this.filteredCourses = this.courses; - if (this.showFilter) { - setTimeout(() => { - this.searchbar.setFocus(); - }, 500); - } - } + constructor(private navCtrl: NavController) { } /** * Go to search courses. @@ -162,89 +35,4 @@ export class CoreCoursesMyCoursesPage implements OnDestroy { openSearch(): void { this.navCtrl.push('CoreCoursesSearchPage'); } - - /** - * The filter has changed. - * - * @param {any} Received Event. - */ - filterChanged(event: any): void { - const newValue = event.target.value && event.target.value.trim().toLowerCase(); - if (!newValue || !this.courses) { - this.filteredCourses = this.courses; - } else { - // Use displayname if avalaible, or fullname if not. - if (this.courses.length > 0 && typeof this.courses[0].displayname != 'undefined') { - this.filteredCourses = this.courses.filter((course) => { - return course.displayname.toLowerCase().indexOf(newValue) > -1; - }); - } else { - this.filteredCourses = this.courses.filter((course) => { - return course.fullname.toLowerCase().indexOf(newValue) > -1; - }); - } - } - } - - /** - * Prefetch all the courses. - * - * @return {Promise} Promise resolved when done. - */ - prefetchCourses(): Promise { - const initialIcon = this.prefetchCoursesData.icon; - - this.prefetchCoursesData.icon = 'spinner'; - this.prefetchCoursesData.badge = ''; - - return this.courseHelper.confirmAndPrefetchCourses(this.courses, (progress) => { - this.prefetchCoursesData.badge = progress.count + ' / ' + progress.total; - }).then(() => { - this.prefetchCoursesData.icon = 'ion-android-refresh'; - }).catch((error) => { - if (!this.isDestroyed) { - this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); - this.prefetchCoursesData.icon = initialIcon; - } - }).finally(() => { - this.prefetchCoursesData.badge = ''; - }); - } - - /** - * Initialize the prefetch icon for the list of courses. - */ - protected initPrefetchCoursesIcon(): void { - if (this.prefetchIconInitialized || !this.downloadAllCoursesEnabled) { - // Already initialized. - return; - } - - this.prefetchIconInitialized = true; - - if (!this.courses || this.courses.length < 2) { - // Not enough courses. - this.prefetchCoursesData.icon = ''; - - return; - } - - this.courseHelper.determineCoursesStatus(this.courses).then((status) => { - let icon = this.courseHelper.getCourseStatusIconAndTitleFromStatus(status).icon; - if (icon == 'spinner') { - // It seems all courses are being downloaded, show a download button instead. - icon = 'cloud-download'; - } - this.prefetchCoursesData.icon = icon; - }); - } - - /** - * Page destroyed. - */ - ngOnDestroy(): void { - this.isDestroyed = true; - this.myCoursesObserver && this.myCoursesObserver.off(); - this.siteUpdatedObserver && this.siteUpdatedObserver.off(); - } }