diff --git a/inc/bans.php b/inc/bans.php
index 6b8c1ebd..f79076de 100644
--- a/inc/bans.php
+++ b/inc/bans.php
@@ -153,57 +153,86 @@ class Bans {
return $ban_list;
}
-
- static public function list_all($offset = 0, $limit = 9001, $board = false) {
- $offset = (int)$offset;
- $limit = (int)$limit;
-
- $query = prepare("SELECT ``bans``.*, `username` FROM ``bans``
- LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`" . ($board ? ' WHERE ``bans``.`board` = :board' : '') . "
- ORDER BY `created` DESC LIMIT $offset, $limit");
- if ($board)
- $query->bindValue(':board', $board);
- $query->execute() or error(db_error());
- $bans = $query->fetchAll(PDO::FETCH_ASSOC);
-
- foreach ($bans as &$ban) {
- $ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
+ static public function stream_json($out = false, $filter_ips = false, $filter_staff = false, $board_access = false) {
+ $query = query("SELECT ``bans``.*, `username` FROM ``bans``
+ LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`
+ ORDER BY `created` DESC") or error(db_error());
+ $bans = $query->fetchAll(PDO::FETCH_ASSOC);
+
+ if ($board_access && $board_access[0] == '*') $board_access = false;
+
+ $out ? fputs($out, "[") : print("[");
+
+ $end = end($bans);
+
+ foreach ($bans as &$ban) {
+ $ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
+
+ if ($ban['post']) {
+ $post = json_decode($ban['post']);
+ $ban['message'] = $post->body;
+ }
+ unset($ban['ipstart'], $ban['ipend'], $ban['post'], $ban['creator']);
+
+ if ($board_access === false || in_array ($ban['board'], $board_access)) {
+ $ban['access'] = true;
+ }
+
+ if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false) {
+ $ban['single_addr'] = true;
+ }
+ if ($filter_staff || ($board_access !== false && !in_array($ban['board'], $board_access))) {
+ $ban['username'] = '?';
+ }
+ if ($filter_ips || ($board_access !== false && !in_array($ban['board'], $board_access))) {
+ list($ban['mask'], $subnet) = explode("/", $ban['mask']);
+ $ban['mask'] = preg_split("/[\.:]/", $ban['mask']);
+ $ban['mask'] = array_slice($ban['mask'], 0, 2);
+ $ban['mask'] = implode(".", $ban['mask']);
+ $ban['mask'] .= ".*";
+ if (isset ($subnet)) {
+ $ban['mask'] .= "/$subnet";
+ }
+ $ban['masked'] = true;
+ }
+
+ $json = json_encode($ban);
+ $out ? fputs($out, $json) : print($json);
+
+ if ($ban['id'] != $end['id']) {
+ $out ? fputs($out, ",") : print(",");
+ }
}
-
- return $bans;
- }
-
- static public function count($board = false) {
- if (!$board) {
- $query = prepare("SELECT COUNT(*) FROM ``bans``");
- } else {
- $query = prepare("SELECT COUNT(*) FROM ``bans`` WHERE `board` = :board");
- }
- $query->bindValue(':board', $board);
- $query->execute() or error(db_error());
- return (int)$query->fetchColumn();
+
+ $out ? fputs($out, "]") : print("]");
+
}
static public function seen($ban_id) {
$query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error());
+ rebuildThemes('bans');
}
static public function purge() {
$query = query("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` < " . time() . " AND `seen` = 1") or error(db_error());
+ rebuildThemes('bans');
}
- static public function delete($ban_id, $modlog = false) {
- global $config, $mod;
+ static public function delete($ban_id, $modlog = false, $boards = false, $dont_rebuild = false) {
+ global $config;
- $query = query("SELECT `ipstart`, `ipend`, `board` FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
- if (!$ban = $query->fetch(PDO::FETCH_ASSOC)) {
- // Ban doesn't exist
- return false;
- }
+ if ($boards && $boards[0] == '*') $boards = false;
- if ($mod && $mod['boards'][0] != '*' && !in_array($ban['board'], $mod['boards']))
- error($config['error']['noaccess']);
+ if ($modlog) {
+ $query = query("SELECT `ipstart`, `ipend` FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
+ if (!$ban = $query->fetch(PDO::FETCH_ASSOC)) {
+ // Ban doesn't exist
+ return false;
+ }
+
+ if ($boards !== false && !in_array($ban['board'], $boards))
+ error($config['error']['noaccess']);
if ($modlog) {
$mask = self::range_to_string(array($ban['ipstart'], $ban['ipend']));
@@ -213,6 +242,8 @@ class Bans {
}
query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error());
+
+ if (!$dont_rebuild) rebuildThemes('bans');
return true;
}
@@ -282,6 +313,9 @@ class Bans {
' (#' . $pdo->lastInsertId() . ')' .
' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason'));
}
+
+ rebuildThemes('bans');
+
return $pdo->lastInsertId();
}
}
diff --git a/inc/mod/pages.php b/inc/mod/pages.php
index 60d448eb..2bb14be9 100644
--- a/inc/mod/pages.php
+++ b/inc/mod/pages.php
@@ -782,8 +782,8 @@ function mod_page_ip($ip) {
if (isset($_POST['ban_id'], $_POST['unban'])) {
if (!hasPermission($config['mod']['unban']))
error($config['error']['noaccess']);
-
- Bans::delete($_POST['ban_id'], true);
+
+ Bans::delete($_POST['ban_id'], true, $mod['boards']);
header('Location: ?/IP/' . $ip . '#bans', true, $config['redirect_http']);
return;
@@ -879,18 +879,16 @@ function mod_ban() {
require_once 'inc/mod/ban.php';
Bans::new_ban($_POST['ip'], $_POST['reason'], $_POST['length'], $_POST['board'] == '*' ? false : $_POST['board']);
-
+
if (isset($_POST['redirect']))
header('Location: ' . $_POST['redirect'], true, $config['redirect_http']);
else
header('Location: ?/', true, $config['redirect_http']);
}
-function mod_bans($page_no = 1) {
- global $config, $mod;
-
- if ($page_no < 1)
- error($config['error']['404']);
+function mod_bans() {
+ global $config;
+ global $mod;
if (!hasPermission($config['mod']['view_banlist']))
error($config['error']['noaccess']);
@@ -908,31 +906,33 @@ function mod_bans($page_no = 1) {
error(sprintf($config['error']['toomanyunban'], $config['mod']['unban_limit'], count($unban)));
foreach ($unban as $id) {
- Bans::delete($id, true);
+ Bans::delete($id, true, $mod['boards'], true);
}
+ rebuildThemes('bans');
header('Location: ?/bans', true, $config['redirect_http']);
return;
}
-
- $board = ($mod['boards'][0] == '*' ? false : $mod['boards'][0]);
-
- $bans = Bans::list_all(($page_no - 1) * $config['mod']['banlist_page'], $config['mod']['banlist_page'], $board);
-
- if (empty($bans) && $page_no > 1)
- error($config['error']['404']);
-
- foreach ($bans as &$ban) {
- if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false)
- $ban['single_addr'] = true;
- }
mod_page(_('Ban list'), 'mod/ban_list.html', array(
- 'bans' => $bans,
- 'count' => Bans::count($board),
- 'token' => make_secure_link_token('bans')
+ 'mod' => $mod,
+ 'boards' => json_encode($mod['boards']),
+ 'token' => make_secure_link_token('bans'),
+ 'token_json' => make_secure_link_token('bans.json')
));
}
+function mod_bans_json() {
+ global $config, $mod;
+
+ if (!hasPermission($config['mod']['ban']))
+ error($config['error']['noaccess']);
+
+ // Compress the json for faster loads
+ if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) ob_start("ob_gzhandler");
+
+ Bans::stream_json(false, false, !hasPermission($config['mod']['view_banstaff']), $mod['boards']);
+}
+
function mod_ban_appeals() {
global $config, $board;
diff --git a/js/local-time.js b/js/local-time.js
index e6b3495d..a4e25d6d 100644
--- a/js/local-time.js
+++ b/js/local-time.js
@@ -25,16 +25,6 @@ onready(function(){
return [Math.pow(10, count - num.toString().length), num].join('').substr(1);
};
- var datelocale =
- { days: [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
- , shortDays: [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat")]
- , months: [_('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'), _('September'), _('October'), _('November'), _('December')]
- , shortMonths: [_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May'), _('Jun'), _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec')]
- , AM: _('AM')
- , PM: _('PM')
- , am: _('am')
- , pm: _('pm')
- };
var dateformat = (typeof strftime === 'undefined') ? function(t) {
return zeropad(t.getMonth() + 1, 2) + "/" + zeropad(t.getDate(), 2) + "/" + t.getFullYear().toString().substring(2) +
" (" + [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat"), _("Sun")][t.getDay()] + ") " +
diff --git a/js/longtable/longtable.js b/js/longtable/longtable.js
new file mode 100644
index 00000000..fe3ad1c6
--- /dev/null
+++ b/js/longtable/longtable.js
@@ -0,0 +1,184 @@
+$.fn.longtable = function(fields, options, data) {
+ var elem = $(this).addClass("longtable");
+
+ var orig_data = data;
+
+ options.row_h = options.row_h || 22;
+ options.checkbox = options.checkbox || false;
+
+ var shown_rows = {};
+
+ var sorted_by = undefined;
+ var sorted_reverse = false;
+
+ var filter = function() { return true; };
+
+ var lt = {
+ _gen_field_td: function(field, id) {
+ var el;
+ if (id === undefined) {
+ el = $("
| ");
+ el.html(fields[field].name || field);
+
+ if (!fields[field].sort_disable) {
+ el.addClass("sortable").click(function() {
+ lt._sort_by(field);
+ });
+ }
+ }
+ else {
+ el = $(" | ");
+ if (fields[field].fmt) { // Special formatting?
+ el.html(fields[field].fmt(data[id]));
+ }
+ else {
+ el.html(data[id][field]);
+ }
+ }
+ el.css("width", fields[field].width);
+ el.css("height", options.row_h);
+ return el;
+ },
+ _gen_tr: function(id) {
+ var el = $("
");
+ $.each(fields, function(field, f) {
+ lt._gen_field_td(field, id).appendTo(el);
+ });
+ if (id !== undefined) {
+ el.addClass("row").addClass("row_"+id);
+ el.css({position: "absolute", top: options.row_h * (id+1)});
+ }
+ return el;
+ },
+ _clean: function() {
+ elem.find('.row').remove();
+ shown_rows = {};
+ },
+ _remove: function(id) {
+ elem.find('.row_'+id).remove();
+ delete shown_rows[id];
+ },
+ _insert: function(id) {
+ var el = lt._gen_tr(id).appendTo(elem);
+ $(elem).trigger("new-row", [data[id], el]);
+ shown_rows[id] = true;
+ },
+
+ _sort_by: function(field) {
+ if (field !== undefined) {
+ if (sorted_by == field) {
+ sorted_reverse = !sorted_reverse;
+ }
+ else {
+ sorted_reverse = !!fields[field].sort_reverse;
+ sorted_by = field;
+ }
+ }
+ lt.sort_by(sorted_by, sorted_reverse);
+ },
+
+ _apply_filter: function() {
+ data = data.filter(filter);
+ },
+ _reset_data: function() {
+ data = orig_data;
+ },
+
+
+ set_filter: function(f) {
+ filter = f;
+ lt._reset_data();
+ lt._apply_filter();
+ lt._sort_by();
+ },
+
+ sort_by: function(field, reverse) {
+ if (field !== undefined) {
+ sorted_by = field;
+ sorted_reverse = reverse;
+
+ var ord = fields[field].sort_fun || function(a,b) { return lt.sort_alphanum(a[field], b[field]); };
+
+ data = data.sort(ord);
+ if (reverse) data = data.reverse();
+ }
+
+ lt.update_data();
+ },
+
+ update_viewport: function() {
+ var first = $(window).scrollTop() - $(elem).offset().top - options.row_h;
+ var last = first + $(window).height();
+
+ first = Math.floor(first / options.row_h);
+ last = Math.ceil (last / options.row_h);
+
+ first = first < 0 ? 0 : first;
+ last = last >= data.length ? data.length - 1 : last;
+
+ $.each(shown_rows, function(id) {
+ if (id < first || id > last) {
+ lt._remove(id);
+ }
+ });
+
+ for (var id = first; id <= last; id++) {
+ if (!shown_rows[id]) lt._insert(id);
+ }
+ },
+
+ update_data: function() {
+ $(elem).height((data.length + 1) * options.row_h);
+
+ lt._clean();
+ lt.update_viewport();
+ },
+
+ get_data: function() {
+ return data;
+ },
+
+ destroy: function() {
+ },
+
+ // http://web.archive.org/web/20130826203933/http://my.opera.com/GreyWyvern/blog/show.dml/1671288
+ sort_alphanum: function(a, b) {
+ function chunkify(t) {
+ var tz = [], x = 0, y = -1, n = 0, i, j;
+
+ while (i = (j = t.charAt(x++)).charCodeAt(0)) {
+ var m = (/* dot: i == 46 || */(i >=48 && i <= 57));
+ if (m !== n) {
+ tz[++y] = "";
+ n = m;
+ }
+ tz[y] += j;
+ }
+ return tz;
+ }
+
+ var aa = chunkify((""+a).toLowerCase());
+ var bb = chunkify((""+b).toLowerCase());
+
+ for (x = 0; aa[x] && bb[x]; x++) {
+ if (aa[x] !== bb[x]) {
+ var c = Number(aa[x]), d = Number(bb[x]);
+ if (c == aa[x] && d == bb[x]) {
+ return c - d;
+ } else return (aa[x] > bb[x]) ? 1 : -1;
+ }
+ }
+ return aa.length - bb.length;
+ }
+ // End of foreign code
+ };
+
+
+
+ lt._gen_tr().appendTo(elem);
+ lt.update_data();
+
+ $(window).on("scroll resize", lt.update_viewport);
+
+ return lt;
+};
diff --git a/js/mod/ban-list.js b/js/mod/ban-list.js
new file mode 100644
index 00000000..30529044
--- /dev/null
+++ b/js/mod/ban-list.js
@@ -0,0 +1,157 @@
+var banlist_init = function(token, my_boards, inMod) {
+ inMod = !inMod;
+
+ var lt;
+
+ var selected = {};
+
+ var time = function() { return Date.now()/1000|0; }
+
+ $.getJSON(inMod ? ("?/bans.json/"+token) : token, function(t) {
+ $("#banlist").on("new-row", function(e, drow, el) {
+ var sel = selected[drow.id];
+ if (sel) {
+ $(el).find('input.unban').prop("checked", true);
+ }
+ $(el).find('input.unban').on("click", function() {
+ selected[drow.id] = $(this).prop("checked");
+ });
+
+
+ if (drow.expires && drow.expires != 0 && drow.expires < time()) {
+ $(el).find("td").css("text-decoration", "line-through");
+ }
+ });
+
+ var selall = "";
+
+ lt = $("#banlist").longtable({
+ mask: {name: selall+_("IP address"), width: "220px", fmt: function(f) {
+ var pre = "";
+ if (inMod && f.access) {
+ pre = "";
+ }
+
+ if (inMod && f.single_addr && !f.masked) {
+ return pre+""+f.mask+"";
+ }
+ return pre+f.mask;
+ } },
+ reason: {name: _("Reason"), width: "calc(100% - 715px - 6 * 4px)", fmt: function(f) {
+ var add = "", suf = '';
+ if (f.seen == 1) add += "";
+ if (f.message) {
+ add += "";
+ suf = "
"+_("Message:")+"
"+f.message;
+ }
+
+ if (add) { add = ""+add+"
"; }
+
+ if (f.reason) return add + f.reason + suf;
+ else return add + "-" + suf;
+ } },
+ board: {name: _("Board"), width: "60px", fmt: function(f) {
+ if (f.board) return "/"+f.board+"/";
+ else return ""+_("all")+"";
+ } },
+ created: {name: _("Set"), width: "100px", fmt: function(f) {
+ return ago(f.created) + _(" ago"); // in AGO form
+ } },
+ // duration?
+ expires: {name: _("Expires"), width: "235px", fmt: function(f) {
+ if (!f.expires || f.expires == 0) return ""+_("never")+"";
+ return strftime(window.post_date, new Date((f.expires|0)*1000), datelocale) +
+ ((f.expires < time()) ? "" : " "+_("in ")+until(f.expires|0)+"");
+ } },
+ username: {name: _("Staff"), width: "100px", fmt: function(f) {
+ var pre='',suf='',un=f.username;
+ if (inMod && f.username && f.username != '?') {
+ pre = "";
+ suf = "";
+ }
+ if (!f.username) {
+ un = ""+_("system")+"";
+ }
+ return pre + un + suf;
+ } }
+ }, {}, t);
+
+ $("#select-all").click(function(e) {
+ var $this = $(this);
+ $("input.unban").prop("checked", $this.prop("checked"));
+ lt.get_data().forEach(function(v) { v.access && (selected[v.id] = $this.prop("checked")); });
+ e.stopPropagation();
+ });
+
+ var filter = function(e) {
+ if ($("#only_mine").prop("checked") && ($.inArray(e.board, my_boards) === -1)) return false;
+ if ($("#only_not_expired").prop("checked") && e.expires && e.expires != 0 && e.expires < time()) return false;
+ if ($("#search").val()) {
+ var terms = $("#search").val().split(" ");
+
+ var fields = ["mask", "reason", "board", "staff", "message"];
+
+ var ret_false = false;
+ terms.forEach(function(t) {
+ var fs = fields;
+
+ var re = /^(mask|reason|board|staff|message):/, ma;
+ if (ma = t.match(re)) {
+ fs = [ma[1]];
+ t = t.replace(re, "");
+ }
+
+ var found = false
+ fs.forEach(function(f) {
+ if (e[f] && e[f].indexOf(t) !== -1) {
+ found = true;
+ }
+ });
+ if (!found) ret_false = true;
+ });
+
+ if (ret_false) return false;
+ }
+
+ return true;
+ };
+
+ $("#only_mine, #only_not_expired, #search").on("click input", function() {
+ lt.set_filter(filter);
+ });
+ lt.set_filter(filter);
+
+ $(".banform").on("submit", function() { return false; });
+
+ $("#unban").on("click", function() {
+ $(".banform .hiddens").remove();
+ $("").appendTo(".banform");
+
+ $.each(selected, function(e) {
+ $("").appendTo(".banform");
+ });
+
+ $(".banform").off("submit").submit();
+ });
+
+ if (device_type == 'desktop') {
+ // Stick topbar
+ var stick_on = $(".banlist-opts").offset().top;
+ var state = true;
+ $(window).on("scroll resize", function() {
+ if ($(window).scrollTop() > stick_on && state == true) {
+ $("body").css("margin-top", $(".banlist-opts").height());
+ $(".banlist-opts").addClass("boardlist top").detach().prependTo("body");
+ $("#banlist tr:not(.row)").addClass("tblhead").detach().appendTo(".banlist-opts");
+ state = !state;
+ }
+ else if ($(window).scrollTop() < stick_on && state == false) {
+ $("body").css("margin-top", "auto");
+ $(".banlist-opts").removeClass("boardlist top").detach().prependTo(".banform");
+ $(".tblhead").detach().prependTo("#banlist");
+ state = !state;
+ }
+ });
+ }
+ });
+}
diff --git a/mod.php b/mod.php
index 189d8a6d..3f14e2ed 100644
--- a/mod.php
+++ b/mod.php
@@ -63,7 +63,7 @@ $pages = array(
'/ban' => 'secure_POST ban', // new ban
'/bans' => 'secure_POST bans', // ban list
- '/bans/(\d+)' => 'secure_POST bans', // ban list
+ '/bans.json' => 'secure bans_json', // ban list JSON
'/ban-appeals' => 'secure_POST ban_appeals', // view ban appeals
'/recent/(\d+)' => 'recent_posts', // view recent posts
diff --git a/stylesheets/longtable/longtable.css b/stylesheets/longtable/longtable.css
new file mode 100644
index 00000000..09d9a0ab
--- /dev/null
+++ b/stylesheets/longtable/longtable.css
@@ -0,0 +1,31 @@
+.longtable {
+ display: block;
+ position: relative;
+ box-sizing: border-box;
+}
+.longtable > tbody {
+ display: block;
+ box-sizing: border-box;
+}
+.longtable > tbody > tr {
+ display: block;
+ box-sizing: border-box;
+ clear: left;
+ width: 100%;
+}
+.longtable > tbody > tr > td {
+ display: block;
+ box-sizing: border-box;
+ float: left;
+ padding: 0;
+}
+.longtable > tbody > tr > th {
+ display: block;
+ box-sizing: border-box;
+ float: left;
+ padding: 0;
+}
+.longtable > tbody > tr > th.sortable {
+ cursor: pointer;
+}
+
diff --git a/templates/main.js b/templates/main.js
index 242ba3a3..1ec4d268 100644
--- a/templates/main.js
+++ b/templates/main.js
@@ -22,6 +22,53 @@ function fmt(s,a) {
return s.replace(/\{([0-9]+)\}/g, function(x) { return a[x[1]]; });
}
+function until($timestamp) {
+ var $difference = $timestamp - Date.now()/1000|0, $num;
+ switch(true){
+ case ($difference < 60):
+ return "" + $difference + ' ' + _('second(s)');
+ case ($difference < 3600): //60*60 = 3600
+ return "" + ($num = Math.round($difference/(60))) + ' ' + _('minute(s)');
+ case ($difference < 86400): //60*60*24 = 86400
+ return "" + ($num = Math.round($difference/(3600))) + ' ' + _('hour(s)');
+ case ($difference < 604800): //60*60*24*7 = 604800
+ return "" + ($num = Math.round($difference/(86400))) + ' ' + _('day(s)');
+ case ($difference < 31536000): //60*60*24*365 = 31536000
+ return "" + ($num = Math.round($difference/(604800))) + ' ' + _('week(s)');
+ default:
+ return "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)');
+ }
+}
+
+function ago($timestamp) {
+ var $difference = (Date.now()/1000|0) - $timestamp, $num;
+ switch(true){
+ case ($difference < 60) :
+ return "" + $difference + ' ' + _('second(s)');
+ case ($difference < 3600): //60*60 = 3600
+ return "" + ($num = Math.round($difference/(60))) + ' ' + _('minute(s)');
+ case ($difference < 86400): //60*60*24 = 86400
+ return "" + ($num = Math.round($difference/(3600))) + ' ' + _('hour(s)');
+ case ($difference < 604800): //60*60*24*7 = 604800
+ return "" + ($num = Math.round($difference/(86400))) + ' ' + _('day(s)');
+ case ($difference < 31536000): //60*60*24*365 = 31536000
+ return "" + ($num = Math.round($difference/(604800))) + ' ' + _('week(s)');
+ default:
+ return "" + ($num = Math.round($difference/(31536000))) + ' ' + _('year(s)');
+ }
+}
+
+var datelocale =
+ { days: [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')]
+ , shortDays: [_("Sun"), _("Mon"), _("Tue"), _("Wed"), _("Thu"), _("Fri"), _("Sat")]
+ , months: [_('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'), _('September'), _('October'), _('November'), _('December')]
+ , shortMonths: [_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May'), _('Jun'), _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec')]
+ , AM: _('AM')
+ , PM: _('PM')
+ , am: _('am')
+ , pm: _('pm')
+ };
+
var saved = {};
diff --git a/templates/mod/ban_list.html b/templates/mod/ban_list.html
index 97c02029..0c67f269 100644
--- a/templates/mod/ban_list.html
+++ b/templates/mod/ban_list.html
@@ -1,104 +1,41 @@
-{% if bans|count == 0 %}
- ({% trans 'There are no active bans.' %})
-{% else %}
-