@@ -22,26 +22,30 @@ import com.embabel.agent.web.htmx.GenericProcessingValues
2222import com.embabel.agent.web.security.EmbabelAuth2User
2323import com.embabel.movie.agent.MovieRequest
2424import com.embabel.movie.domain.MovieBuff
25+ import com.embabel.movie.domain.MovieService
2526import org.slf4j.LoggerFactory
27+ import org.springframework.http.MediaType
2628import org.springframework.security.core.annotation.AuthenticationPrincipal
2729import org.springframework.security.oauth2.core.user.OAuth2User
2830import org.springframework.stereotype.Controller
2931import org.springframework.ui.Model
30- import org.springframework.web.bind.annotation.GetMapping
31- import org.springframework.web.bind.annotation.ModelAttribute
32- import org.springframework.web.bind.annotation.PostMapping
33- import org.springframework.web.bind.annotation.RequestMapping
32+ import org.springframework.web.bind.annotation.*
3433
3534@Controller
3635@RequestMapping(value = [" /" , " /movie" ])
3736class MovieHtmxController (
3837 private val agentPlatform : AgentPlatform ,
38+ private val movieService : MovieService ,
3939) {
4040
4141 private val logger = LoggerFactory .getLogger(MovieHtmxController ::class .java)
4242
4343 @GetMapping
44- fun findMovies (model : Model ): String {
44+ fun findMovies (
45+ model : Model ,
46+ @AuthenticationPrincipal principal : OAuth2User ,
47+ ): String {
48+ model.addAttribute(" user" , movieBuff(principal))
4549 model.addAttribute(
4650 " movieRequest" , MovieRequest (
4751 preference = " A film with a remarkable musical score" ,
@@ -57,9 +61,7 @@ class MovieHtmxController(
5761 @AuthenticationPrincipal
5862 principal : OAuth2User ,
5963 ): String {
60-
61- val movieBuff = (principal as ? EmbabelAuth2User )?.getUser() as ? MovieBuff
62- ? : error(" User is not a movie buff. Please register as a movie buff to find movies." )
64+ val movieBuff = movieBuff(principal)
6365 logger.info(" Finding movies for user {} named {}" , movieBuff.email, movieBuff.name)
6466
6567 // Convert form to domain objects
@@ -79,6 +81,7 @@ class MovieHtmxController(
7981 )
8082
8183 model.addAttribute(" movieRequest" , movieRequest)
84+ model.addAttribute(" user" , movieBuff)
8285 GenericProcessingValues (
8386 agentProcess = agentProcess,
8487 pageTitle = " Finding Movies" ,
@@ -89,5 +92,94 @@ class MovieHtmxController(
8992 agentPlatform.start(agentProcess)
9093 return " common/processing"
9194 }
95+
96+ /* *
97+ * View all ratings for the current user
98+ */
99+ @GetMapping(" /ratings" )
100+ fun viewRatings (
101+ model : Model ,
102+ @AuthenticationPrincipal principal : OAuth2User
103+ ): String {
104+ val movieBuff = movieBuff(principal)
105+ val limit = 10
106+ val ratings = movieService.getUserRatings(movieBuff, 0 , limit)
107+
108+ model.addAttribute(" movieBuff" , movieBuff)
109+ model.addAttribute(" ratings" , ratings)
110+ model.addAttribute(" offset" , ratings.size)
111+ model.addAttribute(" limit" , limit)
112+ model.addAttribute(" hasMore" , ratings.size < movieBuff.movieRatings.size)
113+
114+ return " movie-ratings"
115+ }
116+
117+ /* *
118+ * Load more ratings for infinite scroll
119+ */
120+ @GetMapping(" /ratings/more" , produces = [MediaType .TEXT_HTML_VALUE ])
121+ fun loadMoreRatings (
122+ @RequestParam offset : Int ,
123+ @RequestParam limit : Int = 10,
124+ model : Model ,
125+ @AuthenticationPrincipal principal : OAuth2User ,
126+ ): String {
127+ val movieBuff = movieBuff(principal)
128+ logger.info(" Loading more ratings for user {} at offset {}" , movieBuff.email, offset)
129+ val ratings = movieService.getUserRatings(movieBuff, offset, limit)
130+
131+ model.addAttribute(" ratings" , ratings)
132+ model.addAttribute(" offset" , offset + ratings.size)
133+ model.addAttribute(" limit" , limit)
134+ model.addAttribute(" hasMore" , offset + ratings.size < movieBuff.movieRatings.size)
135+
136+ return " fragments/rating-items"
137+ }
138+
139+ /* *
140+ * Show form to add a new rating
141+ */
142+ @GetMapping(" /ratings/add" )
143+ fun showRatingForm (model : Model ): String {
144+ return " add-rating-form"
145+ }
146+
147+ /* *
148+ * Process a new rating submission
149+ */
150+ @PostMapping(" /ratings/add" )
151+ fun addRating (
152+ @RequestParam title : String ,
153+ @RequestParam rating : Int ,
154+ @AuthenticationPrincipal principal : OAuth2User ,
155+ model : Model
156+ ): String {
157+ val movieBuff = movieBuff(principal)
158+
159+ // Since OneThroughTen is just a typealias for Int, we can use the rating directly
160+ // after validating it's in the correct range
161+ if (rating !in 1 .. 10 ) {
162+ error(" Rating must be between 1 and 10" )
163+ }
164+
165+ movieService.rate(movieBuff, title, rating)
166+ logger.info(" Added rating for movie '{}' with score {} by user {}" , title, rating, movieBuff.email)
167+
168+ // Return the newly added rating as HTML fragment
169+ val updatedMovieBuff = movieService.findMovieBuffByEmail(movieBuff.email)
170+ ? : error(" Could not find movie buff after adding rating" )
171+
172+ val latestRating = updatedMovieBuff.movieRatings.minByOrNull { it.movie.title }
173+
174+ model.addAttribute(" ratings" , listOfNotNull(latestRating))
175+ return " fragments/rating-items"
176+ }
177+
178+ private fun movieBuff (
179+ principal : OAuth2User
180+ ): MovieBuff {
181+ return (principal as ? EmbabelAuth2User )?.getUser() as ? MovieBuff
182+ ? : error(" User is not a movie buff. Please register as a movie buff to perform this action." )
183+ }
92184}
93185
0 commit comments