Skip to content

Commit 330c67f

Browse files
authored
feat(News): Cache news items for the home page (#2326)
1 parent 201bc26 commit 330c67f

9 files changed

Lines changed: 73 additions & 58 deletions

src/app/home/admin-latest-news/admin-latest-news.component.spec.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@ import { News } from '../../domain/news';
66
import { of } from 'rxjs';
77
import { MockProvider } from 'ng-mocks';
88
import { ActivatedRoute } from '@angular/router';
9+
import { UserService } from '../../services/user.service';
910

1011
describe('AdminLatestNewsComponent', () => {
1112
let component: AdminLatestNewsComponent;
1213
let fixture: ComponentFixture<AdminLatestNewsComponent>;
1314

1415
beforeEach(async () => {
15-
const newsServiceSpy = jasmine.createSpyObj<NewsService>(['getAllNews']);
16+
const newsServiceSpy = jasmine.createSpyObj<NewsService>([
17+
'getNewsPageNews',
18+
'getHomePageNews'
19+
]);
20+
const userServiceSpy = jasmine.createSpyObj<UserService>(['isSignedIn']);
1621
const news1 = new News({
1722
id: 1,
1823
date: '2026-02-01 19:14:23.0',
@@ -38,11 +43,17 @@ describe('AdminLatestNewsComponent', () => {
3843
owner: undefined
3944
});
4045

41-
newsServiceSpy.getAllNews.and.callFake(() => of<News[]>([news1, news2, news3]));
46+
newsServiceSpy.getNewsPageNews.and.callFake(() => of<News[]>([news1, news2, news3]));
47+
newsServiceSpy.getHomePageNews.and.callFake(() => of<News[]>([news1, news2, news3]));
48+
userServiceSpy.isSignedIn.and.callFake(() => true);
4249

4350
await TestBed.configureTestingModule({
4451
imports: [AdminLatestNewsComponent],
45-
providers: [{ provide: NewsService, useValue: newsServiceSpy }, MockProvider(ActivatedRoute)]
52+
providers: [
53+
{ provide: NewsService, useValue: newsServiceSpy },
54+
{ provide: UserService, useValue: userServiceSpy },
55+
MockProvider(ActivatedRoute)
56+
]
4657
}).compileComponents();
4758

4859
fixture = TestBed.createComponent(AdminLatestNewsComponent);
@@ -54,11 +65,10 @@ describe('AdminLatestNewsComponent', () => {
5465
expect(component).toBeTruthy();
5566
});
5667

57-
it('should filter and sort topics', fakeAsync(() => {
68+
it('should sort topics', fakeAsync(() => {
5869
component.ngOnInit();
5970
tick();
6071
expect(component['topics']).toBeTruthy();
61-
expect(component['topics'].length).toBe(2);
6272
expect(component['topics'][0].title).toBe('Test News 3');
6373
}));
6474
});

src/app/home/admin-latest-news/admin-latest-news.component.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
22
import { LatestNewsComponent } from '../latest-news/latest-news.component';
33
import { News } from '../../domain/news';
44
import { NewsService } from '../../services/news.service';
5+
import { UserService } from '../../services/user.service';
56

67
@Component({
78
imports: [LatestNewsComponent],
@@ -12,13 +13,19 @@ export class AdminLatestNewsComponent {
1213
protected loaded: boolean = false;
1314
protected topics: News[] = [];
1415

15-
constructor(private newsService: NewsService) {}
16+
constructor(
17+
private newsService: NewsService,
18+
private userService: UserService
19+
) {}
1620

1721
ngOnInit(): void {
18-
this.newsService.getAllNews().subscribe((news) => {
19-
this.topics = news
20-
.filter((news) => news.type === 'public')
21-
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
22+
const newsType = this.userService.isSignedIn() ? 'publicAndTeacher' : 'publicOnly';
23+
this.retrieveNews(newsType);
24+
}
25+
26+
private retrieveNews(newsType: string): void {
27+
this.newsService.getHomePageNews(newsType).subscribe((news) => {
28+
this.topics = news.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
2229
this.loaded = true;
2330
});
2431
}

src/app/home/home.component.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
22
import { HomeComponent } from './home.component';
33
import { ConfigService } from '../services/config.service';
44
import { provideHttpClient } from '@angular/common/http';
5-
import { MockComponent, MockProvider } from 'ng-mocks';
5+
import { MockComponent, MockProviders } from 'ng-mocks';
66
import { CallToActionComponent } from '../modules/shared/call-to-action/call-to-action.component';
77
import { provideAnimations } from '@angular/platform-browser/animations';
88
import { ActivatedRoute } from '@angular/router';
9+
import { UserService } from '../services/user.service';
910

1011
describe('HomeComponent', () => {
1112
let component: HomeComponent;
@@ -18,7 +19,7 @@ describe('HomeComponent', () => {
1819
ConfigService,
1920
provideAnimations(),
2021
provideHttpClient(),
21-
MockProvider(ActivatedRoute)
22+
MockProviders(ActivatedRoute, UserService)
2223
]
2324
}).compileComponents();
2425
}));

src/app/home/latest-news/latest-news.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<ng-container i18n>What's New?</ng-container>&nbsp;
99
</h2>
1010
<ul>
11-
@for (topic of threeTopics; track topic; let index = $index) {
11+
@for (topic of topics; track topic; let index = $index) {
1212
@if (!xsScreen || index === 0) {
1313
<li [ngClass]="{ 'mat-small': smallScreen, 'mat-body-1': !smallScreen }">
1414
@if (isDiscourseNewsAvailable) {

src/app/home/latest-news/latest-news.component.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export class LatestNewsComponent {
2121
@Input() isDiscourseNewsAvailable: boolean;
2222
@Input() loaded: boolean;
2323
protected smallScreen: boolean;
24-
protected threeTopics: Topic[];
2524
@Input() topics: Topic[];
2625
protected xsScreen: boolean;
2726

@@ -39,8 +38,4 @@ export class LatestNewsComponent {
3938
this.xsScreen = result.matches;
4039
});
4140
}
42-
43-
ngOnChanges(): void {
44-
this.threeTopics = this.topics?.slice(0, 3) ?? [];
45-
}
4641
}

src/app/news/news.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ <h1 class="accent flex justify-center items-center gap-2 mat-display-1">
88
<section>
99
<div class="content-block">
1010
<app-timeline>
11-
@for (newsItem of allNewsItems; track newsItem; let i = $index) {
11+
@for (newsItem of newsItems; track newsItem; let i = $index) {
1212
@if (i < 10 || showAll) {
1313
<app-timeline-item id="{{ newsItem.id }}">
1414
<app-timeline-item-label>
@@ -37,7 +37,7 @@ <h2 class="accent-1">{{ newsItem.title }}</h2>
3737
}
3838
}
3939
</app-timeline>
40-
@if (allNewsItems.length > 10 && !showAll) {
40+
@if (newsItems.length > 10 && !showAll) {
4141
<div class="content-block__actions">
4242
<a mat-raised-button color="accent" (click)="showAll = true" i18n>Show More</a>
4343
</div>

src/app/news/news.component.spec.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import { ActivatedRoute } from '@angular/router';
12
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { MockProvider } from 'ng-mocks';
24
import { NewsComponent } from './news.component';
35
import { NewsService } from '../services/news.service';
46
import { News } from '../domain/news';
5-
import { BehaviorSubject, Observable } from 'rxjs';
7+
import { Observable } from 'rxjs';
68
import { User } from '../domain/user';
7-
import { ActivatedRoute } from '@angular/router';
8-
import { MockProvider, MockProviders } from 'ng-mocks';
99
import { UserService } from '../services/user.service';
1010

1111
const createNewsItem = (id, date, type, title, news, owner) => {
@@ -39,7 +39,7 @@ const news2Text =
3939
'We have been working on a new portal website. The new website will have a more modern user interface.';
4040

4141
export class MockNewsService {
42-
getAllNews(): Observable<News[]> {
42+
getNewsPageNews(): Observable<News[]> {
4343
return new Observable((observer) => {
4444
const allNewsItems: News[] = [];
4545
const user1 = createUser(100, 'Spongebob', 'Squarepants', 'Spongebob Squarepants');
@@ -76,11 +76,8 @@ describe('NewsComponent', () => {
7676
};
7777

7878
beforeEach(() => {
79-
const userServiceSpy = jasmine.createSpyObj<UserService>(['getUser']);
80-
const user = new User({ roles: ['teacher'] });
81-
userServiceSpy.getUser.and.callFake(() => {
82-
return new BehaviorSubject<User>(user);
83-
});
79+
const userServiceSpy = jasmine.createSpyObj<UserService>(['isSignedIn']);
80+
userServiceSpy.isSignedIn.and.callFake(() => true);
8481

8582
TestBed.configureTestingModule({
8683
imports: [NewsComponent],
@@ -105,28 +102,28 @@ describe('NewsComponent', () => {
105102
});
106103

107104
it('should display the news date', () => {
108-
const newsItem1 = getNewsItem(1);
105+
const newsItem1 = getNewsItem(0);
109106
const date1 = getNewsDate(newsItem1);
110107
expect(date1).toContain('Oct 16, 2018');
111-
const newsItem2 = getNewsItem(0);
108+
const newsItem2 = getNewsItem(1);
112109
const date2 = getNewsDate(newsItem2);
113110
expect(date2).toContain('Sep 21, 2018');
114111
});
115112

116113
it('should display the news title', () => {
117-
const newsItem1 = getNewsItem(1);
114+
const newsItem1 = getNewsItem(0);
118115
const title1 = getNewsTitle(newsItem1);
119116
expect(title1).toContain(news1Title);
120-
const newsItem2 = getNewsItem(0);
117+
const newsItem2 = getNewsItem(1);
121118
const title2 = getNewsTitle(newsItem2);
122119
expect(title2).toContain(news2Title);
123120
});
124121

125122
it('should display the news text', () => {
126-
const newsItem1 = getNewsItem(1);
123+
const newsItem1 = getNewsItem(0);
127124
const text1 = getNewsText(newsItem1);
128125
expect(text1).toContain(news1Text);
129-
const newsItem2 = getNewsItem(0);
126+
const newsItem2 = getNewsItem(1);
130127
const text2 = getNewsText(newsItem2);
131128
expect(text2).toContain(news2Text);
132129
});

src/app/news/news.component.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { UserService } from '../services/user.service';
3131
templateUrl: './news.component.html'
3232
})
3333
export class NewsComponent implements OnInit {
34-
allNewsItems: any = [];
34+
newsItems: any = [];
3535
newsShowMore: boolean[] = [];
3636
showAll: boolean = false;
3737
showTeacherNews: boolean = false;
@@ -44,30 +44,18 @@ export class NewsComponent implements OnInit {
4444
) {}
4545

4646
ngOnInit() {
47-
this.showTeacherNewsIfLoggedIn();
48-
this.retrieveNews();
47+
const newsType = this.userService.isSignedIn() ? 'publicAndTeacher' : 'publicOnly';
48+
this.retrieveNews(newsType);
4949
}
5050

51-
private showTeacherNewsIfLoggedIn(): void {
52-
this.userService.getUser().subscribe((user) => {
53-
this.showTeacherNews = user && user.roles?.length > 0;
54-
});
55-
}
56-
57-
private retrieveNews(): void {
58-
this.newsService.getAllNews().subscribe((allNewsItems: News[]) => {
59-
this.prepareNewsItems(allNewsItems);
60-
this.newsShowMore = new Array(this.allNewsItems.length).fill(false);
51+
private retrieveNews(newsType: string): void {
52+
this.newsService.getNewsPageNews(newsType).subscribe((news: News[]) => {
53+
this.newsItems = news.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
54+
this.newsShowMore = new Array(this.newsItems.length).fill(false);
6155
this.scrollToFragmentNewsItem();
6256
});
6357
}
6458

65-
private prepareNewsItems(allNewsItems: News[]) {
66-
this.allNewsItems = allNewsItems
67-
.filter((newsItem) => this.showTeacherNews || newsItem.type === 'public')
68-
.reverse();
69-
}
70-
7159
private scrollToFragmentNewsItem() {
7260
setTimeout(() => {
7361
const fragment = this.route.snapshot.fragment;

src/app/services/news.service.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
1+
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
12
import { Injectable } from '@angular/core';
2-
import { HttpClient, HttpHeaders } from '@angular/common/http';
33
import { News } from '../domain/news';
44
import { Observable } from 'rxjs';
55

66
@Injectable({
77
providedIn: 'root'
88
})
99
export class NewsService {
10-
private newsUrl = '/api/news';
10+
private allNewsEndpoint = '/api/news';
11+
private homeNewsEndpoint = '/api/news/home';
1112

1213
constructor(private http: HttpClient) {}
1314

14-
getAllNews(): Observable<News[]> {
15+
getNewsPageNews(type: string): Observable<News[]> {
16+
const params = this.buildUrlParams(type);
17+
return this.getNews(this.allNewsEndpoint, params);
18+
}
19+
20+
getHomePageNews(type: string): Observable<News[]> {
21+
const params = this.buildUrlParams(type);
22+
return this.getNews(this.homeNewsEndpoint, params);
23+
}
24+
25+
private getNews(endpoint: string, params: HttpParams): Observable<News[]> {
1526
const headers = new HttpHeaders({ 'Cache-Control': 'no-cache' });
16-
return this.http.get(this.newsUrl, { headers: headers }) as Observable<News[]>;
27+
return this.http.get(endpoint, { headers: headers, params: params }) as Observable<News[]>;
28+
}
29+
30+
private buildUrlParams(type: string): HttpParams {
31+
let params = new HttpParams();
32+
params = params.set('type', type);
33+
return params;
1734
}
1835
}

0 commit comments

Comments
 (0)