MOBILE-2302 courses: Implement course overview
parent
cacab75855
commit
5dab527836
|
@ -0,0 +1,178 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="157 -1509 148 125"
|
||||
preserveAspectRatio="xMinYMid meet"
|
||||
version="1.1"
|
||||
id="svg23"
|
||||
sodipodi:docname="activities.svg"
|
||||
inkscape:version="0.92.1 r15371">
|
||||
<metadata
|
||||
id="metadata27">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview25"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.981125"
|
||||
inkscape:cx="38.889548"
|
||||
inkscape:cy="62.5"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Group_42" />
|
||||
<defs
|
||||
id="defs7">
|
||||
<style
|
||||
id="style2">
|
||||
.cls-1 {
|
||||
clip-path: url(#clip-Activities);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #c4c8cc;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
<clipPath
|
||||
id="clip-Activities">
|
||||
<rect
|
||||
x="157"
|
||||
y="-1509"
|
||||
width="148"
|
||||
height="125"
|
||||
id="rect4" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
id="Activities"
|
||||
class="cls-1"
|
||||
clip-path="url(#clip-Activities)">
|
||||
<g
|
||||
id="Group_42"
|
||||
data-name="Group 42"
|
||||
transform="translate(-268 -1985)">
|
||||
<ellipse
|
||||
id="Ellipse_37"
|
||||
data-name="Ellipse 37"
|
||||
class="cls-2"
|
||||
cx="74"
|
||||
cy="14.785"
|
||||
rx="74"
|
||||
ry="14.785"
|
||||
transform="translate(425 571.43)"
|
||||
style="fill:#000000;fill-opacity:0.06666667" />
|
||||
<rect
|
||||
id="Rectangle_80"
|
||||
data-name="Rectangle 80"
|
||||
class="cls-3"
|
||||
width="94.182"
|
||||
height="110.215"
|
||||
transform="translate(451.909 476)" />
|
||||
<g
|
||||
id="Group_41"
|
||||
data-name="Group 41"
|
||||
transform="translate(467.043 493)">
|
||||
<rect
|
||||
id="Rectangle_81"
|
||||
data-name="Rectangle 81"
|
||||
class="cls-4"
|
||||
width="44.456"
|
||||
height="5.625"
|
||||
transform="translate(21.16 0.549)" />
|
||||
<rect
|
||||
id="Rectangle_82"
|
||||
data-name="Rectangle 82"
|
||||
class="cls-4"
|
||||
width="33.342"
|
||||
height="5.625"
|
||||
transform="translate(21.16 11.652)" />
|
||||
<rect
|
||||
id="Rectangle_83"
|
||||
data-name="Rectangle 83"
|
||||
class="cls-4"
|
||||
width="44.456"
|
||||
height="5.625"
|
||||
transform="translate(21.16 30.772)" />
|
||||
<rect
|
||||
id="Rectangle_84"
|
||||
data-name="Rectangle 84"
|
||||
class="cls-4"
|
||||
width="33.342"
|
||||
height="5.625"
|
||||
transform="translate(21.16 41.875)" />
|
||||
<rect
|
||||
id="Rectangle_85"
|
||||
data-name="Rectangle 85"
|
||||
class="cls-4"
|
||||
width="44.456"
|
||||
height="5.625"
|
||||
transform="translate(21.16 61.291)" />
|
||||
<rect
|
||||
id="Rectangle_86"
|
||||
data-name="Rectangle 86"
|
||||
class="cls-4"
|
||||
width="33.342"
|
||||
height="5.625"
|
||||
transform="translate(21.16 72.393)" />
|
||||
<ellipse
|
||||
id="Ellipse_38"
|
||||
data-name="Ellipse 38"
|
||||
class="cls-4"
|
||||
cx="7.007"
|
||||
cy="7"
|
||||
rx="7.007"
|
||||
ry="7"
|
||||
transform="translate(0 0)" />
|
||||
<ellipse
|
||||
id="Ellipse_39"
|
||||
data-name="Ellipse 39"
|
||||
class="cls-4"
|
||||
cx="7.007"
|
||||
cy="7"
|
||||
rx="7.007"
|
||||
ry="7"
|
||||
transform="translate(0 31)" />
|
||||
<ellipse
|
||||
id="Ellipse_40"
|
||||
data-name="Ellipse 40"
|
||||
class="cls-4"
|
||||
cx="7.007"
|
||||
cy="7"
|
||||
rx="7.007"
|
||||
ry="7"
|
||||
transform="translate(0 61)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,257 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="157 -1305 148 125"
|
||||
preserveAspectRatio="xMinYMid meet"
|
||||
version="1.1"
|
||||
id="svg34"
|
||||
sodipodi:docname="courses.svg"
|
||||
inkscape:version="0.92.1 r15371">
|
||||
<metadata
|
||||
id="metadata38">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="744"
|
||||
inkscape:window-height="480"
|
||||
id="namedview36"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.888"
|
||||
inkscape:cx="74"
|
||||
inkscape:cy="62.5"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Group_44" />
|
||||
<defs
|
||||
id="defs7">
|
||||
<style
|
||||
id="style2">
|
||||
.cls-1 {
|
||||
clip-path: url(#clip-Courses);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #c4c8cc;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
<clipPath
|
||||
id="clip-Courses">
|
||||
<rect
|
||||
x="157"
|
||||
y="-1305"
|
||||
width="148"
|
||||
height="125"
|
||||
id="rect4" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
id="Courses"
|
||||
class="cls-1"
|
||||
clip-path="url(#clip-Courses)">
|
||||
<g
|
||||
id="Group_44"
|
||||
data-name="Group 44"
|
||||
transform="translate(-268 -1781)">
|
||||
<ellipse
|
||||
id="Ellipse_41"
|
||||
data-name="Ellipse 41"
|
||||
class="cls-2"
|
||||
cx="74"
|
||||
cy="14.785"
|
||||
rx="74"
|
||||
ry="14.785"
|
||||
transform="translate(425 571.43)"
|
||||
style="fill:#000000;fill-opacity:0.06666667" />
|
||||
<rect
|
||||
id="Rectangle_87"
|
||||
data-name="Rectangle 87"
|
||||
class="cls-3"
|
||||
width="95.097"
|
||||
height="110.215"
|
||||
transform="translate(451.909 476)" />
|
||||
<g
|
||||
id="Group_43"
|
||||
data-name="Group 43"
|
||||
transform="translate(464.04 494)">
|
||||
<rect
|
||||
id="Rectangle_88"
|
||||
data-name="Rectangle 88"
|
||||
class="cls-4"
|
||||
width="31.043"
|
||||
height="34"
|
||||
transform="translate(0)" />
|
||||
<rect
|
||||
id="Rectangle_89"
|
||||
data-name="Rectangle 89"
|
||||
class="cls-4"
|
||||
width="31.043"
|
||||
height="34"
|
||||
transform="translate(0 42)" />
|
||||
<rect
|
||||
id="Rectangle_90"
|
||||
data-name="Rectangle 90"
|
||||
class="cls-4"
|
||||
width="31.067"
|
||||
height="34"
|
||||
transform="translate(39.005)" />
|
||||
<rect
|
||||
id="Rectangle_91"
|
||||
data-name="Rectangle 91"
|
||||
class="cls-4"
|
||||
width="31.067"
|
||||
height="34"
|
||||
transform="translate(39.005 42)" />
|
||||
<rect
|
||||
id="Rectangle_92"
|
||||
data-name="Rectangle 92"
|
||||
class="cls-3"
|
||||
width="23.023"
|
||||
height="3.18"
|
||||
transform="translate(3.081 16.549)" />
|
||||
<rect
|
||||
id="Rectangle_93"
|
||||
data-name="Rectangle 93"
|
||||
class="cls-3"
|
||||
width="23.023"
|
||||
height="3.18"
|
||||
transform="translate(3.081 58.549)" />
|
||||
<rect
|
||||
id="Rectangle_94"
|
||||
data-name="Rectangle 94"
|
||||
class="cls-3"
|
||||
width="23.023"
|
||||
height="3.18"
|
||||
transform="translate(43.122 16.549)" />
|
||||
<rect
|
||||
id="Rectangle_95"
|
||||
data-name="Rectangle 95"
|
||||
class="cls-3"
|
||||
width="23.023"
|
||||
height="3.18"
|
||||
transform="translate(43.122 58.549)" />
|
||||
<rect
|
||||
id="Rectangle_96"
|
||||
data-name="Rectangle 96"
|
||||
class="cls-3"
|
||||
width="14.014"
|
||||
height="3.18"
|
||||
transform="translate(3.081 21.825)" />
|
||||
<rect
|
||||
id="Rectangle_97"
|
||||
data-name="Rectangle 97"
|
||||
class="cls-3"
|
||||
width="18.845"
|
||||
height="3.18"
|
||||
transform="translate(3.081 26.825)" />
|
||||
<rect
|
||||
id="Rectangle_98"
|
||||
data-name="Rectangle 98"
|
||||
class="cls-3"
|
||||
width="14.014"
|
||||
height="3.18"
|
||||
transform="translate(3.081 63.825)" />
|
||||
<rect
|
||||
id="Rectangle_99"
|
||||
data-name="Rectangle 99"
|
||||
class="cls-3"
|
||||
width="18.845"
|
||||
height="3.18"
|
||||
transform="translate(3.081 68.825)" />
|
||||
<rect
|
||||
id="Rectangle_100"
|
||||
data-name="Rectangle 100"
|
||||
class="cls-3"
|
||||
width="14.014"
|
||||
height="3.18"
|
||||
transform="translate(43.122 21.825)" />
|
||||
<rect
|
||||
id="Rectangle_101"
|
||||
data-name="Rectangle 101"
|
||||
class="cls-3"
|
||||
width="18.845"
|
||||
height="3.18"
|
||||
transform="translate(43.122 26.825)" />
|
||||
<rect
|
||||
id="Rectangle_102"
|
||||
data-name="Rectangle 102"
|
||||
class="cls-3"
|
||||
width="14.014"
|
||||
height="3.18"
|
||||
transform="translate(43.122 63.825)" />
|
||||
<rect
|
||||
id="Rectangle_103"
|
||||
data-name="Rectangle 103"
|
||||
class="cls-3"
|
||||
width="18.845"
|
||||
height="3.18"
|
||||
transform="translate(43.122 68.825)" />
|
||||
<ellipse
|
||||
id="Ellipse_42"
|
||||
data-name="Ellipse 42"
|
||||
class="cls-3"
|
||||
cx="5.658"
|
||||
cy="5.652"
|
||||
rx="5.658"
|
||||
ry="5.652"
|
||||
transform="translate(3.003 3.55)" />
|
||||
<ellipse
|
||||
id="Ellipse_43"
|
||||
data-name="Ellipse 43"
|
||||
class="cls-3"
|
||||
cx="5.658"
|
||||
cy="5.652"
|
||||
rx="5.658"
|
||||
ry="5.652"
|
||||
transform="translate(3.003 45.55)" />
|
||||
<ellipse
|
||||
id="Ellipse_44"
|
||||
data-name="Ellipse 44"
|
||||
class="cls-3"
|
||||
cx="5.658"
|
||||
cy="5.652"
|
||||
rx="5.658"
|
||||
ry="5.652"
|
||||
transform="translate(43.044 3.55)" />
|
||||
<ellipse
|
||||
id="Ellipse_45"
|
||||
data-name="Ellipse 45"
|
||||
class="cls-3"
|
||||
cx="5.658"
|
||||
cy="5.652"
|
||||
rx="5.658"
|
||||
ry="5.652"
|
||||
transform="translate(43.044 45.55)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.7 KiB |
|
@ -18,26 +18,31 @@ import { IonicModule } from 'ionic-angular';
|
|||
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 { CoreCoursesOverviewEventsComponent } from '../components/overview-events/overview-events';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseListItemComponent
|
||||
CoreCoursesCourseListItemComponent,
|
||||
CoreCoursesOverviewEventsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseListItemComponent
|
||||
CoreCoursesCourseListItemComponent,
|
||||
CoreCoursesOverviewEventsComponent
|
||||
]
|
||||
})
|
||||
export class CoreCoursesComponentsModule {}
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<title>{{course.progress}}%</title>
|
||||
<circle class="circle percent-{{course.progress}}" r="27.5" cx="35" cy="35"/>
|
||||
<!-- Commenting circle because it throws an error in console:
|
||||
Cannot assign to read only property 'className' of object '[object SVGCircleElement]'
|
||||
<circle class="circle percent-{{course.progress}}" r="27.5" cx="35" cy="35"/> -->
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<ng-template #eventTemplate let-event="event">
|
||||
<a ion-item core-link captureLink="true" class="mm-course-module-handler item-media" [href]="event.url" [title]="event.name" [class.item-badge-right-phone]="event.action && event.action.showitemcount">
|
||||
<img [src]="event.iconUrl" core-external-content alt="" role="presentation" *ngIf="event.iconUrl">
|
||||
<button ion-button clear class="item-note hidden-phone text-right" (click)="action($event, event.action.url)" [title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
|
||||
{{event.action.name}}
|
||||
<ion-badge color="dark" class="badge-inline text-center" item-end *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
|
||||
</button>
|
||||
<ion-badge color="dark" class="hidden-tablet" item-end *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
|
||||
<p class="item-heading"><core-format-text [text]="event.name"></core-format-text></p>
|
||||
<p>{{event.timesort * 1000 | coreFormatDate:"dfmediumdate" }} <core-format-text *ngIf="showCourse" [text]="event.course.fullnamedisplay"></core-format-text></p>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="recentlyOverdue.length > 0">
|
||||
<h5 class="assertive mm-timelinetitle">{{ 'core.courses.recentlyoverdue' | translate }}</h5>
|
||||
<ul ion-list class="block">
|
||||
<li *ngFor="let event of recentlyOverdue">
|
||||
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="today.length > 0">
|
||||
<h5 class="mm-timelinetitle">{{ 'core.today' | translate }}</h5>
|
||||
<ul ion-list class="block">
|
||||
<li *ngFor="let event of today">
|
||||
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="next7Days.length > 0">
|
||||
<h5 class="mm-timelinetitle">{{ 'core.courses.next7days' | translate }}</h5>
|
||||
<ul ion-list class="block">
|
||||
<li *ngFor="let event of next7Days">
|
||||
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="next30Days.length > 0">
|
||||
<h5 class="mm-timelinetitle">{{ 'core.courses.next30days' | translate }}</h5>
|
||||
<ul ion-list class="block">
|
||||
<li *ngFor="let event of next30Days">
|
||||
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="future.length > 0">
|
||||
<h5 class="mm-timelinetitle">{{ 'core.courses.future' | translate }}</h5>
|
||||
<ul ion-list class="block">
|
||||
<li *ngFor="let event of future">
|
||||
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
<div padding class="text-center" *ngIf="canLoadMore && !empty">
|
||||
<!-- Button and spinner to show more attempts. -->
|
||||
<button *ngIf="!loadingMore" ion-button block (click)="loadMoreEvents()">{{ 'core.loadmore' | translate }}</button>
|
||||
<ion-spinner *ngIf="loadingMore"></ion-spinner>
|
||||
</div>
|
||||
|
||||
<core-empty-box *ngIf="empty && showCourse" image="assets/img/icons/activities.svg" [message]="'core.courses.noevents' | translate"></core-empty-box>
|
||||
<core-empty-box *ngIf="empty && !showCourse" [message]="'core.courses.noevents' | translate"></core-empty-box>
|
|
@ -0,0 +1,2 @@
|
|||
core-courses-course-progress {
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
// (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, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core';
|
||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
|
||||
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
|
||||
import * as moment from 'moment';
|
||||
|
||||
/**
|
||||
* Directive to render a list of events in course overview.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-courses-overview-events',
|
||||
templateUrl: 'overview-events.html'
|
||||
})
|
||||
export class CoreCoursesOverviewEventsComponent implements OnChanges {
|
||||
@Input() events: any[]; // The events to render.
|
||||
@Input() showCourse?: boolean|string; // Whether to show the course name.
|
||||
@Input() canLoadMore?: boolean; // Whether more events can be loaded.
|
||||
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded.
|
||||
|
||||
empty: boolean;
|
||||
loadingMore: boolean;
|
||||
recentlyOverdue: any[] = [];
|
||||
today: any[] = [];
|
||||
next7Days: any[] = [];
|
||||
next30Days: any[] = [];
|
||||
future: any[] = [];
|
||||
|
||||
constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider) {
|
||||
this.loadMore = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}) {
|
||||
this.showCourse = this.utils.isTrueOrOne(this.showCourse);
|
||||
|
||||
if (changes.events) {
|
||||
this.updateEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the events by time.
|
||||
*
|
||||
* @param {number} start Number of days to start getting events from today. E.g. -1 will get events from yesterday.
|
||||
* @param {number} [end] Number of days after the start.
|
||||
*/
|
||||
protected filterEventsByTime(start: number, end?: number) {
|
||||
start = moment().add(start, 'days').unix();
|
||||
end = typeof end != 'undefined' ? moment().add(end, 'days').unix() : end;
|
||||
|
||||
return this.events.filter((event) => {
|
||||
if (end) {
|
||||
return start <= event.timesort && event.timesort < end;
|
||||
}
|
||||
|
||||
return start <= event.timesort;
|
||||
}).map((event) => {
|
||||
// @todo: event.iconUrl = this.courseProvider.getModuleIconSrc(event.icon.component);
|
||||
return event;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the events displayed.
|
||||
*/
|
||||
protected updateEvents() {
|
||||
this.empty = !this.events || this.events.length <= 0;
|
||||
if (!this.empty) {
|
||||
this.recentlyOverdue = this.filterEventsByTime(-14, 0);
|
||||
this.today = this.filterEventsByTime(0, 1);
|
||||
this.next7Days = this.filterEventsByTime(1, 7);
|
||||
this.next30Days = this.filterEventsByTime(7, 30);
|
||||
this.future = this.filterEventsByTime(30);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more events clicked.
|
||||
*/
|
||||
loadMoreEvents() {
|
||||
this.loadingMore = true;
|
||||
this.loadMore.emit();
|
||||
// this.loadMore().finally(function() {
|
||||
// scope.loadingMore = false;
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Action clicked.
|
||||
*
|
||||
* @param {Event} e Click event.
|
||||
* @param {string} url Url of the action.
|
||||
*/
|
||||
action(e: Event, url: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Fix URL format.
|
||||
url = this.textUtils.decodeHTMLEntities(url);
|
||||
|
||||
let modal = this.domUtils.showModalLoading();
|
||||
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
|
||||
// @todo
|
||||
// $mmContentLinksHelper.handleLink(url).then((treated) => {
|
||||
// if (!treated) {
|
||||
// return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
|
||||
// }
|
||||
// }).finally(() => {
|
||||
// modal.dismiss();
|
||||
// });
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CoreCoursesProvider } from './providers/courses';
|
||||
import { CoreCoursesMainMenuHandler } from './providers/handlers';
|
||||
import { CoreCoursesMyOverviewProvider } from './providers/my-overview';
|
||||
import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
|
||||
|
||||
@NgModule({
|
||||
|
@ -23,7 +24,8 @@ import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
|
|||
],
|
||||
providers: [
|
||||
CoreCoursesProvider,
|
||||
CoreCoursesMainMenuHandler
|
||||
CoreCoursesMainMenuHandler,
|
||||
CoreCoursesMyOverviewProvider
|
||||
],
|
||||
exports: []
|
||||
})
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.",
|
||||
"categories": "Course categories",
|
||||
"confirmselfenrol": "Are you sure you want to enrol yourself in this course?",
|
||||
"courseoverview": "Course overview",
|
||||
"courses": "Courses",
|
||||
"downloadcourses": "Download courses",
|
||||
"enrolme": "Enrol me",
|
||||
|
@ -13,19 +14,34 @@
|
|||
"errorselfenrol": "An error occurred while self enrolling.",
|
||||
"filtermycourses": "Filter my courses",
|
||||
"frontpage": "Front page",
|
||||
"future": "Future",
|
||||
"inprogress": "In progress",
|
||||
"morecourses": "More courses",
|
||||
"mycourses": "My courses",
|
||||
"next30days": "Next 30 days",
|
||||
"next7days": "Next 7 days",
|
||||
"nocourses": "No course information to show.",
|
||||
"nocoursesfuture": "No future courses",
|
||||
"nocoursesinprogress": "No in progress courses",
|
||||
"nocoursesoverview": "No courses",
|
||||
"nocoursespast": "No past courses",
|
||||
"nocoursesyet": "No courses in this category",
|
||||
"noevents": "No upcoming activities due",
|
||||
"nosearchresults": "There were no results from your search",
|
||||
"notenroled": "You are not enrolled in this course",
|
||||
"notenrollable": "You cannot enrol yourself in this course.",
|
||||
"password": "Enrolment key",
|
||||
"past": "Past",
|
||||
"paymentrequired": "This course requires a payment for entry.",
|
||||
"paypalaccepted": "PayPal payments accepted",
|
||||
"recentlyoverdue": "Recently overdue",
|
||||
"search": "Search",
|
||||
"searchcourses": "Search courses",
|
||||
"searchcoursesadvice": "You can use the search courses button to find courses to access as a guest or enrol yourself in courses that allow it.",
|
||||
"selfenrolment": "Self enrolment",
|
||||
"sendpaymentbutton": "Send payment via PayPal",
|
||||
"sortbycourses": "Sort by courses",
|
||||
"sortbydates": "Sort by dates",
|
||||
"timeline": "Timeline",
|
||||
"totalcoursesearchresults": "Total courses: {{$a}}"
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title>{{ 'core.courses.courseoverview' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<button *ngIf="searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="search"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="timeline.loaded || timelineCourses.loaded || courses.loaded" (ionRefresh)="refreshMyOverview($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<div class="tabs tabs-striped tabs-free mm-tabs-color">
|
||||
<a ion-button class="tab-item" [class.active]="tabShown == 'timeline'" (click)="switchTab('timeline')">{{ 'core.courses.timeline' | translate }}</a>
|
||||
<a ion-button class="tab-item" [class.active]="tabShown == 'courses'" (click)="switchTab('courses')">{{ 'core.courses.courses' | translate }}</a>
|
||||
</div>
|
||||
|
||||
<div padding>
|
||||
<ion-item class="mm-full-width-centered mm-timelinesort" [hidden]="tabShown != 'timeline' || !(timeline.loaded || timelineCourses.loaded)">
|
||||
<ion-select [(ngModel)]="timeline.sort" (ngModelChange)="switchSort()">
|
||||
<ion-option value="sortbydates">{{ 'core.courses.sortbydates' | translate }}</ion-option>
|
||||
<ion-option value="sortbycourses">{{ 'core.courses.sortbycourses' | translate }}</ion-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<core-loading [hideUntil]="timeline.loaded" [hidden]="tabShown != 'timeline' || timeline.sort != 'sortbydates'" class="mm-loading-center">
|
||||
<core-courses-overview-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMoreTimeline()"></core-courses-overview-events>
|
||||
</core-loading>
|
||||
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="tabShown != 'timeline' || timeline.sort != 'sortbycourses'" class="block mm-loading-center">
|
||||
<div class="block mm-course-grid">
|
||||
<core-courses-course-progress *ngFor="let course of timelineCourses.courses" [course]="course" roundProgress="true" showSummary="true">
|
||||
<core-courses-overview-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMoreCourse(course)"></core-courses-overview-events>
|
||||
</core-courses-course-progress>
|
||||
</div>
|
||||
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursesoverview' | translate"></core-empty-box>
|
||||
</core-loading>
|
||||
<core-loading [hideUntil]="courses.loaded" [hidden]="tabShown != 'courses'" class="mm-loading-center">
|
||||
<ion-row class="row-no-padding padding-bottom" [hidden]="showFilter">
|
||||
<ion-col col-11>
|
||||
<ion-item class="mm-full-width-centered">
|
||||
<ion-select [(ngModel)]="courses.selected">
|
||||
<ion-option value="inprogress">{{ 'core.courses.inprogress' | translate }}</ion-option>
|
||||
<ion-option value="future">{{ 'core.courses.future' | translate }}</ion-option>
|
||||
<ion-option value="past">{{ 'core.courses.past' | translate }}</ion-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
<ion-col col-1 class="text-right" [hidden]="!courses[courses.selected] || !courses[courses.selected].length">
|
||||
<!-- @todo: Context menu. -->
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<div [hidden]="!showFilter" class="mm-filter-box">
|
||||
<button ion-button icon-only clear color="dark" (click)="switchFilter()">
|
||||
<ion-icon name="close-circle"></ion-icon>
|
||||
</button>
|
||||
<ion-item>
|
||||
<ion-input type="text" name="filter" [(ngModel)]="courses.filter" (ngModelChange)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate"></ion-input>
|
||||
<ion-icon name="funnel" class="placeholder-icon"></ion-icon>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<div [class.mm-course-grid]="showGrid">
|
||||
<core-courses-course-progress *ngFor="let course of filteredCourses" [course]="course" [roundProgress]="showGrid" [showSummary]="showGrid"></core-courses-course-progress>
|
||||
</div>
|
||||
|
||||
<core-empty-box *ngIf="courses[courses.selected].length == 0 && courses.selected == 'inprogress'" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursesinprogress' | translate"></core-empty-box>
|
||||
<core-empty-box *ngIf="courses[courses.selected].length == 0 && courses.selected == 'future'" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursesfuture' | translate"></core-empty-box>
|
||||
<core-empty-box *ngIf="courses[courses.selected].length == 0 && courses.selected == 'past'" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursespast' | translate"></core-empty-box>
|
||||
</core-loading>
|
||||
</div>
|
||||
</ion-content>
|
|
@ -0,0 +1,33 @@
|
|||
// (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 { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreCoursesMyOverviewPage } from './my-overview';
|
||||
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCoursesMyOverviewPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreCoursesComponentsModule,
|
||||
IonicPageModule.forChild(CoreCoursesMyOverviewPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class CoreCoursesMyOverviewPageModule {}
|
|
@ -0,0 +1,3 @@
|
|||
page-core-courses-my-courses {
|
||||
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { IonicPage, NavController } from 'ionic-angular';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { CoreCoursesProvider } from '../../providers/courses';
|
||||
import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview';
|
||||
import * as moment from 'moment';
|
||||
|
||||
/**
|
||||
* Page that displays My Overview.
|
||||
*/
|
||||
@IonicPage()
|
||||
@Component({
|
||||
selector: 'page-core-courses-my-overview',
|
||||
templateUrl: 'my-overview.html',
|
||||
})
|
||||
export class CoreCoursesMyOverviewPage {
|
||||
tabShown = 'courses';
|
||||
timeline = {
|
||||
sort: 'sortbydates',
|
||||
events: [],
|
||||
loaded: false,
|
||||
canLoadMore: undefined
|
||||
};
|
||||
timelineCourses = {
|
||||
courses: [],
|
||||
loaded: false,
|
||||
canLoadMore: false
|
||||
};
|
||||
courses = {
|
||||
selected: 'inprogress',
|
||||
loaded: false,
|
||||
filter: '',
|
||||
past: [],
|
||||
inprogress: [],
|
||||
future: []
|
||||
};
|
||||
showGrid = true;
|
||||
showFilter = false;
|
||||
searchEnabled: boolean;
|
||||
filteredCourses: any[];
|
||||
|
||||
protected prefetchIconInitialized = false;
|
||||
protected myCoursesObserver;
|
||||
protected siteUpdatedObserver;
|
||||
|
||||
constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad() {
|
||||
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
|
||||
|
||||
this.switchTab(this.tabShown);
|
||||
|
||||
// @todo: Course download.
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the timeline.
|
||||
*
|
||||
* @param {number} [afterEventId] The last event id.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchMyOverviewTimeline(afterEventId?: number) : Promise<any> {
|
||||
return this.myOverviewProvider.getActionEventsByTimesort(afterEventId).then((events) => {
|
||||
this.timeline.events = events.events;
|
||||
this.timeline.canLoadMore = events.canLoadMore;
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the timeline by courses.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchMyOverviewTimelineByCourses() : Promise<any> {
|
||||
return this.fetchUserCourses().then((courses) => {
|
||||
let today = moment().unix(),
|
||||
courseIds;
|
||||
courses = courses.filter((course) => {
|
||||
return course.startdate <= today && (!course.enddate || course.enddate >= today);
|
||||
});
|
||||
|
||||
this.timelineCourses.courses = courses;
|
||||
if (courses.length > 0) {
|
||||
courseIds = courses.map((course) => {
|
||||
return course.id;
|
||||
});
|
||||
|
||||
return this.myOverviewProvider.getActionEventsByCourses(courseIds).then((courseEvents) => {
|
||||
this.timelineCourses.courses.forEach((course) => {
|
||||
course.events = courseEvents[course.id].events;
|
||||
course.canLoadMore = courseEvents[course.id].canLoadMore;
|
||||
});
|
||||
});
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the courses for my overview.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchMyOverviewCourses() : Promise<any> {
|
||||
return this.fetchUserCourses().then((courses) => {
|
||||
const today = moment().unix();
|
||||
|
||||
this.courses.past = [];
|
||||
this.courses.inprogress = [];
|
||||
this.courses.future = [];
|
||||
|
||||
courses.forEach((course) => {
|
||||
if (course.startdate > today) {
|
||||
// Courses that have not started yet.
|
||||
this.courses.future.push(course);
|
||||
} else if (course.enddate && course.enddate < today) {
|
||||
// Courses that have already ended.
|
||||
this.courses.past.push(course);
|
||||
} else {
|
||||
// Courses still in progress.
|
||||
this.courses.inprogress.push(course);
|
||||
}
|
||||
});
|
||||
|
||||
this.courses.filter = '';
|
||||
this.showFilter = false;
|
||||
this.filteredCourses = this.courses[this.courses.selected];
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user courses.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchUserCourses() : Promise<any> {
|
||||
let courseIds;
|
||||
return this.coursesProvider.getUserCourses().then((courses) => {
|
||||
courseIds = courses.map((course) => {
|
||||
return course.id;
|
||||
});
|
||||
|
||||
// Load course options of the course.
|
||||
return this.coursesProvider.getCoursesOptions(courseIds).then((options) => {
|
||||
courses.forEach((course) => {
|
||||
course.showProgress = true;
|
||||
course.progress = isNaN(parseInt(course.progress, 10)) ? false : parseInt(course.progress, 10);
|
||||
|
||||
course.navOptions = options.navOptions[course.id];
|
||||
course.admOptions = options.admOptions[course.id];
|
||||
});
|
||||
|
||||
return courses.sort((a, b) => {
|
||||
const compareA = a.fullname.toLowerCase(),
|
||||
compareB = b.fullname.toLowerCase();
|
||||
|
||||
return compareA.localeCompare(compareB);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the filter.
|
||||
*/
|
||||
switchFilter() {
|
||||
this.showFilter = !this.showFilter;
|
||||
this.courses.filter = '';
|
||||
this.filteredCourses = this.courses[this.courses.selected];
|
||||
}
|
||||
|
||||
/**
|
||||
* The filter has changed.
|
||||
*
|
||||
* @param {string} newValue New filter value.
|
||||
*/
|
||||
filterChanged(newValue: string) {
|
||||
if (!newValue || !this.courses[this.courses.selected]) {
|
||||
this.filteredCourses = this.courses[this.courses.selected];
|
||||
} else {
|
||||
this.filteredCourses = this.courses[this.courses.selected].filter((course) => {
|
||||
return course.fullname.indexOf(newValue) > -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch grid/list view.
|
||||
*/
|
||||
switchGrid() {
|
||||
this.showGrid = !this.showGrid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} refresher Refresher.
|
||||
*/
|
||||
refreshMyOverview(refresher: any) {
|
||||
let promises = [];
|
||||
|
||||
if (this.tabShown == 'timeline') {
|
||||
promises.push(this.myOverviewProvider.invalidateActionEventsByTimesort());
|
||||
promises.push(this.myOverviewProvider.invalidateActionEventsByCourses());
|
||||
}
|
||||
|
||||
promises.push(this.coursesProvider.invalidateUserCourses());
|
||||
// promises.push(this.coursesDelegate.clearAndInvalidateCoursesOptions());
|
||||
|
||||
return Promise.all(promises).finally(() => {
|
||||
switch (this.tabShown) {
|
||||
case 'timeline':
|
||||
switch (this.timeline.sort) {
|
||||
case 'sortbydates':
|
||||
return this.fetchMyOverviewTimeline();
|
||||
case 'sortbycourses':
|
||||
return this.fetchMyOverviewTimelineByCourses();
|
||||
}
|
||||
break;
|
||||
case 'courses':
|
||||
return this.fetchMyOverviewCourses();
|
||||
}
|
||||
}).finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change timeline sort being viewed.
|
||||
*/
|
||||
switchSort() {
|
||||
switch (this.timeline.sort) {
|
||||
case 'sortbydates':
|
||||
if (!this.timeline.loaded) {
|
||||
this.fetchMyOverviewTimeline().finally(() => {
|
||||
this.timeline.loaded = true;
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'sortbycourses':
|
||||
if (!this.timelineCourses.loaded) {
|
||||
this.fetchMyOverviewTimelineByCourses().finally(() => {
|
||||
this.timelineCourses.loaded = true;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change tab being viewed.
|
||||
*
|
||||
* @param {string} tab Tab to display.
|
||||
*/
|
||||
switchTab(tab: string) {
|
||||
this.tabShown = tab;
|
||||
switch (this.tabShown) {
|
||||
case 'timeline':
|
||||
if (!this.timeline.loaded) {
|
||||
this.fetchMyOverviewTimeline().finally(() => {
|
||||
this.timeline.loaded = true;
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'courses':
|
||||
if (!this.courses.loaded) {
|
||||
this.fetchMyOverviewCourses().finally(() => {
|
||||
this.courses.loaded = true;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more events.
|
||||
*/
|
||||
loadMoreTimeline() : Promise<any> {
|
||||
return this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more events.
|
||||
*
|
||||
* @param {any} course Course.
|
||||
*/
|
||||
loadMoreCourse(course) {
|
||||
return this.myOverviewProvider.getActionEventsByCourse(course.id, course.canLoadMore).then((courseEvents) => {
|
||||
course.events = course.events.concat(courseEvents.events);
|
||||
course.canLoadMore = courseEvents.canLoadMore;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to search courses.
|
||||
*/
|
||||
openSearch() {
|
||||
this.navCtrl.push('CoreCoursesSearchPage');
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { CoreCoursesProvider } from './courses';
|
||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate';
|
||||
import { CoreCoursesMyOverviewProvider } from '../providers/my-overview';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into main menu.
|
||||
|
@ -23,8 +24,9 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/pro
|
|||
export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
|
||||
name = 'mmCourses';
|
||||
priority = 1100;
|
||||
isOverviewEnabled: boolean;
|
||||
|
||||
constructor(private coursesProvider: CoreCoursesProvider) {}
|
||||
constructor(private coursesProvider: CoreCoursesProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
|
@ -32,21 +34,16 @@ export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
|
|||
* @return {boolean} Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): boolean|Promise<boolean> {
|
||||
let myCoursesDisabled = this.coursesProvider.isMyCoursesDisabledInSite();
|
||||
// Check if my overview is enabled.
|
||||
return this.myOverviewProvider.isEnabled().then((enabled) => {
|
||||
this.isOverviewEnabled = enabled;
|
||||
if (enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if overview side menu is available, so it won't show My courses.
|
||||
// var $mmaMyOverview = $mmAddonManager.get('$mmaMyOverview');
|
||||
// if ($mmaMyOverview) {
|
||||
// return $mmaMyOverview.isSideMenuAvailable().then(function(enabled) {
|
||||
// if (enabled) {
|
||||
// return false;
|
||||
// }
|
||||
// // Addon not enabled, check my courses.
|
||||
// return !myCoursesDisabled;
|
||||
// });
|
||||
// }
|
||||
// Addon not present, check my courses.
|
||||
return !myCoursesDisabled;
|
||||
// My overview not enabled, check if my courses is enabled.
|
||||
return !this.coursesProvider.isMyCoursesDisabledInSite();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,11 +52,20 @@ export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
|
|||
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData {
|
||||
return {
|
||||
icon: 'ionic',
|
||||
title: 'core.courses.mycourses',
|
||||
page: 'CoreCoursesMyCoursesPage',
|
||||
class: 'mm-mycourses-handler'
|
||||
};
|
||||
if (this.isOverviewEnabled) {
|
||||
return {
|
||||
icon: 'ionic',
|
||||
title: 'core.courses.courseoverview',
|
||||
page: 'CoreCoursesMyOverviewPage',
|
||||
class: 'mm-courseoverview-handler'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
icon: 'ionic',
|
||||
title: 'core.courses.mycourses',
|
||||
page: 'CoreCoursesMyCoursesPage',
|
||||
class: 'mm-mycourses-handler'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import { CoreSitesProvider } from '../../../providers/sites';
|
||||
import { CoreSite } from '../../../classes/site';
|
||||
import * as moment from 'moment';
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding course overview.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CoreCoursesMyOverviewProvider {
|
||||
public static EVENTS_LIMIT = 20;
|
||||
public static EVENTS_LIMIT_PER_COURSE = 10;
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider) {}
|
||||
|
||||
/**
|
||||
* Get calendar action events for the given course.
|
||||
*
|
||||
* @param {number} courseId Only events in this course.
|
||||
* @param {number} [afterEventId] The last seen event id.
|
||||
* @param {string} [siteId] Site ID. If not defined, use current site.
|
||||
* @return {Promise<{events: any[], canLoadMore: number}>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getActionEventsByCourse(courseId: number, afterEventId?: number, siteId?: string) :
|
||||
Promise<{events: any[], canLoadMore: number}> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
let time = moment().subtract(14, 'days').unix(), // Check two weeks ago.
|
||||
data: any = {
|
||||
timesortfrom: time,
|
||||
courseid: courseId,
|
||||
limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getActionEventsByCourseCacheKey(courseId)
|
||||
};
|
||||
|
||||
if (afterEventId) {
|
||||
data.aftereventid = afterEventId;
|
||||
}
|
||||
|
||||
return site.read('core_calendar_get_action_events_by_course', data, preSets).then((courseEvents) : any => {
|
||||
if (courseEvents && courseEvents.events) {
|
||||
return this.treatCourseEvents(courseEvents, time);
|
||||
}
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for get calendar action events for the given course value WS call.
|
||||
*
|
||||
* @param {number} courseId Only events in this course.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getActionEventsByCourseCacheKey(courseId: number) : string {
|
||||
return this.getActionEventsByCoursesCacheKey() + ':' + courseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calendar action events for a given list of courses.
|
||||
*
|
||||
* @param {number[]} courseIds Course IDs.
|
||||
* @param {string} [siteId] Site ID. If not defined, use current site.
|
||||
* @return {Promise<{[s: string]: {events: any[], canLoadMore: number}}>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getActionEventsByCourses(courseIds: number[], siteId?: string) : Promise<{[s: string]: {events: any[], canLoadMore: number}}> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
let time = moment().subtract(14, 'days').unix(), // Check two weeks ago.
|
||||
data = {
|
||||
timesortfrom: time,
|
||||
courseids: courseIds,
|
||||
limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getActionEventsByCoursesCacheKey()
|
||||
};
|
||||
|
||||
return site.read('core_calendar_get_action_events_by_courses', data, preSets).then((events) : any => {
|
||||
if (events && events.groupedbycourse) {
|
||||
let courseEvents = {};
|
||||
|
||||
events.groupedbycourse.forEach((course) => {
|
||||
courseEvents[course.courseid] = this.treatCourseEvents(course, time);
|
||||
});
|
||||
|
||||
return courseEvents;
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for get calendar action events for a given list of courses value WS call.
|
||||
*
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getActionEventsByCoursesCacheKey() : string {
|
||||
return this.getRootCacheKey() + 'bycourse';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calendar action events based on the timesort value.
|
||||
*
|
||||
* @param {number} [afterEventId] The last seen event id.
|
||||
* @param {string} [siteId] Site ID. If not defined, use current site.
|
||||
* @return {Promise<{events: any[], canLoadMore: number}>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getActionEventsByTimesort(afterEventId: number, siteId?: string) : Promise<{events: any[], canLoadMore: number}> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
let time = moment().subtract(14, 'days').unix(), // Check two weeks ago.
|
||||
data: any = {
|
||||
timesortfrom: time,
|
||||
limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getActionEventsByTimesortCacheKey(afterEventId, data.limitnum),
|
||||
getCacheUsingCacheKey: true,
|
||||
uniqueCacheKey: true
|
||||
};
|
||||
|
||||
if (afterEventId) {
|
||||
data.aftereventid = afterEventId;
|
||||
}
|
||||
|
||||
return site.read('core_calendar_get_action_events_by_timesort', data, preSets).then((events) : any => {
|
||||
if (events && events.events) {
|
||||
let canLoadMore = events.events.length >= data.limitnum ? events.lastid : undefined;
|
||||
|
||||
// Filter events by time in case it uses cache.
|
||||
events = events.events.filter((element) => {
|
||||
return element.timesort >= time;
|
||||
});
|
||||
|
||||
return {
|
||||
events: events,
|
||||
canLoadMore: canLoadMore
|
||||
};
|
||||
}
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix cache key for calendar action events based on the timesort value WS calls.
|
||||
*
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getActionEventsByTimesortPrefixCacheKey() : string {
|
||||
return this.getRootCacheKey() + 'bytimesort:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for get calendar action events based on the timesort value WS call.
|
||||
*
|
||||
* @param {number} [afterEventId] The last seen event id.
|
||||
* @param {number} [limit] Limit num of the call.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getActionEventsByTimesortCacheKey(afterEventId?: number, limit?: number) : string {
|
||||
afterEventId = afterEventId || 0;
|
||||
limit = limit || 0;
|
||||
return this.getActionEventsByTimesortPrefixCacheKey() + afterEventId + ':' + limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root cache key for the WS calls related to overview.
|
||||
*
|
||||
* @return {string} Root cache key.
|
||||
*/
|
||||
protected getRootCacheKey() : string {
|
||||
return 'myoverview:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates get calendar action events for a given list of courses WS call.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to invalidate. If not defined, use current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateActionEventsByCourses(siteId?: string) : Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByCoursesCacheKey());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates get calendar action events based on the timesort value WS call.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to invalidate. If not defined, use current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateActionEventsByTimesort(siteId?: string) : Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByTimesortPrefixCacheKey());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not My Overview is available for a certain site.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with true if available, resolved with false or rejected otherwise.
|
||||
*/
|
||||
isAvailable(siteId?: string) : Promise<boolean> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.wsAvailable('core_calendar_get_action_events_by_courses');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if My Overview 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('$mmSideMenuDelegate_mmaMyOverview');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if My Overview is available and not disabled.
|
||||
*
|
||||
* @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false otherwise.
|
||||
*/
|
||||
isEnabled() : Promise<boolean> {
|
||||
if (!this.isDisabledInSite()) {
|
||||
return this.isAvailable().catch(() => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles course events, filtering and treating if more can be loaded.
|
||||
*
|
||||
* @param {any} course Object containing response course events info.
|
||||
* @param {number} timeFrom Current time to filter events from.
|
||||
* @return {{events: any[], canLoadMore: number}} Object with course events and last loaded event id if more can be loaded.
|
||||
*/
|
||||
protected treatCourseEvents(course: any, timeFrom: number) : {events: any[], canLoadMore: number} {
|
||||
let canLoadMore : number =
|
||||
course.events.length >= CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined;
|
||||
|
||||
// Filter events by time in case it uses cache.
|
||||
course.events = course.events.filter((element) => {
|
||||
return element.timesort >= timeFrom;
|
||||
});
|
||||
|
||||
return {
|
||||
events: course.events,
|
||||
canLoadMore: canLoadMore
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue