nitrpg: migrate data management from `json_store` to `mongodb`
[nit.git] / contrib / nitrpg / src / statistics.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 # Statistics about the Game.
18 #
19 # This module uses `GameReactor` to extract statistics about the game from
20 # triggered `Github::Event`.
21 module statistics
22
23 import game
24 import github::hooks
25 import counter
26
27 redef class GameEntity
28
29 # Statistics manager for this entity.
30 fun stats: GameStatsManager is abstract
31 end
32
33 redef class Game
34
35 redef var stats is lazy do return new GameStatsManager(game, self)
36
37 redef fun pretty do
38 var res = new FlatBuffer
39 res.append super
40 res.append "# stats:\n"
41 res.append stats.pretty
42 return res.write_to_string
43 end
44
45 redef fun save do
46 super
47 stats.save
48 end
49 end
50
51 redef class Player
52
53 redef var stats is lazy do return new GameStatsManager(game, self)
54
55 redef fun nitcoins do return stats["nitcoins"]
56 redef fun nitcoins=(nc) do stats["nitcoins"] = nc
57
58 redef fun pretty do
59 var res = new FlatBuffer
60 res.append super
61 res.append "# stats:\n"
62 res.append stats.pretty
63 return res.write_to_string
64 end
65
66 redef fun save do
67 super
68 stats.save
69 end
70 end
71
72 # Store game stats for defined period.
73 class GameStatsManager
74 super GameEntity
75 super Counter[String]
76
77 redef var game
78
79 # The GameEntity monitored by these statistics.
80 var owner: GameEntity
81
82 # Current date to extract stats
83 private var date = new Tm.gmtime
84
85 # Returns the `GameStats` instance for the overall statistics.
86 var overall: GameStats = load_stats_for("all") is lazy
87
88 # Returns the `GameStats` instance for the current year statistics.
89 var yearly: GameStats = load_stats_for(date.strftime("%Y")) is lazy
90
91 # Returns the `GameStats` instance for the current month statistics.
92 var monthly: GameStats = load_stats_for(date.strftime("%Y-%m")) is lazy
93
94 # Returns the `GameStats` instance for the current day statistics.
95 var daily: GameStats = load_stats_for(date.strftime("%Y-%m-%d")) is lazy
96
97 # Returns the `GameStats` instance for the current week statistics.
98 var weekly: GameStats = load_stats_for(date.strftime("%Y-W%U")) is lazy
99
100 # Load statistics for a `period` key.
101 fun load_stats_for(period: String): GameStats do
102 var req = new JsonObject
103 req["period"] = period
104 req["owner"] = owner.key
105 var obj = game.db.collection("statistics").find(req)
106 if obj isa JsonObject then
107 return new GameStats.from_json(game, period, owner, obj)
108 else
109 return new GameStats(game, period, owner)
110 end
111 end
112
113 redef fun [](key) do return overall[key]
114
115 redef fun []=(key, value) do
116 overall[key] = value
117 yearly[key] = value
118 monthly[key] = value
119 daily[key] = value
120 weekly[key] = value
121 end
122
123 redef fun inc(e) do
124 overall.inc(e)
125 yearly.inc(e)
126 monthly.inc(e)
127 daily.inc(e)
128 weekly.inc(e)
129 end
130
131 redef fun dec(e) do
132 overall.dec(e)
133 yearly.dec(e)
134 monthly.dec(e)
135 daily.dec(e)
136 weekly.dec(e)
137 end
138
139 redef fun save do
140 overall.save
141 yearly.save
142 monthly.save
143 daily.save
144 weekly.save
145 end
146
147 redef fun pretty do return overall.pretty
148 end
149
150 # Game statistics structure that can be saved as a `GameEntity`.
151 class GameStats
152 super GameEntity
153 super Counter[String]
154
155 redef var game
156
157 redef var collection_name = "statistics"
158
159 # The period these stats are about.
160 var period: String
161
162 # The game entity these stats are about.
163 var owner: GameEntity
164
165 redef var key = "{owner.key}-{period}" is lazy
166
167 # Load `self` from saved data.
168 init from_json(game: Game, period: String, owner: GameEntity, json: JsonObject) do
169 var values = json.get_or_null("values")
170 if not values isa JsonObject then return
171 for k, v in values do self[k] = v.as(Int)
172 end
173
174 redef fun to_json do
175 var obj = super
176 obj["period"] = period
177 obj["owner"] = owner.key
178 var values = new JsonObject
179 values.recover_with(self)
180 obj["values"] = values
181 return obj
182 end
183
184 redef fun pretty do
185 var res = new FlatBuffer
186 for k, v in self do
187 res.append "# {v} {k}\n"
188 end
189 return res.write_to_string
190 end
191 end
192
193 # `GameReactor` that computes statistics about the game.
194 class StatisticsReactor
195 super GameReactor
196
197 redef fun react_event(game, e) do e.react_stats_event(game)
198 end
199
200 redef class GithubEvent
201 # Reacts to a statistics related event.
202 #
203 # Called by `StatisticsReactor::react_event`.
204 # No-op by default.
205 private fun react_stats_event(game: Game) do end
206 end
207
208 redef class IssuesEvent
209
210 # Count opened and closed issues.
211 redef fun react_stats_event(game) do
212 var player = issue.user.player(game)
213 if action == "opened" then
214 game.stats.inc("issues")
215 game.stats.inc("issues_open")
216 game.save
217 player.stats.inc("issues")
218 player.stats.inc("issues_open")
219 player.save
220 else if action == "reopened" then
221 game.stats.inc("issues_open")
222 game.save
223 player.stats.inc("issues_open")
224 player.save
225 else if action == "closed" then
226 game.stats.dec("issues_open")
227 game.save
228 player.stats.dec("issues_open")
229 player.save
230 end
231 end
232 end
233
234 redef class PullRequestEvent
235
236 # Count opened and closed pull requests.
237 redef fun react_stats_event(game) do
238 var player = pull.user.player(game)
239 if action == "opened" then
240 game.stats.inc("pulls")
241 game.stats.inc("pulls_open")
242 game.save
243 player.stats.inc("pulls")
244 player.stats.inc("pulls_open")
245 player.save
246 else if action == "reopened" then
247 game.stats.inc("pulls_open")
248 game.save
249 player.stats.inc("pulls_open")
250 player.save
251 else if action == "closed" then
252 game.stats.dec("pulls_open")
253 player.stats.dec("pulls_open")
254 if pull.merged then
255 game.stats["commits"] += pull.commits
256 player.stats["commits"] += pull.commits
257 end
258 game.save
259 player.save
260 end
261 end
262 end
263
264 redef class IssueCommentEvent
265
266 # Count posted comments
267 redef fun react_stats_event(game) do
268 if action == "created" then
269 var player = comment.user.player(game)
270 game.stats.inc("comments")
271 player.stats.inc("comments")
272 # FIXME use a more precise way to locate reviews
273 if comment.is_ack then
274 game.stats.inc("reviews")
275 player.stats.inc("reviews")
276 end
277 game.save
278 player.save
279 end
280 end
281 end