Merge: concurrent_collections: Add implementation of has method
[nit.git] / contrib / nitrpg / src / web.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Display `nitrpg` data as a website.
18 module web
19
20 import nitcorn
21 import templates
22
23 # A custom action forn `nitrpg`.
24 class RpgAction
25 super Action
26
27 # Root URL is used as a prefix for all URL generated by the actions.
28 var root_url: String
29
30 # Github oauth token used for GithubAPI.
31 var auth: String is lazy do return get_github_oauth
32
33 # API client used to import data from Github.
34 var api: GithubAPI is lazy do
35 var api = new GithubAPI(auth)
36 return api
37 end
38
39 init do
40 super
41 if auth.is_empty then
42 print "Error: Invalid Github oauth token!"
43 exit 1
44 end
45 end
46
47 # Return an Error reponse page.
48 fun bad_request(msg: String): HttpResponse do
49 var rsp = new HttpResponse(400)
50 var page = new NitRpgPage(root_url)
51 var error = new ErrorPanel(msg)
52 page.flow_panels.add error
53 rsp.body = page
54 return rsp
55 end
56
57 # Returns the game with `name` or null if no game exists with this name.
58 fun load_game(name: String): nullable Game do
59 var repo = api.load_repo(name)
60 if repo == null then return null
61 var game = new Game.from_mongo(api, repo)
62 game.root_url = root_url
63 return game
64 end
65
66 # Returns the list of saved games from NitRPG data.
67 fun load_games: Array[Game] do
68 var res = new Array[Game]
69 # TODO should be option
70 var mongo = new MongoClient("mongodb://mongo:27017")
71 var db = mongo.database("nitrpg")
72 for obj in db.collection("games").find_all(new JsonObject) do
73 var repo = api.load_repo(obj["name"].to_s)
74 assert repo != null
75 var game = new Game(api, repo)
76 game.from_json(obj)
77 game.root_url = root_url
78 res.add game
79 end
80 return res
81 end
82 end
83
84 # Repo overview page.
85 class RpgHome
86 super RpgAction
87
88 # Response page stub.
89 var page: NitRpgPage is noinit
90
91 redef fun answer(request, url) do
92 var readme = load_readme
93 var games = load_games
94 var response = new HttpResponse(200)
95 page = new NitRpgPage(root_url)
96 page.side_panels.add new GamesShortListPanel(root_url, games)
97 page.flow_panels.add new MDPanel(readme)
98 response.body = page
99 return response
100 end
101
102 # Load the string content of the nitrpg readme file.
103 private fun load_readme: String do
104 var readme = "README.md"
105 if not readme.file_exists then
106 return "Unable to locate README file."
107 end
108 var file = new FileReader.open(readme)
109 var text = file.read_all
110 file.close
111 return text
112 end
113 end
114
115 # Display the list of active game.
116 class ListGames
117 super RpgAction
118
119 # Response page stub.
120 var page: NitRpgPage is noinit
121
122 redef fun answer(request, url) do
123 var games = load_games
124 var response = new HttpResponse(200)
125 page = new NitRpgPage(root_url)
126 page.breadcrumbs = new Breadcrumbs
127 page.breadcrumbs.add_link(root_url / "games", "games")
128 page.flow_panels.add new GamesListPanel(root_url, games)
129 response.body = page
130 return response
131 end
132 end
133
134 # An action that require a game.
135 class GameAction
136 super RpgAction
137
138 # Response page stub.
139 var page: NitRpgPage is noinit
140
141 # Target game.
142 var game: Game is noinit
143
144 redef fun answer(request, url) is abstract
145
146 # Check errors and prepare response.
147 private fun prepare_response(request: HttpRequest, url: String): HttpResponse do
148 var owner = request.param("owner")
149 var repo_name = request.param("repo")
150 if owner == null or repo_name == null then
151 var msg = "Bad request: should look like /games/:owner/:repo."
152 return bad_request(msg)
153 end
154 var game = load_game("{owner}/{repo_name}")
155 if game == null then
156 var msg = api.last_error.message
157 return bad_request("Repo Error: {msg}")
158 end
159 self.game = game
160 var response = new HttpResponse(200)
161 page = new NitRpgPage(root_url)
162 page.side_panels.add new GameStatusPanel(game)
163 page.breadcrumbs = new Breadcrumbs
164 page.breadcrumbs.add_link(game.url, game.name)
165 prepare_pagination(request)
166 return response
167 end
168
169 # Parse pagination related parameters.
170 private fun prepare_pagination(request: HttpRequest) do
171 var args = request.get_args
172 list_from = args.get_or_default("pfrom", "0").to_i
173 list_limit = args.get_or_default("plimit", "10").to_i
174 end
175
176 # Limit of events to display in lists.
177 var list_limit = 10
178
179 # From where to start the display of events related lists.
180 var list_from = 0
181
182 # TODO should also check 201, 203 ...
183 private fun is_response_error(response: HttpResponse): Bool do
184 return response.status_code != 200
185 end
186 end
187
188 # Repo overview page.
189 class RepoHome
190 super GameAction
191
192 redef fun answer(request, url) do
193 var rsp = prepare_response(request, url)
194 if is_response_error(rsp) then return rsp
195 page.side_panels.add new ShortListPlayersPanel(game)
196 page.flow_panels.add new PodiumPanel(game)
197 page.flow_panels.add new EventListPanel(game, list_limit, list_from)
198 page.flow_panels.add new AchievementsListPanel(game)
199 rsp.body = page
200 return rsp
201 end
202 end
203
204 # Repo players list.
205 class ListPlayers
206 super GameAction
207
208 redef fun answer(request, url) do
209 var rsp = prepare_response(request, url)
210 if is_response_error(rsp) then return rsp
211 page.breadcrumbs.add_link(game.url / "players", "players")
212 page.flow_panels.add new ListPlayersPanel(game)
213 rsp.body = page
214 return rsp
215 end
216 end
217
218 # Player details page.
219 class PlayerHome
220 super GameAction
221
222 redef fun answer(request, url) do
223 var rsp = prepare_response(request, url)
224 if is_response_error(rsp) then return rsp
225 var name = request.param("player")
226 if name == null then
227 var msg = "Bad request: should look like /:owner/:repo/:players/:name."
228 return bad_request(msg)
229 end
230 var player = game.load_player(name)
231 if player == null then
232 return bad_request("Request Error: unknown player {name}.")
233 end
234 page.breadcrumbs.add_link(game.url / "players", "players")
235 page.breadcrumbs.add_link(player.url, name)
236 page.side_panels.clear
237 page.side_panels.add new PlayerStatusPanel(game, player)
238 page.flow_panels.add new PlayerReviewsPanel(game, player)
239 page.flow_panels.add new PlayerWorkPanel(game, player)
240 page.flow_panels.add new AchievementsListPanel(player)
241 page.flow_panels.add new EventListPanel(player, list_limit, list_from)
242 rsp.body = page
243 return rsp
244 end
245 end
246
247 # Display the list of achievements unlocked for this game.
248 class ListAchievements
249 super GameAction
250
251 redef fun answer(request, url) do
252 var rsp = prepare_response(request, url)
253 if is_response_error(rsp) then return rsp
254 page.breadcrumbs.add_link(game.url / "achievements", "achievements")
255 page.flow_panels.add new AchievementsListPanel(game)
256 rsp.body = page
257 return rsp
258 end
259 end
260
261 # Player details page.
262 class AchievementHome
263 super GameAction
264
265 redef fun answer(request, url) do
266 var rsp = prepare_response(request, url)
267 if is_response_error(rsp) then return rsp
268 var name = request.param("achievement")
269 if name == null then
270 var msg = "Bad request: should look like /:owner/:repo/achievements/:achievement."
271 return bad_request(msg)
272 end
273 var achievement = game.load_achievement(name)
274 if achievement == null then
275 return bad_request("Request Error: unknown achievement {name}.")
276 end
277 page.breadcrumbs.add_link(game.url / "achievements", "achievements")
278 page.breadcrumbs.add_link(achievement.url, achievement.name)
279 page.flow_panels.add new AchievementPanel(achievement)
280 page.flow_panels.add new EventListPanel(achievement, list_limit, list_from)
281 rsp.body = page
282 return rsp
283 end
284 end
285
286 if args.length != 3 then
287 print "Error: missing argument"
288 print ""
289 print "Usage:"
290 print "web <host> <port> <root_url>"
291 exit 1
292 end
293
294 var host = args[0]
295 var port = args[1]
296 var root = args[2]
297
298 var iface = "{host}:{port}"
299 var vh = new VirtualHost(iface)
300 vh.routes.add new Route("/styles/", new FileServer("www/styles"))
301 vh.routes.add new Route("/games/:owner/:repo/players/:player", new PlayerHome(root))
302 vh.routes.add new Route("/games/:owner/:repo/players", new ListPlayers(root))
303 vh.routes.add new Route("/games/:owner/:repo/achievements/:achievement", new AchievementHome(root))
304 vh.routes.add new Route("/games/:owner/:repo/achievements", new ListAchievements(root))
305 vh.routes.add new Route("/games/:owner/:repo", new RepoHome(root))
306 vh.routes.add new Route("/games", new ListGames(root))
307 vh.routes.add new Route("/", new RpgHome(root))
308
309 var fac = new HttpFactory.and_libevent
310 fac.config.virtual_hosts.add vh
311
312 print "Launching server on http://{iface}/"
313 fac.run