mirror of https://github.com/bvn13/covid19-ru.git
added chart, added full statistics endpoint, refactoring
parent
567d8d7841
commit
0010beca9f
5
buildit
5
buildit
|
@ -3,12 +3,11 @@
|
||||||
version=0.0.2
|
version=0.0.2
|
||||||
|
|
||||||
./gradlew :covid19-db-migrator:clean :covid19-db-migrator:assemble
|
./gradlew :covid19-db-migrator:clean :covid19-db-migrator:assemble
|
||||||
./gradlew :covid19-api:clean :covid19-api:assemble
|
./gradlew :covid19-site:clean :covid19-site:assemble
|
||||||
./gradlew :covid19-scheduler:clean :covid19-scheduler:assemble
|
./gradlew :covid19-scheduler:clean :covid19-scheduler:assemble
|
||||||
|
|
||||||
[ ! -d "build" ] && mkdir "build";
|
[ ! -d "build" ] && mkdir "build";
|
||||||
|
|
||||||
cp "covid19-db-migrator/build/libs/covid19-db-migrator-$version.jar" "build/covid19-db-migrator-$version.jar"
|
cp "covid19-db-migrator/build/libs/covid19-db-migrator-$version.jar" "build/covid19-db-migrator-$version.jar"
|
||||||
cp "covid19-api/build/libs/covid19-api-$version.jar" "build/covid19-api-$version.jar"
|
cp "covid19-site/build/libs/covid19-site-$version.jar" "build/covid19-site-$version.jar"
|
||||||
cp "covid19-scheduler/build/libs/covid19-scheduler-$version.jar" "build/covid19-scheduler-$version.jar"
|
cp "covid19-scheduler/build/libs/covid19-scheduler-$version.jar" "build/covid19-scheduler-$version.jar"
|
||||||
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright [2020] [bvn13]
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.bvn13.covid19.api.controllers;
|
|
||||||
|
|
||||||
import com.bvn13.covid19.api.model.CovidStatsInfo;
|
|
||||||
import com.bvn13.covid19.api.model.CovidStatsResponse;
|
|
||||||
import com.bvn13.covid19.api.service.CovidStatsMaker;
|
|
||||||
import com.bvn13.covid19.model.entities.CovidStat;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api")
|
|
||||||
public class ApiController {
|
|
||||||
|
|
||||||
private final CovidStatsMaker covidStatsMaker;
|
|
||||||
|
|
||||||
@Value("${app.zone-id}")
|
|
||||||
private String zoneIdStr;
|
|
||||||
private ZoneId zoneId;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
zoneId = ZoneId.of(zoneIdStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public CovidStatsResponse getStatistics() {
|
|
||||||
return covidStatsMaker.findLastUpdateInfo()
|
|
||||||
.map(updateInfo -> CovidStatsResponse.builder()
|
|
||||||
.datetime(updateInfo.getDatetime())
|
|
||||||
.updatedOn(updateInfo.getCreatedOn().atZone(zoneId))
|
|
||||||
.stats(convertStats(covidStatsMaker.findCovidStatsByUpdateInfoId(updateInfo.getId())))
|
|
||||||
.build())
|
|
||||||
.orElse(CovidStatsResponse.builder().build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<CovidStatsInfo> convertStats(Collection<CovidStat> stats) {
|
|
||||||
return stats.stream()
|
|
||||||
.map(stat -> CovidStatsInfo.builder()
|
|
||||||
.region(stat.getRegion().getName())
|
|
||||||
.sick(stat.getSick())
|
|
||||||
.healed(stat.getHealed())
|
|
||||||
.died(stat.getDied())
|
|
||||||
.build())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@ dependencies {
|
||||||
compile(project(':covid19-model'))
|
compile(project(':covid19-model'))
|
||||||
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-cache'
|
implementation 'org.springframework.boot:spring-boot-starter-cache'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api;
|
package com.bvn13.covid19.site;
|
||||||
|
|
||||||
import com.bvn13.covid19.model.Covid19ModelConfig;
|
import com.bvn13.covid19.model.Covid19ModelConfig;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
|
@ -27,10 +27,10 @@ import org.springframework.context.annotation.Import;
|
||||||
@Import({
|
@Import({
|
||||||
Covid19ModelConfig.class
|
Covid19ModelConfig.class
|
||||||
})
|
})
|
||||||
public class Covid19ApiApplication {
|
public class Covid19SiteApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Covid19ApiApplication.class, args);
|
SpringApplication.run(Covid19SiteApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.bvn13.covid19.site.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Configuration
|
||||||
|
public class MainConfig {
|
||||||
|
|
||||||
|
@Value("${app.main-url}")
|
||||||
|
private String mainUrl;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.bvn13.covid19.site.controllers;
|
||||||
|
|
||||||
|
import com.bvn13.covid19.model.entities.CovidStat;
|
||||||
|
import com.bvn13.covid19.site.model.CovidAllStats;
|
||||||
|
import com.bvn13.covid19.site.model.CovidData;
|
||||||
|
import com.bvn13.covid19.site.model.CovidDayStats;
|
||||||
|
import com.bvn13.covid19.site.service.CovidStatsMaker;
|
||||||
|
import com.bvn13.covid19.site.service.CovidStatsResponseMaker;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/stats")
|
||||||
|
public class AllStatsController {
|
||||||
|
|
||||||
|
private final CovidStatsMaker covidStatsMaker;
|
||||||
|
private final CovidStatsResponseMaker covidStatsResponseMaker;
|
||||||
|
|
||||||
|
@Value("${app.zone-id}")
|
||||||
|
private String zoneIdStr;
|
||||||
|
private ZoneId zoneId;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
zoneId = ZoneId.of(zoneIdStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
public CovidAllStats getStatistics(@RequestParam("region") String regionName) {
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(regionName)) {
|
||||||
|
return constructResponseForRegion(regionName);
|
||||||
|
} else {
|
||||||
|
return constructResponseForAllRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private CovidAllStats constructResponseForRegion(String regionName) {
|
||||||
|
return CovidAllStats.builder()
|
||||||
|
.regions(Collections.singletonList(regionName))
|
||||||
|
.progress(covidStatsMaker.findAllLastUpdatesPerDay().stream()
|
||||||
|
.map(covidUpdate -> CovidDayStats.builder()
|
||||||
|
.datetime(covidUpdate.getDatetime())
|
||||||
|
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId))
|
||||||
|
.stats(convertStatsToData(findCovidStatsByUpdateIdAndRegion(covidUpdate.getId(), regionName)))
|
||||||
|
.build())
|
||||||
|
.sorted(CovidDayStats::compareTo)
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CovidAllStats constructResponseForAllRegions() {
|
||||||
|
return CovidAllStats.builder()
|
||||||
|
.regions(covidStatsMaker.findAllRegionsNames())
|
||||||
|
.progress(covidStatsMaker.findAllLastUpdatesPerDay().stream()
|
||||||
|
.map(covidUpdate -> CovidDayStats.builder()
|
||||||
|
.datetime(covidUpdate.getDatetime())
|
||||||
|
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId))
|
||||||
|
.stats(convertStatsToData(findCovidStatsByUpdateId(covidUpdate.getId())))
|
||||||
|
.build())
|
||||||
|
.sorted(CovidDayStats::compareTo)
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CovidData> convertStatsToData(Collection<CovidStat> stats) {
|
||||||
|
return covidStatsResponseMaker.convertStats(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CovidStat> findCovidStatsByUpdateIdAndRegion(long updateId, String region) {
|
||||||
|
return covidStatsMaker.findCovidStatsByUpdateInfoId(updateId).stream()
|
||||||
|
.filter(covidStat -> region.equals(covidStat.getRegion().getName()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CovidStat> findCovidStatsByUpdateId(long updateId) {
|
||||||
|
return new ArrayList<>(covidStatsMaker.findCovidStatsByUpdateInfoId(updateId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
Copyright [2020] [bvn13]
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.bvn13.covid19.site.controllers;
|
||||||
|
|
||||||
|
import com.bvn13.covid19.site.model.CovidDayStats;
|
||||||
|
import com.bvn13.covid19.site.service.CovidStatsMaker;
|
||||||
|
import com.bvn13.covid19.site.service.CovidStatsResponseMaker;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/stats")
|
||||||
|
public class LastStatsController {
|
||||||
|
|
||||||
|
private final CovidStatsMaker covidStatsMaker;
|
||||||
|
private final CovidStatsResponseMaker covidStatsResponseMaker;
|
||||||
|
|
||||||
|
@Value("${app.zone-id}")
|
||||||
|
private String zoneIdStr;
|
||||||
|
private ZoneId zoneId;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
zoneId = ZoneId.of(zoneIdStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/last")
|
||||||
|
public CovidDayStats getStatistics() {
|
||||||
|
return covidStatsMaker.findLastUpdate()
|
||||||
|
.map(covidUpdate -> CovidDayStats.builder()
|
||||||
|
.datetime(covidUpdate.getDatetime())
|
||||||
|
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId))
|
||||||
|
.stats(covidStatsResponseMaker.convertStats(covidStatsMaker.findCovidStatsByUpdateInfoId(covidUpdate.getId())))
|
||||||
|
.build())
|
||||||
|
.orElse(CovidDayStats.builder().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.bvn13.covid19.site.controllers;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/")
|
||||||
|
public class MainController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String mainPage() {
|
||||||
|
return "main-page";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.bvn13.covid19.site.controllers;
|
||||||
|
|
||||||
|
import com.bvn13.covid19.site.service.CovidStatsMaker;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/regions")
|
||||||
|
public class RegionsController {
|
||||||
|
|
||||||
|
private final CovidStatsMaker covidStatsMaker;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<String> getAllRegions() {
|
||||||
|
return covidStatsMaker.findAllRegionsNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api.converters;
|
package com.bvn13.covid19.site.converters;
|
||||||
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api.dtos;
|
package com.bvn13.covid19.site.dtos;
|
||||||
|
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.bvn13.covid19.site.model;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@Value
|
||||||
|
public class CovidAllStats {
|
||||||
|
|
||||||
|
List<String> regions;
|
||||||
|
List<CovidDayStats> progress;
|
||||||
|
|
||||||
|
}
|
|
@ -14,28 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api.model;
|
package com.bvn13.covid19.site.model;
|
||||||
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
@Value
|
@Value
|
||||||
public class CovidStatsInfo {
|
public class CovidData {
|
||||||
|
|
||||||
String region;
|
String region;
|
||||||
long sick;
|
long sick;
|
||||||
long healed;
|
long healed;
|
||||||
long died;
|
long died;
|
||||||
|
|
||||||
@Builder
|
|
||||||
@Value
|
|
||||||
public static class Delta {
|
|
||||||
long sick;
|
|
||||||
long healed;
|
|
||||||
long died;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api.model;
|
package com.bvn13.covid19.site.model;
|
||||||
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.Singular;
|
import lombok.Singular;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
|
@ -25,11 +26,24 @@ import java.util.List;
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
@Value
|
@Value
|
||||||
public class CovidStatsResponse {
|
public class CovidDayStats implements Comparable<CovidDayStats> {
|
||||||
|
|
||||||
ZonedDateTime updatedOn;
|
ZonedDateTime updatedOn;
|
||||||
ZonedDateTime datetime;
|
ZonedDateTime datetime;
|
||||||
|
|
||||||
@Singular(value = "stats")
|
@Singular(value = "stats")
|
||||||
List<CovidStatsInfo> stats;
|
List<CovidData> stats;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NonNull CovidDayStats another) {
|
||||||
|
if (this.equals(another)) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return updatedOn.isBefore(another.getUpdatedOn())
|
||||||
|
? -1
|
||||||
|
: updatedOn.isEqual(another.getUpdatedOn())
|
||||||
|
? 0
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api.repositories;
|
package com.bvn13.covid19.site.repositories;
|
||||||
|
|
||||||
import com.bvn13.covid19.model.entities.CovidStat;
|
import com.bvn13.covid19.model.entities.CovidStat;
|
||||||
import com.bvn13.covid19.model.entities.CovidUpdate;
|
import com.bvn13.covid19.model.entities.CovidUpdate;
|
|
@ -14,22 +14,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api.repositories;
|
package com.bvn13.covid19.site.repositories;
|
||||||
|
|
||||||
import com.bvn13.covid19.api.dtos.CovidUpdateInfoDto;
|
import com.bvn13.covid19.site.dtos.CovidUpdateInfoDto;
|
||||||
import com.bvn13.covid19.model.entities.CovidUpdate;
|
import com.bvn13.covid19.model.entities.CovidUpdate;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface CovidUpdateInfosRepository extends JpaRepository<CovidUpdate, Long> {
|
public interface CovidUpdatesRepository extends JpaRepository<CovidUpdate, Long> {
|
||||||
|
|
||||||
@Query("select new com.bvn13.covid19.api.dtos.CovidUpdateInfoDto(max(U.createdOn)) from CovidUpdate U")
|
@Query("select new com.bvn13.covid19.site.dtos.CovidUpdateInfoDto(max(U.createdOn)) from CovidUpdate U")
|
||||||
Optional<CovidUpdateInfoDto> findLastUpdateInfo();
|
Optional<CovidUpdateInfoDto> findLastUpdate();
|
||||||
|
|
||||||
|
@Query("select U from CovidUpdate U where U.id in (select max(U1.id) from CovidUpdate U1 group by U1.datetime)")
|
||||||
|
Collection<CovidUpdate> findAllLastUpdatesPerDay();
|
||||||
|
|
||||||
Optional<CovidUpdate> findFirstByCreatedOn(LocalDateTime createdOn);
|
Optional<CovidUpdate> findFirstByCreatedOn(LocalDateTime createdOn);
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.bvn13.covid19.site.repositories;
|
||||||
|
|
||||||
|
import com.bvn13.covid19.model.entities.Region;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RegionsRepository extends JpaRepository<Region, Long> {
|
||||||
|
}
|
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.bvn13.covid19.api.service;
|
package com.bvn13.covid19.site.service;
|
||||||
|
|
||||||
import com.bvn13.covid19.api.repositories.CovidStatsRepository;
|
|
||||||
import com.bvn13.covid19.api.repositories.CovidUpdateInfosRepository;
|
|
||||||
import com.bvn13.covid19.model.entities.CovidStat;
|
import com.bvn13.covid19.model.entities.CovidStat;
|
||||||
import com.bvn13.covid19.model.entities.CovidUpdate;
|
import com.bvn13.covid19.model.entities.CovidUpdate;
|
||||||
|
import com.bvn13.covid19.model.entities.Region;
|
||||||
|
import com.bvn13.covid19.site.repositories.CovidStatsRepository;
|
||||||
|
import com.bvn13.covid19.site.repositories.CovidUpdatesRepository;
|
||||||
|
import com.bvn13.covid19.site.repositories.RegionsRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
@ -27,30 +29,33 @@ import org.springframework.stereotype.Component;
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Component
|
@Component
|
||||||
public class CovidStatsMaker {
|
public class CovidStatsMaker {
|
||||||
|
|
||||||
private final CovidStatsRepository covidRepository;
|
private final CovidStatsRepository statsRepository;
|
||||||
private final CovidUpdateInfosRepository updatesRepository;
|
private final CovidUpdatesRepository updatesRepository;
|
||||||
|
private final RegionsRepository regionsRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Collection<CovidStat> getLastCovidStats() {
|
public Collection<CovidStat> getLastCovidStats() {
|
||||||
return updatesRepository.findLastUpdateInfo()
|
return updatesRepository.findLastUpdate()
|
||||||
.flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn()))
|
.flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn()))
|
||||||
.map(covidRepository::findAllByUpdateInfo)
|
.map(statsRepository::findAllByUpdateInfo)
|
||||||
.orElse(Collections.emptyList());
|
.orElse(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
cacheNames = "covid-last-update-info",
|
cacheNames = "covid-last-update",
|
||||||
unless = "#result == null"
|
unless = "#result == null"
|
||||||
)
|
)
|
||||||
@Transactional
|
@Transactional
|
||||||
public Optional<CovidUpdate> findLastUpdateInfo() {
|
public Optional<CovidUpdate> findLastUpdate() {
|
||||||
return updatesRepository.findLastUpdateInfo()
|
return updatesRepository.findLastUpdate()
|
||||||
.flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn()));
|
.flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +65,23 @@ public class CovidStatsMaker {
|
||||||
unless = "#result == null || #result.size() <= 0"
|
unless = "#result == null || #result.size() <= 0"
|
||||||
)
|
)
|
||||||
public Collection<CovidStat> findCovidStatsByUpdateInfoId(long updateInfoId) {
|
public Collection<CovidStat> findCovidStatsByUpdateInfoId(long updateInfoId) {
|
||||||
return covidRepository.findAllByUpdateInfo_Id(updateInfoId);
|
return statsRepository.findAllByUpdateInfo_Id(updateInfoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(
|
||||||
|
cacheNames = "covid-all-days-updates",
|
||||||
|
unless = "#result == null || #result.size() <= 0"
|
||||||
|
)
|
||||||
|
public Collection<CovidUpdate> findAllLastUpdatesPerDay() {
|
||||||
|
return updatesRepository.findAllLastUpdatesPerDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(
|
||||||
|
cacheNames = "covid-regions",
|
||||||
|
unless = "#result == null || #result.size() <= 0"
|
||||||
|
)
|
||||||
|
public List<String> findAllRegionsNames() {
|
||||||
|
return regionsRepository.findAll().stream().map(Region::getName).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.bvn13.covid19.site.service;
|
||||||
|
|
||||||
|
import com.bvn13.covid19.model.entities.CovidStat;
|
||||||
|
import com.bvn13.covid19.site.model.CovidData;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CovidStatsResponseMaker {
|
||||||
|
|
||||||
|
public List<CovidData> convertStats(Collection<CovidStat> stats) {
|
||||||
|
return stats.stream()
|
||||||
|
.map(stat -> CovidData.builder()
|
||||||
|
.region(stat.getRegion().getName())
|
||||||
|
.sick(stat.getSick())
|
||||||
|
.healed(stat.getHealed())
|
||||||
|
.died(stat.getDied())
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ server:
|
||||||
|
|
||||||
app:
|
app:
|
||||||
zone-id: Europe/Moscow
|
zone-id: Europe/Moscow
|
||||||
|
main-url: http://localhost:8080
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
|
@ -12,7 +13,7 @@ spring:
|
||||||
type: caffeine
|
type: caffeine
|
||||||
caffeine:
|
caffeine:
|
||||||
spec: expireAfterWrite=15m
|
spec: expireAfterWrite=15m
|
||||||
cache-names: covid-last-update-info, covid-stats-by-update-info-id
|
cache-names: covid-last-update, covid-all-days-updates, covid-stats-by-update-info-id, covid-regions
|
||||||
|
|
||||||
flyway:
|
flyway:
|
||||||
enabled: false
|
enabled: false
|
|
@ -0,0 +1 @@
|
||||||
|
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n():"function"==typeof define&&define.amd?define(n):n()}(0,function(){"use strict";function e(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})}function n(e){return!(!e||"undefined"==typeof e.length)}function t(){}function o(e){if(!(this instanceof o))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],c(e,this)}function r(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var o;try{o=t(e._value)}catch(r){return void f(n.promise,r)}i(n.promise,o)}else(1===e._state?i:f)(n.promise,e._value)})):e._deferreds.push(n)}function i(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var t=n.then;if(n instanceof o)return e._state=3,e._value=n,void u(e);if("function"==typeof t)return void c(function(e,n){return function(){e.apply(n,arguments)}}(t,n),e)}e._state=1,e._value=n,u(e)}catch(r){f(e,r)}}function f(e,n){e._state=2,e._value=n,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var n=0,t=e._deferreds.length;t>n;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype["catch"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype["finally"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&"object"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=o});
|
|
@ -0,0 +1,158 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Home page</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
|
||||||
|
<!-- <link rel="stylesheet" th:href="@{/css/chart.min.css}">-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
<span th:text="'Today is: ' + ${#dates.format(#dates.createNow(), 'dd MMM yyyy HH:mm')}" th:remove="tag"></span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a th:href="${@mainConfig.getMainUrl()} + '/stats/last'">Последние данные</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a th:href="${@mainConfig.getMainUrl()} + '/stats/all'">Все данные</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="region">Регион</label>
|
||||||
|
<select id="region"></select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="width: 75%;">
|
||||||
|
<canvas id="chart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
var mainUrl = [[${@mainConfig.getMainUrl()}]];
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
(function ($) {
|
||||||
|
function setUpRegions(regions) {
|
||||||
|
$("#region > option").each((i, el) => {
|
||||||
|
$(el).remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
_.forEach(regions, (region) => {
|
||||||
|
$("#region").append("<option value='" + region + "'>" + region + "</option>");
|
||||||
|
});
|
||||||
|
|
||||||
|
onRegionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegion() {
|
||||||
|
return document.getElementById("region").value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRegionChanged() {
|
||||||
|
var region = getRegion();
|
||||||
|
loadStatsForRegion(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStatsForRegion(region) {
|
||||||
|
fetch(mainUrl + '/stats/all?region=' + region)
|
||||||
|
.then(value => {
|
||||||
|
return value.json()
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
console.log(json);
|
||||||
|
showData(json);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showData(json) {
|
||||||
|
var labels = _.map(json.progress, (progress) => progress.datetime.substr(0, 10));
|
||||||
|
var sick = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].sick : 0);
|
||||||
|
var healed = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].healed : 0);
|
||||||
|
var died = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].died : 0);
|
||||||
|
|
||||||
|
var myChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Всего',
|
||||||
|
data: sick,
|
||||||
|
backgroundColor: 'red',
|
||||||
|
borderColor: 'red',
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: false
|
||||||
|
}, {
|
||||||
|
label: 'Выздоровело',
|
||||||
|
data: healed,
|
||||||
|
backgroundColor: 'green',
|
||||||
|
borderColor: 'green',
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: false
|
||||||
|
}, {
|
||||||
|
label: 'Умерло',
|
||||||
|
data: died,
|
||||||
|
backgroundColor: 'black',
|
||||||
|
borderColor: 'black',
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
tension: 0.000001
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: 'Дата'
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: 'Количество (чел.)'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
myChart.canvas.parentNode.style.width = '1024px';
|
||||||
|
// myChart.canvas.parentNode.style.height = '1024px';
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#region").change(() => onRegionChanged());
|
||||||
|
|
||||||
|
var ctx = document.getElementById('chart').getContext('2d');
|
||||||
|
|
||||||
|
fetch(mainUrl + '/regions')
|
||||||
|
.then(value => {
|
||||||
|
return value.json()
|
||||||
|
})
|
||||||
|
.then(json => setUpRegions(json));
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script th:src="@{/js/jquery-3.5.0.min.js}"></script>
|
||||||
|
<script th:src="@{/js/polyfill.min.js}"></script>
|
||||||
|
<script th:src="@{/js/fetch.min.js}"></script>
|
||||||
|
<script th:src="@{/js/chart.min.js}"></script>
|
||||||
|
<script th:src="@{/js/lodash.js}"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bvn13.covid19.api;
|
package com.bvn13.covid19.site;
|
||||||
|
|
||||||
import com.bvn13.covid19.model.Covid19ModelConfig;
|
import com.bvn13.covid19.model.Covid19ModelConfig;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -14,7 +14,7 @@ import org.springframework.test.context.support.AnnotationConfigContextLoader;
|
||||||
classes = { Covid19ModelConfig.class },
|
classes = { Covid19ModelConfig.class },
|
||||||
loader = AnnotationConfigContextLoader.class)
|
loader = AnnotationConfigContextLoader.class)
|
||||||
@Profile("test")
|
@Profile("test")
|
||||||
class Covid19ApiApplicationTests {
|
class Covid19SiteApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
|
@ -8,5 +8,5 @@ rootProject.name = 'covid19'
|
||||||
|
|
||||||
include ':covid19-model'
|
include ':covid19-model'
|
||||||
include ':covid19-db-migrator'
|
include ':covid19-db-migrator'
|
||||||
include ':covid19-api'
|
include ':covid19-site'
|
||||||
include ':covid19-scheduler'
|
include ':covid19-scheduler'
|
Loading…
Reference in New Issue