Wiki-Quellcode von RecentlyUpdatedService

Version 1.1 von Daniel Herrmann am 2025/06/22 18:48

Zeige letzte Bearbeiter
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)
237 #set ($documentFullname = "${result.wiki}:${result.fullname}")
238 #set ($document = $xwiki.getDocument($documentFullname))
239 #if ($result.type.equals('page') || $result.type.equals('blogpost'))
240 #set ($discard = $result.put('href', $document.getURL()))
241 #elseif ($result.type.equals('attachment'))
242 #set ($discard = $result.put('href', $document.getAttachmentURL($result.filename[0], 'viewattachrev')))
243 #elseif ($result.type.equals('comment'))
244 #set ($discard = $result.put('href', "$document.getURL('view', 'viewer=comments')#xwikicomment_${result.number}"))
245 #set ($discard = $result.put('title_', $document.title))
246 #end
247 #end
248 #end
249
250 #macro (fetchResults_updateMetadata $results $options)
251 ## Init needed metadata
252 #foreach ($type in $TYPES)
253 #set ($typeMetadata = $options.typesMetadata.get("$type"))
254 #set ($typeMetadata.fetchedCountUsed = 0)
255 #end
256 ## Find how many results of each type were used in the end
257 #foreach ($result in $results)
258 #set ($typeMetadata = $options.typesMetadata.get("$result.type"))
259 #set ($typeMetadata.fetchedCountUsed = $typeMetadata.fetchedCountUsed + 1)
260 #end
261 ## Update type metadata
262 #set ($options.typesMetadata.hasNext = false)
263 #foreach ($type in $TYPES)
264 #set ($typeMetadata = $options.typesMetadata.get("$type"))
265 #set ($typeMetadata.offset = $typeMetadata.offset + $typeMetadata.fetchedCountUsed)
266 ## Compute if there are next results
267 #set ($hasReachedLimit = $typeMetadata.fetchedCountTotal <= $options.limit)
268 #set ($hasUsedAllCurrentResults = $typeMetadata.fetchedCountTotal == $typeMetadata.fetchedCountUsed)
269 #set ($hasUsedEveryResults = $hasReachedLimit && $hasUsedAllCurrentResults)
270 #set ($typeMetadata.hasNext = !$hasUsedEveryResults)
271 #if ($typeMetadata.hasNext)
272 #set ($options.typesMetadata.hasNext = true)
273 #end
274 #end
275 #end
276
277 #macro (fetchResults $options $return)
278 #set ($queryResults = [])
279 #fetchResults_queries($queryResults, $options)
280 #fetchResults_mergeSortLimit($queryResults $options $results)
281 #fetchResults_resolveProperties($results)
282 #fetchResults_updateMetadata($results, $options)
283 ## Return
284 #set ($return = $NULL)
285 #setVariable("$return" $results)
286 #end
287
288
289 ## -----------------------------------------------------------------
290 ## -----------------------------------------------------------------
291 ## -----------------------------------------------------------------
292 ## Display results macros
293 ## -----------------------------------------------------------------
294
295 #set ($authorExceptions = ['XWiki.superadmin', 'XWiki.XWikiGuest'])
296
297 #macro (displayResultAuthorName $result)
298 <span class="result-last-author-name">
299 #if ($authorExceptions.contains($result.author))
300 $escapetool.xml($stringtool.substringAfter("$result.author", '.'))
301 #else
302 $xwiki.getUserName($result.author)
303 #end
304 </span>
305 #end
306
307 #macro (displayResultAuthorAvatarURL $result $options)
308 #set ($noavatarURL = $xwiki.getSkinFile('icons/xwiki/noavatar.png', true))
309 #if ("$options.showProfilePic" == "true")
310 <span class="result-last-author-avatar">
311 #if ($authorExceptions.contains($result.author))
312 <img class="avatar avatar_48" src="$noavatarURL" alt="$escapetool.xml($result.author)" />
313 #else
314 #resizedUserAvatar("$result.author", 48)
315 #end
316 </span>
317 #end
318 #end
319
320 #macro (displayResultTitle $result)
321 #set ($title = $result.title_)
322 #if ("$!title" == '')
323 #set ($title = $result.name)
324 #end
325 <span class="result-title">
326 <a href="$result.href">$escapetool.xml("$title")</a>
327 </span>
328 #end
329
330 #macro (displayResultDate $result)
331 <span class="result-date">
332 #if ($datetool.difference($datetool.getDate(), $result.date).getDays() == 0)
333 $services.date.displayTimeAgo($result.date)
334 #else
335 $xwiki.formatDate($result.date)
336 #end
337 </span>
338 #end
339
340 #macro (displayResultTypeIcon $result)
341 <span class="result-icon">
342 $services.icon.renderHTML($$ICON_BY_TYPE[$result.type])
343 </span>
344 #end
345
346 #macro (displayResult_concise $result $options)
347 <div class="result-item">
348 #displayResultTypeIcon($result)
349 #displayResultTitle($result)
350 <span class="result-metadata">
351 #displayResultDate($result)
352 <span class="separator">⋅</span>
353 <span class="result-last-author-text">$AUTHORING_TEXT_BY_TYPE[$result.type] by</span>
354 #displayResultAuthorAvatarURL($result, $options)
355 #displayResultAuthorName($result)
356 </span>
357 </div>
358 #end
359
360 #macro (displayResult_social $result $options)
361 #set ($normalizedAuthor = "$result.author")
362 #if (!$normalizedAuthor.contains(":"))
363 #set($normalizedAuthor = "$xcontext.getDatabase():$normalizedAuthor")
364 #end
365 #set ($socialThemeIsSameAuthor = $normalizedAuthor.equals($options.socialThemePreviousAuthor))
366 #set ($options.socialThemePreviousAuthor = $normalizedAuthor)
367 #set ($socialThemeCollapseClass = '')
368 #if ($socialThemeIsSameAuthor)
369 #set ($socialThemeCollapseClass = 'collapsed')
370 #end
371 <div class="result-container $socialThemeCollapseClass">
372 #displayResultAuthorAvatarURL($result, $options)
373 <div class="result-items">
374 #displayResultAuthorName($result)
375 <div class="result-item">
376 #displayResultTypeIcon($result)
377 #displayResultTitle($result)
378 <span class="result-metadata">
379 <span class="result-last-author-text">$AUTHORING_TEXT_BY_TYPE[$result.type]</span>
380 #displayResultDate($result)
381 </span>
382 </div>
383 </div>
384 </div>
385 #end
386
387 #macro (displayResult_sidebar $result $options)
388 <div class="result-item">
389 #displayResultTypeIcon($result)
390 #displayResultTitle($result)
391 <span class="result-metadata">
392 <span class="result-last-author-text">$AUTHORING_TEXT_BY_TYPE[$result.type]</span>
393 #displayResultAuthorAvatarURL($result, $options)
394 #displayResultDate($result)
395 </span>
396 </div>
397 #end
398
399 #macro (displayResults $results $options)
400 #foreach ($result in $results)
401 <li>
402 #if ($options.theme == 'sidebar')
403 #displayResult_sidebar($result $options)
404 #elseif ($options.theme == 'social')
405 #displayResult_social($result $options)
406 #else
407 #displayResult_concise($result $options)
408 #end
409 </li>
410 #end
411 ## Add hidden div containing options and metadata from those results,
412 ## that will be passed back to the service when clicking the "show more" button
413 <div
414 class="recently-updated-options hidden"
415 data-options="$!escapetool.xml($jsontool.serialize($options))"
416 data-has-next="$!escapetool.xml($options.typesMetadata.hasNext)">
417 </div>
418 #end
419 {{/velocity}}
420
421 {{velocity}}
422 #if ("$!request.fromAjax" == "true")
423 #set ($options = $jsontool.fromString($request.macroOptions))
424 #fetchResults($options, $results)
425 #displayResults($results $options)
426 #end
427 {{/velocity}}