Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventsListPager
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 15
1332
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 getRow
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getLimitSelectList
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 isHeaderRowNeeded
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 formatRow
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNavigationBar
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getMeetingTypeMsg
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getYearFromTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMonthFromTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDayFromTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 offsetTimestamp
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getSubqueryInfo
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 buildQueryInfo
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getDateRangeCond
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\Pager;
6
7use MediaWiki\Cache\LinkBatchFactory;
8use MediaWiki\Extension\CampaignEvents\Database\CampaignsDatabaseHelper;
9use MediaWiki\Extension\CampaignEvents\Event\EventRegistration;
10use MediaWiki\Extension\CampaignEvents\Event\Store\EventStore;
11use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFactory;
12use MediaWiki\Extension\CampaignEvents\MWEntity\PageURLResolver;
13use MediaWiki\Extension\CampaignEvents\MWEntity\UserLinker;
14use MediaWiki\Extension\CampaignEvents\Organizers\OrganizersStore;
15use MediaWiki\Extension\CampaignEvents\Widget\TextWithIconWidget;
16use MediaWiki\Pager\IndexPager;
17use MediaWiki\Pager\ReverseChronologicalPager;
18use MediaWiki\User\UserOptionsLookup;
19use MediaWiki\Utils\MWTimestamp;
20use OOUI\HtmlSnippet;
21use OOUI\Tag;
22use stdClass;
23use UnexpectedValueException;
24use Wikimedia\Rdbms\OrExpressionGroup;
25use Wikimedia\Timestamp\TimestampException;
26
27class 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}