From 7f86e3b658c563cd58ab0615a607c624de6786b1 Mon Sep 17 00:00:00 2001 From: sylvain Date: Fri, 13 Jun 2025 12:07:29 +0200 Subject: [PATCH 1/2] ajout de la logique complete du back pour les news article + ajout de la table article --- .../controller/FavoriteArticleController.java | 36 ++++++++++++++++++ .../backend/controller/UserController.java | 12 +++--- .../backend/entity/FavoriteArticle.java | 38 +++++++++++++++++++ .../repository/FavoriteArticleRepository.java | 11 ++++++ .../service/FavoriteArticleService.java | 30 +++++++++++++++ app/src/main/resources/application.yml | 2 +- docker-compose.yml | 2 +- 7 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/controller/FavoriteArticleController.java create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/entity/FavoriteArticle.java create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/repository/FavoriteArticleRepository.java create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/FavoriteArticleController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/FavoriteArticleController.java new file mode 100644 index 0000000..e71aa0d --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/FavoriteArticleController.java @@ -0,0 +1,36 @@ +package fr.parisnanterre.greentrip.backend.controller; +import fr.parisnanterre.greentrip.backend.entity.FavoriteArticle; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.service.FavoriteArticleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import java.util.List; + + + +@RestController +@RequestMapping("/api/v1/favorites") +public class FavoriteArticleController { + + @Autowired + private FavoriteArticleService service; + + @GetMapping + public List getFavorites(@AuthenticationPrincipal User user) { + return service.getFavorites(user); + } + + @PostMapping + public ResponseEntity addFavorite(@AuthenticationPrincipal User user, @RequestBody FavoriteArticle article) { + service.addFavorite(user, article); + return ResponseEntity.ok().build(); + } + + @DeleteMapping + public ResponseEntity removeFavorite(@AuthenticationPrincipal User user, @RequestParam String url) { + service.removeFavorite(user, url); + return ResponseEntity.ok().build(); + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java index 757712e..2e09e54 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java @@ -1,16 +1,14 @@ package fr.parisnanterre.greentrip.backend.controller; +import fr.parisnanterre.greentrip.backend.entity.FavoriteArticle; import fr.parisnanterre.greentrip.backend.entity.User; -import fr.parisnanterre.greentrip.backend.repository.UserRepository; -import org.springframework.http.HttpStatus; +import fr.parisnanterre.greentrip.backend.service.FavoriteArticleService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import java.util.HashMap; -import java.util.Map; +import java.util.List; @RestController @RequestMapping("/api/v1/user") diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/FavoriteArticle.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/FavoriteArticle.java new file mode 100644 index 0000000..5d4b89a --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/FavoriteArticle.java @@ -0,0 +1,38 @@ +package fr.parisnanterre.greentrip.backend.entity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import fr.parisnanterre.greentrip.backend.entity.User; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + + +@Entity +@Table(name = "favorite_article") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FavoriteArticle { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + private String url; + private String description; + private String imageUrl; + private String publishedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/FavoriteArticleRepository.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/FavoriteArticleRepository.java new file mode 100644 index 0000000..a897bf8 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/FavoriteArticleRepository.java @@ -0,0 +1,11 @@ +package fr.parisnanterre.greentrip.backend.repository; +import fr.parisnanterre.greentrip.backend.entity.FavoriteArticle; +import fr.parisnanterre.greentrip.backend.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface FavoriteArticleRepository extends JpaRepository { + List findByUser(User user); + boolean existsByUserAndUrl(User user, String url); + void deleteByUserAndUrl(User user, String url); +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java new file mode 100644 index 0000000..f8fb31f --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java @@ -0,0 +1,30 @@ +package fr.parisnanterre.greentrip.backend.service; +import fr.parisnanterre.greentrip.backend.entity.FavoriteArticle; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.repository.FavoriteArticleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.List; + + +@Service +public class FavoriteArticleService { + + @Autowired + private FavoriteArticleRepository repository; + + public List getFavorites(User user) { + return repository.findByUser(user); + } + + public void addFavorite(User user, FavoriteArticle article) { + if (!repository.existsByUserAndUrl(user, article.getUrl())) { + article.setUser(user); + repository.save(article); + } + } + + public void removeFavorite(User user, String url) { + repository.deleteByUserAndUrl(user, url); + } +} diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 26e3d2e..080f6df 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -9,7 +9,7 @@ spring: add-mappings: true datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://mysql-docker:3306/greentrip + url: jdbc:mysql://mysql-docker:3307/greentrip username: root password: root hikari: diff --git a/docker-compose.yml b/docker-compose.yml index 4021d35..319285f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: greentrip ports: - - "3306:3306" + - "3307:3306" healthcheck: test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot" ] interval: 10s From d08306fdafa80648da9384baad85e565e664579d Mon Sep 17 00:00:00 2001 From: sylvain Date: Fri, 13 Jun 2025 22:49:24 +0200 Subject: [PATCH 2/2] Ajout du tracking de clic --- .../backend/config/SecurityConfiguration.java | 3 +- .../controller/NewsTrackingController.java | 107 ++++++++++++++++++ .../backend/controller/UserController.java | 10 +- .../backend/dto/ArticleViewRequest.java | 22 ++++ .../greentrip/backend/entity/ArticleView.java | 26 +++++ .../repository/ArticleViewRepository.java | 14 +++ .../service/FavoriteArticleService.java | 7 +- .../backend/service/NewsService.java | 20 ++++ 8 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/controller/NewsTrackingController.java create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/dto/ArticleViewRequest.java create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/entity/ArticleView.java create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/repository/ArticleViewRepository.java create mode 100644 app/src/main/java/fr/parisnanterre/greentrip/backend/service/NewsService.java diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java index fb728ed..945b5de 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java @@ -46,7 +46,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/v1/user/me", "/api/v1/auth/logout", "/api/trips/**", - "/api/waypoints/**" + "/api/waypoints/**", + "/api/v1/news/views/export" ).authenticated() .anyRequest().authenticated() ) diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/NewsTrackingController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/NewsTrackingController.java new file mode 100644 index 0000000..0b4dde0 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/NewsTrackingController.java @@ -0,0 +1,107 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import fr.parisnanterre.greentrip.backend.dto.ArticleViewRequest; +import fr.parisnanterre.greentrip.backend.entity.ArticleView; +import fr.parisnanterre.greentrip.backend.entity.User; +import fr.parisnanterre.greentrip.backend.repository.ArticleViewRepository; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import fr.parisnanterre.greentrip.backend.service.NewsService; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@RestController + +@RequestMapping("/api/v1/news") +public class NewsTrackingController { + + @Autowired + private ArticleViewRepository repository; + + @Autowired + private NewsService newsService; + + @GetMapping("/external-news") + public ResponseEntity fetchExternalNews(@RequestParam String topic) { + try { + String response = newsService.fetchNewsByTopic(topic); + return ResponseEntity.ok(response); + } catch (Exception e) { + return ResponseEntity.status(500).body("Erreur lors de la récupération des actualités : " + e.getMessage()); + } + } + + + @PostMapping("/view") + public ResponseEntity trackArticleView(@AuthenticationPrincipal User user, + @RequestBody ArticleViewRequest request) { + ArticleView view = new ArticleView(); + view.setTitle(request.getTitle()); + view.setUrl(request.getUrl()); + view.setViewedAt(LocalDateTime.now()); + view.setUser(user); + repository.save(view); + return ResponseEntity.ok().build(); + } + + @GetMapping("/views") + public ResponseEntity getUserViews(@AuthenticationPrincipal User user) { + return ResponseEntity.ok(repository.findByUserOrderByViewedAtDesc(user)); + } + + @GetMapping("/views/stats") + public ResponseEntity getStats(@AuthenticationPrincipal User user) { + var views = repository.findByUser(user); + long total = views.size(); + String lastTitle = views.isEmpty() ? null : views.get(views.size() - 1).getTitle(); + return ResponseEntity.ok(new StatsResponse(total, lastTitle)); + } + public record StatsResponse(long totalViews, String lastViewedTitle) {} + + @GetMapping("/views/last-days") + public ResponseEntity getLastNDaysViews(@AuthenticationPrincipal User user, + @RequestParam int days) { + LocalDateTime from = LocalDateTime.now().minusDays(days); + return ResponseEntity.ok(repository.findByUserAndViewedAtAfter(user, from)); + } + + @GetMapping("/views/top") + public ResponseEntity getTopArticles() { + List allViews = repository.findAll(); + Map grouped = allViews.stream() + .collect(Collectors.groupingBy(ArticleView::getUrl, Collectors.counting())); + + List top = grouped.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(10) + .map(e -> new TopArticle(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + + return ResponseEntity.ok(top); + } + + public record TopArticle(String url, long views) {} + + @GetMapping("/views/export") + public void exportCsv(@AuthenticationPrincipal User user, HttpServletResponse response) throws IOException { + List views = repository.findByUserOrderByViewedAtDesc(user); + response.setContentType("text/csv"); + response.setHeader("Content-Disposition", "attachment; filename=views.csv"); + + PrintWriter writer = response.getWriter(); + writer.println("Title,URL,ViewedAt"); + for (ArticleView v : views) { + writer.printf("\"%s\",%s,%s\n", + v.getTitle().replaceAll("\"", "\"\""), + v.getUrl(), + v.getViewedAt()); + } + writer.flush(); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java index 2e09e54..1180fdd 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/UserController.java @@ -7,7 +7,15 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; - +import fr.parisnanterre.greentrip.backend.repository.UserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; +import java.util.Map; +import fr.parisnanterre.greentrip.backend.entity.User; import java.util.List; @RestController diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/dto/ArticleViewRequest.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/dto/ArticleViewRequest.java new file mode 100644 index 0000000..57d3eff --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/dto/ArticleViewRequest.java @@ -0,0 +1,22 @@ +package fr.parisnanterre.greentrip.backend.dto; + +public class ArticleViewRequest { + private String title; + private String url; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/ArticleView.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/ArticleView.java new file mode 100644 index 0000000..ed64184 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/entity/ArticleView.java @@ -0,0 +1,26 @@ +package fr.parisnanterre.greentrip.backend.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Data +@NoArgsConstructor +public class ArticleView { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + private String url; + + private LocalDateTime viewedAt; + + @ManyToOne + private User user; +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/ArticleViewRepository.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/ArticleViewRepository.java new file mode 100644 index 0000000..e6c5d60 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/repository/ArticleViewRepository.java @@ -0,0 +1,14 @@ +package fr.parisnanterre.greentrip.backend.repository; + +import fr.parisnanterre.greentrip.backend.entity.ArticleView; +import org.springframework.data.jpa.repository.JpaRepository; +import fr.parisnanterre.greentrip.backend.entity.User; +import java.time.LocalDateTime; +import java.util.List; + +public interface ArticleViewRepository extends JpaRepository { + List findByUser(User user); + List findByUserOrderByViewedAtDesc(User user); + List findByUserAndViewedAtAfter(User user, LocalDateTime after); + +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java index f8fb31f..e01053a 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/FavoriteArticleService.java @@ -2,11 +2,11 @@ import fr.parisnanterre.greentrip.backend.entity.FavoriteArticle; import fr.parisnanterre.greentrip.backend.entity.User; import fr.parisnanterre.greentrip.backend.repository.FavoriteArticleRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; import java.util.List; +import org.springframework.stereotype.Service; - +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; @Service public class FavoriteArticleService { @@ -24,6 +24,7 @@ public void addFavorite(User user, FavoriteArticle article) { } } + @Transactional public void removeFavorite(User user, String url) { repository.deleteByUserAndUrl(user, url); } diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/NewsService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/NewsService.java new file mode 100644 index 0000000..1a67c85 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/NewsService.java @@ -0,0 +1,20 @@ +package fr.parisnanterre.greentrip.backend.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class NewsService { + + @Autowired + private RestTemplate restTemplate; + + private static final String API_KEY = "pub_3388b5bcd2cc4e9ea999762df279e09c"; + private static final String BASE_URL = "https://newsdata.io/api/1/news"; + + public String fetchNewsByTopic(String topic) { + String url = BASE_URL + "?apikey=" + API_KEY + "&language=fr&q=" + topic; + return restTemplate.getForObject(url, String.class); + } +}