archweb.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. /* archweb.js
  2. * Homepage: https://projects.archlinux.org/archweb.git/
  3. * Copyright: 2007-2014 The Archweb Team
  4. * License: GPLv2
  5. *
  6. * This file is part of Archweb.
  7. *
  8. * Archweb is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License version 2 as
  10. * published by the Free Software Foundation.
  11. *
  12. * Archweb is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with Archweb. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. /*'use strict';*/
  21. /* tablesorter custom parsers for various pages:
  22. * devel/index.html, mirrors/status.html, todolists/view.html */
  23. if (typeof $ !== 'undefined' && typeof $.tablesorter !== 'undefined') {
  24. $.tablesorter.addParser({
  25. id: 'pkgcount',
  26. is: function(s) { return false; },
  27. format: function(s) {
  28. var m = s.match(/\d+/);
  29. return m ? parseInt(m[0], 10) : 0;
  30. },
  31. type: 'numeric'
  32. });
  33. $.tablesorter.addParser({
  34. id: 'todostatus',
  35. is: function(s) { return false; },
  36. format: function(s) {
  37. if (s.match(/incomplete/i)) {
  38. return 1;
  39. } else if (s.match(/in-progress/i)) {
  40. return 0.5;
  41. }
  42. return 0;
  43. },
  44. type: 'numeric'
  45. });
  46. $.tablesorter.addParser({
  47. /* sorts numeric, but put '', 'unknown', and '∞' last. */
  48. id: 'mostlydigit',
  49. special: ['', 'unknown', '∞'],
  50. is: function(s, table) {
  51. var c = table.config;
  52. return ($.inArray(s, this.special) > -1) || $.tablesorter.isDigit(s, c);
  53. },
  54. format: function(s, t) {
  55. if ($.inArray(s, this.special) > -1) {
  56. return Number.MAX_VALUE;
  57. }
  58. return $.tablesorter.formatFloat(s, t);
  59. },
  60. type: 'numeric'
  61. });
  62. $.tablesorter.addParser({
  63. /* sorts duration; put '', 'unknown', and '∞' last. */
  64. id: 'duration',
  65. re: /^([0-9]+):([0-5][0-9])$/,
  66. special: ['', 'unknown', '∞'],
  67. is: function(s) {
  68. return ($.inArray(s, this.special) > -1) || this.re.test(s);
  69. },
  70. format: function(s) {
  71. if ($.inArray(s, this.special) > -1) {
  72. return Number.MAX_VALUE;
  73. }
  74. var matches = this.re.exec(s);
  75. if (!matches) {
  76. return Number.MAX_VALUE;
  77. }
  78. return matches[1] * 60 + matches[2];
  79. },
  80. type: 'numeric'
  81. });
  82. $.tablesorter.addParser({
  83. id: 'epochdate',
  84. is: function(s) { return false; },
  85. format: function(s, t, c) {
  86. /* TODO: this assumes our magic class is the only one */
  87. var epoch = $(c).attr('class');
  88. if (epoch.indexOf('epoch-') !== 0) {
  89. return 0;
  90. }
  91. return epoch.slice(6);
  92. },
  93. type: 'numeric'
  94. });
  95. $.tablesorter.addParser({
  96. id: 'longDateTime',
  97. re: /^(\d{4})-(\d{2})-(\d{2}) ([012]\d):([0-5]\d)(:([0-5]\d))?( (\w+))?$/,
  98. is: function(s) {
  99. return this.re.test(s);
  100. },
  101. format: function(s, t) {
  102. var matches = this.re.exec(s);
  103. if (!matches) {
  104. return 0;
  105. }
  106. /* skip group 6, group 7 is optional seconds */
  107. if (matches[7] === undefined) {
  108. matches[7] = 0;
  109. }
  110. /* The awesomeness of the JS date constructor. Month needs to be
  111. * between 0-11, because things have to be difficult. */
  112. var date = new Date(matches[1], matches[2] - 1, matches[3],
  113. matches[4], matches[5], matches[7]);
  114. return $.tablesorter.formatFloat(date.getTime(), t);
  115. },
  116. type: 'numeric'
  117. });
  118. $.tablesorter.addParser({
  119. id: 'filesize',
  120. re: /^(\d+(?:\.\d+)?)[ \u00a0](bytes?|[KMGTPEZY]i?B)$/,
  121. is: function(s) {
  122. return this.re.test(s);
  123. },
  124. format: function(s) {
  125. var matches = this.re.exec(s);
  126. if (!matches) {
  127. return 0;
  128. }
  129. var size = parseFloat(matches[1]),
  130. suffix = matches[2];
  131. switch(suffix) {
  132. /* intentional fall-through at each level */
  133. case 'YB':
  134. case 'YiB':
  135. size *= 1024;
  136. case 'ZB':
  137. case 'ZiB':
  138. size *= 1024;
  139. case 'EB':
  140. case 'EiB':
  141. size *= 1024;
  142. case 'PB':
  143. case 'PiB':
  144. size *= 1024;
  145. case 'TB':
  146. case 'TiB':
  147. size *= 1024;
  148. case 'GB':
  149. case 'GiB':
  150. size *= 1024;
  151. case 'MB':
  152. case 'MiB':
  153. size *= 1024;
  154. case 'KB':
  155. case 'KiB':
  156. size *= 1024;
  157. }
  158. return size;
  159. },
  160. type: 'numeric'
  161. });
  162. $.tablesorter.removeParser = function(id) {
  163. $.tablesorter.parsers = $.grep($.tablesorter.parsers,
  164. function(ele, i) {
  165. return ele.id !== id;
  166. });
  167. };
  168. // We don't use currency, and the parser is over-zealous at deciding it
  169. // matches. Kill it from the parser selection.
  170. $.tablesorter.removeParser('currency');
  171. }
  172. (function($) {
  173. $.fn.enableCheckboxRangeSelection = function() {
  174. var lastCheckbox = null,
  175. spec = this;
  176. spec.unbind("click.checkboxrange");
  177. spec.bind("click.checkboxrange", function(e) {
  178. if (lastCheckbox !== null && e.shiftKey) {
  179. spec.slice(
  180. Math.min(spec.index(lastCheckbox), spec.index(e.target)),
  181. Math.max(spec.index(lastCheckbox), spec.index(e.target)) + 1
  182. ).attr({checked: e.target.checked ? "checked" : ""});
  183. }
  184. lastCheckbox = e.target;
  185. });
  186. };
  187. })(jQuery);
  188. /* news/add.html */
  189. function enablePreview() {
  190. $('#news-preview-button').click(function(event) {
  191. event.preventDefault();
  192. $.post('/news/preview/', {
  193. data: $('#id_content').val(),
  194. csrfmiddlewaretoken: $('#newsform input[name=csrfmiddlewaretoken]').val()
  195. },
  196. function(data) {
  197. $('#news-preview-data').html(data);
  198. $('#news-preview').show();
  199. }
  200. );
  201. $('#news-preview-title').html($('#id_title').val());
  202. });
  203. }
  204. /* packages/details.html */
  205. function ajaxifyFiles() {
  206. $('#filelink').click(function(event) {
  207. event.preventDefault();
  208. $.getJSON(this.href + 'json/', function(data) {
  209. // Map each file item into an <li/> with the correct class
  210. var list_items = $.map(data.files, function(value, i) {
  211. var cls = value.match(/\/$/) ? 'd' : 'f';
  212. return ['<li class="', cls, '">', value, '</li>'];
  213. });
  214. $('#pkgfilelist').empty();
  215. if (data.pkg_last_update > data.files_last_update) {
  216. $('#pkgfilelist').append('<p class="message">Note: This file list was generated from a previous version of the package; it may be out of date.</p>');
  217. }
  218. if (list_items.length > 0) {
  219. $('#pkgfilelist').append('<ul>' + list_items.join('') + '</ul>');
  220. } else if (data.files_last_update === null) {
  221. $('#pkgfilelist').append('<p class="message">No file list available.</p>');
  222. } else {
  223. $('#pkgfilelist').append('<p class="message">Package has no files.</p>');
  224. }
  225. });
  226. });
  227. }
  228. function collapseDependsList(list) {
  229. list = $(list);
  230. // Hide everything past a given limit. Don't do anything if we don't have
  231. // enough items, or the link already exists.
  232. var limit = 20,
  233. linkid = list.attr('id') + 'link',
  234. items = list.find('li').slice(limit);
  235. if (items.length <= 1 || $('#' + linkid).length > 0) {
  236. return;
  237. }
  238. items.hide();
  239. list.after('<p><a id="' + linkid + '" href="#">Show More…</a></p>');
  240. // add link and wire it up to show the hidden items
  241. $('#' + linkid).click(function(event) {
  242. event.preventDefault();
  243. list.find('li').show();
  244. // remove the full <p/> node from the DOM
  245. $(this).parent().remove();
  246. });
  247. }
  248. function collapseRelatedTo(elements) {
  249. var limit = 5;
  250. $(elements).each(function(idx, ele) {
  251. ele = $(ele);
  252. // Hide everything past a given limit. Don't do anything if we don't
  253. // have enough items, or the link already exists.
  254. var items = ele.find('span.related').slice(limit);
  255. if (items.length <= 1 || ele.find('a.morelink').length > 0) {
  256. return;
  257. }
  258. items.hide();
  259. ele.append('<a class="morelink" href="#">More…</a>');
  260. // add link and wire it up to show the hidden items
  261. ele.find('a.morelink').click(function(event) {
  262. event.preventDefault();
  263. ele.find('span.related').show();
  264. $(this).remove();
  265. });
  266. });
  267. }
  268. /* packages/differences.html */
  269. function filter_packages() {
  270. /* start with all rows, and then remove ones we shouldn't show */
  271. var rows = $('#tbody_differences').children(),
  272. all_rows = rows;
  273. if (!$('#id_multilib').is(':checked')) {
  274. rows = rows.not('.multilib').not('.multilib-testing');
  275. }
  276. var arch = $('#id_archonly').val();
  277. if (arch !== 'all') {
  278. rows = rows.filter('.' + arch);
  279. }
  280. if (!$('#id_minor').is(':checked')) {
  281. /* this check is done last because it is the most expensive */
  282. var pat = /(.*)-(.+)/;
  283. rows = rows.filter(function(index) {
  284. var cells = $(this).children('td');
  285. /* all this just to get the split version out of the table cell */
  286. var ver_a = cells.eq(2).text().match(pat);
  287. if (!ver_a) {
  288. return true;
  289. }
  290. var ver_b = cells.eq(3).text().match(pat);
  291. if (!ver_b) {
  292. return true;
  293. }
  294. /* first check pkgver */
  295. if (ver_a[1] !== ver_b[1]) {
  296. return true;
  297. }
  298. /* pkgver matched, so see if rounded pkgrel matches */
  299. if (Math.floor(parseFloat(ver_a[2])) ===
  300. Math.floor(parseFloat(ver_b[2]))) {
  301. return false;
  302. }
  303. /* pkgrel didn't match, so keep the row */
  304. return true;
  305. });
  306. }
  307. /* hide all rows, then show the set we care about */
  308. all_rows.hide();
  309. rows.show();
  310. /* make sure we update the odd/even styling from sorting */
  311. $('.results').trigger('applyWidgets', [false]);
  312. }
  313. function filter_packages_reset() {
  314. $('#id_archonly').val('both');
  315. $('#id_multilib').removeAttr('checked');
  316. $('#id_minor').removeAttr('checked');
  317. filter_packages();
  318. }
  319. /* todolists/view.html */
  320. function todolist_flag() {
  321. // TODO: fix usage of this
  322. var link = this;
  323. $.getJSON(link.href, function(data) {
  324. $(link).text(data.status).removeClass(
  325. 'complete inprogress incomplete').addClass(
  326. data.css_class.toLowerCase());
  327. /* let tablesorter know the cell value has changed */
  328. $('.results').trigger('updateCell', [$(link).closest('td')[0], false, null]);
  329. });
  330. return false;
  331. }
  332. function filter_pkgs_list(filter_ele, tbody_ele) {
  333. /* start with all rows, and then remove ones we shouldn't show */
  334. var rows = $(tbody_ele).children(),
  335. all_rows = rows;
  336. /* apply the filters, cheaper ones first */
  337. if ($('#id_mine_only').is(':checked')) {
  338. rows = rows.filter('.mine');
  339. }
  340. /* apply arch and repo filters */
  341. $(filter_ele + ' .arch_filter').add(
  342. filter_ele + ' .repo_filter').each(function() {
  343. if (!$(this).is(':checked')) {
  344. rows = rows.not('.' + $(this).val());
  345. }
  346. });
  347. /* more expensive filter because of 'has' call */
  348. if ($('#id_incomplete').is(':checked')) {
  349. rows = rows.has('.incomplete');
  350. }
  351. /* hide all rows, then show the set we care about */
  352. all_rows.hide();
  353. rows.show();
  354. $('#filter-count').text(rows.length);
  355. /* make sure we update the odd/even styling from sorting */
  356. $('.results').trigger('applyWidgets', [false]);
  357. }
  358. function filter_pkgs_reset(callback) {
  359. $('#id_incomplete').removeAttr('checked');
  360. $('#id_mine_only').removeAttr('checked');
  361. $('.arch_filter').attr('checked', 'checked');
  362. $('.repo_filter').attr('checked', 'checked');
  363. callback();
  364. }
  365. function filter_todolist_save(list_id) {
  366. var state = $('#todolist_filter').serializeArray();
  367. localStorage['filter_todolist_' + list_id] = JSON.stringify(state);
  368. }
  369. function filter_todolist_load(list_id) {
  370. var state = localStorage['filter_todolist_' + list_id];
  371. if (!state)
  372. return;
  373. state = JSON.parse(state);
  374. $('#todolist_filter input[type="checkbox"]').removeAttr('checked');
  375. $.each(state, function (i, v) {
  376. // this assumes our only filters are checkboxes
  377. $('#todolist_filter input[name="' + v['name'] + '"]').attr('checked', 'checked');
  378. });
  379. }
  380. function filter_report_save(report_id) {
  381. var state = $('#report_filter').serializeArray();
  382. localStorage['filter_report_' + report_id] = JSON.stringify(state);
  383. }
  384. function filter_report_load(report_id) {
  385. var state = localStorage['filter_report_' + report_id];
  386. if (!state)
  387. return;
  388. state = JSON.parse(state);
  389. $('#report_filter input[type="checkbox"]').removeAttr('checked');
  390. $.each(state, function (i, v) {
  391. // this assumes our only filters are checkboxes
  392. $('#report_filter input[name="' + v['name'] + '"]').attr('checked', 'checked');
  393. });
  394. }
  395. /* signoffs.html */
  396. function signoff_package() {
  397. // TODO: fix usage of this
  398. var link = this;
  399. $.getJSON(link.href, function(data) {
  400. link = $(link);
  401. var signoff = null,
  402. cell = link.closest('td');
  403. if (data.created) {
  404. signoff = $('<li>').addClass('signed-username').text(data.user);
  405. var list = cell.children('ul.signoff-list');
  406. if (list.size() === 0) {
  407. list = $('<ul class="signoff-list">').prependTo(cell);
  408. }
  409. list.append(signoff);
  410. } else if(data.user) {
  411. signoff = link.closest('td').find('li').filter(function(index) {
  412. return $(this).text() == data.user;
  413. });
  414. }
  415. if (signoff && data.revoked) {
  416. signoff.text(signoff.text() + ' (revoked)');
  417. }
  418. /* update the approved column to reflect reality */
  419. var approved = link.closest('tr').children('.approval');
  420. approved.attr('class', 'approval');
  421. if (data.known_bad) {
  422. approved.text('Bad').addClass('signoff-bad');
  423. } else if (!data.enabled) {
  424. approved.text('Disabled').addClass('signoff-disabled');
  425. } else if (data.approved) {
  426. approved.text('Yes').addClass('signoff-yes');
  427. } else {
  428. approved.text('No').addClass('signoff-no');
  429. }
  430. link.removeAttr('title');
  431. /* Form our new link. The current will be something like
  432. * '/packages/repo/arch/package/...' */
  433. var base_href = link.attr('href').split('/').slice(0, 5).join('/');
  434. if (data.revoked) {
  435. link.text('Signoff');
  436. link.attr('href', base_href + '/signoff/');
  437. /* should we be hiding the link? */
  438. if (data.known_bad || !data.enabled) {
  439. link.remove();
  440. }
  441. } else {
  442. link.text('Revoke Signoff');
  443. link.attr('href', base_href + '/signoff/revoke/');
  444. }
  445. /* let tablesorter know the cell value has changed */
  446. $('.results').trigger('updateCell', [approved[0], false, null]);
  447. });
  448. return false;
  449. }
  450. function filter_signoffs() {
  451. /* start with all rows, and then remove ones we shouldn't show */
  452. var rows = $('#tbody_signoffs').children(),
  453. all_rows = rows;
  454. /* apply arch and repo filters */
  455. $('#signoffs_filter .arch_filter').add(
  456. '#signoffs_filter .repo_filter').each(function() {
  457. if (!$(this).is(':checked')) {
  458. rows = rows.not('.' + $(this).val());
  459. }
  460. });
  461. /* and then the slightly more expensive pending check */
  462. if ($('#id_pending').is(':checked')) {
  463. rows = rows.has('td.signoff-no');
  464. }
  465. /* hide all rows, then show the set we care about */
  466. all_rows.hide();
  467. rows.show();
  468. $('#filter-count').text(rows.length);
  469. /* make sure we update the odd/even styling from sorting */
  470. $('.results').trigger('applyWidgets', [false]);
  471. filter_signoffs_save();
  472. }
  473. function filter_signoffs_reset() {
  474. $('#signoffs_filter .arch_filter').attr('checked', 'checked');
  475. $('#signoffs_filter .repo_filter').attr('checked', 'checked');
  476. $('#id_pending').removeAttr('checked');
  477. filter_signoffs();
  478. }
  479. function filter_signoffs_save() {
  480. var state = $('#signoffs_filter').serializeArray();
  481. localStorage['filter_signoffs'] = JSON.stringify(state);
  482. }
  483. function filter_signoffs_load() {
  484. var state = localStorage['filter_signoffs'];
  485. if (!state)
  486. return;
  487. state = JSON.parse(state);
  488. $('#signoffs_filter input[type="checkbox"]').removeAttr('checked');
  489. $.each(state, function (i, v) {
  490. // this assumes our only filters are checkboxes
  491. $('#signoffs_filter input[name="' + v['name'] + '"]').attr('checked', 'checked');
  492. });
  493. }
  494. function collapseNotes(elements) {
  495. // Remove any trailing <br/> tags from the note contents
  496. $(elements).children('br').filter(':last-child').filter(function(i, e) { return !e.nextSibling; }).remove();
  497. var maxElements = 8;
  498. $(elements).each(function(idx, ele) {
  499. ele = $(ele);
  500. // Hide everything past a given limit. Don't do anything if we don't
  501. // have enough items, or the link already exists.
  502. var contents = ele.contents();
  503. if (contents.length <= maxElements || ele.find('a.morelink').length > 0) {
  504. return;
  505. }
  506. contents.slice(maxElements).wrapAll('<div class="hide"/>');
  507. ele.append('<br class="morelink-spacer"/><a class="morelink" href="#">Show More…</a>');
  508. // add link and wire it up to show the hidden items
  509. ele.find('a.morelink').click(function(event) {
  510. event.preventDefault();
  511. $(this).remove();
  512. ele.find('br.morelink-spacer').remove();
  513. // move the div contents back and delete the empty div
  514. var hidden = ele.find('div.hide');
  515. hidden.contents().appendTo(ele);
  516. hidden.remove();
  517. });
  518. });
  519. }
  520. /* visualizations */
  521. function format_filesize(size, decimals) {
  522. /*var labels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];*/
  523. var labels = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
  524. label = 0;
  525. while (size > 2048.0 && label < labels.length - 1) {
  526. label++;
  527. size /= 1024.0;
  528. }
  529. if (decimals === undefined) {
  530. decimals = 2;
  531. }
  532. return size.toFixed(decimals) + ' ' + labels[label];
  533. }
  534. /* HTML5 input type and attribute enhancements */
  535. function modify_attributes(to_change) {
  536. /* jQuery doesn't let us change the 'type' attribute directly due to IE
  537. woes, so instead we can clone and replace, setting the type. */
  538. $.each(to_change, function(id, attrs) {
  539. var obj = $(id);
  540. obj.replaceWith(obj.clone().attr(attrs));
  541. });
  542. }