Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Soft-cancel and reinstate episodes #904

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions schema/patches/4.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
BEGIN;

ALTER TABLE schedule.show_season_timeslot
ADD COLUMN cancelled_at TIMESTAMPTZ NULL DEFAULT NULL;

INSERT INTO metadata.metadata_key
VALUES (
18,
'cancel-reason',
false,
'Reason for Timeslot cancellation',
300,
'Reasons for Timeslot cancellation',
false
);

COMMIT;
2 changes: 1 addition & 1 deletion src/Classes/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function status()
* @param array $params Parameters for the query
* @param bool $rollback Deprecated.
*
* @return A pg result reference
* @return resource A pg result reference
*
* @throws MyRadioException If the query fails
* @assert ('SELECT * FROM public.tableethatreallydoesntexist') throws MyRadioException
Expand Down
2 changes: 1 addition & 1 deletion src/Classes/MyRadioEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ public static function sendEmailToList(MyRadio_List $to, $subject, $message, MyR
*
* @param array $to An array of User objects
* @param string $subject email subject
* @param sting $message email message
* @param string $message email message
*/
public static function sendEmailToUserSet($to, $subject, $message, MyRadio_User $from = null)
{
Expand Down
7 changes: 4 additions & 3 deletions src/Classes/ServiceAPI/MyRadio_Season.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class MyRadio_Season extends MyRadio_Metadata_Common
private $subtype_id;
protected $owner;

protected function __construct($season_id)
protected function __construct($season_id, $include_cancelled_timeslots = false)
{
$this->season_id = (int) $season_id;
//Init Database
Expand Down Expand Up @@ -74,8 +74,9 @@ protected function __construct($season_id)
) AS requested_durations, (
SELECT array_to_json(array(
SELECT show_season_timeslot_id FROM schedule.show_season_timeslot
WHERE show_season_id=$1
ORDER BY start_time ASC
WHERE show_season_id=$1 ' .
$include_cancelled_timeslots ? '' : 'AND cancelled_at IS NULL' .
' ORDER BY start_time ASC
))
) AS timeslots, (
SELECT array_to_json(array(
Expand Down
206 changes: 191 additions & 15 deletions src/Classes/ServiceAPI/MyRadio_Timeslot.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class MyRadio_Timeslot extends MyRadio_Metadata_Common
private $timeslot_num;
protected $owner;
protected $credits;
private $cancelled_at;

protected function __construct($timeslot_id)
{
Expand All @@ -49,7 +50,7 @@ protected function __construct($timeslot_id)
// Note that credits have different metadata timeranges to text
// This is annoying, but needs to be this way.
$result = self::$db->fetchOne(
'SELECT show_season_timeslot_id, show_season_id, start_time, duration, memberid, (
'SELECT show_season_timeslot_id, show_season_id, start_time, duration, memberid, cancelled_at, (
SELECT array_to_json(array(
SELECT metadata_key_id FROM schedule.timeslot_metadata
WHERE show_season_timeslot_id=$1
Expand Down Expand Up @@ -118,6 +119,7 @@ protected function __construct($timeslot_id)
$this->duration = $result['duration'];
$this->owner = MyRadio_User::getInstance($result['memberid']);
$this->timeslot_num = (int)$result['timeslot_num'];
$this->cancelled_at = empty($result['cancelled_at']) ? null : $result['cancelled_at'];

$metadata_types = json_decode($result['metadata_types']);
$metadata = json_decode($result['metadata']);
Expand Down Expand Up @@ -234,6 +236,7 @@ public function getTimeslotAfter($filter = [1])
INNER JOIN schedule.show USING (show_id)
WHERE start_time >= $1 AND start_time <= $2
AND show_type_id = ANY ($3)
AND cancelled_at IS NULL
ORDER BY start_time ASC LIMIT 1',
[
CoreUtils::getTimestamp($this->getEndTime() - 300),
Expand Down Expand Up @@ -327,7 +330,7 @@ public static function searchMeta($query, $string_keys = null, $effective_from =
*/
public function toDataSource($mixins = [])
{
return array_merge(
$data = array_merge(
$this->getSeason()->toDataSource($mixins),
[
'timeslot_id' => $this->getID(),
Expand All @@ -338,21 +341,39 @@ public function toDataSource($mixins = [])
'time' => $this->getStartTime(),
'start_time' => CoreUtils::happyTime($this->getStartTime()),
'duration' => $this->getDuration(),
'cancelled' => $this->isCancelled(),
'mixcloud_status' => $this->getMeta('upload_state'),
'mixcloud_starttime' => $this->getMeta('upload_starttime'),
'mixcloud_endtime' => $this->getMeta('upload_endtime'),
'rejectlink' => [
'display' => 'icon',
'value' => 'trash',
'title' => 'Cancel Episode',
'url' => URLUtils::makeURL(
'Scheduler',
'cancelEpisode',
['show_season_timeslot_id' => $this->getID()]
),
],
]
);

if ($this->isCancelled()) {
// overriding rejectlink to avoid nonsense
$data['rejectlink'] = [
'display' => 'icon',
'value' => 'repeat',
'title' => 'Reinstate Episode',
'url' => URLUtils::makeURL(
'Scheduler',
'reinstateEpisode',
['show_season_timeslot_id' => $this->getID()]
),
];
} else {
$data['rejectlink'] = [
'display' => 'icon',
'value' => 'trash',
'title' => 'Cancel Episode',
'url' => URLUtils::makeURL(
'Scheduler',
'cancelEpisode',
['show_season_timeslot_id' => $this->getID()]
),
];
}

return $data;
}

/**
Expand Down Expand Up @@ -622,6 +643,7 @@ public static function getWeekSchedule($weekno, $year = null)
(start_time >= $1 AND start_time <= $2)
)
AND show_type_id = 1
AND cancelled_at IS NULL
ORDER BY start_time ASC',
[$startTimestamp, $endTimestamp]
);
Expand Down Expand Up @@ -749,7 +771,7 @@ public static function getCurrentAndNext($time = null, $n = 1, $filter = [1])
}

/**
* Deletes this Timeslot from the Schedule, and everything associated with it.
* Remove the current timeslot from the schedule. Keep it around in the database though.
*
* This is a proxy for several other methods, depending on the User and the current time:<br>
* (1) If the User has Cancel Show Privileges, then they can remove it at any time, notifying Creditors
Expand Down Expand Up @@ -793,7 +815,7 @@ public function cancelTimeslot($reason)

private function cancelTimeslotAdmin($reason)
{
$r = $this->deleteTimeslot();
$r = $this->setCancelled($reason);
if (!$r) {
return false;
}
Expand All @@ -815,7 +837,7 @@ private function cancelTimeslotAdmin($reason)

private function cancelTimeslotSelfService($reason)
{
$r = $this->deleteTimeslot();
$r = $this->setCancelled($reason);
if (!$r) {
return false;
}
Expand Down Expand Up @@ -865,6 +887,7 @@ private function cancelTimeslotRequest($reason)
/**
* Deletes the timeslot. Nothing else. See the cancelTimeslot... methods for recommended removal usage.
*
* @deprecated This deletes it permanently. To cancel without deleting, use setCancelled
* @return bool success/fail
*/
private function deleteTimeslot()
Expand All @@ -878,6 +901,132 @@ private function deleteTimeslot()
return $r;
}

/**
* Sets this timeslot as cancelled by the current user with the given reason.
* This is done by setting the `cancelled_at` field to the current time, and adding a `cancel-reason` meta field
*
* @todo do we just want to use the presence of the meta field as a sign that it's cancelled?
* Initially I went with no, because that requires another join on each listing - [email protected]
*
* @todo handle cancel requests - set the metadata "memberid" to who requested and "approvedid" to whoever approved it
* (not sure if it's that important)
*
* @param string $reason the cancellation reason
* @throws MyRadioException if the cancel failed for some reason
*/
private function setCancelled(string $reason)
{
$r = self::$db->fetchOne(
'UPDATE schedule.show_season_timeslot
SET cancelled_at = NOW()
WHERE show_season_timeslot_id=$1',
[$this->getID()]
);

if (empty($r)) {
throw new MyRadioException("Couldn't cancel the timeslot");
}

$this->setMeta("cancel-reason", $reason);

$this->updateCacheObject();
return $r;
}

/**
* Reinstate this timeslot onto the schedule.
*/
private function reinstate()
{
$r = self::$db->fetchOne(
'UPDATE schedule.show_season_timeslot
SET cancelled_at = NULL
WHERE show_season_timeslot_id=$1',
[$this->getID()]
);

if (empty($r)) {
throw new MyRadioException("Couldn't cancel the timeslot");
}

$this->setMeta('cancel-reason', null);

$this->updateCacheObject();
self::$cache->purge();

// Email the people
$email = "Hi #NAME, \r\n\r\n Please note that an episode of your show, " . $this->getMeta('title')
. ' has been reinstated onto the schedule. The episode is now at the following time: '
. CoreUtils::happyTime($this->getStartTime())
. "\r\n\r\n";
$email .= "Regards\r\n" . Config::$long_name . ' Programming Team';

MyRadioEmail::sendEmailToUserSet(
$this->getSeason()->getShow()->getCreditObjects(),
'Episode of ' . $this->getMeta('title') . ' Reinstated',
$email
);

return $r;
}

private function reinstateRequest($reason)
{
$email = $this->getMeta('title')
. ' on ' . CoreUtils::happyTime($this->getStartTime())
. ' has requested to reinstate this episode, because ' . $reason
. "\r\n\r\n";
$email .= "To reinstate the timeslot, visit ";
$email .= URLUtils::makeURL(
'Scheduler',
'reinstateEpisode',
['show_season_timeslot_id' => $this->getID(), 'reason' => base64_encode($reason)]
);

MyRadioEmail::sendEmailToList(MyRadio_List::getByName('presenting'), 'Show Reinstate Request', $email);
return true;
}

/**
* Reinstate previously cancelled timeslot.
*
* If the user has cancel show privileges (therefore, reinstate), put it back directly.
* Otherwise, send an email to programming.
*
* @param string $reason Why the episode should be reinstated. Ignored if the current user can reinstate.
* @return bool if it succeeded or not
*/
public function reinstateTimeslot(string $reason)
{
if (empty($this->cancelled_at)) {
// This isn't even cancelled. What are you playing at?
return false;
}
//Get if the User has permission to drop the episode
if (MyRadio_User::getInstance()->hasAuth(AUTH_DELETESHOWS)) {
//Yep, do an administrative reinstate
$r = $this->reinstate();
} elseif ($this->getSeason()->getShow()->isCurrentUserAnOwner()) {
//Get if the User is a Creditor
// Reinstatement still requires approval of Programming
$r = $this->reinstateRequest($reason);
} else {
//They can't do this.
return false;
}

return !empty($r);
}

/**
* Has this timeslot been soft-cancelled?
* @return bool
*/
public function isCancelled()
{
return !empty($this->cancelled_at);
}

/**
* Move this Timeslot to a new time.
* @param $newStart
Expand Down Expand Up @@ -1187,6 +1336,33 @@ public static function getCancelForm()
);
}

public static function getReinstateRequest()
{
return (
new MyRadioForm(
'sched_cancel',
'Scheduler',
'reinstateEpisode',
[
'debug' => false,
'title' => 'Request to Reinstate Episode',
]
)
)->addField(
new MyRadioFormField(
'reason',
MyRadioFormField::TYPE_BLOCKTEXT,
['label' => 'Please explain why this Episode should be reinstated on the Schedule']
)
)->addField(
new MyRadioFormField(
'show_season_timeslot_id',
MyRadioFormField::TYPE_HIDDEN,
['value' => $_REQUEST['show_season_timeslot_id']]
)
);
}

public function getMoveForm()
{
$title = $this->getMeta('title') . ' - ' . CoreUtils::happyTime($this->getStartTime());
Expand Down
37 changes: 37 additions & 0 deletions src/Controllers/Scheduler/reinstateEpisode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* Presents a form to the user to enable them to reinstate an Episode.
*/
use \MyRadio\Config;
use \MyRadio\MyRadioException;
use \MyRadio\MyRadio\URLUtils;
use \MyRadio\ServiceAPI\MyRadio_Timeslot;
use MyRadio\ServiceAPI\MyRadio_User;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
//Submitted
//Get data
$data = MyRadio_Timeslot::getReinstateRequest()->readValues();
//Cancel
/** @var MyRadio_Timeslot $timeslot */
$timeslot = MyRadio_Timeslot::getInstance($data['show_season_timeslot_id']);
$result = $timeslot->reinstateTimeslot($data['reason']);

if (!$result) {
$message = 'This episode couldn\'t be reinstated.';
} else if (MyRadio_User::getCurrentUser()->hasAuth(AUTH_DELETESHOWS)) {
$message = 'Episode reinstated.';
} else {
$message = 'Your reinstatement request has been sent. You will receive an email informing you of updates.';
}

URLUtils::backWithMessage($message);
} else {
//Not Submitted

if (!isset($_REQUEST['show_season_timeslot_id'])) {
throw new MyRadioException('No timeslotid provided.', 400);
}

MyRadio_Timeslot::getReinstateRequest()->render();
}
Loading