2
0
Fork 0

Merge pull request #1877 from dpalou/MOBILE-2975

Mobile 2975
main
Juan Leyva 2019-05-02 14:07:14 +02:00 committed by GitHub
commit 80546a07be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 594 additions and 279 deletions

View File

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesDashboardProvider } from '@core/courses/providers/dashboard';
import * as moment from 'moment';
/**
@ -26,7 +27,7 @@ export class AddonBlockTimelineProvider {
// Cache key was maintained when moving the functions to this file. It comes from core myoverview.
protected ROOT_CACHE_KEY = 'myoverview:';
constructor(private sitesProvider: CoreSitesProvider) { }
constructor(private sitesProvider: CoreSitesProvider, private dashboardProvider: CoreCoursesDashboardProvider) { }
/**
* Get calendar action events for the given course.
@ -218,6 +219,11 @@ export class AddonBlockTimelineProvider {
*/
isAvailable(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
// First check if dashboard is disabled.
if (this.dashboardProvider.isDisabledInSite(site)) {
return false;
}
return site.wsAvailable('core_calendar_get_action_events_by_courses') &&
site.wsAvailable('core_calendar_get_action_events_by_timesort');
});

View File

@ -29,7 +29,7 @@
</ion-item>
<ion-card-content>
<core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id"></core-format-text>
<ion-item>
<ion-item *ngIf="commentsEnabled">
<core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid" contextLevel="user"></core-comments>
</ion-item>
<core-file *ngFor="let file of entry.attachmentfiles" [file]="file" [component]="this.component" [componentId]="entry.id"></core-file>

View File

@ -18,6 +18,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonBlogProvider } from '../../providers/blog';
import { CoreCommentsProvider } from '@core/comments/providers/comments';
/**
* Component that displays the blog entries.
@ -47,9 +48,11 @@ export class AddonBlogEntriesComponent implements OnInit {
showMyIssuesToggle = false;
onlyMyEntries = false;
component = AddonBlogProvider.COMPONENT;
commentsEnabled: boolean;
constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider,
protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) {
protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider,
protected commentsProvider: CoreCommentsProvider) {
this.currentUserId = sitesProvider.getCurrentSiteUserId();
}
@ -81,6 +84,8 @@ export class AddonBlogEntriesComponent implements OnInit {
this.filter['tagid'] = this.tagId;
}
this.commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite();
this.fetchEntries().then(() => {
this.blogProvider.logView(this.filter).catch(() => {
// Ignore errors.

View File

@ -1,4 +1,4 @@
<a ion-item text-wrap (click)="showComments()" detail-none>
<a *ngIf="commentsEnabled" ion-item text-wrap (click)="showComments()" detail-none>
<h2>{{plugin.name}}</h2>
<core-comments contextLevel="module" [instanceId]="assign.cmid" component="assignsubmission_comments" [itemId]="submission.id" area="submission_comments" [title]="plugin.name"></core-comments>
</a>

View File

@ -27,8 +27,12 @@ import { AddonModAssignSubmissionPluginComponent } from '../../../classes/submis
export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSubmissionPluginComponent {
@ViewChild(CoreCommentsCommentsComponent) commentsComponent: CoreCommentsCommentsComponent;
commentsEnabled: boolean;
constructor(protected commentsProvider: CoreCommentsProvider) {
super();
this.commentsEnabled = !commentsProvider.areCommentsDisabledInSite();
}
/**

View File

@ -31,7 +31,7 @@
<core-rating-rate *ngIf="data && entry && ratingInfo && (!data.approval || entry.approved)" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale" [userId]="entry.userid" (onLoading)="setLoadingRating($event)" (onUpdate)="ratingUpdated()"></core-rating-rate>
<core-rating-aggregate *ngIf="data && entry && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module" [instanceId]="data.coursemodule" [itemId]="entry.id" [courseId]="courseId" [aggregateMethod]="data.assessed" [scaleId]="data.scale"></core-rating-aggregate>
<ion-item *ngIf="data && entry && entry.id > 0">
<ion-item *ngIf="data && entry && entry.id > 0 && commentsEnabled">
<core-comments contextLevel="module" [instanceId]="data.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry" [displaySpinner]="false" (onLoading)="setLoadingComments($event)"></core-comments>
</ion-item>

View File

@ -26,6 +26,7 @@ import { AddonModDataHelperProvider } from '../../providers/helper';
import { AddonModDataSyncProvider } from '../../providers/sync';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataComponentsModule } from '../../components/components.module';
import { CoreCommentsProvider } from '@core/comments/providers/comments';
/**
* Page that displays the view entry page.
@ -68,13 +69,14 @@ export class AddonModDataEntryPage implements OnDestroy {
jsData;
ratingInfo: CoreRatingInfo;
isPullingToRefresh = false; // Whether the last fetching of data was started by a pull-to-refresh action
commentsEnabled: boolean;
constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider,
protected dataHelper: AddonModDataHelperProvider,
sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected eventsProvider: CoreEventsProvider,
private cdr: ChangeDetectorRef) {
private cdr: ChangeDetectorRef, protected commentsProvider: CoreCommentsProvider) {
this.module = params.get('module') || {};
this.entryId = params.get('entryId') || null;
this.courseId = params.get('courseId');
@ -91,6 +93,7 @@ export class AddonModDataEntryPage implements OnDestroy {
* View loaded.
*/
ionViewDidLoad(): void {
this.commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite();
this.fetchEntryData();
// Refresh data if this discussion is synchronized automatically.

View File

@ -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);
}

View File

@ -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<boolean>} Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
areBlocksDisabled(siteId?: string): Promise<boolean> {
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);
}
}

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChange } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreCommentsProvider } from '../../providers/comments';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
/**
* Component that displays the count of comments.
@ -23,7 +25,7 @@ import { CoreCommentsProvider } from '../../providers/comments';
selector: 'core-comments',
templateUrl: 'core-comments.html',
})
export class CoreCommentsCommentsComponent implements OnChanges {
export class CoreCommentsCommentsComponent implements OnChanges, OnDestroy {
@Input() contextLevel: string;
@Input() instanceId: number;
@Input() component: string;
@ -36,9 +38,26 @@ export class CoreCommentsCommentsComponent implements OnChanges {
commentsLoaded = false;
commentsCount: number;
disabled = false;
constructor(private navCtrl: NavController, private commentsProvider: CoreCommentsProvider) {
protected updateSiteObserver;
constructor(private navCtrl: NavController, private commentsProvider: CoreCommentsProvider,
sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) {
this.onLoading = new EventEmitter<boolean>();
this.disabled = this.commentsProvider.areCommentsDisabledInSite();
// Update visibility if current site info is updated.
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => {
const wasDisabled = this.disabled;
this.disabled = this.commentsProvider.areCommentsDisabledInSite();
if (wasDisabled && !this.disabled) {
this.fetchData();
}
}, sitesProvider.getCurrentSiteId());
}
/**
@ -59,6 +78,10 @@ export class CoreCommentsCommentsComponent implements OnChanges {
}
protected fetchData(): void {
if (this.disabled) {
return;
}
this.commentsLoaded = false;
this.onLoading.emit(true);
@ -77,7 +100,7 @@ export class CoreCommentsCommentsComponent implements OnChanges {
* Opens the comments page.
*/
openComments(): void {
if (this.commentsCount > 0) {
if (!this.disabled && this.commentsCount > 0) {
// Open a new state with the interpolated contents.
this.navCtrl.push('CoreCommentsViewerPage', {
contextLevel: this.contextLevel,
@ -90,4 +113,11 @@ export class CoreCommentsCommentsComponent implements OnChanges {
});
}
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.updateSiteObserver && this.updateSiteObserver.off();
}
}

View File

@ -1,4 +1,4 @@
<core-loading [hideUntil]="commentsLoaded || !displaySpinner">
<core-loading *ngIf="!disabled" [hideUntil]="commentsLoaded || !displaySpinner">
<div (click)="openComments()" *ngIf="commentsCount >= 0">
{{ 'core.commentscount' | translate : {'$a': commentsCount} }}
</div>

View File

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSite } from '@classes/site';
/**
* Service that provides some features regarding comments.
@ -25,6 +26,30 @@ export class CoreCommentsProvider {
constructor(private sitesProvider: CoreSitesProvider) {}
/**
* Check if Calendar is disabled in a certain site.
*
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's disabled.
*/
areCommentsDisabledInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.isFeatureDisabled('NoDelegate_CoreComments');
}
/**
* Check if comments are disabled in a certain site.
*
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<boolean>} Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
areCommentsDisabled(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.areCommentsDisabledInSite(site);
});
}
/**
* Get cache key for get comments data WS calls.
*

View File

@ -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

View File

@ -0,0 +1,14 @@
<core-loading [hideUntil]="coursesLoaded">
<ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate">
</ion-searchbar>
<ion-grid no-padding>
<ion-row no-padding>
<ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch>
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true"></core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="!courses || !courses.length" icon="ionic" [message]="'core.courses.nocourses' | translate">
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
</core-empty-box>
</core-loading>

View File

@ -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<any>} Promise resolved when done.
*/
protected fetchCourses(): Promise<any> {
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<any>} Promise resolved when done.
*/
prefetchCourses(): Promise<any> {
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();
}
}

View File

@ -7,7 +7,12 @@
<ion-icon name="search"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000" [content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
<!-- Action for dashboard and site home. -->
<core-context-menu-item *ngIf="(siteHomeEnabled || dashboardEnabled) && (downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000" [content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
<!-- Actions when both site home and dashboard are disabled. -->
<core-context-menu-item *ngIf="!siteHomeEnabled && !dashboardEnabled && mcComponent && mcComponent.downloadAllCoursesEnabled && mcComponent.courses && mcComponent.courses.length >= 2" [priority]="800" [content]="'core.courses.downloadcourses' | translate" (action)="mcComponent.prefetchCourses()" [iconAction]="mcComponent.prefetchCoursesData.icon" [closeOnClick]="false" [badge]="mcComponent.prefetchCoursesData.badge"></core-context-menu-item>
<core-context-menu-item *ngIf="!siteHomeEnabled && !dashboardEnabled && mcComponent && mcComponent.courses && mcComponent.courses.length > 5" [priority]="700" [content]="'core.courses.filtermycourses' | translate" (action)="mcComponent.switchFilter()" [iconAction]="'funnel'"></core-context-menu-item>
</core-context-menu>
</ion-buttons>
</ion-navbar>
@ -46,5 +51,17 @@
</ion-content>
</ng-template>
</core-tab>
<!-- Tab to display if both site home and dashboard are disabled. -->
<core-tab [show]="!siteHomeEnabled && !dashboardEnabled" [title]="'core.courses.mymoodle' | translate">
<ng-template>
<ion-content>
<ion-refresher [enabled]="mcComponent && mcComponent.coursesLoaded" (ionRefresh)="refreshMyCourses($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-courses-my-courses></core-courses-my-courses>
</ion-content>
</ng-template>
</core-tab>
</core-tabs>
</ion-content>

View File

@ -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<CoreBlockComponent>;
@ViewChild(CoreCoursesMyCoursesComponent) mcComponent: CoreCoursesMyCoursesComponent;
firstSelectedTab: number;
siteHomeEnabled = false;
@ -140,8 +142,8 @@ export class CoreCoursesDashboardPage implements OnDestroy {
* @return {Promise<any>} Promise resolved when done.
*/
protected loadDashboardContent(): Promise<any> {
return this.dashboardProvider.isAvailable().then((enabled) => {
if (enabled) {
return this.dashboardProvider.isAvailable().then((available) => {
if (available) {
this.userId = this.sitesProvider.getCurrentSiteUserId();
return this.dashboardProvider.getDashboardBlocks().then((blocks) => {
@ -152,10 +154,14 @@ export class CoreCoursesDashboardPage implements OnDestroy {
// Cannot get the blocks, just show dashboard if needed.
this.loadFallbackBlocks();
});
} else if (!this.dashboardProvider.isDisabledInSite()) {
// Not available, but not disabled either. Use fallback.
this.loadFallbackBlocks();
} else {
// Disabled.
this.blocks = [];
}
// Not enabled, check separated tabs.
this.loadFallbackBlocks();
}).finally(() => {
this.dashboardEnabled = this.blockDelegate.hasSupportedBlock(this.blocks);
this.dashboardLoaded = true;
@ -186,6 +192,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.
*/

View File

@ -3,33 +3,20 @@
<ion-title>{{ 'core.courses.mycourses' | translate }}</ion-title>
<ion-buttons end>
<button *ngIf="searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
<button *ngIf="mcComponent && mcComponent.searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
<ion-icon name="search"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2" [priority]="800" [content]="'core.courses.downloadcourses' | translate" (action)="prefetchCourses()" [iconAction]="prefetchCoursesData.icon" [closeOnClick]="false" [badge]="prefetchCoursesData.badge"></core-context-menu-item>
<core-context-menu-item [hidden]="!courses || courses.length <= 5" [priority]="700" [content]="'core.courses.filtermycourses' | translate" (action)="switchFilter()" [iconAction]="'funnel'"></core-context-menu-item>
<core-context-menu *ngIf="mcComponent">
<core-context-menu-item [hidden]="!mcComponent.downloadAllCoursesEnabled || !mcComponent.courses || mcComponent.courses.length < 2" [priority]="800" [content]="'core.courses.downloadcourses' | translate" (action)="mcComponent.prefetchCourses()" [iconAction]="mcComponent.prefetchCoursesData.icon" [closeOnClick]="false" [badge]="mcComponent.prefetchCoursesData.badge"></core-context-menu-item>
<core-context-menu-item [hidden]="!mcComponent.courses || mcComponent.courses.length <= 5" [priority]="700" [content]="'core.courses.filtermycourses' | translate" (action)="mcComponent.switchFilter()" [iconAction]="'funnel'"></core-context-menu-item>
</core-context-menu>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="coursesLoaded" (ionRefresh)="refreshCourses($event)">
<ion-refresher [enabled]="mcComponent && mcComponent.coursesLoaded" (ionRefresh)="mcComponent.refreshCourses($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="coursesLoaded">
<ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate">
</ion-searchbar>
<ion-grid no-padding>
<ion-row no-padding>
<ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch>
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true"></core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="!courses || !courses.length" icon="ionic" [message]="'core.courses.nocourses' | translate">
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
</core-empty-box>
</core-loading>
<core-courses-my-courses></core-courses-my-courses>
</ion-content>

View File

@ -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<any>} Promise resolved when done.
*/
protected fetchCourses(): Promise<any> {
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<any>} Promise resolved when done.
*/
prefetchCourses(): Promise<any> {
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();
}
}

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
import { CoreDashboardMainMenuHandler } from './mainmenu-handler';
/**
* Handler to treat links to my overview.
@ -23,10 +24,9 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper';
@Injectable()
export class CoreCoursesDashboardLinkHandler extends CoreContentLinksHandlerBase {
name = 'CoreCoursesMyOverviewLinkHandler';
featureName = 'CoreMainMenuDelegate_CoreCourses';
pattern = /\/my\/?$/;
constructor(private loginHelper: CoreLoginHelperProvider) {
constructor(private loginHelper: CoreLoginHelperProvider, private mainMenuHandler: CoreDashboardMainMenuHandler) {
super();
}
@ -48,4 +48,17 @@ export class CoreCoursesDashboardLinkHandler extends CoreContentLinksHandlerBase
}
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
*
* @param {string} siteId The site ID.
* @param {string} url The URL to treat.
* @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param {number} [courseId] Course ID related to the URL. Optional but recommended.
* @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
return this.mainMenuHandler.isEnabledForSite(siteId);
}
}

View File

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSite } from '@classes/site';
/**
* Service that provides some features regarding course overview.
@ -83,7 +84,36 @@ export class CoreCoursesDashboardProvider {
*/
isAvailable(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
// First check if it's disabled.
if (this.isDisabledInSite(site)) {
return false;
}
return site.wsAvailable('core_block_get_dashboard_blocks');
});
}
/**
* Check if Site Home is disabled in a certain site.
*
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<boolean>} Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
isDisabled(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.isDisabledInSite(site);
});
}
/**
* Check if Site Home is disabled in a certain site.
*
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's disabled.
*/
isDisabledInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.isFeatureDisabled('CoreMainMenuDelegate_CoreCoursesDashboard');
}
}

View File

@ -13,22 +13,25 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesProvider } from './courses';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate';
import { CoreCoursesDashboardProvider } from '../providers/dashboard';
import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome';
import { AddonBlockTimelineProvider } from '@addon/block/timeline/providers/timeline';
import { CoreBlockDelegate } from '@core/block/providers/delegate';
/**
* Handler to add Dashboard into main menu.
*/
@Injectable()
export class CoreDashboardMainMenuHandler implements CoreMainMenuHandler {
name = 'CoreDashboard'; // Old name CoreCourses cannot be used because it would be all disabled by site.
name = 'CoreHome'; // This handler contains several different features, so we use a generic name like "CoreHome".
priority = 1100;
constructor(private coursesProvider: CoreCoursesProvider, private dashboardProvider: CoreCoursesDashboardProvider,
private siteHomeProvider: CoreSiteHomeProvider, private timelineProvider: AddonBlockTimelineProvider) { }
private siteHomeProvider: CoreSiteHomeProvider, private timelineProvider: AddonBlockTimelineProvider,
private blockDelegate: CoreBlockDelegate, private sitesProvider: CoreSitesProvider) { }
/**
* Check if the handler is enabled on a site level.
@ -36,15 +39,40 @@ export class CoreDashboardMainMenuHandler implements CoreMainMenuHandler {
* @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.isEnabledForSite();
}
/**
* Check if the handler is enabled on a certain site.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabledForSite(siteId?: string): Promise<boolean> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const promises = [];
let blocksEnabled,
dashboardAvailable;
// Check if blocks and 3.6 dashboard is enabled.
promises.push(this.blockDelegate.areBlocksDisabled(siteId).then((disabled) => {
blocksEnabled = !disabled;
}));
promises.push(this.dashboardProvider.isAvailable().then((available) => {
dashboardAvailable = available;
}));
// Check if 3.6 dashboard is enabled.
return this.dashboardProvider.isAvailable().then((enabled) => {
if (enabled) {
return Promise.all(promises).then(() => {
if (dashboardAvailable && blocksEnabled) {
return true;
}
// Check if my overview is enabled.
return this.timelineProvider.isAvailable().then((enabled) => {
if (enabled) {
if (enabled && blocksEnabled) {
return true;
}

View File

@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
import { Component, Input, OnChanges, SimpleChange, OnDestroy } from '@angular/core';
import { ModalController } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreRatingProvider, CoreRatingInfo, CoreRatingInfoItem } from '@core/rating/providers/rating';
import { CoreSitesProvider } from '@providers/sites';
/**
* Component that displays the aggregation of a rating item.
@ -24,7 +25,7 @@ import { CoreRatingProvider, CoreRatingInfo, CoreRatingInfoItem } from '@core/ra
selector: 'core-rating-aggregate',
templateUrl: 'core-rating-aggregate.html'
})
export class CoreRatingAggregateComponent implements OnChanges {
export class CoreRatingAggregateComponent implements OnChanges, OnDestroy {
@Input() ratingInfo: CoreRatingInfo;
@Input() contextLevel: string;
@Input() instanceId: number;
@ -33,12 +34,23 @@ export class CoreRatingAggregateComponent implements OnChanges {
@Input() scaleId: number;
@Input() courseId?: number;
disabled = false;
protected labelKey: string;
protected showCount: boolean;
protected item: CoreRatingInfoItem;
protected aggregateObserver;
protected updateSiteObserver;
constructor(private eventsProvider: CoreEventsProvider, private modalCtrl: ModalController) {}
constructor(private eventsProvider: CoreEventsProvider, private modalCtrl: ModalController,
private ratingProvider: CoreRatingProvider, sitesProvider: CoreSitesProvider) {
this.disabled = this.ratingProvider.isRatingDisabledInSite();
// Update visibility if current site info is updated.
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => {
this.disabled = this.ratingProvider.isRatingDisabledInSite();
}, sitesProvider.getCurrentSiteId());
}
/**
* Detect changes on input properties.
@ -86,7 +98,7 @@ export class CoreRatingAggregateComponent implements OnChanges {
* Open the individual ratings page.
*/
openRatings(): void {
if (!this.ratingInfo.canviewall || !this.item.count) {
if (!this.ratingInfo.canviewall || !this.item.count || this.disabled) {
return;
}
@ -108,5 +120,6 @@ export class CoreRatingAggregateComponent implements OnChanges {
*/
ngOnDestroy(): void {
this.aggregateObserver && this.aggregateObserver.off();
this.updateSiteObserver && this.updateSiteObserver.off();
}
}

View File

@ -1,4 +1,4 @@
<a *ngIf="item && item.canviewaggregate && labelKey" ion-item text-wrap [attr.detail-none]="ratingInfo.canviewall && item.count ? null : true" (click)="openRatings()">
<a *ngIf="item && item.canviewaggregate && labelKey && !disabled" ion-item text-wrap [attr.detail-none]="ratingInfo.canviewall && item.count ? null : true" (click)="openRatings()">
{{ labelKey | translate }}{{ 'core.labelsep' | translate }} {{ item.aggregatestr || '-' }}
<span *ngIf="showCount && item.count > 0">({{ item.count }})</span>
</a>

View File

@ -1,4 +1,4 @@
<ion-item text-wrap *ngIf="item && (item.canrate || item.rating != null)">
<ion-item text-wrap *ngIf="item && (item.canrate || item.rating != null) && !disabled">
<ion-label>{{ 'core.rating.rating' | translate }}</ion-label>
<ion-select text-start [(ngModel)]="rating" (ngModelChange)="userRatingChanged()" interface="action-sheet" [disabled]="!item.canrate">
<ion-option *ngFor="let scaleItem of scale.items" [value]="scaleItem.value">{{ scaleItem.name }}</ion-option>

View File

@ -12,11 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange, OnDestroy } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreRatingProvider, CoreRatingInfo, CoreRatingInfoItem, CoreRatingScale } from '@core/rating/providers/rating';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreRatingOfflineProvider } from '@core/rating/providers/offline';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
/**
* Component that displays the user rating select.
@ -25,7 +27,7 @@ import { CoreRatingOfflineProvider } from '@core/rating/providers/offline';
selector: 'core-rating-rate',
templateUrl: 'core-rating-rate.html'
})
export class CoreRatingRateComponent implements OnChanges {
export class CoreRatingRateComponent implements OnChanges, OnDestroy {
@Input() ratingInfo: CoreRatingInfo;
@Input() contextLevel: string; // Context level: course, module, user, etc.
@Input() instanceId: number; // Context instance id.
@ -41,11 +43,22 @@ export class CoreRatingRateComponent implements OnChanges {
item: CoreRatingInfoItem;
scale: CoreRatingScale;
rating: number;
disabled = false;
protected updateSiteObserver;
constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService, eventsProvider: CoreEventsProvider,
private ratingProvider: CoreRatingProvider, private ratingOffline: CoreRatingOfflineProvider,
sitesProvider: CoreSitesProvider) {
constructor(private domUtils: CoreDomUtilsProvider, private translate: TranslateService,
private ratingProvider: CoreRatingProvider, private ratingOffline: CoreRatingOfflineProvider) {
this.onLoading = new EventEmitter<boolean>();
this.onUpdate = new EventEmitter<void>();
this.disabled = this.ratingProvider.isRatingDisabledInSite();
// Update visibility if current site info is updated.
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => {
this.disabled = this.ratingProvider.isRatingDisabledInSite();
}, sitesProvider.getCurrentSiteId());
}
/**
@ -113,4 +126,11 @@ export class CoreRatingRateComponent implements OnChanges {
modal.dismiss();
});
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.updateSiteObserver && this.updateSiteObserver.off();
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSiteWSPreSets } from '@classes/site';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
@ -302,6 +302,30 @@ export class CoreRatingProvider {
});
}
/**
* Check if rating is disabled in a certain site.
*
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's disabled.
*/
isRatingDisabledInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.isFeatureDisabled('NoDelegate_CoreRating');
}
/**
* Check if rating is disabled in a certain site.
*
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<boolean>} Promise resolved with true if disabled, rejected or resolved with false otherwise.
*/
isRatingDisabled(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return this.isRatingDisabledInSite(site);
});
}
/**
* Prefetch individual ratings.
*