| 1 | package edu.ucsb.cs156.dining.controllers; | |
| 2 | ||
| 3 | import edu.ucsb.cs156.dining.repositories.ReviewRepository; | |
| 4 | import edu.ucsb.cs156.dining.repositories.projections.CommonsRatingProjection; | |
| 5 | import edu.ucsb.cs156.dining.repositories.projections.CommonsReviewRow; | |
| 6 | import edu.ucsb.cs156.dining.repositories.projections.ItemRatingProjection; | |
| 7 | import edu.ucsb.cs156.dining.statuses.ModerationStatus; | |
| 8 | import edu.ucsb.cs156.dining.util.StatsWindow; | |
| 9 | import edu.ucsb.cs156.dining.util.TimeBucket; | |
| 10 | import io.swagger.v3.oas.annotations.Operation; | |
| 11 | import io.swagger.v3.oas.annotations.tags.Tag; | |
| 12 | import java.time.LocalDate; | |
| 13 | import java.time.LocalDateTime; | |
| 14 | import java.util.ArrayList; | |
| 15 | import java.util.Comparator; | |
| 16 | import java.util.HashMap; | |
| 17 | import java.util.List; | |
| 18 | import java.util.Map; | |
| 19 | import lombok.extern.slf4j.Slf4j; | |
| 20 | import org.springframework.beans.factory.annotation.Autowired; | |
| 21 | import org.springframework.data.domain.PageRequest; | |
| 22 | import org.springframework.security.access.prepost.PreAuthorize; | |
| 23 | import org.springframework.web.bind.annotation.GetMapping; | |
| 24 | import org.springframework.web.bind.annotation.RequestMapping; | |
| 25 | import org.springframework.web.bind.annotation.RequestParam; | |
| 26 | import org.springframework.web.bind.annotation.RestController; | |
| 27 | ||
| 28 | @Tag(name = "Statistics") | |
| 29 | @RequestMapping("/api/statistics") | |
| 30 | @RestController | |
| 31 | @Slf4j | |
| 32 | public class StatisticsController extends ApiController { | |
| 33 | ||
| 34 | private static final int DEFAULT_LIMIT = 10; | |
| 35 | private static final int MAX_LIMIT = 50; | |
| 36 | private static final long DEFAULT_MIN_REVIEWS = 3; | |
| 37 | ||
| 38 | @Autowired ReviewRepository reviewRepository; | |
| 39 | ||
| 40 | public record StatisticsSummary( | |
| 41 | long totalApprovedReviews, | |
| 42 | long totalMenuItemsReviewed, | |
| 43 | long totalCommonsCovered, | |
| 44 | LocalDateTime lastUpdated) {} | |
| 45 | ||
| 46 | public record RatedItem( | |
| 47 | Long itemId, | |
| 48 | String name, | |
| 49 | String diningCommonsCode, | |
| 50 | String mealCode, | |
| 51 | Double avgStars, | |
| 52 | Long reviewCount) {} | |
| 53 | ||
| 54 | public record CommonsAverage(String diningCommonsCode, Double avgStars, Long reviewCount) {} | |
| 55 | ||
| 56 | public record CommonsTimeseriesPoint( | |
| 57 | String diningCommonsCode, LocalDate bucketStart, Double avgStars, Long reviewCount) {} | |
| 58 | ||
| 59 | private record TimeseriesKey(String diningCommonsCode, LocalDate bucketStart) {} | |
| 60 | ||
| 61 | @Operation(summary = "Get a summary of review statistics") | |
| 62 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 63 | @GetMapping(value = "", produces = "application/json") | |
| 64 | public StatisticsSummary getSummary() { | |
| 65 |
1
1. getSummary : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::getSummary → KILLED |
return new StatisticsSummary( |
| 66 | reviewRepository.countByStatus(ModerationStatus.APPROVED), | |
| 67 | reviewRepository.countDistinctItemsByStatus(ModerationStatus.APPROVED), | |
| 68 | reviewRepository.countDistinctCommonsByStatus(ModerationStatus.APPROVED), | |
| 69 | reviewRepository.findMaxDateEditedByStatus(ModerationStatus.APPROVED)); | |
| 70 | } | |
| 71 | ||
| 72 | LocalDateTime sinceFor(StatsWindow window) { | |
| 73 |
1
1. sinceFor : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::sinceFor → KILLED |
return window.since(LocalDateTime.now()); |
| 74 | } | |
| 75 | ||
| 76 | private static int clampLimit(int limit) { | |
| 77 |
1
1. clampLimit : replaced int return with 0 for edu/ucsb/cs156/dining/controllers/StatisticsController::clampLimit → KILLED |
return Math.min(Math.max(limit, 1), MAX_LIMIT); |
| 78 | } | |
| 79 | ||
| 80 | private static long clampMinReviews(long minReviews) { | |
| 81 |
1
1. clampMinReviews : replaced long return with 0 for edu/ucsb/cs156/dining/controllers/StatisticsController::clampMinReviews → KILLED |
return Math.max(minReviews, 1L); |
| 82 | } | |
| 83 | ||
| 84 | private RatedItem toRatedItem(ItemRatingProjection p) { | |
| 85 |
1
1. toRatedItem : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::toRatedItem → KILLED |
return new RatedItem( |
| 86 | p.getItemId(), | |
| 87 | p.getName(), | |
| 88 | p.getDiningCommonsCode(), | |
| 89 | p.getMealCode(), | |
| 90 | p.getAvgStars(), | |
| 91 | p.getReviewCount()); | |
| 92 | } | |
| 93 | ||
| 94 | private CommonsAverage toCommonsAverage(CommonsRatingProjection p) { | |
| 95 |
1
1. toCommonsAverage : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::toCommonsAverage → KILLED |
return new CommonsAverage(p.getDiningCommonsCode(), p.getAvgStars(), p.getReviewCount()); |
| 96 | } | |
| 97 | ||
| 98 | @Operation(summary = "Get best rated menu items by average stars") | |
| 99 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 100 | @GetMapping(value = "/items/best", produces = "application/json") | |
| 101 | public List<RatedItem> bestRatedItems( | |
| 102 | @RequestParam(defaultValue = "ALL") StatsWindow window, | |
| 103 | @RequestParam(defaultValue = "" + DEFAULT_LIMIT) int limit, | |
| 104 | @RequestParam(name = "minReviews", defaultValue = "" + DEFAULT_MIN_REVIEWS) long minReviews) { | |
| 105 | int clampedLimit = clampLimit(limit); | |
| 106 | long clampedMinReviews = clampMinReviews(minReviews); | |
| 107 | ||
| 108 | LocalDateTime since = sinceFor(window); | |
| 109 | List<ItemRatingProjection> projections = | |
| 110 | reviewRepository.findTopRatedItems( | |
| 111 | ModerationStatus.APPROVED, since, clampedMinReviews, PageRequest.of(0, clampedLimit)); | |
| 112 | ||
| 113 |
1
1. bestRatedItems : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::bestRatedItems → KILLED |
return projections.stream().map(this::toRatedItem).toList(); |
| 114 | } | |
| 115 | ||
| 116 | @Operation(summary = "Get worst rated menu items by average stars") | |
| 117 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 118 | @GetMapping(value = "/items/worst", produces = "application/json") | |
| 119 | public List<RatedItem> worstRatedItems( | |
| 120 | @RequestParam(defaultValue = "ALL") StatsWindow window, | |
| 121 | @RequestParam(defaultValue = "" + DEFAULT_LIMIT) int limit, | |
| 122 | @RequestParam(name = "minReviews", defaultValue = "" + DEFAULT_MIN_REVIEWS) long minReviews) { | |
| 123 | int clampedLimit = clampLimit(limit); | |
| 124 | long clampedMinReviews = clampMinReviews(minReviews); | |
| 125 | ||
| 126 | LocalDateTime since = sinceFor(window); | |
| 127 | List<ItemRatingProjection> projections = | |
| 128 | reviewRepository.findBottomRatedItems( | |
| 129 | ModerationStatus.APPROVED, since, clampedMinReviews, PageRequest.of(0, clampedLimit)); | |
| 130 | ||
| 131 |
1
1. worstRatedItems : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::worstRatedItems → KILLED |
return projections.stream().map(this::toRatedItem).toList(); |
| 132 | } | |
| 133 | ||
| 134 | @Operation(summary = "Get average star ratings grouped by dining commons") | |
| 135 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 136 | @GetMapping(value = "/commons/averages", produces = "application/json") | |
| 137 | public List<CommonsAverage> commonsAverages( | |
| 138 | @RequestParam(defaultValue = "ALL") StatsWindow window) { | |
| 139 | LocalDateTime since = sinceFor(window); | |
| 140 | List<CommonsRatingProjection> projections = | |
| 141 | reviewRepository.findCommonsAverages(ModerationStatus.APPROVED, since); | |
| 142 | ||
| 143 |
1
1. commonsAverages : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::commonsAverages → KILLED |
return projections.stream().map(this::toCommonsAverage).toList(); |
| 144 | } | |
| 145 | ||
| 146 | static List<CommonsTimeseriesPoint> computeTimeseries( | |
| 147 | List<CommonsReviewRow> rows, TimeBucket bucket) { | |
| 148 | Map<TimeseriesKey, List<Integer>> starsByKey = new HashMap<>(); | |
| 149 | ||
| 150 | for (CommonsReviewRow row : rows) { | |
| 151 | LocalDate bucketStart = bucket.floor(row.getDateItemServed()); | |
| 152 | TimeseriesKey key = new TimeseriesKey(row.getDiningCommonsCode(), bucketStart); | |
| 153 |
1
1. lambda$computeTimeseries$0 : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::lambda$computeTimeseries$0 → KILLED |
starsByKey.computeIfAbsent(key, ignored -> new ArrayList<>()).add(row.getItemsStars()); |
| 154 | } | |
| 155 | ||
| 156 |
1
1. computeTimeseries : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::computeTimeseries → KILLED |
return starsByKey.entrySet().stream() |
| 157 | .map( | |
| 158 | entry -> { | |
| 159 | List<Integer> stars = entry.getValue(); | |
| 160 | double avgStars = stars.stream().mapToInt(Integer::intValue).average().orElse(0.0); | |
| 161 |
1
1. lambda$computeTimeseries$1 : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::lambda$computeTimeseries$1 → KILLED |
return new CommonsTimeseriesPoint( |
| 162 | entry.getKey().diningCommonsCode(), | |
| 163 | entry.getKey().bucketStart(), | |
| 164 | avgStars, | |
| 165 | (long) stars.size()); | |
| 166 | }) | |
| 167 | .sorted( | |
| 168 | Comparator.comparing(CommonsTimeseriesPoint::diningCommonsCode) | |
| 169 | .thenComparing(CommonsTimeseriesPoint::bucketStart)) | |
| 170 | .toList(); | |
| 171 | } | |
| 172 | ||
| 173 | @Operation(summary = "Get per-commons average ratings over time buckets") | |
| 174 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 175 | @GetMapping(value = "/commons/timeseries", produces = "application/json") | |
| 176 | public List<CommonsTimeseriesPoint> commonsTimeseries( | |
| 177 | @RequestParam(defaultValue = "DAY") TimeBucket bucket, | |
| 178 | @RequestParam(defaultValue = "ALL") StatsWindow window) { | |
| 179 | LocalDateTime since = sinceFor(window); | |
| 180 | List<CommonsReviewRow> rows = | |
| 181 | reviewRepository.findCommonsReviewRows(ModerationStatus.APPROVED, since); | |
| 182 | ||
| 183 |
1
1. commonsTimeseries : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::commonsTimeseries → KILLED |
return computeTimeseries(rows, bucket); |
| 184 | } | |
| 185 | } | |
Mutations | ||
| 65 |
1.1 |
|
| 73 |
1.1 |
|
| 77 |
1.1 |
|
| 81 |
1.1 |
|
| 85 |
1.1 |
|
| 95 |
1.1 |
|
| 113 |
1.1 |
|
| 131 |
1.1 |
|
| 143 |
1.1 |
|
| 153 |
1.1 |
|
| 156 |
1.1 |
|
| 161 |
1.1 |
|
| 183 |
1.1 |