Wiki-Quellcode von RecentlyUpdatedService

Version 2.1 von Daniel Herrmann am 2025/06/23 19:49

Verstecke letzte Bearbeiter
Daniel Herrmann 1.1 1 {{velocity output="false"}}
2
3 ## -----------------------------------------------------------------
4 ## -----------------------------------------------------------------
5 ## -----------------------------------------------------------------
6 ## Constants & Globals
7 ## -----------------------------------------------------------------
8
9 #set ($TYPES = ['page', 'blogpost', 'attachment', 'comment'])
10
11 #set ($ICON_BY_TYPE = {
12 "page": 'page',
13 "blogpost": 'page',
14 "attachment": 'attach',
15 "comment": 'comment'
16 })
17 #set ($AUTHORING_TEXT_BY_TYPE = {
18 "page": 'contributed',
19 "blogpost": 'contributed',
20 "attachment": 'attached',
21 "comment": 'commented'
22 })
23
24 ## See https://solr.apache.org/guide/solr/latest/query-guide/standard-query-parser.html#escaping-special-characters
25 ## And https://jira.xwiki.org/browse/XCOMMONS-2926
26 #set ($solrSpecialChars = ['+', '-', '&&', '||', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '/', '\', ' '])
27 #set ($escapedSolrSpecialChars = ['\+', '\-', '\&&', '\||', '\!', '\(', '\)', '\{', '\}', '\[', '\]', '\^', '\"', '\~', '\*', '\?', '\:', '\/', '\\', '\ '])
28 #macro (escapeSolr $v)
29 $stringtool.replaceEach($v, $solrSpecialChars, $escapedSolrSpecialChars)##
30 #end
31
32 #macro (addSolrParameterListWithSign $listString $fq $fieldName $onlyComma, $forAuthors)
33 #if ("$!listString" != '')
34 #set ($solrList = " ")
35 #set ($sep = "[\s]*,[\s]*|[\s]+")
36 #if ("$onlyComma" == "true")
37 #set ($sep = ",")
38 #end
39 #foreach ($signedV in $listString.split($sep))
40 #set ($signedV = $signedV.trim())
41 #if ($signedV.startsWith("-") || $signedV.startsWith("+"))
42 #set ($sign = $signedV.charAt(0))
43 #set ($v = $signedV.substring(1))
44 #else
45 #set ($sign = "")
46 #set ($v = $signedV)
47 #end
48 #set ($escaped = "#escapeSolr($v)")
49 #if ($forAuthors && !$v.contains(":"))
50 #set ($solrList = "$solrList $sign*\:$escaped")
51 #end
52 #set ($solrList = "$solrList $sign$escaped")
53 #end
54 #set($solrList = $solrList.trim())
55 #if (!$solrList.isEmpty())
56 #set ($discard = $fq.add("$fieldName:($solrList)"))
57 #end
58 #end
59 #end
60
61 #macro (queryElements $type $options $return)
62 #set ($fq = [])
63 #if ($type == "attachment")
64 #set($typeField = "ATTACHMENT")
65 #set($authorField = "attauthor")
66 #set($versionField = "attdate")
67 #elseif ($type == "comment")
68 #set($typeField = "OBJECT_PROPERTY")
69 #set($authorField = "propertyvalue_")
70 #set($versionField = "_version_")
71 #else
72 #set($typeField = "DOCUMENT")
73 #set($authorField = "author")
74 #set($versionField = "date")
75 #end
76 #if (!$services.user.getProperties().displayHiddenDocuments())
77 #set($discard = $fq.add("hidden:false"))
78 #end
79 #set ($query = $services.query.createQuery('name:*', 'solr'))
80 #set ($discard = $fq.add("type:$typeField"))
81 #if ($type == "comment")
82 #set ($discard = $fq.add('class:XWiki.XWikiComments'))
83 #set ($discard = $fq.add('propertyname:(author)'))
84 #end
85 #if ("$!options.wiki" != '')
86 #set ($discard = $fq.add("wiki:(#escapeSolr($options.wiki))"))
87 #end
88 #addSolrParameterListWithSign($options.spaces, $fq, "space_prefix", true)
89 #addSolrParameterListWithSign($options.authors, $fq, $authorField, false, true)
90 #if ($type == 'page')
91 #if ("$!options.tags" == '')
92 #set ($discard = $fq.add('class:(-Blog.BlogPostClass)'))
93 #else
94 #set ($discard = $fq.add('class:(-Blog.BlogPostClass AND XWiki.TagClass)'))
95 #addSolrParameterListWithSign($options.tags, $fq, "property.XWiki.TagClass.tags_string")
96 #end
97 #elseif ($type == 'blogpost')
98 #if ("$!options.tags" == '')
99 #set ($discard = $fq.add('class:(Blog.BlogPostClass)'))
100 #else
101 #set ($discard = $fq.add('class:(Blog.BlogPostClass AND XWiki.TagClass)'))
102 #addSolrParameterListWithSign($options.tags, $fq, "property.XWiki.TagClass.tags_string")
103 #end
104 #end
105 ## Query params
106 #set ($discard = $query.bindValue('fq', $fq))
107 #set ($discard = $query.bindValue('sort', "$versionField desc"))
108 #set ($discard = $query.setOffset($options.offset))
109 #set ($discard = $query.setLimit($options.limit))
110 #set ($results = $query.execute())
111 #if ($results.isEmpty())
112 ## Return
113 #set ($return = $NULL)
114 #setVariable("$return" [])
115 #else
116 ## Fix some properties
117 #foreach ($element in $results[0].results)
118 #set ($discard = $element.put('type', $type))
119 #if ($type == "attachment")
120 #set ($discard = $element.put('date', $element.attdate[0]))
121 #set ($discard = $element.put('title_', $element.filename[0]))
122 #set ($discard = $element.put('author', $element.attauthor[0]))
123 #elseif ($type == "comment")
124 #addCommentData($element)
125 #end
126 #end
127 ## Return
128 #set ($return = $NULL)
129 #setVariable("$return" $results[0].results)
130 #end
131 #end
132
133 #macro (addCommentData $comment)
134 ## Comment filters
135 #set ($fq = [])
136 #set ($query = $services.query.createQuery("name:$comment.name", 'solr'))
137 #set ($discard = $fq.add('type:OBJECT_PROPERTY'))
138 #set ($discard = $fq.add('class:XWiki.XWikiComments'))
139 #set ($discard = $fq.add("wiki:(#escapeSolr($comment.wiki))"))
140 #set ($discard = $fq.add("space:(#escapeSolr($comment.space))"))
141 #set ($discard = $fq.add("number:($comment.number)"))
142 ## Query params
143 #set ($discard = $query.bindValue('fq', $fq))
144 #set ($discard = $query.setOffset(0))
145 #set ($discard = $query.setLimit(3))
146 ## Add data to result
147 #foreach ($property in $query.execute()[0].results)
148 #if ($property.propertyname == 'date')
149 #set ($date = $null)
150 #try('exception')
151 #set ($dateFormat = $datetool.getDateFormat("EEE MMM dd HH:mm:ss z yyyy", 'en', $datetool.getTimeZone()))
152 #set ($date = $dateFormat.parse($property.propertyvalue_[0]))
153 #end
154 #set ($discard = $comment.put('date', $date))
155 #elseif ($property.propertyname == 'author')
156 #set ($discard = $comment.put('author', "$property.propertyvalue_[0]"))
157 #elseif ($property.propertyname == 'comment')
158 #set ($discard = $comment.put('comment', $property.propertyvalue_[0]))
159 #end
160 #end
161 #end
162
163 ## For each possible type of result,
164 ## fetch new results by executing the associated query
165 #macro (fetchResults_queries $queryResults $options)
166 #foreach ($type in $TYPES)
167 #set ($typeMetadata = $options.typesMetadata.get("$type"))
168 ## Customize options for query
169 #set ($typeOptions = $options.clone())
170 ## Update offset based on previous used results count
171 ## If this is the first time fetching results, init some values
172 #if ("$!typeMetadata.offset" == '')
173 #set ($typeMetadata.offset = 0)
174 #set ($typeMetadata.hasNext = true)
175 #end
176 ## Set the limit to (limit + 1), so that we are able to tell
177 ## whether the query has more results to come after the set limit
178 #set ($typeOptions.offset = $typeMetadata.offset)
179 #set ($typeOptions.limit = $options.limit + 1)
180 ## Fetch results if needed
181 #if ($options.types.contains($type) && $typeMetadata.hasNext)
182 #queryElements($type $typeOptions $results)
183 #set ($typeMetadata.fetchedCountTotal = $results.size())
184 #set ($discard = $queryResults.add($results))
185 #else
186 #set ($typeMetadata.fetchedCountTotal = 0)
187 #end
188 #end
189 #end
190
191 ## Results can be of type document, attachment, or comment
192 ## As results are fetched from three different requests (1 for each type)
193 ## We need to merge those results together into one unique list of results.
194 ## During the merge, we have to make sure we respects two things:
195 ## - results are still sorted by most recent ones first
196 ## - the number of results is no more than the limit defined in the macro parameter
197 #macro (fetchResults_mergeSortLimit $resultLists $options $return)
198 #set ($results = [])
199 #foreach ($discard in [1..$options.limit])
200 #set ($mostRecentDate = $NULL)
201 #set ($listIndex = $NULL)
202 ## Search throught the lists of results the most recent result
203 ## Lists are already sorted by themselves,
204 ## so we just need to compare the first result of each list
205 #foreach ($resultList in $resultLists)
206 #if ($resultList.size() > 0)
207 ## Flag result if it is the most recent found until now
208 #set ($date = $resultList[0].date)
209 #if ("$!mostRecentDate" == "" || $datetool.difference($mostRecentDate, $date).getMilliseconds() > 0)
210 #if ("$!date" != "")
211 #set ($mostRecentDate = $date)
212 #end
213 #set ($listIndex = $foreach.index)
214 #end
215 #end
216 #end
217 ## Use the most recent result found
218 #if ($listIndex == $NULL)
219 #break
220 #else
221 #set ($discard = $results.add($resultLists[$listIndex][0]))
222 #set ($discard = $resultLists[$listIndex].remove(0))
223 #end
224 #end
225 ## Return
226 #set ($return = $NULL)
227 #setVariable("$return" $results)
228 #end
229
230 ## Resolve some properties for results
231 ## Properties computed here are the heavy ones to compute,
232 ## because they need to use the document object associated to the result.
233 ## So we compute those properties the latest possible,
234 ## only for the results we are sure to display
235 #macro (fetchResults_resolveProperties $results)
236 #foreach ($result in $results)
Daniel Herrmann 2.1 237 #set ($documentName = $result.name.replaceAll('\.', '\\\.'))
238 #set ($documentFullname = "${result.wiki}:${result.space}.${documentName}")
Daniel Herrmann 1.1 239 #set ($document = $xwiki.getDocument($documentFullname))
240 #if ($result.type.equals('page') || $result.type.equals('blogpost'))
241 #set ($discard = $result.put('href', $document.getURL()))
242 #elseif ($result.type.equals('attachment'))
243 #set ($discard = $result.put('href', $document.getAttachmentURL($result.filename[0], 'viewattachrev')))
244 #elseif ($result.type.equals('comment'))
245 #set ($discard = $result.put('href', "$document.getURL('view', 'viewer=comments')#xwikicomment_${result.number}"))
246 #set ($discard = $result.put('title_', $document.title))
247 #end
248 #end
249 #end
250
251 #macro (fetchResults_updateMetadata $results $options)
252 ## Init needed metadata
253 #foreach ($type in $TYPES)
254 #set ($typeMetadata = $options.typesMetadata.get("$type"))
255 #set ($typeMetadata.fetchedCountUsed = 0)
256 #end
257 ## Find how many results of each type were used in the end
258 #foreach ($result in $results)
259 #set ($typeMetadata = $options.typesMetadata.get("$result.type"))
260 #set ($typeMetadata.fetchedCountUsed = $typeMetadata.fetchedCountUsed + 1)
261 #end
262 ## Update type metadata
263 #set ($options.typesMetadata.hasNext = false)
264 #foreach ($type in $TYPES)
265 #set ($typeMetadata = $options.typesMetadata.get("$type"))
266 #set ($typeMetadata.offset = $typeMetadata.offset + $typeMetadata.fetchedCountUsed)
267 ## Compute if there are next results
268 #set ($hasReachedLimit = $typeMetadata.fetchedCountTotal <= $options.limit)
269 #set ($hasUsedAllCurrentResults = $typeMetadata.fetchedCountTotal == $typeMetadata.fetchedCountUsed)
270 #set ($hasUsedEveryResults = $hasReachedLimit && $hasUsedAllCurrentResults)
271 #set ($typeMetadata.hasNext = !$hasUsedEveryResults)
272 #if ($typeMetadata.hasNext)
273 #set ($options.typesMetadata.hasNext = true)
274 #end
275 #end
276 #end
277
278 #macro (fetchResults $options $return)
279 #set ($queryResults = [])
280 #fetchResults_queries($queryResults, $options)
281 #fetchResults_mergeSortLimit($queryResults $options $results)
282 #fetchResults_resolveProperties($results)
283 #fetchResults_updateMetadata($results, $options)
284 ## Return
285 #set ($return = $NULL)
286 #setVariable("$return" $results)
287 #end
288
289
290 ## -----------------------------------------------------------------
291 ## -----------------------------------------------------------------
292 ## -----------------------------------------------------------------
293 ## Display results macros
294 ## -----------------------------------------------------------------
295
296 #set ($authorExceptions = ['XWiki.superadmin', 'XWiki.XWikiGuest'])
297
298 #macro (displayResultAuthorName $result)
299 <span class="result-last-author-name">
300 #if ($authorExceptions.contains($result.author))
301 $escapetool.xml($stringtool.substringAfter("$result.author", '.'))
302 #else
303 $xwiki.getUserName($result.author)
304 #end
305 </span>
306 #end
307
308 #macro (displayResultAuthorAvatarURL $result $options)
309 #set ($noavatarURL = $xwiki.getSkinFile('icons/xwiki/noavatar.png', true))
310 #if ("$options.showProfilePic" == "true")
311 <span class="result-last-author-avatar">
312 #if ($authorExceptions.contains($result.author))
313 <img class="avatar avatar_48" src="$noavatarURL" alt="$escapetool.xml($result.author)" />
314 #else
315 #resizedUserAvatar("$result.author", 48)
316 #end
317 </span>
318 #end
319 #end
320
321 #macro (displayResultTitle $result)
322 #set ($title = $result.title_)
323 #if ("$!title" == '')
324 #set ($title = $result.name)
325 #end
326 <span class="result-title">
327 <a href="$result.href">$escapetool.xml("$title")</a>
328 </span>
329 #end
330
331 #macro (displayResultDate $result)
332 <span class="result-date">
333 #if ($datetool.difference($datetool.getDate(), $result.date).getDays() == 0)
334 $services.date.displayTimeAgo($result.date)
335 #else
336 $xwiki.formatDate($result.date)
337 #end
338 </span>
339 #end
340
341 #macro (displayResultTypeIcon $result)
342 <span class="result-icon">
343 $services.icon.renderHTML($$ICON_BY_TYPE[$result.type])
344 </span>
345 #end
346
347 #macro (displayResult_concise $result $options)
348 <div class="result-item">
349 #displayResultTypeIcon($result)
350 #displayResultTitle($result)
351 <span class="result-metadata">
352 #displayResultDate($result)
353 <span class="separator">⋅</span>
354 <span class="result-last-author-text">$AUTHORING_TEXT_BY_TYPE[$result.type] by</span>
355 #displayResultAuthorAvatarURL($result, $options)
356 #displayResultAuthorName($result)
357 </span>
358 </div>
359 #end
360
361 #macro (displayResult_social $result $options)
362 #set ($normalizedAuthor = "$result.author")
363 #if (!$normalizedAuthor.contains(":"))
364 #set($normalizedAuthor = "$xcontext.getDatabase():$normalizedAuthor")
365 #end
366 #set ($socialThemeIsSameAuthor = $normalizedAuthor.equals($options.socialThemePreviousAuthor))
367 #set ($options.socialThemePreviousAuthor = $normalizedAuthor)
368 #set ($socialThemeCollapseClass = '')
369 #if ($socialThemeIsSameAuthor)
370 #set ($socialThemeCollapseClass = 'collapsed')
371 #end
372 <div class="result-container $socialThemeCollapseClass">
373 #displayResultAuthorAvatarURL($result, $options)
374 <div class="result-items">
375 #displayResultAuthorName($result)
376 <div class="result-item">
377 #displayResultTypeIcon($result)
378 #displayResultTitle($result)
379 <span class="result-metadata">
380 <span class="result-last-author-text">$AUTHORING_TEXT_BY_TYPE[$result.type]</span>
381 #displayResultDate($result)
382 </span>
383 </div>
384 </div>
385 </div>
386 #end
387
388 #macro (displayResult_sidebar $result $options)
389 <div class="result-item">
390 #displayResultTypeIcon($result)
391 #displayResultTitle($result)
392 <span class="result-metadata">
393 <span class="result-last-author-text">$AUTHORING_TEXT_BY_TYPE[$result.type]</span>
394 #displayResultAuthorAvatarURL($result, $options)
395 #displayResultDate($result)
396 </span>
397 </div>
398 #end
399
400 #macro (displayResults $results $options)
401 #foreach ($result in $results)
402 <li>
403 #if ($options.theme == 'sidebar')
404 #displayResult_sidebar($result $options)
405 #elseif ($options.theme == 'social')
406 #displayResult_social($result $options)
407 #else
408 #displayResult_concise($result $options)
409 #end
410 </li>
411 #end
412 ## Add hidden div containing options and metadata from those results,
413 ## that will be passed back to the service when clicking the "show more" button
414 <div
415 class="recently-updated-options hidden"
416 data-options="$!escapetool.xml($jsontool.serialize($options))"
417 data-has-next="$!escapetool.xml($options.typesMetadata.hasNext)">
418 </div>
419 #end
420 {{/velocity}}
421
422 {{velocity}}
423 #if ("$!request.fromAjax" == "true")
424 #set ($options = $jsontool.fromString($request.macroOptions))
425 #fetchResults($options, $results)
426 #displayResults($results $options)
427 #end
428 {{/velocity}}
429