Wiki source code of Prototype: Activity Time Centered
Last modified by Vincent Massol on 2024/11/19 16:13
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | {{velocity}} | ||
| 2 | #if(!$request.details) | ||
| 3 | This is a demo of a performant activity stream. The principle of this activity stream is to display results by groups of #if ($request.delay)$request.delay#{else}5#end minutes. A first query is run to detect all groups of #if ($request.delay)$request.delay#{else}5#end minutes (configurable) that are needed to display #if ($request.delay)$request.max#{else}20#end (configurable) groups. Then one other query is run to retrieve all the necessary data to display the #if ($request.delay)$request.max#{else}20#end groups. For each group, the display is different based on the number of results. | ||
| 4 | |||
| 5 | If you click on a group the detailed changes are retrieved using an AJAX call. | ||
| 6 | |||
| 7 | Security wise, the display will check security for groups of less than 100 changes. Otherwise only the number of changes are displayed. | ||
| 8 | |||
| 9 | For now the display has not been made to look pretty. | ||
| 10 | #end | ||
| 11 | {{/velocity}} | ||
| 12 | {{groovy}} | ||
| 13 | import org.joda.time.DateTime; | ||
| 14 | import java.text.SimpleDateFormat; | ||
| 15 | |||
| 16 | spacefilter = "'XWiki','Scheduler', 'Sandbox', 'IRC'" | ||
| 17 | wikifilter = "'import'" | ||
| 18 | pagefilter = "'Main.ActStream', 'Main.ActStream2'" | ||
| 19 | |||
| 20 | def cache = new HashMap() | ||
| 21 | nbqueries = 0; | ||
| 22 | nbchecks = 0; | ||
| 23 | |||
| 24 | def displayUsers(usersList) { | ||
| 25 | def str = "" | ||
| 26 | def maxusers = 5; | ||
| 27 | def nbusers = 0; | ||
| 28 | for (user in usersList) { | ||
| 29 | nbusers++; | ||
| 30 | if (nbusers>maxusers) | ||
| 31 | break; | ||
| 32 | str += """<span style="margin-left: 10px;">{{useravatar height="50" username="${user}"/}}</span>""" | ||
| 33 | } | ||
| 34 | return str; | ||
| 35 | } | ||
| 36 | |||
| 37 | |||
| 38 | def getIntervals(nb, delay, secDelay) { | ||
| 39 | nbqueries++; | ||
| 40 | def hql = "select distinct round( (datediff(current_timestamp(), act.date)*24*3600 + (hour(current_timestamp())-hour(act.date))*3600 + (minute(current_timestamp())-minute(act.date))*60 + (second(current_timestamp()) + ${secDelay} -second(act.date)))/60/${delay} - 0.5) from ActivityEventImpl as act where act.space not in (${spacefilter}) and act.page not in (${pagefilter}) and act.wiki not in (${wikifilter}) and act.hidden<>1 order by act.date desc" | ||
| 41 | return xwiki.search(hql, nb * 2, 0); | ||
| 42 | } | ||
| 43 | |||
| 44 | def filter(list) { | ||
| 45 | if (list.size()>100) | ||
| 46 | return list; | ||
| 47 | def list1 = new ArrayList(); | ||
| 48 | for (event in list) { | ||
| 49 | def pageName = "${event[0]}:${event[1]}" | ||
| 50 | nbchecks++; | ||
| 51 | def pagedoc = xwiki.getDocument(pageName) | ||
| 52 | if (pagedoc!=null) { | ||
| 53 | list1.add(event); | ||
| 54 | } else { | ||
| 55 | println "Got rid of ${pageName}" | ||
| 56 | } | ||
| 57 | } | ||
| 58 | return list1; | ||
| 59 | } | ||
| 60 | |||
| 61 | def getEvents(starti, endi, delay, currentDate) { | ||
| 62 | nbqueries++; | ||
| 63 | def sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm") | ||
| 64 | def nextDate = currentDate.minusMinutes(delay * starti); | ||
| 65 | def previousDate = currentDate.minusMinutes(delay * endi); | ||
| 66 | def spreviousDate = sdf.format(previousDate.toDate()); | ||
| 67 | def snextDate = sdf.format(nextDate.toDate()) | ||
| 68 | |||
| 69 | def hql = "select act.wiki, act.page, act.user, act.title, act.version, act.type, act.date, act.param2 from ActivityEventImpl as act where act.space not in (${spacefilter}) and act.page not in (${pagefilter}) and act.wiki not in (${wikifilter}) and act.date>'${spreviousDate}' and act.date<'${snextDate}' and act.hidden<>1 order by act.date desc" | ||
| 70 | if (request.debug) { | ||
| 71 | println "* ${starti} ${endi} ${delay} ${previousDate} ${spreviousDate} ${nextDate} ${snextDate} ${hql}"; | ||
| 72 | } | ||
| 73 | |||
| 74 | return xwiki.search(hql); | ||
| 75 | } | ||
| 76 | |||
| 77 | def getEventsWithCache(i, starti, endi, delay, currentDate, nbgroups, cache, withadddelay) { | ||
| 78 | // let's check in cache | ||
| 79 | def result = cache.get(i) | ||
| 80 | if (result!=null) { | ||
| 81 | return filter(result) | ||
| 82 | } | ||
| 83 | |||
| 84 | def endi2 = calcEnd(i+nbgroups, withadddelay) | ||
| 85 | def sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm") | ||
| 86 | def nextDate = currentDate.minusMinutes(delay * starti); | ||
| 87 | def previousDate = currentDate.minusMinutes(delay * endi2); | ||
| 88 | def spreviousDate = sdf.format(previousDate.toDate()); | ||
| 89 | def snextDate = sdf.format(nextDate.toDate()) | ||
| 90 | |||
| 91 | def hql = "select act.wiki, act.page, act.user, act.title, act.version, act.type, act.date from ActivityEventImpl as act where act.space not in (${spacefilter}) and act.page not in (${pagefilter}) and act.wiki not in (${wikifilter}) and act.date>'${spreviousDate}' and act.page<>'Sandbox.PushTest3' and act.date<'${snextDate}' and act.hidden<>1 order by act.date desc" | ||
| 92 | |||
| 93 | // now we have more data so we need to put it in cache | ||
| 94 | def list = xwiki.search(hql) | ||
| 95 | nbqueries ++; | ||
| 96 | def start = i; | ||
| 97 | def list2 = new ArrayList() | ||
| 98 | for (event in list) { | ||
| 99 | def minDate = null; | ||
| 100 | while (minDate==null) { | ||
| 101 | minDate = currentDate.minusMinutes(delay * calcEnd(start, withadddelay)); | ||
| 102 | if (event[6].time<minDate.toDate().time) { | ||
| 103 | cache.put(start, list2); | ||
| 104 | list2 = new ArrayList(); | ||
| 105 | start++; | ||
| 106 | minDate = null; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | list2.add(event); | ||
| 110 | } | ||
| 111 | cache.put(start, list2); | ||
| 112 | start++; | ||
| 113 | while (start<=i+nbgroups) { | ||
| 114 | cache.put(start, new ArrayList()) | ||
| 115 | start++; | ||
| 116 | } | ||
| 117 | def newlist = cache.get(i); | ||
| 118 | return filter(newlist); | ||
| 119 | } | ||
| 120 | |||
| 121 | def calcEnd(i, withadddelay) { | ||
| 122 | return (withadddelay&&(i+1>6)) ? (i + 1 + (i - 6)) : (i+1) | ||
| 123 | } | ||
| 124 | |||
| 125 | def calcStart(i, withadddelay) { | ||
| 126 | return (withadddelay&&(i>6)) ? (i + (i - 6) -1) : i | ||
| 127 | } | ||
| 128 | |||
| 129 | def findComment(pagedoc, commentNb) { | ||
| 130 | def cobj = pagedoc.getObject("XWiki.XWikiComments", Integer.parseInt(commentNb)) | ||
| 131 | if (cobj) { | ||
| 132 | pagedoc.use(cobj); | ||
| 133 | return pagedoc.getValue("comment") | ||
| 134 | } | ||
| 135 | return ""; | ||
| 136 | } | ||
| 137 | |||
| 138 | def getMessage(list, nochange) { | ||
| 139 | |||
| 140 | def action = "" | ||
| 141 | def target = "" | ||
| 142 | def users = new ArrayList() | ||
| 143 | |||
| 144 | def nb = (list==null) ? 0 : list.size(); | ||
| 145 | if (nb==0) { | ||
| 146 | if (nochange) { | ||
| 147 | target = "" | ||
| 148 | action = "no change"; | ||
| 149 | } | ||
| 150 | } else if (nb==1) { | ||
| 151 | def res = list.get(0) | ||
| 152 | users.add(res[2]); | ||
| 153 | def author = xwiki.getUserName(res[2], false) | ||
| 154 | def changedDoc = xwiki.getDocument("${res[0]}:${res[1]}") | ||
| 155 | if (changedDoc!=null) { | ||
| 156 | def stitle = changedDoc.displayTitle | ||
| 157 | if (changedDoc.version=="1.1") { | ||
| 158 | target = "${stitle} in wiki ${res[0]}" | ||
| 159 | action = "1 create by ${author}" | ||
| 160 | } else | ||
| 161 | target = "${stitle} in wiki ${res[0]}" | ||
| 162 | action = "1 update by ${author}" | ||
| 163 | } | ||
| 164 | } else if (nb<30) { | ||
| 165 | def titleList = new ArrayList() | ||
| 166 | def wikis = new ArrayList() | ||
| 167 | |||
| 168 | for (item in list) { | ||
| 169 | def changedDoc = xwiki.getDocument("${item[0]}:${item[1]}") | ||
| 170 | if (changedDoc!=null) { | ||
| 171 | def stitle = changedDoc.displayTitle | ||
| 172 | if (!titleList.contains(stitle)) | ||
| 173 | titleList.add(stitle) | ||
| 174 | if (!wikis.contains(item[0])) | ||
| 175 | wikis.add(item[0]) | ||
| 176 | if (!users.contains(item[2])) | ||
| 177 | users.add(item[2]) | ||
| 178 | } else { | ||
| 179 | nb--; | ||
| 180 | } | ||
| 181 | } | ||
| 182 | def pageText = "" | ||
| 183 | def userText = "" | ||
| 184 | if (users.size()==1) { | ||
| 185 | def user = users.get(0) | ||
| 186 | userText = "by " + xwiki.getUserName(user, false) | ||
| 187 | } else { | ||
| 188 | userText = "by ${users.size()} users" | ||
| 189 | } | ||
| 190 | if (titleList.size()<3) { | ||
| 191 | pageText = titleList.join(",") | ||
| 192 | } else { | ||
| 193 | pageText = titleList.size() + " pages" | ||
| 194 | } | ||
| 195 | if (wikis.size()==1) { | ||
| 196 | def wiki = wikis.get(0) | ||
| 197 | target = "${pageText} in ${wiki}" | ||
| 198 | action = "${nb} changes ${userText}" | ||
| 199 | } else { | ||
| 200 | def nbwikis = wikis.size(); | ||
| 201 | target = "${pageText} in ${nbwikis} wikis" | ||
| 202 | action = "${nb} changes ${userText}" | ||
| 203 | } | ||
| 204 | } else { | ||
| 205 | def wikis = new ArrayList() | ||
| 206 | for (item in list) { | ||
| 207 | if (!wikis.contains(item[0])) | ||
| 208 | wikis.add(item[0]) | ||
| 209 | if (!users.contains(item[2])) | ||
| 210 | users.add(item[2]) | ||
| 211 | } | ||
| 212 | def userText = "" | ||
| 213 | if (users.size()==1) { | ||
| 214 | def user = users.get(0) | ||
| 215 | userText = "by " + xwiki.getUserName(user, false) | ||
| 216 | } else { | ||
| 217 | userText = "by ${users.size()} users" | ||
| 218 | } | ||
| 219 | if (wikis.size()==1) { | ||
| 220 | def wiki = wikis.get(0) | ||
| 221 | target = "Pages in ${wiki}" | ||
| 222 | action = "${nb} changes ${userText}" | ||
| 223 | } else if (wikis.size()<4) { | ||
| 224 | def swikis = wikis.join(","); | ||
| 225 | target = "Pages in wikis ${swikis}" | ||
| 226 | action = "${nb} changes ${userText}" | ||
| 227 | } else { | ||
| 228 | def nbwikis = wikis.size(); | ||
| 229 | target = "Pages in ${nbwikis} wikis" | ||
| 230 | action = "${nb} changes ${userText}" | ||
| 231 | } | ||
| 232 | } | ||
| 233 | return [ target: target, action: action, users : users ]; | ||
| 234 | } | ||
| 235 | |||
| 236 | // add jsx | ||
| 237 | xwiki.jsx.use("Proposal.ActivityTimeCentered"); | ||
| 238 | xwiki.ssx.use("Main.Activity") | ||
| 239 | |||
| 240 | if (request.details) { | ||
| 241 | def starti = Integer.parseInt(request.start) | ||
| 242 | def endi = Integer.parseInt(request.end) | ||
| 243 | def delay = Integer.parseInt(request.delay) | ||
| 244 | def ctime = Long.parseLong(request.time) | ||
| 245 | def cdate = new DateTime(ctime); | ||
| 246 | def events = getEvents(starti, endi, delay, cdate) | ||
| 247 | if (events.size()>100) { | ||
| 248 | println "* Too many changes to show" | ||
| 249 | } else { | ||
| 250 | for (event in events) { | ||
| 251 | def pageName = "${event[0]}:${event[1]}" | ||
| 252 | def pagedoc = xwiki.getDocument(pageName) | ||
| 253 | if (pagedoc!=null) { | ||
| 254 | def authorName = xwiki.getUserName(event[2]) | ||
| 255 | def comment = "" | ||
| 256 | if (event[5]=="addComment") | ||
| 257 | comment = findComment(pagedoc, event[7]) | ||
| 258 | println """* Page [[$pagedoc.displayTitle>>${pageName}]] ${event[5]} by {{html}}${authorName}{{/html}} to version ${event[4]} on ${event[6]} ${comment}""" | ||
| 259 | } | ||
| 260 | } | ||
| 261 | } | ||
| 262 | } else { | ||
| 263 | def time1 = (new Date()).getTime(); | ||
| 264 | println """{{html clean=false wiki="true"}}<div class="activity">""" | ||
| 265 | |||
| 266 | def delay = 5; | ||
| 267 | if (request.delay) | ||
| 268 | delay = Integer.parseInt(request.delay) | ||
| 269 | def max = 20; | ||
| 270 | if (request.max) | ||
| 271 | max = Integer.parseInt(request.max) | ||
| 272 | |||
| 273 | def currentDate = new DateTime() | ||
| 274 | def sec = currentDate.getSecondOfDay() | ||
| 275 | def secDelay = (int) (600 - sec + 600*Math.floor(sec/600)) | ||
| 276 | currentDate = currentDate.plusSeconds(secDelay); | ||
| 277 | |||
| 278 | |||
| 279 | def intervals = getIntervals(max, delay, secDelay); | ||
| 280 | def maxi = (int) ((intervals.size()>0) ? intervals.get(intervals.size()-1) : 1); | ||
| 281 | |||
| 282 | if (request.debug) { | ||
| 283 | println intervals; | ||
| 284 | println maxi | ||
| 285 | } | ||
| 286 | |||
| 287 | def st = 0; | ||
| 288 | def currentTitle = ""; | ||
| 289 | |||
| 290 | for (interval in intervals) { | ||
| 291 | // we should stop when we have enough | ||
| 292 | if (st>=max) | ||
| 293 | break; | ||
| 294 | |||
| 295 | def i = (int) interval; | ||
| 296 | def starti = calcStart(i, false); | ||
| 297 | def endi = calcEnd(i, false); | ||
| 298 | if (request.debug) { | ||
| 299 | println "<li>Run ${i} ${starti} ${endi} ${delay} ${currentDate} ${maxi}</li>" | ||
| 300 | } | ||
| 301 | def list = getEventsWithCache(i, starti, endi, delay, currentDate, maxi , cache, false) | ||
| 302 | // def list = getEvents(starti, endi, delay, currentDate) | ||
| 303 | def message = getMessage(list, (request.withnochange=="1")); | ||
| 304 | |||
| 305 | if (message.action!="") { | ||
| 306 | def mn = starti*delay - (int) (secDelay/60) | ||
| 307 | def newDate = currentDate.minusMinutes(mn) | ||
| 308 | def stime = "" | ||
| 309 | def stitle = "" | ||
| 310 | if (newDate.getDayOfYear()+1==currentDate.getDayOfYear()) { | ||
| 311 | def sdf2 = new SimpleDateFormat("HH:mm") | ||
| 312 | stime = sdf2.format(newDate.toDate()) | ||
| 313 | stitle = "Yesterday" | ||
| 314 | } else if (newDate.getDayOfYear()!=currentDate.getDayOfYear()) { | ||
| 315 | def sdf2a = new SimpleDateFormat("E MMM dd") | ||
| 316 | def sdf2b = new SimpleDateFormat("HH:mm") | ||
| 317 | stime = sdf2b.format(newDate.toDate()) | ||
| 318 | stitle = sdf2a.format(newDate.toDate()) | ||
| 319 | } else { | ||
| 320 | stitle = "Today" | ||
| 321 | |||
| 322 | if (mn>60) { | ||
| 323 | def hours = Math.floor(10*mn/60)/10; | ||
| 324 | if (hours>24) { | ||
| 325 | def rdays = (int) Math.floor(mn/60/24); | ||
| 326 | def rmn = mn - rdays*60*24; | ||
| 327 | def rhours = (int) Math.floor(rmn/60); | ||
| 328 | rmn = rmn - rhours*60; | ||
| 329 | stime = "${rdays} days ${rhours} hours and ${rmn} minutes ago"; | ||
| 330 | } else { | ||
| 331 | def rhours = (int) Math.floor(mn/60); | ||
| 332 | def rmn = mn - 60*rhours; | ||
| 333 | |||
| 334 | stime = "${rhours} hours and ${rmn} minutes ago"; | ||
| 335 | } | ||
| 336 | } else if (mn>0) { | ||
| 337 | stime = "${mn} minutes ago" | ||
| 338 | } else { | ||
| 339 | stime = "now" | ||
| 340 | } | ||
| 341 | } | ||
| 342 | |||
| 343 | if (currentTitle!=stitle) { | ||
| 344 | if (currentTitle!="") | ||
| 345 | println "</ul>" | ||
| 346 | println """<h2>${stitle}</h2><ul class="activityList">""" | ||
| 347 | currentTitle = stitle; | ||
| 348 | } | ||
| 349 | |||
| 350 | |||
| 351 | |||
| 352 | println """<li class="activityItem activityPage"><div class="activityHeader"><span class="activityAuthor"><a class="typePage type" href="javascript:void(0)" onclick="load('${doc.getURL("get")}', ${starti}, ${endi}, ${delay}, ${currentDate.toDate().time}); return false;">${message.target}</a></span><span class="activityAction">${message.action}</span><span class="activityTime">${stime}</span>""" | ||
| 353 | println """<div id="users">""" + displayUsers(message.users) + "</div>" | ||
| 354 | println """<div id="data${starti}"></div></li>""" | ||
| 355 | st++; | ||
| 356 | } | ||
| 357 | } | ||
| 358 | println "</ul></div><br />" | ||
| 359 | |||
| 360 | def time2 = (new Date()).getTime(); | ||
| 361 | def dtime = time2-time1 | ||
| 362 | |||
| 363 | println """<p>run in ${dtime} ms with ${nbqueries} queries ${nbchecks} security checks</p>""" | ||
| 364 | println "{{/html}}" | ||
| 365 | } | ||
| 366 | |||
| 367 | {{/groovy}} |