diff --git a/README.md b/README.md index 4761814..33f9748 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ This is a Moodle plugin which makes the student experience of planned outages ni The main idea is that instead of an outage being a very booleon on/off situation, this plugin creates the concept of graduated outages where at predefined times before an outage and after, different levels of warning and access can be provided to students and testers letting them know what is about to happen and why. +![Screenshot as of 2016-09-06](docs/2016-09-06_screenshot.png?raw=true) + Why it is an auth plugin? ------------------------- diff --git a/auth.php b/auth.php index b8838f9..e03e823 100644 --- a/auth.php +++ b/auth.php @@ -49,4 +49,11 @@ class auth_plugin_outage extends auth_plugin_base public function user_login($username, $password) { return false; } + + /** + * Login page hook. + */ + public function loginpage_hook() { + \auth_outage\outagelib::inject(); + } } diff --git a/classes/models/outage.php b/classes/models/outage.php index aca44be..7827e38 100644 --- a/classes/models/outage.php +++ b/classes/models/outage.php @@ -27,8 +27,7 @@ namespace auth_outage\models; use auth_outage\outagelib; -class outage -{ +class outage { /** * @var int Outage ID (auto generated by the DB). */ @@ -85,9 +84,30 @@ class outage if (is_object($data) || is_array($data)) { outagelib::data2object($data, $this); + + // FIXME types are wrong. Is this behaving as expected? + $fields = ['createdby', 'id', 'lastmodified', 'modifiedby', 'starttime', 'stoptime', 'warningduration']; + foreach ($fields as $f) { + $this->$f = ($this->$f === null) ? null : (int)$this->$f; + } + return; } throw new \InvalidArgumentException('$data must be null (default), an array or an object.'); } + + public function is_ongoing($time = null) { + if ($time === null) { + $time = time(); + } + if (!is_int($time) || ($time <= 0)) { + throw new \InvalidArgumentException('$time must be an positive int.'); + } + if (is_null($this->starttime) || is_null($this->stoptime)) { + return false; + } + + return (($this->starttime <= $time) && ($time < $this->stoptime)); + } } \ No newline at end of file diff --git a/classes/outagedb.php b/classes/outagedb.php index 59e2821..f7ef64e 100644 --- a/classes/outagedb.php +++ b/classes/outagedb.php @@ -135,4 +135,39 @@ final class outagedb { $DB->delete_records('auth_outage', ['id' => $id]); } -} \ No newline at end of file + + /** + * Gets the most important active outage, considering importance as: + * - Ongoing outages more important than outages in warning period. + * - Outages that start earlier are more important. + * - Outages that stop later are more important. + * @param int|null $time Timestamp considered to check for outages, null for current date/time. + * @return outage|null The outage or null if no active outages were found. + */ + public static function getactive($time = null) { + global $DB; + + if ($time === null) { + $time = time(); + } + if (!is_int($time)) { + throw new \InvalidArgumentException('$time must be null or an int.'); + } + + // TODO Is there a way to use Moodle API instead of writing SQL (conditions not equals)? + // TODO Query not fully using indexes (starttime + 90) + // Gets any active outage (already started or during warning period). + // Gets only one record if available, the one that starts(ed) first and that stops last. + $data = $DB->get_record_sql(' + SELECT * + FROM {auth_outage} + WHERE (starttime - warningduration <= :datetime1 AND stoptime >= :datetime2) + ORDER BY starttime ASC, stoptime DESC, title ASC + LIMIT 1 + ', + ['datetime1' => $time, 'datetime2' => $time] + ); + + return ($data === false) ? null : new \auth_outage\models\outage($data); + } +} diff --git a/classes/outagelib.php b/classes/outagelib.php index 909ee8b..a6081b7 100644 --- a/classes/outagelib.php +++ b/classes/outagelib.php @@ -28,8 +28,9 @@ if (!defined('MOODLE_INTERNAL')) { * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class outagelib -{ +class outagelib { + private static $initialized = false; + /** * Initializes admin pages for outage. * @@ -39,9 +40,41 @@ class outagelib global $PAGE; admin_externalpage_setup('auth_outage_manage'); $PAGE->set_url(new \moodle_url('/auth/outage/list.php')); + return self::get_renderer(); + } + + /** + * Returns the outage renderer. + * @return \renderer_base + */ + public static function get_renderer() { + global $PAGE; return $PAGE->get_renderer('auth_outage'); } + /** + * Will check for ongoing or warning outages and will attach the message bar as required. + */ + public static function inject() { + global $CFG; + global $PAGE; + + // Many hooks can call it, execute only once. + if (self::$initialized) { + return; + } + self::$initialized = true; + + if (($active = outagedb::getactive()) == null) { + return; + } + + // FIXME Code below is raising error at http://moodle.test/my/ for example. + // $PAGE->add_body_class('auth_outage_active'); + $CFG->additionalhtmltopofbody = self::get_renderer()->renderbar($active) + . $CFG->additionalhtmltopofbody; + } + /** * Loads data from an object or array into another object. It ensures no new fields are created in the $obj. * diff --git a/docs/2016-09-06_screenshot.png b/docs/2016-09-06_screenshot.png new file mode 100644 index 0000000..1ce7244 Binary files /dev/null and b/docs/2016-09-06_screenshot.png differ diff --git a/lang/en/auth_outage.php b/lang/en/auth_outage.php index 0febcae..efe9311 100644 --- a/lang/en/auth_outage.php +++ b/lang/en/auth_outage.php @@ -32,6 +32,8 @@ $string['defaultwarningtimedescription'] = 'Default warning time (in minutes) fo $string['description'] = 'Public description'; $string['menudefaults'] = 'Default Settings'; $string['menumanage'] = 'Manage'; +$string['messageoutageongoing'] = 'Our system will be under maintenance until {$a->stop}.'; +$string['messageoutagewarning'] = 'There is an scheduled downtime from {$a->start} until {$a->stop}.'; $string['modify'] = 'Modify'; $string['modifyoutage'] = 'Modify Outage'; $string['outagecreate'] = 'Create Outage'; diff --git a/lib.php b/lib.php index 56943f8..31903ec 100644 --- a/lib.php +++ b/lib.php @@ -23,3 +23,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; + +// FIXME hook not installing in courses/index.php page. + +function auth_outage_extend_navigation_user() { + \auth_outage\outagelib::inject(); +} + +function auth_outage_extend_navigation_frontpage() { + \auth_outage\outagelib::inject(); +} diff --git a/renderer.php b/renderer.php index 0ec3f45..18d6444 100644 --- a/renderer.php +++ b/renderer.php @@ -29,8 +29,7 @@ if (!defined('MOODLE_INTERNAL')) { * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class auth_outage_renderer extends plugin_renderer_base -{ +class auth_outage_renderer extends plugin_renderer_base { public function rendersubtitle($subtitlekey) { if (!is_string($subtitlekey)) { throw new \InvalidArgumentException('$subtitle is not a string.'); @@ -125,4 +124,31 @@ class auth_outage_renderer extends plugin_renderer_base ) ); } + + public function renderbar($outage) { + global $PAGE; + + $PAGE->requires->css(new moodle_url('/auth/outage/res/outage.css')); + + $start = userdate($outage->starttime, get_string('strftimedatetimeshort')); + $stop = userdate($outage->stoptime, get_string('strftimedatetimeshort')); + + $message = get_string( + $outage->is_ongoing() ? 'messageoutageongoing' : 'messageoutagewarning', + 'auth_outage', + ['start' => $start, 'stop' => $stop] + ); + + return + html_writer::div( + html_writer::div( + html_writer::div($outage->title, 'auth_outage_warningbar_box_title') + . html_writer::div($message, 'auth_outage_warningbar_box_message'), + 'auth_outage_warningbar_box' + ), + 'auth_outage_warningbar' + ) + . + html_writer::div(' ', 'auth_outage_warningbar_spacer'); + } } diff --git a/res/outage.css b/res/outage.css new file mode 100644 index 0000000..d703444 --- /dev/null +++ b/res/outage.css @@ -0,0 +1,35 @@ +.auth_outage_warningbar { + position: fixed; + top: 0px; + left: 0px; + height: 90px; + z-index: 9999; + width: 100%; + text-align: center; + background-color: white; +} + +.auth_outage_warningbar_box { + border-top: 2px dashed #a00000; + border-bottom: 2px dashed #a00000; + background-color: #ffcccc; + color: #a00000; +} + +.auth_outage_warningbar_box_title { + font-size: 200%; + font-weight: bold; + margin: 10px 0; +} + +.auth_outage_warningbar_box_message { + margin-bottom: 5px; +} + +.navbar.navbar-fixed-top { + top: 90px; +} + +.auth_outage_warningbar_spacer { + height: 80px; +} \ No newline at end of file diff --git a/settings.php b/settings.php index 1507e1b..fa082c6 100644 --- a/settings.php +++ b/settings.php @@ -25,7 +25,6 @@ defined('MOODLE_INTERNAL') || die; // FIXME If plugin not installed, it is still generating the category Outage under Auth Plugins. - if ($hassiteconfig) { // Configure default settings page. $settings->visiblename = get_string('menudefaults', 'auth_outage'); diff --git a/tests/outage_test.php b/tests/outage_test.php index b62097d..0c0bd87 100644 --- a/tests/outage_test.php +++ b/tests/outage_test.php @@ -28,8 +28,7 @@ use auth_outage\models\outage; defined('MOODLE_INTERNAL') || die(); -class outage_test extends basic_testcase -{ +class outage_test extends basic_testcase { public function test_constructor() { $outage = new outage(); // Very important, this should never change. @@ -39,4 +38,38 @@ class outage_test extends basic_testcase self::assertNull($v); } } + + public function test_isongoing() { + $now = time(); + + // In the past. + $outage = new outage([ + 'starttime' => $now + (-3 * 60 * 60), + 'stoptime' => $now + (-2 * 60 * 60), + 'warningduration' => 2 * 60 * 60, + 'title' => '', + 'description' => '' + ]); + self::assertFalse($outage->is_ongoing($now)); + + // In the present (ongoing). + $outage = new outage([ + 'starttime' => $now + (-1 * 60 * 60), + 'stoptime' => $now + (1 * 60 * 60), + 'warningduration' => 2 * 60 * 60, + 'title' => '', + 'description' => '' + ]); + self::assertTrue($outage->is_ongoing($now)); + + // In the future. + $outage = new outage([ + 'starttime' => $now + (1 * 60 * 60), + 'stoptime' => $now + (2 * 60 * 60), + 'warningduration' => 2 * 60 * 60, + 'title' => '', + 'description' => '' + ]); + self::assertFalse($outage->is_ongoing($now)); + } } diff --git a/tests/outagedb_test.php b/tests/outagedb_test.php index 5226c64..77704f1 100644 --- a/tests/outagedb_test.php +++ b/tests/outagedb_test.php @@ -29,8 +29,7 @@ use auth_outage\outagedb; defined('MOODLE_INTERNAL') || die(); -class outagedb_test extends advanced_testcase -{ +class outagedb_test extends advanced_testcase { /** * Make sure we can save and update. */ @@ -95,7 +94,6 @@ class outagedb_test extends advanced_testcase * Perform some tests on the data itself, checking values after inserted and updated. */ public function test_basiccrud() { - return; $this->resetAfterTest(true); // Create some outages. @@ -134,6 +132,64 @@ class outagedb_test extends advanced_testcase } } + public function test_getactive() { + $this->resetAfterTest(true); + + // Have a consistent time for now (no seconds variation), helps debugging. + $now = time(); + + // Should never fail. + self::assertEquals([], outagedb::getall(), 'Ensure there are no other outages that can affect the test.'); + self::assertNull(outagedb::getactive($now), 'There should be no active outage at this point.'); + + // An outage that starts in the future and is not in warning period. + self::saveoutage($now, 2, 3, 1); + self::assertNull(outagedb::getactive($now), 'No active outages yet.'); + + // An outage that is already in the past. + self::saveoutage($now, -3, -2, 1); + self::assertNull(outagedb::getactive($now), 'No active outages yet.'); + + // An outage in warning period. + $activeid = self::saveoutage($now, 1, 2, 2); + self::assertSame($activeid, outagedb::getactive($now)->id, 'Wrong active outage picked.'); + + // Another outage in warning period, but ignored as it starts after the previous one. + self::saveoutage($now, 2, 3, 3); + self::assertSame($activeid, outagedb::getactive($now)->id, 'Wrong active outage picked.'); + + // An ongoing outage. + $activeid = self::saveoutage($now, -2, 2, 1); + self::assertSame($activeid, outagedb::getactive($now)->id, 'Wrong active outage picked.'); + + // Another ongoing outage but ignored because it started after the previous one. + self::saveoutage($now, -1, 2, 1); + self::assertSame($activeid, outagedb::getactive($now)->id, 'Wrong active outage picked.'); + + // Another ongoing outage starting at the same time, but ignored as it stops before the previous one. + self::saveoutage($now, -2, 1, 1); + self::assertSame($activeid, outagedb::getactive($now)->id, 'Wrong active outage picked.'); + } + + /** + * Helper function to create an outage then save it to the database. + * + * @param $now int Timestamp for now, such as time(). + * @param $start int In how many hours this outage starts. Can be negative. + * @param $stop int In how many hours this outage finishes. Can be negative. + * @param $warning int Warning duration in hours. + * @return int Id the of created outage. + */ + private static function saveoutage($now, $start, $stop, $warning) { + return outagedb::save(new outage([ + 'starttime' => $now + ($start * 60 * 60), + 'stoptime' => $now + ($stop * 60 * 60), + 'warningduration' => ($warning * 60 * 60), + 'title' => 'Test Outage', + 'description' => 'Test Outage Description.' + ])); + } + /** * Helper function to create an outage for tests. *