Commit 7db7a08a authored by Leigh Stoller's avatar Leigh Stoller

Checkpoint news system, only admins see it for now.

parent 8fa9e601
......@@ -321,6 +321,22 @@ CREATE TABLE `apt_instances` (
PRIMARY KEY (`uuid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- Table structure for table `apt_news`
--
DROP TABLE IF EXISTS `apt_news`;
CREATE TABLE `apt_news` (
`idx` int(11) NOT NULL auto_increment,
`title` tinytext,
`created` datetime default NULL,
`author` varchar(32) default NULL,
`author_idx` mediumint(8) unsigned NOT NULL default '0',
`portals` set('emulab','aptlab','cloudlab','phantomnet') default NULL,
`body` text,
PRIMARY KEY (`idx`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- Table structure for table `apt_profile_favorites`
--
......
use strict;
use libdb;
sub DoUpdate($$$)
{
my ($dbhandle, $dbname, $version) = @_;
if (!DBTableExists("apt_news")) {
DBQueryFatal("CREATE TABLE `apt_news` ( ".
" `idx` int(11) NOT NULL auto_increment, ".
" `title` tinytext, ".
" `created` datetime default NULL, ".
" `author` varchar(32) default NULL, ".
" `author_idx` mediumint(8) ".
" unsigned NOT NULL default '0', ".
" `portals` set('emulab','aptlab','cloudlab',".
" 'phantomnet') default NULL, ".
" `body` text, ".
" PRIMARY KEY (`idx`) ".
") ENGINE=MyISAM DEFAULT CHARSET=latin1");
}
return 0;
}
# Local Variables:
# mode:perl
# End:
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
chdir("..");
include("defs.php3");
chdir("apt");
include("quickvm_sup.php");
# Must be after quickvm_sup.php since it changes the auth domain.
$page_title = "Edit News Item";
#
# Get current user.
#
RedirectSecure();
$this_user = CheckLoginOrRedirect();
$this_idx = $this_user->uid_idx();
if (!ISADMIN()) {
SPITUSERERROR("You do not have permission to view this page");
exit();
}
#
# Verify page arguments. Cluster is a domain that we turn into a URN.
#
$optargs = OptionalPageArguments("edit", PAGEARG_INTEGER);
if (isset($edit)) {
$editing = 1;
$idx = $edit;
}
else {
$editing = 0;
}
SPITHEADER(1);
# Place to hang the toplevel template.
echo "<div id='main-body'></div>\n";
$defaults = array("title" => "", "body" => "");
if ($editing) {
$query_result = DBQueryFatal("select * from apt_news ".
"where idx='$idx'");
if (!mysql_num_rows($query_result)) {
SPITUSERERROR("No such news item");
exit();
}
$row = mysql_fetch_array($query_result);
$defaults = array("idx" => $row["idx"],
"title" => $row["title"],
"body" => $row["body"],
"author" => $row["author"],
"created"=> DateStringGMT($row["created"]));
}
echo "<script type='text/plain' id='form-json'>\n";
echo htmlentities(json_encode($defaults)) . "\n";
echo "</script>\n";
echo "<script type='text/javascript'>\n";
echo " window.EDITING = $editing;\n";
if ($editing) {
echo " window.IDX = $idx;\n";
}
echo "</script>\n";
REQUIRE_UNDERSCORE();
REQUIRE_SUP();
REQUIRE_MOMENT();
REQUIRE_APTFORMS();
REQUIRE_MARKED();
SPITREQUIRE("js/edit-news.js");
AddTemplateList(array("edit-news", "oops-modal", "waitwait-modal",
"renderer-modal"));
SPITFOOTER();
?>
$(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['edit-news',
'oops-modal',
'waitwait-modal',
'renderer-modal']);
var mainString = templates['edit-news'];
var oopsString = templates['oops-modal'];
var waitwaitString = templates['waitwait-modal'];
var rendererString = templates['renderer-modal'];
var mainTemplate = _.template(mainString);
function initialize()
{
window.APT_OPTIONS.initialize(sup);
var fields = JSON.parse(_.unescape($('#form-json')[0].textContent));
GeneratePageBody(fields);
// Now we can do this.
$('#oops_div').html(oopsString);
$('#waitwait_div').html(waitwaitString);
$('#renderer_div').html(rendererString);
}
//
// Moved into a separate function since we want to regen the form
// after each submit, which happens via ajax on this page.
//
function GeneratePageBody(formfields)
{
// Generate the template.
var html = mainTemplate({
formfields: formfields,
editing: window.EDITING,
});
html = aptforms.FormatFormFieldsHorizontal(html, {"wide" : true});
$('#main-body').html(html);
// This activates the popover subsystem.
$('[data-toggle="popover"]').popover({
trigger: 'hover',
container: 'body'
});
/*
* A double click handler that will render the body in a modal.
*/
$('#body').dblclick(function() {
var text = $(this).val();
$('#renderer_modal_div').html(marked(text));
sup.ShowModal("#renderer_modal");
});
//
// Handle submit button.
//
$('#news-submit-button').click(function (event) {
event.preventDefault();
SubmitForm();
});
aptforms.EnableUnsavedWarning('#edit-news-form');
}
//
// Submit the form.
//
function SubmitForm()
{
var submit_callback = function(json) {
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
window.location.replace(json.value);
};
var checkonly_callback = function(json) {
if (json.code) {
if (json.code != 2) {
sup.SpitOops("oops", json.value);
}
return;
}
aptforms.SubmitForm('#edit-news-form', "news",
(window.EDITING ? "modify" : "create"),
submit_callback);
};
aptforms.CheckForm('#edit-news-form', "news",
(window.EDITING ? "modify" : "create"),
checkonly_callback);
}
$(document).ready(initialize);
});
$(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['news',
'news-item',
'oops-modal',
'confirm-modal']);
var newsString = templates['news'];
var oopsString = templates['oops-modal'];
var confirmString = templates['confirm-modal'];
var newsitemString = templates['news-item'];
var newsitemTemplate = _.template(newsitemString);
function initialize()
{
window.APT_OPTIONS.initialize(sup);
$('#main-body').html(newsString);
$('#oops_div').html(oopsString);
$('#confirm_div').html(confirmString);
/*
* Always a handler for more news items
*/
$('#more-entries').click(function (event) {
event.preventDefault();
DisplayNewsItems();
});
// Show the new new link if an admin.
if (window.ISADMIN) {
$('#new-news').removeClass("hidden");
}
/*
* Ask for and display news items
*/
DisplayNewsItems();
}
/*
* Ask for several news items, starting at the current index.
*/
function DisplayNewsItems()
{
var callback = function(json) {
console.info(json.value);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
// No more entries, kill the More Entries clicker.
if (json.value.length == 0) {
$('#more-entries').addClass("hidden");
return;
}
_.each(json.value, function(blob) {
//console.info(blob);
var idx = blob.idx;
// Convert the body from markdown.
blob.body = marked(blob.body);
var html = newsitemTemplate({"fields" : blob,
"isadmin" : window.ISADMIN});
$('#blog-main').append(html);
$('#post-' + idx + ' .format-date').each(function() {
var date = $.trim($(this).html());
if (date != "") {
$(this).html(moment($(this).html()).format("lll"));
}
});
$('#post-' + idx + ' #delete-button').click(function() {
DeletePost(idx);
});
$('#post-' + idx + ' [data-toggle="popover"]').popover({
trigger: 'click',
placement: 'auto',
container: 'body',
});
// Update our current news index for more entries later.
window.IDX = idx;
});
// But we want to get more at the next lower index.
window.IDX--;
}
var xmlthing = sup.CallServerMethod(null, "news",
"getnews",
{"idx" : window.IDX,
"count" : 6});
xmlthing.done(callback);
}
/*
* Delete a Post.
*/
function DeletePost(idx) {
// Callback for the delete request.
var callback = function (json) {
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
$('#post-' + idx).remove();
};
// Bind the confirm button in the modal. Do the deletion.
$('#confirm_modal #confirm_delete').click(function () {
sup.HideModal('#confirm_modal');
var xmlthing = sup.CallServerMethod(null, "news",
"delete",
{"idx" : idx});
xmlthing.done(callback);
});
// Handler so we know the user closed the modal. We need to
// clear the confirm button handler.
$('#confirm_modal').on('hidden.bs.modal', function (e) {
$('#confirm_modal #confirm_delete').unbind("click");
$('#confirm_modal').off('hidden.bs.modal');
})
sup.ShowModal("#confirm_modal");
}
$(document).ready(initialize);
});
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
#
# Server side of creating a dataset.
#
function Do_CreateNews($idx = null)
{
global $this_user;
global $ajax_args;
global $DBFieldErrstr, $PORTAL_GENESIS;
if (!ISADMIN()) {
SPITAJAX_ERROR(1, "Only admins please");
return;
}
$this_idx = $this_user->uid_idx();
$this_uid = $this_user->uid();
# Allow for form precheck only. So JS code knows it will be fast.
$checkonly = isset($ajax_args["checkonly"]) && $ajax_args["checkonly"];
if (!isset($ajax_args["formfields"])) {
SPITAJAX_ERROR(1, "Missing formfields");
return;
}
$formfields = $ajax_args["formfields"];
$errors = array();
$required = array("title", "body");
foreach ($required as $field) {
if (!isset($formfields[$field]) || $formfields[$field] == "") {
$errors[$field] = "Missing field";
}
}
if (count($errors)) {
SPITAJAX_ERROR(2, $errors);
return;
}
if (!TBvalid_fulltext($formfields["title"])) {
$errors["title"] = $DBFieldErrstr;
}
if (!TBvalid_html_fulltext($formfields["body"])) {
$errors["body"] = $DBFieldErrstr;
}
if (count($errors)) {
SPITAJAX_ERROR(2, $errors);
return;
}
if ($checkonly) {
SPITAJAX_RESPONSE(0);
return;
}
$title = addslashes($formfields["title"]);
$body = addslashes($formfields["body"]);
if ($idx) {
$query_result =
DBQueryWarn("update apt_news set ".
" title='$title',body='$body' ".
"where idx='$idx'");
}
else {
$query_result =
DBQueryWarn("insert into apt_news set ".
" title='$title',created=now(),author='$this_uid', ".
" portals='$PORTAL_GENESIS', ".
" author_idx='$this_idx',body='$body'");
}
if (!$query_result) {
SPITAJAX_ERROR(-1, "Could not insert new news item");
return;
}
SPITAJAX_RESPONSE("news.php");
}
#
# Server side of modifying a news item
#
function Do_ModifyNews()
{
global $ajax_args;
if (!isset($ajax_args["formfields"])) {
SPITAJAX_ERROR(1, "Missing formfields");
return;
}
$formfields = $ajax_args["formfields"];
if (!isset($formfields["idx"])) {
SPITAJAX_ERROR(1, "Missing news index");
return;
}
if (!TBvalid_integer($formfields["idx"])) {
SPITAJAX_ERROR(1, "Invalid news index");
return;
}
return Do_CreateNews($formfields["idx"]);
}
function Do_DeleteNews()
{
global $this_user;
global $ajax_args;
$this_idx = $this_user->uid_idx();
$this_uid = $this_user->uid();
if (!ISADMIN()) {
SPITAJAX_ERROR(1, "Only admins please");
return;
}
if (!isset($ajax_args["idx"])) {
SPITAJAX_ERROR(1, "Missing news index");
return;
}
if (!TBvalid_integer($ajax_args["idx"])) {
SPITAJAX_ERROR(1, "Invalid news index");
return;
}
$idx = $ajax_args["idx"];
if (!DBQueryWarn("delete from apt_news where idx='$idx'")) {
SPITAJAX_ERROR(-1, "Could not delete news item");
return;
}
SPITAJAX_RESPONSE(0);
}
function Do_GetNews()
{
global $this_user;
global $ajax_args, $PORTAL_GENESIS;
$idxclause= "";
$count = 6;
if (isset($ajax_args["idx"]) && $ajax_args["idx"] != -1) {
if (!TBvalid_integer($ajax_args["idx"])) {
SPITAJAX_ERROR(1, "Invalid news index");
return;
}
$idx = $ajax_args["idx"];
$idxclause = "idx<=$idx and ";
}
if (isset($ajax_args["count"])) {
if (!TBvalid_integer($ajax_args["count"])) {
SPITAJAX_ERROR(1, "Invalid news count");
return;
}
$count = $ajax_args["count"];
}
$query_result = DBQueryWarn("select * from apt_news ".
"where $idxclause ".
" FIND_IN_SET('$PORTAL_GENESIS',portals) ".
"order by idx desc limit $count");
if (!$query_result) {
SPITAJAX_ERROR(1, "Could not get news items");
return;
}
$news = array();
while ($row = mysql_fetch_array($query_result)) {
$blob = array("idx" => $row["idx"],
"title" => $row["title"],
"body" => $row["body"],
"author" => $row["author"],
"created"=> DateStringGMT($row["created"]));
$news[] = $blob;
}
SPITAJAX_RESPONSE($news);
}
# Local Variables:
# mode:php
# End:
?>
<?php
#
# Copyright (c) 2000-2016 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
#
# }}}
#
chdir("..");
include("defs.php3");
chdir("apt");
include("quickvm_sup.php");
# Must be after quickvm_sup.php since it changes the auth domain.
$page_title = "News";
#
# Get current user.
#
RedirectSecure();
$this_user = CheckLogin($check_status);
$isadmin = 0;
# Guests are okay on this page.
if (isset($this_user) && ISADMIN()) {
$isadmin = 1;
}
#
# Verify page arguments.
#
$optargs = OptionalPageArguments("idx", PAGEARG_INTEGER);
SPITHEADER(1);
# Place to hang the toplevel template.
echo "<div id='main-body'></div>\n";
echo "<script type='text/javascript'>\n";
if (isset($idx)) {
echo " window.IDX = $idx;\n";
}
else {
echo " window.IDX = -1;\n";
}
echo " window.ISADMIN = $isadmin;\n";
echo "</script>\n";
REQUIRE_UNDERSCORE();
REQUIRE_SUP();
REQUIRE_MOMENT();
REQUIRE_MARKED();
SPITREQUIRE("js/news.js");
AddTemplateList(array("news", "oops-modal", "news-item", "confirm-modal"));
SPITFOOTER();
?>
......@@ -279,6 +279,15 @@ $PAGEHEADER_FUNCTION = function($thinheader = 0, $ignore1 = NULL,
$then = time() - (90 * 3600 * 24);
echo " <li><a href='activity.php?user=$login_uid&min=$then'>
My History</a></li>\n";
if (ISADMIN() && HaveNews()) {
echo " <li><a href='news.php'>News ";
if (NewNews()) {
echo "<span class='glyphicon glyphicon-asterisk ".
" text-success' ".
" style='margin-bottom: 4px;'></span> ";
}
echo " </a></li>\n";
}
}
echo " </ul>
</li>\n";
......@@ -311,7 +320,9 @@ $PAGEHEADER_FUNCTION = function($thinheader = 0, $ignore1 = NULL,
<li><a href='list-reservations.php'>
List Reservations</a></li>
<li><a href='reserve.php'>
Create Reservation</a></li>";
Create Reservation</a></li>
<li><a href='edit-news.php'>
Add a news item</a></li>";
echo " </ul>
</li>\n";
}
......@@ -869,4 +880,23 @@ function CheckLoginOrRedirect($modifier = 0)
return $this_user;
}
#
# See if there is recent news and news of any kind.
#
function HaveNews()
{
$query_result = DBQueryFatal("select idx from apt_news limit 1");
return mysql_num_rows($query_result);
}
function NewNews()
{
# Within the last week.
$query_result =
DBQueryFatal("select idx from apt_news ".
"where (UNIX_TIMESTAMP(now()) - ".
" UNIX_TIMESTAMP(created)) < (24 * 3600 * 7) ".
"limit 1");
return mysql_num_rows($query_result);
}
?>
......@@ -310,6 +310,17 @@ $routing = array("myprofiles" =>
"Do_GetReservation",
"Delete" =>
"Do_Delete")),
"news" =>
array("file" => "news.ajax",
"guest" => true,
"methods" => array("create" =>
"Do_CreateNews",
"modify" =>
"Do_ModifyNews",
"delete" =>
"Do_DeleteNews",
"getnews" =>
"Do_GetNews")),
);
#
......
<div class='col-sm-10 col-sm-offset-1
col-xs-12'>
<div class='panel panel-default'>
<div class='panel-heading'>
<h4 class="text-center">
<% if (editing) { %>Modify<% } else { %>Create<% } %> News Item</h4>
</div>
<div class='panel-body'>
<span id="general_error" style="color:red;"></span>
<form id='edit-news-form'
class='form-horizontal' role='form'
data-format="wide" method='post'>
<% if (editing) { %>
<input type='hidden' name=idx value='<%- formfields.idx %>'>
<% } %>
<div class="form-group">
<input name=title
value="<%- formfields.title %>"
class="form-control format-me"
data-key="title"
data-label="Title" type="text">
</div>
<div class="form-group">
<div class="format-me"
data-key='body'
data-label='Body'
data-help='Use <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target=_blank>Markdown format</a>, double click to see a rendering.'>
<textarea name=body
id='body'
rows=10
class='form-control'
type='textarea'><%= formfields.body %></textarea>