<?php
declare( strict_types=1 );
namespace MediaWiki\Extension\CampaignEvents\Pager;
use IContextSource;
use LogicException;
use MediaWiki\Extension\CampaignEvents\Database\CampaignsDatabaseHelper;
use MediaWiki\Extension\CampaignEvents\Event\EventRegistration;
use MediaWiki\Extension\CampaignEvents\Event\Store\EventStore;
use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup;
use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFactory;
use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsPage;
use MediaWiki\Extension\CampaignEvents\MWEntity\MWDatabaseProxy;
use MediaWiki\Extension\CampaignEvents\MWEntity\MWUserProxy;
use MediaWiki\Extension\CampaignEvents\MWEntity\PageURLResolver;
use MediaWiki\Extension\CampaignEvents\Special\SpecialEditEventRegistration;
use MediaWiki\Extension\CampaignEvents\Special\SpecialEventDetails;
use MediaWiki\Linker\LinkRenderer;
use OOUI\ButtonWidget;
use SpecialPage;
use stdClass;
use TablePager;
class EventsPager extends TablePager {
public const STATUS_ANY = 'any';
public const STATUS_OPEN = 'open';
public const STATUS_CLOSED = 'closed';
private const SORT_INDEXES = [
'event_start' => [ 'event_start', 'event_name', 'event_id' ],
'event_name' => [ 'event_name', 'event_start', 'event_id' ],
'num_participants' => [ 'num_participants', 'event_start', 'event_id' ],
];
/** @var CampaignsCentralUserLookup */
private $centralUserLookup;
/** @var CampaignsPageFactory */
private $campaignsPageFactory;
/** @var PageURLResolver */
private $pageURLResolver;
/** @var string */
private $search;
/** @var string */
private $status;
/** @var array<int,ICampaignsPage> Cache of event page objects, keyed by event ID */
private $eventPageCache = [];
/**
* @param IContextSource $context
* @param LinkRenderer $linkRenderer
* @param CampaignsDatabaseHelper $databaseHelper
* @param CampaignsCentralUserLookup $centralUserLookup
* @param CampaignsPageFactory $campaignsPageFactory
* @param PageURLResolver $pageURLResolver
* @param string $search
* @param string $status One of the self::STATUS_* constants
*/
public function __construct(
IContextSource $context,
LinkRenderer $linkRenderer,
CampaignsDatabaseHelper $databaseHelper,
CampaignsCentralUserLookup $centralUserLookup,
CampaignsPageFactory $campaignsPageFactory,
PageURLResolver $pageURLResolver,
string $search,
string $status
) {
// Set the database before calling the parent constructor, otherwise it'll use the local one.
$dbWrapper = $databaseHelper->getDBConnection( DB_REPLICA );
if ( !$dbWrapper instanceof MWDatabaseProxy ) {
throw new LogicException( "Wrong DB class?!" );
}
$this->mDb = $dbWrapper->getMWDatabase();
parent::__construct( $context, $linkRenderer );
$this->centralUserLookup = $centralUserLookup;
$this->campaignsPageFactory = $campaignsPageFactory;
$this->pageURLResolver = $pageURLResolver;
$this->search = $search;
$this->status = $status;
}
/**
* @inheritDoc
*/
public function getQueryInfo(): array {
$conds = [];
if ( $this->search !== '' ) {
// TODO Make this case-insensitive. Not easy right now because the name is a binary string and the DBAL does
// not provide a method for converting it to a non-binary value on which LOWER can be applied.
$conds[] = 'event_name' . $this->mDb->buildLike(
$this->mDb->anyString(), $this->search, $this->mDb->anyString() );
}
switch ( $this->status ) {
case self::STATUS_ANY:
break;
case self::STATUS_OPEN:
$conds['event_status'] = EventStore::getEventStatusDBVal( EventRegistration::STATUS_OPEN );
break;
case self::STATUS_CLOSED:
$conds['event_status'] = EventStore::getEventStatusDBVal( EventRegistration::STATUS_CLOSED );
break;
default:
// Invalid statuses can only be entered by messing with the HTML or query params, ignore.
}
$campaignsUser = new MWUserProxy( $this->getUser(), $this->getAuthority() );
// Use a subquery and a temporary table to work around IndexPager not using HAVING for aggregates (T308694)
// and to support postgres (which doesn't allow aliases in HAVING).
$subquery = $this->mDb->buildSelectSubquery(
[ 'campaign_events', 'ce_participants', 'ce_organizers' ],
[
'event_id',
'event_name',
'event_page_namespace',
'event_page_title',
'event_page_prefixedtext',
'event_page_wiki',
'event_status',
'event_start',
'event_meeting_type',
'num_participants' => 'COUNT(cep_id)'
],
array_merge(
$conds,
[
'event_deleted_at' => null,
'cep_unregistered_at' => null,
'ceo_user_id' => $this->centralUserLookup->getCentralID( $campaignsUser )
]
),
__METHOD__,
[
'GROUP BY' => [
'cep_event_id',
'event_id',
'event_name',
'event_page_namespace',
'event_page_title',
'event_page_prefixedtext',
'event_page_wiki',
'event_status',
'event_start',
'event_meeting_type'
]
],
[
'ce_participants' => [
'LEFT JOIN',
'event_id=cep_event_id'
],
'ce_organizers' => [
'LEFT JOIN',
'event_id=ceo_event_id'
]
]
);
return [
'tables' => [ 'tmp' => $subquery ],
'fields' => [
'event_id',
'event_name',
'event_page_namespace',
'event_page_title',
'event_page_prefixedtext',
'event_page_wiki',
'event_status',
'event_start',
'event_meeting_type',
'num_participants'
],
'conds' => [],
'options' => [],
'join_conds' => []
];
}
/**
* @inheritDoc
*/
public function formatValue( $name, $value ): string {
switch ( $name ) {
case 'event_start':
return htmlspecialchars( $this->getLanguage()->userDate( $value, $this->getUser() ) );
case 'event_name':
return $this->getLinkRenderer()->makeKnownLink(
SpecialPage::getTitleFor( SpecialEventDetails::PAGE_NAME, $this->mCurrentRow->event_id ),
$value,
[ 'class' => 'ext-campaignevents-eventspager-eventpage-link' ]
);
case 'event_location':
$meetingType = EventStore::getMeetingTypeFromDBVal( $this->mCurrentRow->event_meeting_type );
if ( $meetingType === EventRegistration::MEETING_TYPE_ONLINE ) {
$msgKey = 'campaignevents-eventslist-location-online';
} elseif ( $meetingType === EventRegistration::MEETING_TYPE_IN_PERSON ) {
$msgKey = 'campaignevents-eventslist-location-in-person';
} elseif ( $meetingType === EventRegistration::MEETING_TYPE_ONLINE_AND_IN_PERSON ) {
$msgKey = 'campaignevents-eventslist-location-online-and-in-person';
} else {
throw new LogicException( "Unexpected meeting type: $meetingType" );
}
return $this->msg( $msgKey )->escaped();
case 'num_participants':
return htmlspecialchars( $this->getLanguage()->formatNum( $value ) );
case 'manage_event':
$eventID = $this->mCurrentRow->event_id;
// This will be replaced with a ButtonMenuSelectWidget in JS.
$btn = new ButtonWidget( [
'framed' => false,
'label' => $this->msg( 'campaignevents-eventslist-manage-btn-info' )->text(),
'title' => $this->msg( 'campaignevents-eventslist-manage-btn-info' )->text(),
'invisibleLabel' => true,
'icon' => 'ellipsis',
'href' => SpecialPage::getTitleFor(
SpecialEditEventRegistration::PAGE_NAME,
$eventID
)->getLocalURL(),
'classes' => [ 'ext-campaignevents-eventspager-manage-btn' ]
] );
$eventStatus = EventStore::getEventStatusFromDBVal( $this->mCurrentRow->event_status );
$eventPage = $this->getEventPageFromRow( $this->mCurrentRow );
$btn->setAttributes( [
'data-event-id' => $eventID,
'data-event-name' => $this->mCurrentRow->event_name,
'data-is-closed' => $eventStatus === EventRegistration::STATUS_CLOSED ? 1 : 0,
'data-event-page-url' => $this->pageURLResolver->getFullUrl( $eventPage )
] );
return $btn->toString();
default:
throw new LogicException( "Unexpected name $name" );
}
}
/**
* @param stdClass $eventRow
* @return ICampaignsPage
*/
private function getEventPageFromRow( stdClass $eventRow ): ICampaignsPage {
$eventID = $eventRow->event_id;
if ( !isset( $this->eventPageCache[$eventID] ) ) {
$this->eventPageCache[$eventID] = $this->campaignsPageFactory->newPageFromDB(
(int)$eventRow->event_page_namespace,
$eventRow->event_page_title,
$eventRow->event_page_prefixedtext,
$eventRow->event_page_wiki
);
}
return $this->eventPageCache[$eventID];
}
/**
* @inheritDoc
*/
protected function getFieldNames(): array {
return [
'event_start' => $this->msg( 'campaignevents-eventslist-column-date' )->text(),
'event_name' => $this->msg( 'campaignevents-eventslist-column-name' )->text(),
'event_location' => $this->msg( 'campaignevents-eventslist-column-location' )->text(),
'num_participants' => $this->msg( 'campaignevents-eventslist-column-participants-number' )->text(),
'manage_event' => ''
];
}
/**
* Overridden to provide additional columns to order by, since most columns are not unique.
* @inheritDoc
*/
public function getIndexField(): array {
// XXX Work around T308697: TablePager and IndexPager seem to be incompatible and the correct
// index is not chosen automatically.
return [ self::SORT_INDEXES[$this->mSort] ];
}
/**
* @inheritDoc
*/
public function getDefaultSort(): string {
return 'event_start';
}
/**
* @inheritDoc
*/
protected function isFieldSortable( $field ): bool {
return array_key_exists( $field, self::SORT_INDEXES );
}
/**
* @inheritDoc
*/
protected function getTableClass() {
return parent::getTableClass() . ' ext-campaignevents-eventspager-table';
}
/**
* @inheritDoc
*/
protected function getCellAttrs( $field, $value ) {
$ret = parent::getCellAttrs( $field, $value );
$addClass = null;
if ( $field === 'manage_event' ) {
$addClass = 'ext-campaignevents-eventspager-cell-manage';
}
if ( $addClass ) {
$ret['class'] = isset( $ret['class'] ) ? $ret['class'] . " $addClass" : $addClass;
}
return $ret;
}
/**
* @inheritDoc
*/
public function getModuleStyles() {
return array_merge(
parent::getModuleStyles(),
[
// Avoid creating a new module for the pager only.
'ext.campaignEvents.specialmyevents.styles',
'oojs-ui.styles.icons-interactions'
]
);
}
/**
* @return string[] An array of (non-style) RL modules.
*/
public function getModules(): array {
// Avoid creating a new module for the pager only.
return [ 'ext.campaignEvents.specialmyevents' ];
}
}