Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 164 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
EventsListPager | |
0.00% |
0 / 164 |
|
0.00% |
0 / 15 |
1332 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
getRow | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getLimitSelectList | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
isHeaderRowNeeded | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
formatRow | |
0.00% |
0 / 59 |
|
0.00% |
0 / 1 |
2 | |||
getIndexField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNavigationBar | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getMeetingTypeMsg | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
getYearFromTimestamp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getMonthFromTimestamp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDayFromTimestamp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
offsetTimestamp | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getSubqueryInfo | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
buildQueryInfo | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
getDateRangeCond | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\Pager; |
6 | |
7 | use MediaWiki\Cache\LinkBatchFactory; |
8 | use MediaWiki\Extension\CampaignEvents\Database\CampaignsDatabaseHelper; |
9 | use MediaWiki\Extension\CampaignEvents\Event\EventRegistration; |
10 | use MediaWiki\Extension\CampaignEvents\Event\Store\EventStore; |
11 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFactory; |
12 | use MediaWiki\Extension\CampaignEvents\MWEntity\PageURLResolver; |
13 | use MediaWiki\Extension\CampaignEvents\MWEntity\UserLinker; |
14 | use MediaWiki\Extension\CampaignEvents\Organizers\OrganizersStore; |
15 | use MediaWiki\Extension\CampaignEvents\Widget\TextWithIconWidget; |
16 | use MediaWiki\Pager\IndexPager; |
17 | use MediaWiki\Pager\ReverseChronologicalPager; |
18 | use MediaWiki\User\UserOptionsLookup; |
19 | use MediaWiki\Utils\MWTimestamp; |
20 | use OOUI\HtmlSnippet; |
21 | use OOUI\Tag; |
22 | use stdClass; |
23 | use UnexpectedValueException; |
24 | use Wikimedia\Rdbms\OrExpressionGroup; |
25 | use Wikimedia\Timestamp\TimestampException; |
26 | |
27 | class EventsListPager extends ReverseChronologicalPager { |
28 | use EventPagerTrait { |
29 | EventPagerTrait::getSubqueryInfo as getDefaultSubqueryInfo; |
30 | } |
31 | |
32 | private UserLinker $userLinker; |
33 | private CampaignsPageFactory $campaignsPageFactory; |
34 | private PageURLResolver $pageURLResolver; |
35 | private OrganizersStore $organizerStore; |
36 | private string $lastHeaderTimestamp; |
37 | private string $search; |
38 | /** One of the EventRegistration::MEETING_TYPE_* constants */ |
39 | private ?int $meetingType; |
40 | private LinkBatchFactory $linkBatchFactory; |
41 | private UserOptionsLookup $options; |
42 | private string $startDate; |
43 | private string $endDate; |
44 | |
45 | /** |
46 | * @param UserLinker $userLinker |
47 | * @param CampaignsPageFactory $pageFactory |
48 | * @param PageURLResolver $pageURLResolver |
49 | * @param OrganizersStore $organizerStore |
50 | * @param LinkBatchFactory $linkBatchFactory |
51 | * @param UserOptionsLookup $options |
52 | * @param CampaignsDatabaseHelper $databaseHelper |
53 | * @param string $search |
54 | * @param int|null $meetingType |
55 | * @param string $startDate |
56 | * @param string $endDate |
57 | */ |
58 | public function __construct( |
59 | UserLinker $userLinker, |
60 | CampaignsPageFactory $pageFactory, |
61 | PageURLResolver $pageURLResolver, |
62 | OrganizersStore $organizerStore, |
63 | LinkBatchFactory $linkBatchFactory, |
64 | UserOptionsLookup $options, |
65 | CampaignsDatabaseHelper $databaseHelper, |
66 | string $search, |
67 | ?int $meetingType, |
68 | string $startDate, |
69 | string $endDate |
70 | ) { |
71 | // Set the database before calling the parent constructor, otherwise it'll use the local one. |
72 | $this->mDb = $databaseHelper->getDBConnection( DB_REPLICA ); |
73 | parent::__construct( $this->getContext(), $this->getLinkRenderer() ); |
74 | |
75 | $this->options = $options; |
76 | $this->userLinker = $userLinker; |
77 | $this->campaignsPageFactory = $pageFactory; |
78 | $this->pageURLResolver = $pageURLResolver; |
79 | $this->organizerStore = $organizerStore; |
80 | $this->linkBatchFactory = $linkBatchFactory; |
81 | $this->startDate = $startDate; |
82 | $this->endDate = $endDate; |
83 | $this->mDefaultDirection = IndexPager::DIR_ASCENDING; |
84 | $this->lastHeaderTimestamp = ''; |
85 | $this->search = $search; |
86 | $this->meetingType = $meetingType; |
87 | } |
88 | |
89 | /** |
90 | * @inheritDoc |
91 | */ |
92 | public function getRow( $row ): string { |
93 | $s = ''; |
94 | |
95 | $timestampField = $this->getTimestampField(); |
96 | $timestamp = $row->$timestampField; |
97 | $closeList = $this->isHeaderRowNeeded( $timestamp ); |
98 | if ( $closeList ) { |
99 | $s .= $this->getEndGroup(); |
100 | } |
101 | if ( $this->isHeaderRowNeeded( $timestamp ) ) { |
102 | $s .= $this->getHeaderRow( $this->getMonthFromTimestamp( $timestamp ) ); |
103 | $this->lastHeaderTimestamp = $timestamp; |
104 | } |
105 | $s .= $this->formatRow( $row ); |
106 | |
107 | return $s; |
108 | } |
109 | |
110 | /** |
111 | * Copied from {@see TablePager::getLimitSelectList()}. |
112 | * XXX This should probably live elsewhere in core and be easier to reuse. |
113 | * |
114 | * @return array |
115 | */ |
116 | public function getLimitSelectList() { |
117 | # Add the current limit from the query string |
118 | # to avoid that the limit is lost after clicking Go next time |
119 | if ( !in_array( $this->mLimit, $this->mLimitsShown, true ) ) { |
120 | $this->mLimitsShown[] = $this->mLimit; |
121 | sort( $this->mLimitsShown ); |
122 | } |
123 | $ret = []; |
124 | foreach ( $this->mLimitsShown as $key => $value ) { |
125 | # The pair is either $index => $limit, in which case the $value |
126 | # will be numeric, or $limit => $text, in which case the $value |
127 | # will be a string. |
128 | if ( is_int( $value ) ) { |
129 | $limit = $value; |
130 | $text = $this->getLanguage()->formatNum( $limit ); |
131 | } else { |
132 | $limit = $key; |
133 | $text = $value; |
134 | } |
135 | $ret[$text] = $limit; |
136 | } |
137 | return $ret; |
138 | } |
139 | |
140 | protected function isHeaderRowNeeded( string $date ): bool { |
141 | if ( !$this->lastHeaderTimestamp ) { |
142 | return true; |
143 | } |
144 | $month = $this->getMonthFromTimestamp( $date ); |
145 | $prevMonth = $this->getMonthFromTimestamp( $this->lastHeaderTimestamp ); |
146 | $year = $this->getYearFromTimestamp( $date ); |
147 | $prevYear = $this->getYearFromTimestamp( $this->lastHeaderTimestamp ); |
148 | return $month !== $prevMonth || $year !== $prevYear; |
149 | } |
150 | |
151 | /** |
152 | * @inheritDoc |
153 | */ |
154 | public function formatRow( $row ) { |
155 | $htmlRow = ( new Tag( 'li' ) ) |
156 | ->addClasses( [ 'ext-campaignevents-events-list-pager-row' ] ); |
157 | $page = $this->getEventPageFromRow( $row ); |
158 | $pageUrlResolver = $this->pageURLResolver; |
159 | $timestampField = $this->getTimestampField(); |
160 | $timestamp = $row->$timestampField; |
161 | $htmlRow->appendContent( ( new Tag() ) |
162 | ->addClasses( [ 'ext-campaignevents-events-list-pager-day' ] ) |
163 | ->appendContent( $this->getDayFromTimestamp( $timestamp ) ) ); |
164 | $detailContainer = ( new Tag() ) |
165 | ->addClasses( [ 'ext-campaignevents-events-list-pager-details' ] ); |
166 | $eventPageLinkElement = ( new Tag( 'a' ) ) |
167 | ->setAttributes( [ |
168 | "href" => $pageUrlResolver->getUrl( $page ), |
169 | "class" => 'ext-campaignevents-events-list-pager-link' |
170 | ] ) |
171 | ->appendContent( $row->event_name ); |
172 | $detailContainer->appendContent( |
173 | ( new Tag( 'h4' ) )->appendContent( $eventPageLinkElement ) |
174 | ); |
175 | $meetingType = $this->msg( $this->getMeetingTypeMsg( $row ) )->text(); |
176 | $detailContainer->appendContent( |
177 | new TextWithIconWidget( [ |
178 | 'icon' => 'clock', |
179 | 'content' => $this->msg( |
180 | 'campaignevents-allevents-date-separator', |
181 | $this->getLanguage()->userDate( $row->event_start_utc, $this->getUser() ), |
182 | $this->getLanguage()->userDate( $row->event_end_utc, $this->getUser() ) |
183 | )->text(), |
184 | 'label' => 'campaignevents-allevents-date-label', |
185 | 'icon_classes' => [ 'ext-campaignevents-eventslist-pager-icon' ], |
186 | ] ) |
187 | ); |
188 | $eventType = new TextWithIconWidget( [ |
189 | 'icon' => 'mapPin', |
190 | 'content' => $meetingType, |
191 | 'label' => 'campaignevents-allevents-meeting-type-label', |
192 | 'icon_classes' => [ 'ext-campaignevents-eventslist-pager-icon' ], |
193 | ] ); |
194 | $userLinker = $this->userLinker; |
195 | $organizerStore = $this->organizerStore; |
196 | $eventID = (int)$row->event_id; |
197 | $organizer = $organizerStore |
198 | ->getEventCreator( $eventID, OrganizersStore::GET_CREATOR_EXCLUDE_DELETED ); |
199 | $organizer ??= $organizerStore->getEventOrganizers( $eventID, 1 )[0]; |
200 | $userLinkElement = new TextWithIconWidget( [ |
201 | 'icon' => 'userRights', |
202 | 'content' => new HtmlSnippet( |
203 | $userLinker->generateUserLinkWithFallback( $organizer->getUser(), $this->getLanguage()->getCode() ) |
204 | ), |
205 | 'label' => 'campaignevents-allevents-organiser-label', |
206 | 'icon_classes' => [ 'ext-campaignevents-eventslist-pager-icon' ], |
207 | ] ); |
208 | $detailContainer->appendContent( |
209 | ( new Tag() )->addClasses( |
210 | [ 'ext-campaignevents-eventslist-pager-bottom' ] |
211 | )->appendContent( $eventType, $userLinkElement ) |
212 | ); |
213 | return $htmlRow->appendContent( $detailContainer ); |
214 | } |
215 | |
216 | /** |
217 | * @inheritDoc |
218 | */ |
219 | public function getIndexField() { |
220 | return [ [ 'event_start_utc', 'event_id' ] ]; |
221 | } |
222 | |
223 | public function getNavigationBar(): string { |
224 | if ( !$this->isNavigationBarShown() ) { |
225 | return ''; |
226 | } |
227 | |
228 | if ( isset( $this->mNavigationBar ) ) { |
229 | return $this->mNavigationBar; |
230 | } |
231 | |
232 | $navBuilder = $this->getNavigationBuilder() |
233 | ->setPrevMsg( 'prevn' ) |
234 | ->setNextMsg( 'nextn' ) |
235 | ->setFirstMsg( 'page_first' ) |
236 | ->setLastMsg( 'page_last' ); |
237 | |
238 | $this->mNavigationBar = $navBuilder->getHtml(); |
239 | |
240 | return $this->mNavigationBar; |
241 | } |
242 | |
243 | /** |
244 | * @param stdClass $row |
245 | * @return string |
246 | */ |
247 | private function getMeetingTypeMsg( stdClass $row ): string { |
248 | $meetingType = EventStore::getMeetingTypeFromDBVal( $row->event_meeting_type ); |
249 | switch ( $meetingType ) { |
250 | case EventRegistration::MEETING_TYPE_IN_PERSON: |
251 | return 'campaignevents-eventslist-location-in-person'; |
252 | case EventRegistration::MEETING_TYPE_ONLINE: |
253 | return 'campaignevents-eventslist-location-online'; |
254 | case EventRegistration::MEETING_TYPE_ONLINE_AND_IN_PERSON: |
255 | return 'campaignevents-eventslist-location-online-and-in-person'; |
256 | default: |
257 | throw new UnexpectedValueException( "Unexpected meeting type $meetingType" ); |
258 | } |
259 | } |
260 | |
261 | /** |
262 | * @param string $timestamp |
263 | * @return string |
264 | */ |
265 | private function getYearFromTimestamp( string $timestamp ): string { |
266 | $timestamp = $this->offsetTimestamp( $timestamp ); |
267 | return $this->getLanguage()->sprintfDate( 'Y', $timestamp ); |
268 | } |
269 | |
270 | /** |
271 | * @param string $timestamp |
272 | * @return string |
273 | */ |
274 | private function getMonthFromTimestamp( string $timestamp ): string { |
275 | $timestamp = $this->offsetTimestamp( $timestamp ); |
276 | // TODO This is not guaranteed to return the month name in a format suitable for section headings (e.g., |
277 | // it may need to be capitalized). |
278 | return $this->getLanguage()->sprintfDate( 'F', $timestamp ); |
279 | } |
280 | |
281 | /** |
282 | * @param string $timestamp |
283 | * @return string |
284 | */ |
285 | private function getDayFromTimestamp( string $timestamp ): string { |
286 | $timestamp = $this->offsetTimestamp( $timestamp ); |
287 | return $this->getLanguage()->sprintfDate( 'j', $timestamp ); |
288 | } |
289 | |
290 | /** |
291 | * @param string $timestamp |
292 | * @return string |
293 | */ |
294 | private function offsetTimestamp( string $timestamp ): string { |
295 | $offset = $this->options |
296 | ->getOption( $this->getUser(), 'timecorrection' ); |
297 | |
298 | return $this->getLanguage()->userAdjust( $timestamp, $offset ); |
299 | } |
300 | |
301 | /** |
302 | * @return array |
303 | */ |
304 | public function getSubqueryInfo(): array { |
305 | $query = $this->getDefaultSubqueryInfo(); |
306 | if ( $this->meetingType !== null ) { |
307 | $query['conds']['event_meeting_type'] = EventStore::meetingTypeToDBVal( $this->meetingType ); |
308 | } |
309 | return $query; |
310 | } |
311 | |
312 | /** |
313 | * @param int|null|string $offset |
314 | * @param int $limit |
315 | * @param bool $order |
316 | * @return array |
317 | */ |
318 | public function buildQueryInfo( $offset, $limit, $order ): array { |
319 | [ $tables, $fields, $conds, $fname, $options, $join_conds ] = parent::buildQueryInfo( $offset, $limit, $order ); |
320 | // this is required to set the offsets correctly |
321 | $offsets = $this->getDateRangeCond( $this->startDate, $this->endDate ); |
322 | if ( $offsets ) { |
323 | [ $startOffset, $endOffset ] = $offsets; |
324 | if ( $startOffset ) { |
325 | $crossStartCondition = $this->mDb->expr( $this->getTimestampField(), '<=', $startOffset ) |
326 | ->and( 'event_end_utc', '>=', $startOffset ); |
327 | |
328 | $withinDatesCondition = $this->mDb->expr( $this->getTimestampField(), '>=', $startOffset ); |
329 | if ( $endOffset ) { |
330 | $withinDatesCondition = $withinDatesCondition->and( 'event_end_utc', '<=', $endOffset ); |
331 | } |
332 | $conds[] = new OrExpressionGroup( $crossStartCondition, $withinDatesCondition ); |
333 | } elseif ( $endOffset ) { |
334 | $conds[] = $this->mDb->expr( $this->getTimestampField(), '<=', $endOffset ); |
335 | } |
336 | } |
337 | return [ $tables, $fields, $conds, $fname, $options, $join_conds ]; |
338 | } |
339 | |
340 | /** |
341 | * @param string $startDate |
342 | * @param string $endDate |
343 | * @return array|null |
344 | */ |
345 | private function getDateRangeCond( string $startDate, string $endDate ): ?array { |
346 | try { |
347 | $startOffset = null; |
348 | if ( $startDate !== '' ) { |
349 | $startTimestamp = MWTimestamp::getInstance( $startDate ); |
350 | $startOffset = $this->mDb->timestamp( $startTimestamp->getTimestamp() ); |
351 | } |
352 | |
353 | if ( $endDate !== '' ) { |
354 | $endTimestamp = MWTimestamp::getInstance( $endDate ); |
355 | // Turned to use '<' for consistency with the parent class, |
356 | // add one second for compatibility with existing use cases |
357 | $endTimestamp->timestamp = $endTimestamp->timestamp->modify( '+1 second' ); |
358 | $this->endOffset = $this->mDb->timestamp( $endTimestamp->getTimestamp() ); |
359 | |
360 | // populate existing variables for compatibility with parent |
361 | $this->mYear = (int)$endTimestamp->format( 'Y' ); |
362 | $this->mMonth = (int)$endTimestamp->format( 'm' ); |
363 | $this->mDay = (int)$endTimestamp->format( 'd' ); |
364 | } |
365 | |
366 | return [ $startOffset, $this->endOffset ]; |
367 | } catch ( TimestampException $ex ) { |
368 | return null; |
369 | } |
370 | } |
371 | } |