Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

409 whats new page #426

Merged
merged 38 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0192dc6
1. Add new page WhatsNewPage.vue with styling
Sep 27, 2023
0af004d
1. Add pagination and improvements
Sep 27, 2023
015d4f9
Add localization
Sep 27, 2023
ab613ba
Added pagination and sample data within the cards
Sep 28, 2023
78cf9c3
Add row feature (2rows x 3 elements) to the what's new page
Sep 28, 2023
bc7d038
1. Add RestTemplate client
Sep 28, 2023
008b8bd
Build pull request fetch logic
Oct 3, 2023
1f5729f
Merge branch 'main' into 409-whats-new-page
Oct 3, 2023
d05ed2b
Add validation and error handling.
Oct 3, 2023
9c8d36a
Add tests
Oct 3, 2023
7e128d5
Add github api version header
Oct 3, 2023
803c6ba
Add error handling related third party api errors
Oct 3, 2023
54c7a23
Optimization
Oct 3, 2023
5c7e609
Add tests
Oct 4, 2023
bec1cc7
Merge branch 'main' into 409-whats-new-page
Oct 9, 2023
96e6894
Change sorting by updated at
Oct 9, 2023
475608e
Add date new date format and localization for the date
Oct 9, 2023
126be17
Add two types of sorting:
Oct 9, 2023
1901130
Add two types of sorting:
Oct 9, 2023
13372e3
Add improvements and pagination.
Oct 10, 2023
ccc3be6
Internal page bugfix
Oct 10, 2023
9b050f1
Improvements
Oct 10, 2023
f362ea9
Improvements
Oct 11, 2023
1cb2c23
1.Add 'What's new page link to the FooterBar.vue. Removed from the he…
Oct 11, 2023
088fbef
Merge branch 'main' into 409-whats-new-page
Oct 18, 2023
228ebc0
Fix
Oct 18, 2023
8fa4780
fix: remove autofix of linter on forked PRs
stritti Oct 19, 2023
9bdc4b2
fix: precent comit on backend action
stritti Oct 19, 2023
51ccaec
Merge branch 'main' into 409-whats-new-page
Oct 21, 2023
82c6931
Add support for dark mode
Oct 21, 2023
9e2087f
Merge remote-tracking branch 'upstream/fix/456-build-fail-at-forked-p…
Oct 21, 2023
2df7879
Merge branch 'main' into 409-whats-new-page
Oct 25, 2023
62d93b2
Merge branch 'main' into 409-whats-new-page
Oct 30, 2023
7bad76f
Fix
Oct 30, 2023
ba6235d
lint fix
Nov 1, 2023
20ea664
remove translations, remove imports & adapt to project pattern
SponsoredByPuma Nov 8, 2023
a93b599
fix frontend build error
SponsoredByPuma Nov 8, 2023
36b24a7
Merge branch 'main' into 409-whats-new-page
Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.diveni.backend.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder){
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,10 @@ public class ErrorMessages {

public static String failedToRetrieveUsernameErrorMessage = "failed to retrieve current user";

public static String noPullRequestsMatchingCriteriaMessage = "There are no pull requests matching the criteria";
public static String invalidPullRequestStateMessage = "Invalid pull request state. Valid states: open, closed, all";
public static String maxPullRequestsPerPageMessage = "Max 100 pull requests can be returned per page";
public static String serverLimitReachedMessage = "API rate limit reached";

private ErrorMessages() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.diveni.backend.controller;

import io.diveni.backend.model.news.PullRequest;
import io.diveni.backend.service.news.GithubApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/news")
public class NewsController {

public static final String STATE_PARAM = "state";
public static final String PAGE_PARAM = "page";
public static final String PER_PAGE_PARAM = "per_page";
public static final String IS_MERGED = "is_merged";
public static final String SORT = "sort";
public static final String SORT_DIRECTION = "direction";

private GithubApiService service;

@Autowired
public NewsController(GithubApiService service) {
this.service = service;
}

@GetMapping("/pull-requests")
public ResponseEntity<PullRequest[]> getPullRequests(@RequestParam(name = STATE_PARAM, defaultValue = "closed") String state,
@RequestParam(name = SORT, defaultValue = "created") String sort,
@RequestParam(name = SORT_DIRECTION, defaultValue = "asc") String direction,
@RequestParam(name = IS_MERGED, defaultValue = "false") Boolean isMerged,
@RequestParam(name = PER_PAGE_PARAM, defaultValue = "50") Integer perPage,
@RequestParam(name = PAGE_PARAM, defaultValue = "1") Integer page) {

return ResponseEntity.ok(service.getPullRequests(state,sort,direction, isMerged, perPage, page));
SponsoredByPuma marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.diveni.backend.model.news;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDate;
SponsoredByPuma marked this conversation as resolved.
Show resolved Hide resolved
import java.time.LocalDateTime;
import java.util.Map;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PullRequest {

private int number;

@JsonProperty("html_url")
private String htmlUrl;

private String title;

@JsonProperty("merged_at")
private LocalDateTime mergedAt;

@JsonProperty("updated_at")
private LocalDateTime updatedAt;

@JsonProperty("user_type")
private String userType;

@JsonProperty("user")
private void unpackNestedProperty(Map<String, String> map) {
userType = map.get("type");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.diveni.backend.service.news;

import io.diveni.backend.controller.ErrorMessages;
import io.diveni.backend.model.news.PullRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.Arrays;
import java.util.Comparator;
SponsoredByPuma marked this conversation as resolved.
Show resolved Hide resolved
import java.util.List;

import static io.diveni.backend.controller.NewsController.*;

@Service
public class GithubApiService {

private static final List<String> VALID_STATES = List.of("open", "closed", "all");
private static final int MAX_PER_PAGE = 100;

private static final String API_VERSION_HEADER = "X-GitHub-Api-Version";

@Value("${vsc.github.api.pr.get}")
private String url;

@Value("${vsc.github.access-token}")
private String authToken;

@Value("${vsc.github.api.version}")
private String apiVersion;

private static final Logger logger = LoggerFactory.getLogger(GithubApiService.class);
private RestTemplate client;

@Autowired
public GithubApiService(RestTemplate client) {
this.client = client;
}

public PullRequest[] getPullRequests(String state, String sort, String direction, boolean isMerged, int perPage, int page) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. Do we even need sort, direction & isMerged as parameters here? If I didnt miss something, these values are hard coded in the frontend and there is no button to change them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for adding those params is extensibility in the future, if needed. For example lets say in the future there is new feature request - add options which give the user ability to sort the prs in the 'What's new page' by different params. This way only the frontend will need changes


HttpHeaders headers = new HttpHeaders();
if (authToken != null && !authToken.equals("null")) {
headers.setBearerAuth(authToken);
}
headers.set(API_VERSION_HEADER, apiVersion);

HttpEntity<Void> entity = new HttpEntity<>(null, headers);

if (!VALID_STATES.contains(state)) {
logger.warn("Bad request: {}", ErrorMessages.noPullRequestsMatchingCriteriaMessage);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ErrorMessages.invalidPullRequestStateMessage);
}

if (perPage > MAX_PER_PAGE) {
logger.warn("Bad request: {}", ErrorMessages.maxPullRequestsPerPageMessage);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ErrorMessages.maxPullRequestsPerPageMessage);
}

UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url)
.queryParam(STATE_PARAM, state)
.queryParam(PAGE_PARAM, page)
.queryParam(PER_PAGE_PARAM, perPage)
.queryParam(SORT, sort).queryParam(SORT_DIRECTION, direction);

ResponseEntity<PullRequest[]> data;
try {
data = client.exchange(builder.toUriString(), HttpMethod.GET, entity, PullRequest[].class);
} catch (Exception ex) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, ErrorMessages.serverLimitReachedMessage);
}
PullRequest[] body = data.getBody();

if (isMerged) {
body = Arrays.stream(body).filter(e -> e.getMergedAt() != null).toArray(PullRequest[]::new);
SponsoredByPuma marked this conversation as resolved.
Show resolved Hide resolved
}

logger.debug("getPullRequests({},{},{})", state, perPage, page);

return body;
}
}
7 changes: 7 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
spring.profiles.active=dev
server.error.include-message=always
spring.config.import=optional:file:./.env[.properties]

vsc.github.access-token = ${GITHUB_ACCESS_TOKEN:null}
vsc.github.owner=Sybit-Education
vsc.github.repository = Diveni
vsc.github.api.version = 2022-11-28
vsc.github.api=https://api.github.com
vsc.github.api.pr.get = ${vsc.github.api}/repos/${vsc.github.owner}/${vsc.github.repository}/pulls
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.diveni.backend.controller;

import io.diveni.backend.model.news.PullRequest;
import io.diveni.backend.service.news.GithubApiService;
SponsoredByPuma marked this conversation as resolved.
Show resolved Hide resolved
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class NewsControllerTest {

private static final String path = "/news/pull-requests";

@Value("${vsc.github.api.pr.get}")
private String url;

@Autowired
private MockMvc mockMvc;

@MockBean
private RestTemplate client;

@Test
public void getPRs_InvalidState_returnsException() throws Exception {
String invalidState = "invalid";
MockHttpServletRequestBuilder requestBuilder = get(path);
requestBuilder.param(NewsController.STATE_PARAM, invalidState);
requestBuilder.param(NewsController.PAGE_PARAM, "1");
requestBuilder.param(NewsController.PER_PAGE_PARAM, "1");
this.mockMvc.perform(requestBuilder)
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(status().reason(ErrorMessages.invalidPullRequestStateMessage));
}

@Test
public void getPRs_InvalidPerPage_returnsException() throws Exception {
MockHttpServletRequestBuilder requestBuilder = get(path);
requestBuilder.param(NewsController.STATE_PARAM, "closed");
requestBuilder.param(NewsController.PAGE_PARAM, "1");
requestBuilder.param(NewsController.PER_PAGE_PARAM, "5000");
this.mockMvc.perform(requestBuilder)
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(status().reason(ErrorMessages.maxPullRequestsPerPageMessage));
}


@Test
public void getPRs_Valid_returnsPrs() throws Exception {
PullRequest[] data = new PullRequest[]{
new PullRequest(403, "test2.com", "test2", LocalDateTime.now(), LocalDateTime.now(), "user"),
new PullRequest(404, "test.com", "test", LocalDateTime.now(), LocalDateTime.now(), "user")
};

when(client.exchange(contains(url), eq(HttpMethod.GET), any(), eq(PullRequest[].class)))
.thenReturn(ResponseEntity.ok(data.clone()));

MockHttpServletRequestBuilder requestBuilder = get(path);
requestBuilder.param(NewsController.STATE_PARAM, "closed");
requestBuilder.param(NewsController.PAGE_PARAM, "1");
requestBuilder.param(NewsController.PER_PAGE_PARAM, "10");

this.mockMvc.perform(requestBuilder)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()")
.value(Matchers.is(2)))
.andExpect(jsonPath("$[0].number").value(data[0].getNumber()))
.andExpect(jsonPath("$[0]['html_url']").value(data[0].getHtmlUrl()))
.andExpect(jsonPath("$[0]['title']").value(data[0].getTitle()))
.andExpect(jsonPath("$[0]['user_type']").value(data[0].getUserType()));

}

@Test
public void getPRs_Valid_returnsServiceUnavailable() throws Exception {

when(client.exchange(contains(url), eq(HttpMethod.GET), any(), eq(PullRequest[].class)))
.thenThrow(new RestClientException("Some error"));

MockHttpServletRequestBuilder requestBuilder = get(path);
requestBuilder.param(NewsController.STATE_PARAM, "closed");
requestBuilder.param(NewsController.PAGE_PARAM, "1");
requestBuilder.param(NewsController.PER_PAGE_PARAM, "10");

this.mockMvc.perform(requestBuilder)
.andDo(print())
.andExpect(status().isServiceUnavailable())
.andExpect(status().reason(ErrorMessages.serverLimitReachedMessage));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.diveni.backend.model.news;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;

public class PullRequestTest {

@Test
public void equal_works(){
LocalDateTime now = LocalDateTime.now();
PullRequest first = new PullRequest(1,"test.com","First",now, now,"Admin");
PullRequest second = new PullRequest(1,"test.com","First",now,now, "Admin");
PullRequest third = new PullRequest(2,"test.com","Second",LocalDateTime.now(),LocalDateTime.now(),"Usr");
Assertions.assertEquals(first,second);
Assertions.assertNotEquals(first,third);
}
}
7 changes: 7 additions & 0 deletions backend/src/test/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
spring.profiles.active=test

vsc.github.access-token = ${GITHUB_ACCESS_TOKEN:null}
vsc.github.owner=Sybit-Education
vsc.github.repository = Diveni
vsc.github.api.version = 2022-11-28
vsc.github.api=https://api.github.com
vsc.github.api.pr.get = ${vsc.github.api}/repos/${vsc.github.owner}/${vsc.github.repository}/pulls
13 changes: 12 additions & 1 deletion frontend/src/components/navigation/TopNavigationBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
<b-form>
<b-button :to="{ name: 'PrepareSessionPage' }" class="px-2 mr-2">New Session</b-button>
</b-form>
<b-form>
SponsoredByPuma marked this conversation as resolved.
Show resolved Hide resolved
<b-button :to="{name: 'WhatsNewPage'}"
variant="outline-info">
{{ $t("page.landing.news.buttons.info.label") }}
</b-button>
</b-form>
<b-form class="px-2 mr-2">

<a href="https://github.com/Sybit-Education/Diveni" target="_blank">
<img :src="require('./images/GitHub-Mark-32px.png')" height="40px" width="40px"/>
</a>
Expand All @@ -27,9 +32,15 @@
<script lang="ts">
import Vue from "vue";
import LocaleDropdown from "@/components/navigation/LocaleDropdown.vue";
import WhatsNewPage from "@/views/WhatsNewPage.vue";

export default Vue.extend({
name: "TopNavigationBar",
computed: {
WhatsNewPage() {
return WhatsNewPage
}
},
components: { LocaleDropdown },
});
</script>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class Constants {

memberUpdateCloseSession = "SESSION_CLOSED";

newsPageSize = 9;

botUserType = "Bot";

// eslint-disable-next-line class-methods-use-this
public joinSessionRoute(sessionID: string) {
return `/sessions/${sessionID}/join`;
Expand Down
Loading