From a4895f06680beaf447ed43ee326423fcc8232815 Mon Sep 17 00:00:00 2001 From: Olivier Keun Date: Fri, 22 Jul 2011 16:25:57 +0200 Subject: News frontpage layout changes Signed-off-by: Dan McGee --- media/archweb.css | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index 85fdb610..ce1ece1d 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -21,7 +21,7 @@ p { margin: .33em 0 1em; } ol, ul { margin-bottom: 1em; padding-left: 2em; } ul { list-style: square; } code { font: 1.2em monospace; background: #ffd; padding: 0.15em 0.25em; } -pre { font: 1.2em monospace; border: 1px solid #bdb; background: #dfd; padding: 0.5em; margin: 0.25em 2em; } +pre { font: 1.2em monospace; border: 1px solid #bdb; background: #dfd; padding: 0.5em; margin: 1em; } pre code { display: block; background: none; } blockquote { margin: 1.5em 2em; } input { vertical-align: middle; } @@ -44,7 +44,7 @@ a:active { color: #e90; } /* headings */ h2 { font-size: 1.5em; margin-bottom: 0.5em; border-bottom: 1px solid #888; } -h3 { font-size: 1.25em; margin-top: 1em; } +h3 { font-size: 1.25em; margin-top: .5em; } h4 { font-size: 1.15em; margin-top: 1em; } h5 { font-size: 1em; margin-top: 1em; } @@ -76,6 +76,12 @@ table.pretty2 { width: auto; margin-top: 0.25em; margin-bottom: 0.5em; border-co table.pretty2 th { padding: 0.35em; background: #eee; border: 1px solid #bbb; } table.pretty2 td { padding: 0.35em; border: 1px dotted #bbb; } +/* definition lists */ +dl { clear:both; } +dl dt, +dl dd { margin-bottom:4px; padding: 8px 0px 4px; font-weight:bold; border-top:1px solid #888; } +dl dt { color: #333; float:left; padding-right:15px; } + /* forms and input styling */ form p { margin: 0.5em 0; } fieldset { border: 0; } @@ -105,14 +111,19 @@ ul.errorlist { color: red; } /* home: news */ #news { margin-top: 1.5em; } -#news h3 { border-bottom: 1px solid #888; } +#news h3 { float:left; padding-bottom: .5em } #news div { margin-bottom: 1em; } #news div p { margin-bottom: 0.5em; } #news .more { font-weight: normal; } -#news .rss-icon { float: right; margin: -1.6em 0.4em 0 0; } -#news h4 { font-size: 1em; margin-top: 1.5em; border-bottom: 1px dotted #bbb; } +#news .rss-icon { float: right; margin-top: 1em; } +#news h4 { clear:both; font-size: 1em; margin-top: 1.5em; border-bottom: 1px dotted #bbb; } #news .timestamp { float: right; font-size: 0.85em; margin: -1.8em 0.5em 0 0; } +/* home: arrowed headings */ +#news h3 a { display: block; background: #1794D1; font-size: 15px; padding: 2px 10px; color: white; } +#news a:active { color: white; } +h3 span.arrow { display: block; width: 0px; height: 0px; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid #1794D1; margin: 0 auto; font-size: 0px; line-height: 0px; } + /* home: pkgsearch box */ #pkgsearch { padding: 1em 0.75em; background: #3ad; color: #fff; border: 1px solid #08b; } #pkgsearch label { width: auto; padding: 0.1em 0; } -- cgit v1.2.3-54-g00ecf From c35e8b867c619a4191b5ab93fa1653cc9847e9e7 Mon Sep 17 00:00:00 2001 From: Olivier Keun Date: Mon, 25 Jul 2011 22:34:26 +0200 Subject: Clean up and reformat default stylesheet Signed-off-by: Dan McGee --- media/archweb.css | 1017 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 835 insertions(+), 182 deletions(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index ce1ece1d..be455680 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -13,273 +13,926 @@ @import url('archnavbar/archnavbar.css'); /* simple reset */ -* { margin: 0; padding: 0; line-height: 1.4; } +* { + margin: 0; + padding: 0; + line-height: 1.4; +} /* general styling */ -body { min-width: 650px; background: #f6f9fc; color: #222; font: normal 100% sans-serif; text-align: center; } -p { margin: .33em 0 1em; } -ol, ul { margin-bottom: 1em; padding-left: 2em; } -ul { list-style: square; } -code { font: 1.2em monospace; background: #ffd; padding: 0.15em 0.25em; } -pre { font: 1.2em monospace; border: 1px solid #bdb; background: #dfd; padding: 0.5em; margin: 1em; } -pre code { display: block; background: none; } -blockquote { margin: 1.5em 2em; } -input { vertical-align: middle; } -select[multiple] { padding-top: 1px; padding-bottom: 1px; } -select[multiple] option { padding-left: 0.3em; padding-right: 0.5em; } -input[type=submit] { padding-left: 0.6em; padding-right: 0.6em; } -.clear { clear: both; } -hr { border: none; border-top: 1px solid #888; } -img { border: 0; } +body { + min-width: 650px; + background: #f6f9fc; + color: #222; + font: normal 100% sans-serif; + text-align: center; +} + +p { + margin: .33em 0 1em; +} + +ol, +ul { + margin-bottom: 1em; + padding-left: 2em; +} + + ul { + list-style: square; + } + +code { + font: 1.2em monospace; + background: #ffd; + padding: 0.15em 0.25em; +} + +pre { + font: 1.2em monospace; + border: 1px solid #bdb; + background: #dfd; + padding: 0.5em; + margin: 1em; +} + + pre code { + display: block; + background: none; + } + +blockquote { + margin: 1.5em 2em; +} + +input { + vertical-align: middle; +} + +select[multiple] { + padding: 1px 0; +} + + select[multiple] option { + padding: 0 0.5em 0 0.3em; + } + +input[type=submit] { + padding-left: 0 0.6em; +} + +.clear { + clear: both; +} + +hr { + border: none; + border-top: 1px solid #888; +} + +img { + border: 0; +} /* scale fonts down to a sane default (16 * .812 = 13px) */ -#content { font-size: 0.812em; } +#content { + font-size: 0.812em; +} /* link style */ -a { text-decoration: none; } -a:link, th a:visited { color: #07b; } -a:visited { color: #666; } -a:hover { text-decoration: underline; color: #666; } -a:active { color: #e90; } +a { + text-decoration: none; +} + + a:link, + th a:visited { + color: #07b; + } + + a:visited { + color: #666; + } + + a:hover { + text-decoration: underline; + color: #666; + } + + a:active { + color: #e90; + } /* headings */ -h2 { font-size: 1.5em; margin-bottom: 0.5em; border-bottom: 1px solid #888; } -h3 { font-size: 1.25em; margin-top: .5em; } -h4 { font-size: 1.15em; margin-top: 1em; } -h5 { font-size: 1em; margin-top: 1em; } +h2 { + font-size: 1.5em; + margin-bottom: 0.5em; + border-bottom: 1px solid #888; +} + +h3 { + font-size: 1.25em; + margin-top: .5em; +} + +h4 { + font-size: 1.15em; + margin-top: 1em; +} + +h5 { + font-size: 1em; + margin-top: 1em; +} /* general layout */ -div#content { width: 95%; margin: 0 auto; text-align: left; } -div#content-left-wrapper { float: left; width: 100%; } /* req to keep content above sidebar in source code */ -div#content-left { margin: 0 340px 0 0; } -div#content-right { float: left; width: 300px; margin-left: -300px; } -div.box { margin-bottom: 1.5em; padding: 0.65em; background: #ecf2f5; border: 1px solid #bcd; } -div#footer { clear: both; margin: 2em 0 1em; } -div#footer p { margin: 0; text-align: center; font-size: 0.85em; } +div#content { + width: 95%; + margin: 0 auto; + text-align: left; +} + +div#content-left-wrapper { + float: left; + width: 100%; /* req to keep content above sidebar in source code */ +} + +div#content-left { + margin: 0 340px 0 0; +} + +div#content-right { + float: left; + width: 300px; + margin-left: -300px; +} + +div.box { + margin-bottom: 1.5em; + padding: 0.65em; + background: #ecf2f5; + border: 1px solid #bcd; +} + +div#footer { + clear: both; + margin: 2em 0 1em; +} + + div#footer p { + margin: 0; + text-align: center; + font-size: 0.85em; + } /* alignment */ -div.center, table.center, img.center { width: auto; margin-left: auto; margin-right: auto; } -p.center, td.center, th.center { text-align: center; } +div.center, +table.center, +img.center { + width: auto; + margin-left: auto; + margin-right: auto; +} + +p.center, +td.center, +th.center { + text-align: center; +} /* table generics */ -table { width: 100%; border-collapse: collapse; } -table .wrap { white-space: normal; } -th, td { white-space: nowrap; text-align: left; } -th { vertical-align: middle; font-weight: bold; } -td { vertical-align: top; } +table { + width: 100%; + border-collapse: collapse; +} + + table .wrap { + white-space: normal; + } + +th, +td { + white-space: nowrap; + text-align: left; +} + + th { + vertical-align: middle; + font-weight: bold; + } + + td { + vertical-align: top; + } /* table pretty styles */ -table.pretty1 { width: auto; margin-top: 0.25em; margin-bottom: 0.5em; border-collapse: collapse; border: 1px solid #bcd; } -table.pretty1 th { padding: 0.35em; background: #e4eeff; border: 1px solid #bcd; } -table.pretty1 td { padding: 0.35em; border: 1px dotted #bcd; } -table.pretty2 { width: auto; margin-top: 0.25em; margin-bottom: 0.5em; border-collapse: collapse; border: 1px solid #bbb; } -table.pretty2 th { padding: 0.35em; background: #eee; border: 1px solid #bbb; } -table.pretty2 td { padding: 0.35em; border: 1px dotted #bbb; } +table.pretty1 { + width: auto; + margin-top: 0.25em; + margin-bottom: 0.5em; + border-collapse: collapse; + border: 1px solid #bcd; +} + + table.pretty1 th { + padding: 0.35em; + background: #e4eeff; + border: 1px solid #bcd; + } + + table.pretty1 td { + padding: 0.35em; + border: 1px dotted #bcd; + } + +table.pretty2 { + width: auto; + margin-top: 0.25em; + margin-bottom: 0.5em; + border-collapse: collapse; + border: 1px solid #bbb; +} + + table.pretty2 th { + padding: 0.35em; + background: #eee; + border: 1px solid #bbb; + } + + table.pretty2 td { + padding: 0.35em; + border: 1px dotted #bbb; + } /* definition lists */ -dl { clear:both; } -dl dt, -dl dd { margin-bottom:4px; padding: 8px 0px 4px; font-weight:bold; border-top:1px solid #888; } -dl dt { color: #333; float:left; padding-right:15px; } +dl { + clear: both; +} + + dl dt, + dl dd { + margin-bottom: 4px; + padding: 8px 0px 4px; + font-weight: bold; + border-top: 1px solid #888; + } + + dl dt { + color: #333; + float:left; + padding-right:15px; + } /* forms and input styling */ -form p { margin: 0.5em 0; } -fieldset { border: 0; } -label { width: 12em; vertical-align: top; display: inline-block; font-weight: bold; } -input[type=text], input[type=password], textarea { padding: 0.10em; } -form.general-form label, form.general-form .form-help { width: 10em; vertical-align: top; display: inline-block; } -form.general-form input[type=text], form.general-form textarea { width: 45%; } +form p { + margin: 0.5em 0; +} + +fieldset { + border: 0; +} + +label { + width: 12em; + vertical-align: top; + display: inline-block; + font-weight: bold; +} + +input[type=text], +input[type=password], +textarea { + padding: 0.10em; +} + +form.general-form label, +form.general-form .form-help { + width: 10em; + vertical-align: top; + display: inline-block; +} + +form.general-form input[type=text], +form.general-form textarea { + width: 45%; +} /* archdev navbar */ -div#archdev-navbar { margin: 1.5em 0; } -div#archdev-navbar ul { list-style: none; margin: -0.5em 0; padding: 0; } -div#archdev-navbar li { display: inline; margin: 0; padding: 0; font-size: 0.9em; } -div#archdev-navbar li a { padding: 0 0.5em; color: #07b; } +div#archdev-navbar { + margin: 1.5em 0; +} + + div#archdev-navbar ul { + list-style: none; + margin: -0.5em 0; + padding: 0; + } + + div#archdev-navbar li { + display: inline; + margin: 0; + padding: 0; + font-size: 0.9em; + } + + div#archdev-navbar li a { + padding: 0 0.5em; + color: #07b; + } /* error/info messages (x pkg is already flagged out-of-date, etc) */ -#sys-message { width: 35em; text-align: center; margin: 1em auto; padding: 0.5em; background: #fff; border: 1px solid #f00; } -#sys-message p { margin: 0; } - -ul.errorlist { color: red; } - -/* +#sys-message { + width: 35em; + text-align: center; + margin: 1em auto; + padding: 0.5em; + background: #fff; + border: 1px solid #f00; +} + + #sys-message p { + margin: 0; + } + +ul.errorlist { + color: red; +} + +/** * PAGE SPECIFIC STYLES */ /* home: introduction */ -#intro p.readmore { margin: -0.5em 0 0 0; font-size: .9em; text-align: right; } +#intro p.readmore { + margin: -0.5em 0 0 0; + font-size: .9em; + text-align: right; +} /* home: news */ -#news { margin-top: 1.5em; } -#news h3 { float:left; padding-bottom: .5em } -#news div { margin-bottom: 1em; } -#news div p { margin-bottom: 0.5em; } -#news .more { font-weight: normal; } -#news .rss-icon { float: right; margin-top: 1em; } -#news h4 { clear:both; font-size: 1em; margin-top: 1.5em; border-bottom: 1px dotted #bbb; } -#news .timestamp { float: right; font-size: 0.85em; margin: -1.8em 0.5em 0 0; } +#news { + margin-top: 1.5em; +} + + #news h3 { + float: left; + padding-bottom: .5em + } + + #news div { + margin-bottom: 1em; + } + + #news div p { + margin-bottom: 0.5em; + } + + #news .more { + font-weight: normal; + } + + #news .rss-icon { + float: right; + margin-top: 1em; + } + + #news h4 { + clear: both; + font-size: 1em; + margin-top: 1.5em; + border-bottom: 1px dotted #bbb; + } + + #news .timestamp { + float: right; + font-size: 0.85em; + margin: -1.8em 0.5em 0 0; + } /* home: arrowed headings */ -#news h3 a { display: block; background: #1794D1; font-size: 15px; padding: 2px 10px; color: white; } -#news a:active { color: white; } -h3 span.arrow { display: block; width: 0px; height: 0px; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid #1794D1; margin: 0 auto; font-size: 0px; line-height: 0px; } +#news h3 a { + display: block; + background: #1794D1; + font-size: 15px; + padding: 2px 10px; + color: white; +} + + #news a:active { + color: white; + } + +h3 span.arrow { + display: block; + width: 0px; + height: 0px; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid #1794D1; + margin: 0 auto; + font-size: 0px; + line-height: 0px; +} /* home: pkgsearch box */ -#pkgsearch { padding: 1em 0.75em; background: #3ad; color: #fff; border: 1px solid #08b; } -#pkgsearch label { width: auto; padding: 0.1em 0; } -#pkgsearch input { width: 10em; float: right; font-size: 1em; color: #000; background: #fff; border: 1px solid #09c; } +#pkgsearch { + padding: 1em 0.75em; + background: #3ad; + color: #fff; + border: 1px solid #08b; +} + + #pkgsearch label { + width: auto; + padding: 0.1em 0; + } + + #pkgsearch input { + width: 10em; + float: right; + font-size: 1em; + color: #000; + background: #fff; + border: 1px solid #09c; + } /* home: recent pkg updates */ -#pkg-updates h3 { margin: 0 0 0.3em; } -#pkg-updates .more { font-weight: normal; } -#pkg-updates .rss-icon { float: right; margin: -2em 0 0 0; } -#pkg-updates table { margin: 0; } -#pkg-updates td.pkg-name { white-space: normal; } -#pkg-updates td.pkg-arch { text-align: right; } -#pkg-updates span.testing, #pkg-updates span.community-testing, span.multilib-testing { font-style: italic; } -#pkg-updates span.staging, #pkg-updates span.community-staging, span.multilib-staging { font-style: italic; color: #ff8040; } +#pkg-updates h3 { + margin: 0 0 0.3em; +} + + #pkg-updates .more { + font-weight: normal; + } + + #pkg-updates .rss-icon { + float: right; + margin: -2em 0 0 0; + } + + #pkg-updates table { + margin: 0; + } + + #pkg-updates td.pkg-name { + white-space: normal; + } + + #pkg-updates td.pkg-arch { + text-align: right; + } + + #pkg-updates span.testing, + #pkg-updates span.community-testing, + span.multilib-testing { + font-style: italic; + } + + #pkg-updates span.staging, + #pkg-updates span.community-staging, + span.multilib-staging { + font-style: italic; + color: #ff8040; + } /* home: sidebar navigation */ -div#nav-sidebar ul { list-style: none; margin: 0.5em 0 0.5em 1em; padding: 0; } +div#nav-sidebar ul { + list-style: none; + margin: 0.5em 0 0.5em 1em; + padding: 0; +} /* home: sponsor banners */ -div#arch-sponsors img { padding: 0.3em 0; } +div#arch-sponsors img { + padding: 0.3em 0; +} /* home: sidebar components (navlist, sponsors, pkgsearch, etc) */ -div.widget { margin-bottom: 1.5em; } +div.widget { + margin-bottom: 1.5em; +} /* feeds page */ -#rss-feeds .rss { padding-right: 20px; background: url(rss.png) top right no-repeat; } +#rss-feeds .rss { + padding-right: 20px; + background: url(rss.png) top right no-repeat; +} /* artwork: logo images */ -#artwork img.inverted { background: #333; padding: 0; } -#artwork div.imagelist img { display: inline; margin: 0.75em; } +#artwork img.inverted { + background: #333; + padding: 0; +} + +#artwork div.imagelist img { + display: inline; + margin: 0.75em; +} /* news: article list */ -.news-nav { float: right; margin-top: -2.2em; } -.news-nav .prev, .news-nav .next { margin-left: 1em; margin-right: 1em; } +.news-nav { + float: right; + margin-top: -2.2em; +} + + .news-nav .prev, + .news-nav .next { + margin: 0 1em; + } /* news: article pages */ -div.news-article .article-info { margin: 0; color: #999; } +div.news-article .article-info { + margin: 0; + color: #999; +} /* news: add/edit article */ -form#newsform { width: 60em; } -form#newsform input[type=text], form#newsform textarea { width: 75%; } +form#newsform { + width: 60em; +} + + form#newsform input[type=text], + form#newsform textarea { + width: 75%; + } /* donate: donor list */ -div#donor-list ul { width: 100%; } -/* max 4 columns, but possibly fewer if screen size doesn't allow for more */ -div#donor-list li { float: left; width: 25%; min-width: 20em; } +div#donor-list ul { + width: 100%; +} + /* max 4 columns, but possibly fewer if screen size doesn't allow for more */ + div#donor-list li { + float: left; + width: 25%; + min-width: 20em; + } /* download page */ -#arch-downloads h3 { border-bottom: 1px dotted #aaa; } -table#download-torrents .cpu-arch { text-align: center; } -table#download-mirrors { width: auto; margin-bottom: 1em; } -table#download-mirrors td.mirror-country { padding-top: 1em; } -table#download-mirrors td.mirror-server { padding-right: 1em; } -table#download-mirrors a { display: block; float: right; width: 4em; } +#arch-downloads h3 { + border-bottom: 1px dotted #aaa; +} + +table#download-torrents .cpu-arch { + text-align: center; +} + +table#download-mirrors { + width: auto; + margin-bottom: 1em; +} + + table#download-mirrors td.mirror-country { + padding-top: 1em; + } + + table#download-mirrors td.mirror-server { + padding-right: 1em; + } + + table#download-mirrors a { + display: block; + float: right; + width: 4em; + } /* pkglists/devlists */ -table.results { font-size: 0.846em; border-top: 1px dotted #999; border-bottom: 1px dotted #999; } -table.results th { padding: 0.5em 1em 0.25em 0.25em; border-bottom: 1px solid #999; white-space: nowrap; background-color:#fff; } -table.results td { padding: .3em 1em .3em 3px; } -table.results tr.odd { background: #fff; } -table.results tr.even { background: #e4eeff; } -/* additional styles for JS sorting */ -table.results th.header { padding-right: 20px; background-image: url(nosort.gif); background-repeat: no-repeat; background-position: center right; cursor: pointer; } -table.results th.headerSortDown { background-color: #e4eeff; background-image: url(desc.gif); } -table.results th.headerSortUp { background-color: #e4eeff; background-image: url(asc.gif); } -table.results .flagged { color: red; } +table.results { + font-size: 0.846em; + border-top: 1px dotted #999; + border-bottom: 1px dotted #999; +} + + table.results th { + padding: 0.5em 1em 0.25em 0.25em; + border-bottom: 1px solid #999; + white-space: nowrap; + background-color:#fff; + } + + /* additional styles for JS sorting */ + table.results th.header { + padding-right: 20px; + background-image: url(nosort.gif); + background-repeat: no-repeat; + background-position: center right; + cursor: pointer; + } + + table.results th.headerSortDown { + background-color: #e4eeff; + background-image: url(desc.gif); + } + + table.results th.headerSortUp { + background-color: #e4eeff; + background-image: url(asc.gif); + } + + table.results td { + padding: .3em 1em .3em 3px; + } + + table.results tr.odd { + background: #fff; + } + + table.results tr.even { + background: #e4eeff; + } + + table.results .flagged { + color: red; + } /* pkglist: layout */ -div#pkglist-about { margin-top: 1.5em; } +div#pkglist-about { + margin-top: 1.5em; +} /* pkglist: results navigation */ -#pkglist-stats-top, #pkglist-stats-bottom { font-size: 0.85em; } -#pkglist-results .pkglist-nav { float: right; margin-top: -2.2em; } -.pkglist-nav .prev { margin-right: 1em; } -.pkglist-nav .next { margin-right: 1em; } +#pkglist-stats-top, +#pkglist-stats-bottom { + font-size: 0.85em; +} + +#pkglist-results .pkglist-nav { + float: right; + margin-top: -2.2em; +} + +.pkglist-nav .prev { + margin-right: 1em; +} + +.pkglist-nav .next { + margin-right: 1em; +} /* search fields and other filter selections */ -.filter-criteria h3 { font-size: 1em; margin-top:0; } -.filter-criteria div { float: left; margin-right: 1.65em; font-size: 0.85em; } -.filter-criteria legend { display: none; } -.filter-criteria label { width: auto; display: block; font-weight: normal; } +.filter-criteria h3 { + font-size: 1em; + margin-top:0; +} + +.filter-criteria div { + float: left; + margin-right: 1.65em; + font-size: 0.85em; +} + +.filter-criteria legend { + display: none; +} + +.filter-criteria label { + width: auto; + display: block; + font-weight: normal; +} /* pkgdetails: details links that float on the right */ -#pkgdetails #detailslinks { float: right; } -#pkgdetails #detailslinks h4 { margin-top: 0; margin-bottom: 0.25em; } -#pkgdetails #detailslinks ul { list-style: none; padding: 0; margin-bottom: 0; font-size: 0.846em; } -#pkgdetails #detailslinks > div { padding: 0.5em; margin-bottom: 1em; background: #eee; border: 1px solid #bbb; } -#pkgdetails #actionlist .flagged { color: red; font-size: 0.9em; font-style: italic; } +#pkgdetails #detailslinks { + float: right; +} + + #pkgdetails #detailslinks h4 { + margin-top: 0; + margin-bottom: 0.25em; + } + + #pkgdetails #detailslinks ul { + list-style: none; + padding: 0; + margin-bottom: 0; + font-size: 0.846em; + } + + #pkgdetails #detailslinks > div { + padding: 0.5em; + margin-bottom: 1em; + background: #eee; + border: 1px solid #bbb; + } + +#pkgdetails #actionlist .flagged { + color: red; + font-size: 0.9em; + font-style: italic; +} /* pkgdetails: pkg info */ -#pkgdetails #pkginfo { width: auto; } -#pkgdetails #pkginfo td { padding: 0.25em 0 0.25em 1.5em; } +#pkgdetails #pkginfo { + width: auto; +} + + #pkgdetails #pkginfo td { + padding: 0.25em 0 0.25em 1.5em; + } /* pkgdetails: flag package */ -form#flag-pkg-form label { width: 10em; } -form#flag-pkg-form textarea, form#flag-pkg-form input[type=text] { width: 45%; } +form#flag-pkg-form label { + width: 10em; +} + +form#flag-pkg-form textarea, +form#flag-pkg-form input[type=text] { + width: 45%; +} /* pkgdetails: deps, required by and file lists */ -#pkgdetails #metadata h3 { background: #555; color: #fff; font-size: 1em; margin-bottom: 0.5em; padding: 0.2em 0.35em; } -#pkgdetails #metadata ul { list-style: none; margin: 0; padding: 0; } -#pkgdetails #metadata li { padding-left: 0.5em; } -#pkgdetails #metadata p { padding-left: 0.5em; } -#pkgdetails #metadata .message { font-style: italic; } -#pkgdetails #metadata br { clear: both; } -#pkgdetails #pkgdeps { float: left; width: 48%; margin-right: 2%; } -#pkgdetails #metadata .virtual-dep { font-style: italic; } -#pkgdetails #metadata .testing-dep { font-style: italic; } -#pkgdetails #metadata .opt-dep { font-style: italic; } -#pkgdetails #metadata .dep-desc { font-style: italic; } -#pkgdetails #pkgreqs { float: left; width: 50%; } -#pkgdetails #pkgfiles { clear: left; padding-top: 1em; } +#pkgdetails #metadata h3 { + background: #555; + color: #fff; + font-size: 1em; + margin-bottom: 0.5em; + padding: 0.2em 0.35em; +} + +#pkgdetails #metadata ul { + list-style: none; + margin: 0; + padding: 0; +} + +#pkgdetails #metadata li { + padding-left: 0.5em; +} + +#pkgdetails #metadata p { + padding-left: 0.5em; +} + +#pkgdetails #metadata .message { + font-style: italic; +} + +#pkgdetails #metadata br { + clear: both; +} + +#pkgdetails #pkgdeps { + float: left; + width: 48%; + margin-right: 2%; +} + +#pkgdetails #metadata .virtual-dep, +#pkgdetails #metadata .testing-dep, +#pkgdetails #metadata .opt-dep, +#pkgdetails #metadata .dep-desc { + font-style: italic; +} +#pkgdetails #pkgreqs { + float: left; + width: 50%; +} + +#pkgdetails #pkgfiles { + clear: left; + padding-top: 1em; +} /* dev/TU biographies */ -div#arch-bio-toc { width: 75%; margin: 0 auto; text-align: center; } -table.arch-bio-entry td.pic { vertical-align: top; padding-right: 15px; padding-top: 10px; } -table.arch-bio-entry td.pic img { padding: 4px; border: 1px solid #ccc; } -table.arch-bio-entry table.bio { margin-bottom: 2em; } -table.arch-bio-entry table.bio th { text-align: left; padding-right: 0.5em; vertical-align: top; white-space: nowrap; } -table.arch-bio-entry table.bio td { width: 100%; padding-bottom: 0.25em; } +div#arch-bio-toc { + width: 75%; + margin: 0 auto; + text-align: center; +} + +table.arch-bio-entry td.pic { + vertical-align: top; + padding-right: 15px; + padding-top: 10px; +} + + table.arch-bio-entry td.pic img { + padding: 4px; + border: 1px solid #ccc; + } + +table.arch-bio-entry table.bio { + margin-bottom: 2em; +} + + table.arch-bio-entry table.bio th { + text-align: left; + padding-right: 0.5em; + vertical-align: top; + white-space: nowrap; + } + + table.arch-bio-entry table.bio td { + width: 100%; + padding-bottom: 0.25em; + } /* dev: login/out */ -p.login-error {} -table#dev-login { width: auto; } +table#dev-login { + width: auto; +} /* dev dashboard: flagged packages */ -form#dash-pkg-notify { text-align: right; padding: 1em 0 0; margin-top: 1em; font-size: 0.85em; border-top: 1px dotted #aaa; } -form#dash-pkg-notify label { width: auto; font-weight: normal; } -form#dash-pkg-notify input { vertical-align: middle; margin: 0 0.25em; } -form#dash-pkg-notify input[type=submit] { margin-top: -0.25em; } -form#dash-pkg-notify p { margin: 0; } - -table.dash-stats .key { width: 50%; } +form#dash-pkg-notify { + text-align: right; + padding: 1em 0 0; + margin-top: 1em; + font-size: 0.85em; + border-top: 1px dotted #aaa; +} + + form#dash-pkg-notify label { + width: auto; + font-weight: normal; + } + + form#dash-pkg-notify input { + vertical-align: middle; + margin: 0 0.25em; + } + + form#dash-pkg-notify input[type=submit] { + margin-top: -0.25em; + } + + form#dash-pkg-notify p { + margin: 0; + } + +table.dash-stats .key { + width: 50%; +} /* dev dashboard: admin actions (add news items, todo list, etc) */ -ul.admin-actions { float: right; list-style: none; margin-top: -2.5em; } -ul.admin-actions li { display: inline; padding-left: 1.5em; } +ul.admin-actions { + float: right; + list-style: none; + margin-top: -2.5em; +} + + ul.admin-actions li { + display: inline; + padding-left: 1.5em; + } /* todo lists (public and private) */ -.todo-table .complete { color: green; } -.todo-table .incomplete { color: red; } -.todo-info { margin: 0; color: #999; } -.todo-list h4 { margin-top: 0; margin-bottom: 0.4em; } +.todo-table .complete { + color: green; +} + +.todo-table .incomplete { + color: red; +} +.todo-info { + margin: 0; color: #999; +} + +.todo-list h4 { + margin-top: 0; + margin-bottom: 0.4em; +} /* dev: signoff page */ -#dev-signoffs ul { list-style: none; margin: 0; padding: 0; } -#dev-signoffs .signoff-yes { color: green; font-weight: bold; } -#dev-signoffs .signoff-no { color: red; } -#dev-signoffs .signed-username { color: #888; margin-left: 0.5em; } +#dev-signoffs ul { + list-style: none; + margin: 0; + padding: 0; +} + +#dev-signoffs .signoff-yes { + color: green; + font-weight: bold; +} + +#dev-signoffs .signoff-no { + color: red; +} + +#dev-signoffs .signed-username { + color: #888; + margin-left: 0.5em; +} /* iso testing feedback form */ -#releng-feedback label { width: auto; display: inline; font-weight: normal; } -#releng-feedback ul { padding-left: 1em; } -#releng-feedback li { list-style: none; } -#releng-feedback ul+.helptext { position: relative; top: -0.9em; } +#releng-feedback label { + width: auto; + display: inline; + font-weight: normal; +} + +#releng-feedback ul { + padding-left: 1em; +} + +#releng-feedback li { + list-style: none; +} + +#releng-feedback ul+.helptext { + position: relative; top: -0.9em; +} /* highlight current website in the navbar */ -#archnavbar.anb-home ul li#anb-home a { color: white !important; } -#archnavbar.anb-packages ul li#anb-packages a { color: white !important; } -#archnavbar.anb-download ul li#anb-download a { color: white !important; } +#archnavbar.anb-home ul li#anb-home a, +#archnavbar.anb-packages ul li#anb-packages a, +#archnavbar.anb-download ul li#anb-download a { + color: white !important; +} -- cgit v1.2.3-54-g00ecf From 5e5182ba9b727e112b3bd16f94f41a2fc5847804 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 19 Aug 2011 13:08:26 -0500 Subject: Add color to releng results success column Signed-off-by: Dan McGee --- media/archweb.css | 8 ++++++++ templates/releng/result_list.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index be455680..92487d93 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -930,6 +930,14 @@ ul.admin-actions { position: relative; top: -0.9em; } +#releng-result .success-yes { + color: green; +} + +#releng-result .success-no { + color: red; +} + /* highlight current website in the navbar */ #archnavbar.anb-home ul li#anb-home a, #archnavbar.anb-packages ul li#anb-packages a, diff --git a/templates/releng/result_list.html b/templates/releng/result_list.html index 0233bf8c..845d330d 100644 --- a/templates/releng/result_list.html +++ b/templates/releng/result_list.html @@ -26,7 +26,7 @@

Results for: {{ test.user_name }} {{ test.created|date }} {{ test.architecture }} - {{ test.success|yesno|capfirst }} + {{ test.success|yesno|capfirst }} {% endfor %} -- cgit v1.2.3-54-g00ecf From 1908b3c4609d1c3acf04c94a490516ff5284d2c7 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 19 Aug 2011 18:03:39 -0500 Subject: Fix CSS validation error Signed-off-by: Dan McGee --- media/archweb.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index 92487d93..865a4f0b 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -78,7 +78,7 @@ select[multiple] { } input[type=submit] { - padding-left: 0 0.6em; + padding-left: 0.6em; } .clear { -- cgit v1.2.3-54-g00ecf From 6b34f60ba641c124055d8ae3e9c4cf425ef540c9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 25 Aug 2011 11:10:06 -0500 Subject: Ensure package depends/required by div clears top details pane Signed-off-by: Dan McGee --- media/archweb.css | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index 865a4f0b..0efd53db 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -739,6 +739,10 @@ form#flag-pkg-form input[type=text] { } /* pkgdetails: deps, required by and file lists */ +#pkgdetails #metadata { + clear: both; +} + #pkgdetails #metadata h3 { background: #555; color: #fff; -- cgit v1.2.3-54-g00ecf From b283e5ff83efcc8e8b160d8e8084ff25d14c3ad3 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 2 Sep 2011 10:51:13 -0500 Subject: Correctly fix archweb.css style error Commit 1908b3c460 attempted to fix a validation error; we really wanted to change the left hand side rather than the right hand side. Signed-off-by: Dan McGee --- media/archweb.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index 0efd53db..0cadd7a7 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -78,7 +78,7 @@ select[multiple] { } input[type=submit] { - padding-left: 0.6em; + padding: 0 0.6em; } .clear { -- cgit v1.2.3-54-g00ecf From d5063bd1d2cae79df7ce6e826c7413fed61ff9db Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 5 Oct 2011 15:45:44 -0500 Subject: Add package visualizations page Why the hell not? Have fun clicking all the pretty buttons. Signed-off-by: Dan McGee --- media/archweb.css | 27 ++++++++++ media/visualize.js | 112 +++++++++++++++++++++++++++++++++++++++++ settings.py | 1 + templates/public/index.html | 8 +-- templates/visualize/index.html | 43 ++++++++++++++++ urls.py | 1 + visualize/__init__.py | 0 visualize/models.py | 0 visualize/tests.py | 0 visualize/urls.py | 9 ++++ visualize/views.py | 59 ++++++++++++++++++++++ 11 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 media/visualize.js create mode 100644 templates/visualize/index.html create mode 100644 visualize/__init__.py create mode 100644 visualize/models.py create mode 100644 visualize/tests.py create mode 100644 visualize/urls.py create mode 100644 visualize/views.py (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index 0cadd7a7..eb0f0ca1 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -948,3 +948,30 @@ ul.admin-actions { #archnavbar.anb-download ul li#anb-download a { color: white !important; } + +/* visualizations page */ +.visualize-buttons { + margin: 0.5em 0.33em; +} + + .visualize-buttons button.active { + depressed: true; + } + +.visualize-chart { + position: relative; + height: 500px; + margin: 0.33em; +} + +#visualize-archrepo .treemap-cell { + border: solid 1px white; + overflow: hidden; + position: absolute; +} + + #visualize-archrepo .treemap-cell span { + padding: 3px; + font-size: 0.85em; + line-height: 1em; + } diff --git a/media/visualize.js b/media/visualize.js new file mode 100644 index 00000000..c1ea598b --- /dev/null +++ b/media/visualize.js @@ -0,0 +1,112 @@ +function packages_treemap(chart_id, orderings, default_order) { + var jq_div = $(chart_id), + color = d3.scale.category20(); + key_func = function(d) { return d.key; }, + value_package_count = function(d) { return d.count; }; + + var treemap = d3.layout.treemap() + .size([jq_div.width(), jq_div.height()]) + /*.sticky(true)*/ + .value(value_package_count) + .sort(function(a, b) { return a.key < b.key; }) + .children(function(d) { return d.data; }); + + var cell_html = function(d) { + if (d.children) { + return ""; + } + return "" + d.name + ": " + treemap.value()(d) + ""; + }; + + var d3_div = d3.select(jq_div.get(0)); + + var prop_px = function(prop, offset) { + return function(d) { + var dist = d[prop] + offset; + if (dist > 0) return dist + "px"; + else return "0px"; + }; + }; + + var cell = function() { + /* the -1 offset comes from the border width we use in the CSS */ + this.style("left", prop_px("x", 0)).style("top", prop_px("y", 0)) + .style("width", prop_px("dx", -1)).style("height", prop_px("dy", -1)); + }; + + var fetch_for_ordering = function(order) { + d3.json(order.url, function(json) { + var nodes = d3_div.data([json]).selectAll("div").data(treemap.nodes, key_func); + /* start out new nodes in the center of the picture area */ + var w_center = jq_div.width() / 2; + var h_center = jq_div.height() / 2; + nodes.enter().append("div") + .attr("class", "treemap-cell") + .attr("title", function(d) { return d.name; }) + .style("left", w_center + "px").style("top", h_center + "px") + .style("width", "0px").style("height", "0px") + .style("display", function(d) { return d.children ? "none" : null; }) + .html(cell_html); + nodes.transition().duration(1500) + .style("background-color", function(d) { return d.children ? null : color(d[order.color_attr]); }) + .call(cell); + nodes.exit().transition().duration(1500).remove(); + }); + }; + + /* start the callback for the default order */ + fetch_for_ordering(orderings[default_order]); + + var make_scale_button = function(name, valuefunc) { + var button_id = chart_id + "-" + name; + /* upon button click, attach new value function and redraw all boxes + * accordingly */ + d3.select(button_id).on("click", function() { + d3_div.selectAll("div") + .data(treemap.value(valuefunc), key_func) + .html(cell_html) + .transition().duration(1500).call(cell); + + /* drop off the '#' sign to convert id to a class prefix */ + d3.selectAll("." + chart_id.substring(1) + "-scaleby") + .classed("active", false); + d3.select(button_id).classed("active", true); + }); + }; + + /* each scale button tweaks our value, e.g. net size function */ + make_scale_button("count", value_package_count); + make_scale_button("flagged", function(d) { return d.flagged; }); + make_scale_button("csize", function(d) { return d.csize; }); + make_scale_button("isize", function(d) { return d.isize; }); + + var make_group_button = function(name, order) { + var button_id = chart_id + "-" + name; + d3.select(button_id).on("click", function() { + fetch_for_ordering(order); + + /* drop off the '#' sign to convert id to a class prefix */ + d3.selectAll("." + chart_id.substring(1) + "-groupby") + .classed("active", false); + d3.select(button_id).classed("active", true); + }); + }; + + $.each(orderings, function(k, v) { + make_group_button(k, v); + }); + + var resize_timeout = null; + var real_resize = function() { + resize_timeout = null; + d3_div.selectAll("div") + .data(treemap.size([jq_div.width(), jq_div.height()]), key_func) + .call(cell); + }; + $(window).resize(function() { + if (resize_timeout) { + clearTimeout(resize_timeout); + } + resize_timeout = setTimeout(real_resize, 200); + }); +} diff --git a/settings.py b/settings.py index 18437098..51f9fcf6 100644 --- a/settings.py +++ b/settings.py @@ -109,6 +109,7 @@ 'public', 'south', # database migration support 'releng', + 'visualize', ) PGP_SERVER = 'pgp.mit.edu:11371' diff --git a/templates/public/index.html b/templates/public/index.html index bea19e0f..b63876ac 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -26,10 +26,10 @@

A simple, lightweight distribution

Our strong community is diverse and helpful, and we pride ourselves on the range of skillsets and uses for Arch that stem from it. Please - check out our forums + check out our forums and mailing lists - to get your feet wet. Also glance through our wiki if you want to learn more about Arch.

@@ -174,8 +174,8 @@

Development

title="View/search the package repository database">Packages
  • Package Groups
  • -
  • Bug Tracker
  • +
  • Visualizations
  • SVN Repositories
  • + +

    Visualizations of Packaging Data

    + +

    Package Treemap

    + +
    +
    + Scale Using: + + + + +
    +
    + Group By: + + +
    +
    +
    + + +{% load cdn %}{% jquery %} + + + + + +{% endblock %} diff --git a/urls.py b/urls.py index c9faf165..cdae51bf 100644 --- a/urls.py +++ b/urls.py @@ -76,6 +76,7 @@ (r'^packages/', include('packages.urls')), (r'^releng/', include('releng.urls')), (r'^todo/', include('todolists.urls')), + (r'^visualize/', include('visualize.urls')), (r'^opensearch/packages/$', 'packages.views.opensearch', {}, 'opensearch-packages'), (r'^todolists/$','todolists.views.public_list'), diff --git a/visualize/__init__.py b/visualize/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/visualize/models.py b/visualize/models.py new file mode 100644 index 00000000..e69de29b diff --git a/visualize/tests.py b/visualize/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/visualize/urls.py b/visualize/urls.py new file mode 100644 index 00000000..57ee0626 --- /dev/null +++ b/visualize/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('visualize.views', + (r'^$', 'index', {}, 'visualize-index'), + (r'^by_arch/$', 'by_arch', {}, 'visualize-byarch'), + (r'^by_repo/$', 'by_repo', {}, 'visualize-byrepo'), +) + +# vim: set ts=4 sw=4 et: diff --git a/visualize/views.py b/visualize/views.py new file mode 100644 index 00000000..68f5d4a5 --- /dev/null +++ b/visualize/views.py @@ -0,0 +1,59 @@ +from django.db.models import Count, Sum +from django.http import HttpResponse +from django.utils import simplejson +from django.views.decorators.cache import cache_page +from django.views.generic.simple import direct_to_template + +from main.models import Package, Arch, Repo + +def index(request): + return direct_to_template(request, 'visualize/index.html', {}) + +def arch_repo_data(): + qs = Package.objects.select_related().values( + 'arch__name', 'repo__name').annotate( + count=Count('pk'), csize=Sum('compressed_size'), + isize=Sum('installed_size'), + flagged=Count('flag_date')).order_by() + arches = Arch.objects.values_list('name', flat=True) + repos = Repo.objects.values_list('name', flat=True) + + # now transform these results into two mappings: one ordered (repo, arch), + # and one ordered (arch, repo). + arch_groups = dict((a, { 'name': a, 'key': ':%s' % a, 'arch': a, 'repo': None, 'data': [] }) for a in arches) + repo_groups = dict((r, { 'name': r, 'key': '%s:' % r, 'arch': None, 'repo': r, 'data': [] }) for r in repos) + for row in qs: + arch = row['arch__name'] + repo = row['repo__name'] + values = { + 'arch': arch, + 'repo': repo, + 'name': '%s (%s)' % (repo, arch), + 'key': '%s:%s' % (repo, arch), + 'csize': row['csize'], + 'isize': row['isize'], + 'count': row['count'], + 'flagged': row['flagged'], + } + arch_groups[arch]['data'].append(values) + repo_groups[repo]['data'].append(values) + + data = { + 'by_arch': { 'name': 'Architectures', 'data': arch_groups.values() }, + 'by_repo': { 'name': 'Repositories', 'data': repo_groups.values() }, + } + return data + +@cache_page(1800) +def by_arch(request): + data = arch_repo_data() + to_json = simplejson.dumps(data['by_arch'], ensure_ascii=False) + return HttpResponse(to_json, mimetype='application/json') + +@cache_page(1800) +def by_repo(request): + data = arch_repo_data() + to_json = simplejson.dumps(data['by_repo'], ensure_ascii=False) + return HttpResponse(to_json, mimetype='application/json') + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From f6527810b0d28db8a07a26e84148d1229b18d29e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 6 Oct 2011 10:20:30 -0500 Subject: Make border style and color consistent on different pages Use dotted and #bbb where appropriate as opposed to the varying styles we had before. Addresses FS#25834. Signed-off-by: Dan McGee --- media/archweb.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index eb0f0ca1..ea2f3fb5 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -273,7 +273,7 @@ dl { margin-bottom: 4px; padding: 8px 0px 4px; font-weight: bold; - border-top: 1px solid #888; + border-top: 1px dotted #bbb; } dl dt { @@ -568,7 +568,7 @@ div#donor-list ul { /* download page */ #arch-downloads h3 { - border-bottom: 1px dotted #aaa; + border-bottom: 1px dotted #bbb; } table#download-torrents .cpu-arch { @@ -840,7 +840,7 @@ form#dash-pkg-notify { padding: 1em 0 0; margin-top: 1em; font-size: 0.85em; - border-top: 1px dotted #aaa; + border-top: 1px dotted #bbb; } form#dash-pkg-notify label { -- cgit v1.2.3-54-g00ecf From ac2278423a3d449fdfe8c813f1f2d391ef9aff08 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 14:59:00 -0500 Subject: Many signoff page improvements Add a new 'SignoffSpecification' model which will capture metadata regarding a specific package if it differs from the norm- e.g. more or less than 2 required signoffs, is known to be bad, a comment from the maintainer, etc. The groundwork is laid here; much of this will still need to be wired up in the future. Enhance the view with a lot more JS prettiness and add revoking of signoffs. The signoff page can be filtered and the links and all the fun stuff are totally dynamic now. Signed-off-by: Dan McGee --- main/models.py | 11 -- media/archweb.css | 1 - media/archweb.js | 72 ++++++-- .../0010_auto__add_signoffspecification.py | 183 +++++++++++++++++++++ packages/models.py | 45 ++++- packages/urls.py | 1 + packages/views.py | 71 +++++--- templates/packages/differences.html | 2 +- templates/packages/signoff_cell.html | 12 ++ templates/packages/signoffs.html | 50 +++--- 10 files changed, 379 insertions(+), 69 deletions(-) create mode 100644 packages/migrations/0010_auto__add_signoffspecification.py create mode 100644 templates/packages/signoff_cell.html (limited to 'media/archweb.css') diff --git a/main/models.py b/main/models.py index 780453c0..d55a9673 100644 --- a/main/models.py +++ b/main/models.py @@ -7,7 +7,6 @@ from main.utils import cache_function, make_choice, set_created_field from packages.models import PackageRelation -from packages.models import Signoff as PackageSignoff from datetime import datetime from itertools import groupby @@ -213,16 +212,6 @@ def maintainers(self): package_relations__pkgbase=self.pkgbase, package_relations__type=PackageRelation.MAINTAINER) - @property - def signoffs(self): - return PackageSignoff.objects.select_related('user').filter( - pkgbase=self.pkgbase, pkgver=self.pkgver, pkgrel=self.pkgrel, - epoch=self.epoch, arch=self.arch, repo=self.repo) - - def approved_for_signoff(self): - count = self.signoffs.filter(revoked__isnull=True).count() - return count >= PackageSignoff.REQUIRED - @cache_function(300) def applicable_arches(self): '''The list of (this arch) + (available agnostic arches).''' diff --git a/media/archweb.css b/media/archweb.css index ea2f3fb5..62dc4fbc 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -912,7 +912,6 @@ ul.admin-actions { #dev-signoffs .signed-username { color: #888; - margin-left: 0.5em; } /* iso testing feedback form */ diff --git a/media/archweb.js b/media/archweb.js index a51ae460..43812b33 100644 --- a/media/archweb.js +++ b/media/archweb.js @@ -139,7 +139,7 @@ function ajaxifyFiles() { /* packages/differences.html */ function filter_packages() { - // start with all rows, and then remove ones we shouldn't show + /* start with all rows, and then remove ones we shouldn't show */ var rows = $('#tbody_differences').children(); var all_rows = rows; if (!$('#id_multilib').is(':checked')) { @@ -150,12 +150,12 @@ function filter_packages() { rows = rows.filter('.' + arch); } if (!$('#id_minor').is(':checked')) { - // this check is done last because it is the most expensive + /* this check is done last because it is the most expensive */ var pat = /(.*)-(.+)/; rows = rows.filter(function(index) { var cells = $(this).children('td'); - // all this just to get the split version out of the table cell + /* all this just to get the split version out of the table cell */ var ver_a = cells.eq(2).find('span').text().match(pat); if (!ver_a) { return true; @@ -166,26 +166,26 @@ function filter_packages() { return true; } - // first check pkgver + /* first check pkgver */ if (ver_a[1] !== ver_b[1]) { return true; } - // pkgver matched, so see if rounded pkgrel matches + /* pkgver matched, so see if rounded pkgrel matches */ if (Math.floor(parseFloat(ver_a[2])) === Math.floor(parseFloat(ver_b[2]))) { return false; } - // pkgrel didn't match, so keep the row + /* pkgrel didn't match, so keep the row */ return true; }); } - // hide all rows, then show the set we care about + /* hide all rows, then show the set we care about */ all_rows.hide(); rows.show(); - // make sure we update the odd/even styling from sorting + /* make sure we update the odd/even styling from sorting */ $('.results').trigger('applyWidgets'); } -function filter_reset() { +function filter_packages_reset() { $('#id_archonly').val('both'); $('#id_multilib').removeAttr('checked'); $('#id_minor').removeAttr('checked'); @@ -213,26 +213,72 @@ function todolist_flag() { function signoff_package() { var link = this; $.getJSON(link.href, function(data) { + link = $(link); + var signoff = null; if (data.created) { - var signoff = $('
  • ').addClass('signed-username').text(data.user); - $(link).append(signoff); + signoff = $('
  • ').addClass('signed-username').text(data.user); + link.closest('td').children('ul').append(signoff); + } else if(data.user) { + signoff = link.closest('td').find('li').filter(function(index) { + return $(this).text() == data.user; + }); + } + console.log(signoff, data.revoked, data.user); + if (signoff && data.revoked) { + signoff.text(signoff.text() + ' (revoked)'); } /* update the approved column to reflect reality */ var approved; if (data.approved) { - approved = $(link).closest('tr').children('.signoff-no'); + approved = link.closest('tr').children('.signoff-no'); approved.text('Yes').addClass( 'signoff-yes').removeClass('signoff-no'); } else { - approved = $(link).closest('tr').children('.signoff-yes'); + approved = link.closest('tr').children('.signoff-yes'); approved.text('No').addClass( 'signoff-no').removeClass('signoff-yes'); } + link.removeAttr('title'); + /* Form our new link. The current will be something like + * '/packages/repo/arch/package/...' */ + var base_href = link.attr('href').split('/').slice(0, 5).join('/'); + if (data.revoked) { + link.text('Signoff'); + link.attr('href', base_href + '/signoff/'); + } else { + link.text('Revoke Signoff'); + link.attr('href', base_href + '/signoff/revoke/'); + } $('.results').trigger('updateCell', approved); }); return false; } +function filter_signoffs() { + /* start with all rows, and then remove ones we shouldn't show */ + var rows = $('#tbody_signoffs').children(); + var all_rows = rows; + $('#signoffs_filter .arch_filter').each(function() { + if (!$(this).is(':checked')) { + console.log($(this).val()); + rows = rows.not('.' + $(this).val()); + } + }); + if ($('#id_pending').is(':checked')) { + rows = rows.has('td.signoff-no'); + } + /* hide all rows, then show the set we care about */ + all_rows.hide(); + rows.show(); + /* make sure we update the odd/even styling from sorting */ + $('.results').trigger('applyWidgets'); +} +function filter_signoffs_reset() { + $('#signoffs_filter .arch_filter').attr('checked', 'checked'); + $('#id_pending').removeAttr('checked'); + filter_signoffs(); +} + /* visualizations */ function format_filesize(size, decimals) { /*var labels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];*/ diff --git a/packages/migrations/0010_auto__add_signoffspecification.py b/packages/migrations/0010_auto__add_signoffspecification.py new file mode 100644 index 00000000..da24824e --- /dev/null +++ b/packages/migrations/0010_auto__add_signoffspecification.py @@ -0,0 +1,183 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_table('packages_signoffspecification', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('pkgbase', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('pkgver', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('pkgrel', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('epoch', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('arch', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.Arch'])), + ('repo', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.Repo'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('required', self.gf('django.db.models.fields.PositiveIntegerField')(default=2)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('known_bad', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('comments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('packages', ['SignoffSpecification']) + + + def backwards(self, orm): + db.delete_table('packages_signoffspecification') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'packages.conflict': { + 'Meta': {'ordering': "['name']", 'object_name': 'Conflict'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'conflicts'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.license': { + 'Meta': {'ordering': "['name']", 'object_name': 'License'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'licenses'", 'to': "orm['main.Package']"}) + }, + 'packages.packagegroup': { + 'Meta': {'object_name': 'PackageGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Package']"}) + }, + 'packages.packagerelation': { + 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_relations'", 'to': "orm['auth.User']"}) + }, + 'packages.provision': { + 'Meta': {'ordering': "['name']", 'object_name': 'Provision'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provides'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.replacement': { + 'Meta': {'ordering': "['name']", 'object_name': 'Replacement'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replaces'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 4cd3b1b5..ad082501 100644 --- a/packages/models.py +++ b/packages/models.py @@ -38,6 +38,49 @@ def __unicode__(self): class Meta: unique_together = (('pkgbase', 'user', 'type'),) +class SignoffSpecification(models.Model): + ''' + A specification for the signoff policy for this particular revision of a + pakcage. The default is requiring two signoffs for a given package. These + are created only if necessary; e.g., if one wanted to override the + required=2 attribute, otherwise a sane default object is used. + ''' + pkgbase = models.CharField(max_length=255, db_index=True) + pkgver = models.CharField(max_length=255) + pkgrel = models.CharField(max_length=255) + epoch = models.PositiveIntegerField(default=0) + arch = models.ForeignKey('main.Arch') + repo = models.ForeignKey('main.Repo') + user = models.ForeignKey(User) + created = models.DateTimeField(editable=False) + required = models.PositiveIntegerField(default=2) + enabled = models.BooleanField(default=True) + known_bad = models.BooleanField(default=False) + comments = models.TextField(null=True, blank=True) + +class SignoffManager(models.Manager): + def get_from_package(self, pkg, user, revoked=False): + '''Utility method to pull all relevant name-version fields from a + package and create a matching signoff.''' + not_revoked = not revoked + return Signoff.objects.get( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, + revoked__isnull=not_revoked, user=user) + + def get_or_create_from_package(self, pkg, user): + '''Utility method to pull all relevant name-version fields from a + package and create a matching signoff.''' + return Signoff.objects.get_or_create( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, + revoked=None, user=user) + + def for_package(self, pkg): + return self.select_related('user').filter( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + class Signoff(models.Model): ''' A signoff for a package (by pkgbase) at a given point in time. These are @@ -55,7 +98,7 @@ class Signoff(models.Model): revoked = models.DateTimeField(null=True) comments = models.TextField(null=True, blank=True) - REQUIRED = 2 + objects = SignoffManager() @property def packages(self): diff --git a/packages/urls.py b/packages/urls.py index d7d01170..576e3279 100644 --- a/packages/urls.py +++ b/packages/urls.py @@ -10,6 +10,7 @@ (r'^unflag/$', 'unflag'), (r'^unflag/all/$', 'unflag_all'), (r'^signoff/$', 'signoff_package'), + (r'^signoff/revoke/$', 'signoff_package', {'revoke': True}), (r'^download/$', 'download'), ) diff --git a/packages/views.py b/packages/views.py index 5114c87f..035d51cb 100644 --- a/packages/views.py +++ b/packages/views.py @@ -25,7 +25,7 @@ from main.models import Package, PackageFile, Arch, Repo from main.utils import make_choice, groupby_preserve_order, PackageStandin from mirrors.models import MirrorUrl -from .models import PackageRelation, PackageGroup, Signoff +from .models import PackageRelation, PackageGroup, SignoffSpecification, Signoff from .utils import (get_group_info, get_differences_info, get_wrong_permissions, get_current_signoffs) @@ -369,14 +369,24 @@ def unflag_all(request, name, repo, arch): pkgs.update(flag_date=None) return redirect(pkg) +DEFAULT_SIGNOFF_SPEC = SignoffSpecification(required=2) + +def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): + if signoffs: + good_signoffs = sum(1 for s in signoffs if not s.revoked) + return good_signoffs >= spec.required + return False + class PackageSignoffGroup(object): '''Encompasses all packages in testing with the same pkgbase.''' - def __init__(self, packages, target_repo=None, signoffs=None): + def __init__(self, packages, user=None): if len(packages) == 0: raise Exception self.packages = packages - self.target_repo = target_repo - self.signoffs = signoffs + self.user = user + self.target_repo = None + self.signoffs = set() + self.specification = DEFAULT_SIGNOFF_SPEC first = packages[0] self.pkgbase = first.pkgbase @@ -406,21 +416,24 @@ def package(self): def find_signoffs(self, all_signoffs): '''Look through a list of Signoff objects for ones matching this particular group and store them on the object.''' - if self.signoffs is None: - self.signoffs = [] for s in all_signoffs: if s.pkgbase != self.pkgbase: continue if self.version and not s.full_version == self.version: continue if s.arch_id == self.arch.id and s.repo_id == self.repo.id: - self.signoffs.append(s) + self.signoffs.add(s) def approved(self): - if self.signoffs: - good_signoffs = [s for s in self.signoffs if not s.revoked] - return len(good_signoffs) >= Signoff.REQUIRED - return False + return approved_by_signoffs(self.signoffs, self.specification) + + def user_signed_off(self, user=None): + '''Did a given user signoff on this package? user can be passed as an + argument, or attached to the group object itself so this can be called + from a template.''' + if user is None: + user = self.user + return user in (s.user for s in self.signoffs if not s.revoked) @permission_required('main.change_package') @never_cache @@ -443,7 +456,7 @@ def signoffs(request): grouped = groupby_preserve_order(packages, same_pkgbase_key) signoff_groups = [] for group in grouped: - signoff_group = PackageSignoffGroup(group) + signoff_group = PackageSignoffGroup(group, user=request.user) signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase, "Unknown") signoff_group.find_signoffs(signoffs) @@ -451,27 +464,43 @@ def signoffs(request): signoff_groups.sort(key=attrgetter('pkgbase')) - return direct_to_template(request, 'packages/signoffs.html', - {'signoff_groups': signoff_groups}) + context = { + 'signoff_groups': signoff_groups, + 'arches': Arch.objects.all(), + } + return direct_to_template(request, 'packages/signoffs.html', context) @permission_required('main.change_package') @never_cache -def signoff_package(request, name, repo, arch): +def signoff_package(request, name, repo, arch, revoke=False): packages = get_list_or_404(Package, pkgbase=name, arch__name=arch, repo__name__iexact=repo, repo__testing=True) - pkg = packages[0] - signoff, created = Signoff.objects.get_or_create( - pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, - epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, user=request.user) + package = packages[0] + + if revoke: + try: + signoff = Signoff.objects.get_from_package( + package, request.user, False) + except Signoff.DoesNotExist: + raise Http404 + signoff.revoked = datetime.utcnow() + signoff.save() + created = False + else: + signoff, created = Signoff.objects.get_or_create_from_package( + package, request.user) + + all_signoffs = Signoff.objects.for_package(package) if request.is_ajax(): data = { 'created': created, - 'approved': pkg.approved_for_signoff(), + 'revoked': bool(signoff.revoked), + 'approved': approved_by_signoffs(all_signoffs), 'user': str(request.user), } - return HttpResponse(simplejson.dumps(data), + return HttpResponse(simplejson.dumps(data, ensure_ascii=False), mimetype='application/json') return redirect('package-signoffs') diff --git a/templates/packages/differences.html b/templates/packages/differences.html index dd1046bc..0400ea37 100644 --- a/templates/packages/differences.html +++ b/templates/packages/differences.html @@ -65,7 +65,7 @@

    Filter Differences View

    $('.results').tablesorter({widgets: ['zebra'], sortList: [[1,0], [0,0]]}); $('#diff_filter select').change(filter_packages); $('#diff_filter input').change(filter_packages); - $('#criteria_reset').click(filter_reset); + $('#criteria_reset').click(filter_differences_reset); // fire function on page load to ensure the current form selections take effect filter_packages(); }); diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html new file mode 100644 index 00000000..fce5d551 --- /dev/null +++ b/templates/packages/signoff_cell.html @@ -0,0 +1,12 @@ +
      + {% for signoff in group.signoffs %} +
    • {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
    • + {% endfor %} +
    +{% if group.user_signed_off %} +
    +{% else %} + +{% endif %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index a8aa4de2..4a2f6c99 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -12,42 +12,46 @@

    Package Signoffs

    {{ signoff_groups|length }} signoff group{{ signoff_groups|pluralize }} found. A "signoff group" consists of packages grouped by pkgbase, architecture, and repository.

    +
    +

    Filter Displayed Signoffs

    +
    +
    + Select filter criteria + {% for arch in arches %} +
    +
    + {% endfor %} +
    +
    +
    +
    +
    +
    + + - + - - - + - + {% for group in signoff_groups %} {% with group.package as pkg %} - + + - + - - - + {% endwith %} {% endfor %} @@ -60,8 +64,12 @@

    Package Signoffs

    {% endblock %} -- cgit v1.2.3-54-g00ecf From caa15d61a3882d1076378ae406bb2effcb63ff87 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 17:29:05 -0500 Subject: Minor tweaks to style and sorting of signoffs Signed-off-by: Dan McGee --- media/archweb.css | 4 ++++ templates/packages/signoffs.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index 62dc4fbc..f817e18d 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -895,6 +895,10 @@ ul.admin-actions { } /* dev: signoff page */ +#dev-signoffs tr:hover { + background: #ffd; +} + #dev-signoffs ul { list-style: none; margin: 0; diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index 8d57a8c5..0bdc6d46 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -65,7 +65,7 @@

    Filter Displayed Signoffs

    $(document).ready(function() { $('a.signoff-link').click(signoff_package); $(".results").tablesorter({widgets: ['zebra'], sortList: [[0,0]], - headers: { 6: { sorter: false } } }); + headers: { 7: { sorter: false } } }); $('#signoffs_filter input').change(filter_signoffs); $('#criteria_reset').click(filter_signoffs_reset); // fire function on page load to ensure the current form selections take effect -- cgit v1.2.3-54-g00ecf From 5f2c3bf98baabf919681525e600639643aa2c119 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 20:39:59 -0500 Subject: Signoffs changes and improvements * Better signoff report with more detail * Show signoff specification in signoffs view * Honor disabled/bad flags and display in approval column * Various other small bugfixes and tweaks Signed-off-by: Dan McGee --- media/archweb.css | 8 +++++-- media/archweb.js | 30 ++++++++++++++++---------- packages/management/commands/signoff_report.py | 13 ++++++++++- packages/models.py | 28 +++++++++++++++++------- packages/utils.py | 16 ++++++++------ packages/views.py | 15 +++++++++---- templates/packages/signoff_cell.html | 2 ++ templates/packages/signoff_report.txt | 13 +++++++++++ templates/packages/signoffs.html | 19 ++++++++++++++-- 9 files changed, 109 insertions(+), 35 deletions(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index f817e18d..303173f2 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -914,8 +914,12 @@ ul.admin-actions { color: red; } -#dev-signoffs .signed-username { - color: #888; +#dev-signoffs .signoff-bad { + color: darkorange; +} + +#dev-signoffs .signoff-disabled { + color: gray; } /* iso testing feedback form */ diff --git a/media/archweb.js b/media/archweb.js index 43812b33..a9f4e0c9 100644 --- a/media/archweb.js +++ b/media/archweb.js @@ -215,28 +215,33 @@ function signoff_package() { $.getJSON(link.href, function(data) { link = $(link); var signoff = null; + var cell = link.closest('td'); if (data.created) { signoff = $('
  • ').addClass('signed-username').text(data.user); - link.closest('td').children('ul').append(signoff); + var list = cell.children('ul'); + if (list.size() == 0) { + list = $('
      ').prependTo(cell); + } + list.append(signoff); } else if(data.user) { signoff = link.closest('td').find('li').filter(function(index) { return $(this).text() == data.user; }); } - console.log(signoff, data.revoked, data.user); if (signoff && data.revoked) { signoff.text(signoff.text() + ' (revoked)'); } /* update the approved column to reflect reality */ - var approved; - if (data.approved) { - approved = link.closest('tr').children('.signoff-no'); - approved.text('Yes').addClass( - 'signoff-yes').removeClass('signoff-no'); + var approved = link.closest('tr').children('.approval'); + approved.attr('class', ''); + if (data.known_bad) { + approved.text('Bad').addClass('signoff-bad'); + } else if (!data.enabled) { + approved.text('Disabled').addClass('signoff-disabled'); + } else if (data.approved) { + approved.text('Yes').addClass('signoff-yes'); } else { - approved = link.closest('tr').children('.signoff-yes'); - approved.text('No').addClass( - 'signoff-no').removeClass('signoff-yes'); + approved.text('No').addClass('signoff-no'); } link.removeAttr('title'); /* Form our new link. The current will be something like @@ -245,6 +250,10 @@ function signoff_package() { if (data.revoked) { link.text('Signoff'); link.attr('href', base_href + '/signoff/'); + /* should we be hiding the link? */ + if (data.known_bad || !data.enabled) { + link.remove(); + } } else { link.text('Revoke Signoff'); link.attr('href', base_href + '/signoff/revoke/'); @@ -260,7 +269,6 @@ function filter_signoffs() { var all_rows = rows; $('#signoffs_filter .arch_filter').each(function() { if (!$(this).is(':checked')) { - console.log($(this).val()); rows = rows.not('.' + $(this).val()); } }); diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py index 02f3d985..3431dada 100644 --- a/packages/management/commands/signoff_report.py +++ b/packages/management/commands/signoff_report.py @@ -56,6 +56,8 @@ def generate_report(email, repo_name): # Collect all existing signoffs for these packages signoff_groups = sorted(get_signoff_groups([repo]), key=attrgetter('target_repo', 'arch', 'pkgbase')) + disabled = [] + bad = [] complete = [] incomplete = [] new = [] @@ -68,10 +70,16 @@ def generate_report(email, repo_name): old_cutoff = now - timedelta(days=old_days) for group in signoff_groups: - if group.approved(): + spec = group.specification + if spec.known_bad: + bad.append(group) + elif not spec.enabled: + disabled.append(group) + elif group.approved(): complete.append(group) else: incomplete.append(group) + if group.package.last_update > new_cutoff: new.append(group) if group.package.last_update < old_cutoff: @@ -96,6 +104,9 @@ def generate_report(email, repo_name): c = Context({ 'repo': repo, 'signoffs_url': signoffs_url, + 'disabled': disabled, + 'bad': bad, + 'all': signoff_groups, 'incomplete': incomplete, 'complete': complete, 'new': new, diff --git a/packages/models.py b/packages/models.py index a2b53a06..b70c21bf 100644 --- a/packages/models.py +++ b/packages/models.py @@ -1,3 +1,5 @@ +from collections import namedtuple + from django.db import models from django.db.models.signals import pre_save, post_save from django.contrib.auth.models import User @@ -42,22 +44,26 @@ class Meta: class SignoffSpecificationManager(models.Manager): def get_from_package(self, pkg): '''Utility method to pull all relevant name-version fields from a - package and get a matching specification.''' + package and get a matching signoff specification.''' return self.get( pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) - def get_or_create_from_package(self, pkg): - '''Utility method to pull all relevant name-version fields from a - package and get or create a matching specification.''' - return self.get_or_create( - pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, - epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + def get_or_default_from_package(self, pkg): + '''utility method to pull all relevant name-version fields from a + package and get a matching signoff specification, or return the default + base case.''' + try: + return self.get( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + except SignoffSpecification.DoesNotExist: + return DEFAULT_SIGNOFF_SPEC class SignoffSpecification(models.Model): ''' A specification for the signoff policy for this particular revision of a - pakcage. The default is requiring two signoffs for a given package. These + package. The default is requiring two signoffs for a given package. These are created only if necessary; e.g., if one wanted to override the required=2 attribute, otherwise a sane default object is used. ''' @@ -89,6 +95,12 @@ def __unicode__(self): return u'%s-%s' % (self.pkgbase, self.full_version) +# fake default signoff spec when we don't have a persisted one in the database +FakeSignoffSpecification = namedtuple('FakeSignoffSpecification', + ('required', 'enabled', 'known_bad', 'comments')) +DEFAULT_SIGNOFF_SPEC = FakeSignoffSpecification(2, True, False, u'') + + class SignoffManager(models.Manager): def get_from_package(self, pkg, user, revoked=False): '''Utility method to pull all relevant name-version fields from a diff --git a/packages/utils.py b/packages/utils.py index 42cfbe0f..60b95e21 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -5,7 +5,8 @@ from main.models import Package, Repo from main.utils import cache_function, groupby_preserve_order, PackageStandin -from .models import PackageGroup, PackageRelation, SignoffSpecification, Signoff +from .models import (PackageGroup, PackageRelation, + SignoffSpecification, Signoff, DEFAULT_SIGNOFF_SPEC) @cache_function(300) def get_group_info(include_arches=None): @@ -148,9 +149,7 @@ def get_wrong_permissions(): return relations -DEFAULT_SIGNOFF_SPEC = SignoffSpecification() - -def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): +def approved_by_signoffs(signoffs, spec): if signoffs: good_signoffs = sum(1 for s in signoffs if not s.revoked) return good_signoffs >= spec.required @@ -158,14 +157,13 @@ def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): class PackageSignoffGroup(object): '''Encompasses all packages in testing with the same pkgbase.''' - def __init__(self, packages, user=None): + def __init__(self, packages): if len(packages) == 0: raise Exception self.packages = packages - self.user = user + self.user = None self.target_repo = None self.signoffs = set() - self.specification = DEFAULT_SIGNOFF_SPEC first = packages[0] self.pkgbase = first.pkgbase @@ -175,6 +173,10 @@ def __init__(self, packages, user=None): self.last_update = first.last_update self.packager = first.packager + self.specification = \ + SignoffSpecification.objects.get_or_default_from_package(first) + self.default_spec = self.specification is DEFAULT_SIGNOFF_SPEC + version = first.full_version if all(version == pkg.full_version for pkg in packages): self.version = version diff --git a/packages/views.py b/packages/views.py index 66bcd3fc..307691e2 100644 --- a/packages/views.py +++ b/packages/views.py @@ -7,8 +7,9 @@ from django.core.mail import send_mail from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Q -from django.http import HttpResponse, Http404 -from django.shortcuts import get_object_or_404, get_list_or_404, redirect +from django.http import HttpResponse, Http404, HttpResponseForbidden +from django.shortcuts import (get_object_or_404, get_list_or_404, + redirect, render) from django.template import loader, Context from django.utils import simplejson from django.views.decorators.cache import never_cache @@ -404,12 +405,16 @@ def signoff_package(request, name, repo, arch, revoke=False): package, request.user) all_signoffs = Signoff.objects.for_package(package) + spec = SignoffSpecification.objects.get_or_default_from_package(package) if request.is_ajax(): data = { 'created': created, 'revoked': bool(signoff.revoked), - 'approved': approved_by_signoffs(all_signoffs), + 'approved': approved_by_signoffs(all_signoffs, spec), + 'required': spec.required, + 'enabled': spec.enabled, + 'known_bad': spec.known_bad, 'user': str(request.user), } return HttpResponse(simplejson.dumps(data, ensure_ascii=False), @@ -429,7 +434,9 @@ def signoff_options(request, name, repo, arch): arch__name=arch, repo__name__iexact=repo, repo__testing=True) package = packages[0] - # TODO ensure submitter is maintainer and/or packager + if request.user != package.packager and \ + request.user not in package.maintainers: + return render(request, '403.html', status=403) try: spec = SignoffSpecification.objects.get_from_package(package) diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html index 0a630119..6c705b4e 100644 --- a/templates/packages/signoff_cell.html +++ b/templates/packages/signoff_cell.html @@ -11,10 +11,12 @@ Revoke Signoff {% else %} +{% if not group.specification.known_bad and group.specification.enabled %} {% endif %} +{% endif %} {% if group.packager == user %}
      Packager Options diff --git a/templates/packages/signoff_report.txt b/templates/packages/signoff_report.txt index 84e3fc6b..81020c8f 100644 --- a/templates/packages/signoff_report.txt +++ b/templates/packages/signoff_report.txt @@ -1,6 +1,19 @@ === {% autoescape off %}Signoff report for [{{ repo|lower }}] === {{ signoffs_url }} +There are currently: +* {{ new|length }} new package{{ new|length|pluralize }} in last {{ new_hours }} hours +* {{ bad|length }} known bad package{{ bad|length|pluralize }} +* {{ disabled|length }} package{{ disabled|length|pluralize }} not accepting signoffs +* {{ complete|length }} fully signed off package{{ complete|length|pluralize }} +* {{ incomplete|length }} package{{ incomplete|length|pluralize }} missing signoffs +* {{ old|length }} package{{ old|length|pluralize }} older than {{ old_days }} days + +(Note: the word 'package' as used here refers to packages as grouped by +pkgbase, architecture, and repository; e.g., one PKGBUILD produces one +package per architecture, even if it is a split package.) + + == New packages in [{{ repo|lower}}] in last {{ new_hours }} hours ({{ new|length }} total) == {% for group in new %} * {{ group.pkgbase }}-{{ group.version }} ({{ group.arch }}){% endfor %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index 9bc7fd74..d517e5e3 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -39,6 +39,7 @@

      Filter Displayed Signoffs

  • + @@ -50,8 +51,22 @@

    Filter Displayed Signoffs

    - + {% if group.specification.known_bad %} + + {% else %} + {% if not group.specification.enabled %} + + {% else %} + + {% endif %} + {% endif %} + {% endfor %} @@ -64,7 +79,7 @@

    Filter Displayed Signoffs

    $(document).ready(function() { $('a.signoff-link').click(signoff_package); $(".results").tablesorter({widgets: ['zebra'], sortList: [[0,0]], - headers: { 7: { sorter: false } } }); + headers: { 7: { sorter: false }, 8: {sorter: false } } }); $('#signoffs_filter input').change(filter_signoffs); $('#criteria_reset').click(filter_signoffs_reset); // fire function on page load to ensure the current form selections take effect -- cgit v1.2.3-54-g00ecf From 21d5f818a60ab2626f941f8ff53e263e802494d5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Fri, 11 Nov 2011 10:42:51 -0600 Subject: Touch up signoff page styles Signed-off-by: Dan McGee --- media/archweb.css | 10 +++++----- templates/packages/signoff_cell.html | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'media/archweb.css') diff --git a/media/archweb.css b/media/archweb.css index 303173f2..c5477422 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -899,26 +899,26 @@ ul.admin-actions { background: #ffd; } -#dev-signoffs ul { +ul.signoff-list { list-style: none; margin: 0; padding: 0; } -#dev-signoffs .signoff-yes { +.signoff-yes { color: green; font-weight: bold; } -#dev-signoffs .signoff-no { +.signoff-no { color: red; } -#dev-signoffs .signoff-bad { +.signoff-bad { color: darkorange; } -#dev-signoffs .signoff-disabled { +.signoff-disabled { color: gray; } diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html index 0bf44ca2..01a5d58d 100644 --- a/templates/packages/signoff_cell.html +++ b/templates/packages/signoff_cell.html @@ -1,6 +1,6 @@ {% spaceless %} {% if group.signoffs %} -
      +
        {% for signoff in group.signoffs %}
      • {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
      • {% endfor %} -- cgit v1.2.3-54-g00ecf From 5b63c29fe1c37ce9d946adedeaf13f5ad94d144a Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 15 Nov 2011 14:03:36 -0600 Subject: Show full names on developer user list pages The old display format doesn't really make sense. Also fix the invalid HTML generated by the PGP tag link- we need to escape using & inside the generated URLs. Signed-off-by: Dan McGee --- main/templatetags/pgp.py | 2 +- media/archweb.css | 4 ++++ templates/public/developer_list.html | 2 +- templates/public/userlist.html | 2 -- 4 files changed, 6 insertions(+), 4 deletions(-) (limited to 'media/archweb.css') diff --git a/main/templatetags/pgp.py b/main/templatetags/pgp.py index f875c11e..67f5e08d 100644 --- a/main/templatetags/pgp.py +++ b/main/templatetags/pgp.py @@ -21,7 +21,7 @@ def pgp_key_link(key_id): pgp_server = getattr(settings, 'PGP_SERVER', None) if not pgp_server: return format_key(key_id) - url = 'http://%s/pks/lookup?op=vindex&fingerprint=on&exact=on&search=0x%s' % \ + url = 'http://%s/pks/lookup?op=vindex&fingerprint=on&exact=on&search=0x%s' % \ (pgp_server, key_id) values = (url, format_key(key_id), key_id[-8:]) return '0x%s' % values diff --git a/media/archweb.css b/media/archweb.css index c5477422..f4bb92fa 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -802,6 +802,10 @@ div#arch-bio-toc { text-align: center; } + div#arch-bio-toc a { + white-space: nowrap; + } + table.arch-bio-entry td.pic { vertical-align: top; padding-right: 15px; diff --git a/templates/public/developer_list.html b/templates/public/developer_list.html index 2abbbfe4..0ac444e5 100644 --- a/templates/public/developer_list.html +++ b/templates/public/developer_list.html @@ -4,7 +4,7 @@

        {% for dev in dev_list %} - {{ dev.first_name }}{{ dev.last_name.0|capfirst}}    + {{ dev.first_name }} {{ dev.last_name }}    {% endfor %}

        diff --git a/templates/public/userlist.html b/templates/public/userlist.html index c51215c3..0077f611 100644 --- a/templates/public/userlist.html +++ b/templates/public/userlist.html @@ -6,7 +6,6 @@ {% block content %} {% cache 600 dev-tu-profiles user_type %}
        -

        Arch Linux {{user_type}}

        {{description}}

        @@ -14,7 +13,6 @@

        Arch Linux {{user_type}}

        {% with users as dev_list %} {% include 'public/developer_list.html' %} {% endwith %} -
        {% endcache %} {% endblock %} -- cgit v1.2.3-54-g00ecf
    Package Base/Version ArchPackage BaseTarget Repo # of PackagesVersion Last UpdatedTarget Repo ApprovedSignoffSignoffs
    {% pkg_details_link pkg %} {{ pkg.full_version }} {{ pkg.arch.name }}{% pkg_details_link pkg %}{{ group.target_repo }} {{ group.packages|length }}{{ pkg.full_version }} {{ pkg.last_update|date }}{{ group.target_repo }} {{ group.approved|yesno|capfirst }} -
      -
    • Signoff -
    • - {% for signoff in group.signoffs %} -
    • - {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
    • - {% endfor %} -
    -
    {% include "packages/signoff_cell.html" %}
    Last Updated Approved SignoffsNotes
    {{ group.packager|default:"Unknown" }} {{ group.packages|length }} {{ group.last_update|date }}{{ group.approved|yesno|capfirst }}BadDisabled{{ group.approved|yesno|capfirst }}{% include "packages/signoff_cell.html" %}{% if not group.default_spec %}{% with group.specification as spec %} + {% if spec.required != 2 %}Required signoffs: {{ spec.required }}
    {% endif %} + {% if not spec.enabled %}Signoffs are not currently enabled
    {% endif %} + {% if spec.known_bad %}Package is known to be bad
    {% endif %} + {{ spec.comments|default:""|linebreaks }} + {% endwith %}{% endif %}