diff options
author | Robert Haas | 2009-05-22 14:41:10 +0000 |
---|---|---|
committer | Robert Haas | 2009-05-22 14:41:10 +0000 |
commit | 5bc9ab5aee7ff20941e0ea6a42aa9c80e5f8f946 (patch) | |
tree | 061857ffc98835b2151dec00f713b16aa6c43200 |
First attempt.
40 files changed, 2288 insertions, 0 deletions
diff --git a/bin/server.fpl b/bin/server.fpl new file mode 100755 index 0000000..ed4b347 --- /dev/null +++ b/bin/server.fpl @@ -0,0 +1,8 @@ +#!/usr/bin/perl + +use lib '/home/rhaas/commitfest/perl-lib'; +use PgCommitFest::Handler; +use strict; +use warnings; + +PgCommitFest::Handler::main_loop(); diff --git a/etc/function.sql b/etc/function.sql new file mode 100644 index 0000000..c65ad59 --- /dev/null +++ b/etc/function.sql @@ -0,0 +1,23 @@ +CREATE LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION most_recent_comments(integer) + RETURNS SETOF patch_comment_view AS $$ +DECLARE + v_patch_id integer; +BEGIN + FOR v_patch_id IN + SELECT + p.id + FROM + patch p + INNER JOIN commitfest_topic t ON p.commitfest_topic_id = t.id + WHERE + t.commitfest_id = $1 + LOOP + RETURN QUERY ( + SELECT * FROM patch_comment_view WHERE patch_id = v_patch_id + ORDER BY creation_time DESC LIMIT 3 + ); + END LOOP; +END +$$ LANGUAGE plpgsql; diff --git a/etc/httpd.conf.template b/etc/httpd.conf.template new file mode 100644 index 0000000..ba6b721 --- /dev/null +++ b/etc/httpd.conf.template @@ -0,0 +1,14 @@ +<VirtualHost *:8080> + DocumentRoot "BASEPATH/html" + RewriteEngine On + # Need mod_cgid installed and configured to handle .fpl files. + RewriteRule ^/$ BASEPATH/bin/server.fpl + RewriteRule ^/action/ BASEPATH/bin/server.fpl +</VirtualHost> + +<Directory "BASEPATH"> + Order allow,deny + Allow from all + Options Indexes FollowSymLinks ExecCGI + AllowOverride None +</Directory> diff --git a/etc/table.sql b/etc/table.sql new file mode 100644 index 0000000..5f941f4 --- /dev/null +++ b/etc/table.sql @@ -0,0 +1,81 @@ +CREATE TABLE person ( + username varchar not null, + sha512password varchar not null, + PRIMARY KEY (username) +); + +CREATE TABLE session ( + id varchar not null, + username varchar not null references person (username), + login_time timestamp not null default now(), + PRIMARY KEY (id) +); + +CREATE TABLE commitfest_status ( + id integer not null, + name varchar not null, + PRIMARY KEY (id) +); +INSERT INTO commitfest_status VALUES (1, 'Future'); -- Not ready yet. +INSERT INTO commitfest_status VALUES (2, 'Open'); -- Submit here. +INSERT INTO commitfest_status VALUES (3, 'In Progress'); -- Review here. +INSERT INTO commitfest_status VALUES (4, 'Closed'); -- All done. + +CREATE TABLE commitfest ( + id serial, + name varchar not null, + commitfest_status_id integer not null references commitfest_status (id), + PRIMARY KEY (id) +); + +CREATE TABLE commitfest_topic ( + id serial, + commitfest_id integer not null references commitfest (id), + name varchar not null, + PRIMARY KEY (id) +); + +CREATE TABLE patch_status ( + id integer not null, + name varchar not null, + PRIMARY KEY (id) +); +INSERT INTO patch_status VALUES (1, 'Needs Review'); +INSERT INTO patch_status VALUES (2, 'Waiting on Author'); +INSERT INTO patch_status VALUES (3, 'Ready for Committer'); +INSERT INTO patch_status VALUES (4, 'Committed'); +INSERT INTO patch_status VALUES (5, 'Returned with Feedback'); +INSERT INTO patch_status VALUES (6, 'Rejected'); + +CREATE TABLE patch ( + id serial, + commitfest_topic_id integer not null references commitfest_topic (id), + name varchar not null, + patch_status_id integer not null references patch_status (id), + author varchar not null, + reviewers varchar not null, + date_closed date, + creation_time timestamp with time zone not null default now(), + PRIMARY KEY (id) +); + +CREATE TABLE patch_comment_type ( + id integer not null, + name varchar not null, + PRIMARY KEY (id) +); +INSERT INTO patch_comment_type VALUES (1, 'Comment'); +INSERT INTO patch_comment_type VALUES (2, 'Patch'); +INSERT INTO patch_comment_type VALUES (3, 'Review'); + +CREATE TABLE patch_comment ( + id serial, + patch_id integer not null references patch (id), + patch_comment_type_id integer not null + references patch_comment_type (id), + message_id varchar, + content varchar, + creator varchar not null references person (username), + creation_time timestamp with time zone not null default now(), + PRIMARY KEY (id) +); diff --git a/etc/view.sql b/etc/view.sql new file mode 100644 index 0000000..af10114 --- /dev/null +++ b/etc/view.sql @@ -0,0 +1,25 @@ +CREATE OR REPLACE VIEW commitfest_view AS +SELECT + v.id, v.name, v.commitfest_status_id, s.name AS commitfest_status +FROM + commitfest v + INNER JOIN commitfest_status s ON v.commitfest_status_id = s.id; + +CREATE OR REPLACE VIEW patch_view AS +SELECT v.id, v.commitfest_topic_id, s.name AS commitfest_topic, + s.commitfest_id, f.name AS commitfest, v.name, + v.patch_status_id, ps.name AS patch_status, v.author, v.reviewers, + v.date_closed, v.creation_time +FROM + patch v + INNER JOIN commitfest_topic s ON v.commitfest_topic_id = s.id + INNER JOIN commitfest f ON s.commitfest_id = f.id + INNER JOIN patch_status ps ON v.patch_status_id = ps.id; + +CREATE OR REPLACE VIEW patch_comment_view AS +SELECT + v.id, v.patch_id, v.patch_comment_type_id, pct.name AS patch_comment_type, + v.message_id, v.content, v.creator, v.creation_time +FROM + patch_comment v + INNER JOIN patch_comment_type pct ON v.patch_comment_type_id = pct.id; diff --git a/html/layout/css/blue/commitfest.css b/html/layout/css/blue/commitfest.css new file mode 100644 index 0000000..5a7df94 --- /dev/null +++ b/html/layout/css/blue/commitfest.css @@ -0,0 +1,213 @@ +/* PostgreSQL.org Documentation Style */ + +@import url("global.css"); +@import url("table.css"); +@import url("text.css"); + +body { + font-size: 76%; +} + +/* Container Definitions */ + +#commitfestContainerWrap { + text-align: center; /* Win IE5 */ +} + +#commitfestContainer { + margin: 0 auto; + width: 90%; + padding-bottom: 2em; + display: block; + text-align: left; /* Win IE5 */ +} + +#commitfestHeader { + background-image: url("/https/git.postgresql.org/layout/images/docs/bg_hdr.png"); + height: 83px; + margin: 0px; + padding: 0px; + display: block; +} + +#commitfestHeaderLogo { + position: relative; + width: 206px; + height: 83px; + border: 0px; + padding: 0px; + margin: 0px; + margin-left: 20px; +} + +#commitfestHeaderLogo img { border: 0px; } + +#commitfestNavSearchContainer { + padding-bottom: 2px; +} + +#commitfestNav { + position: relative; + text-align: left; + margin-left: 10px; + margin-top: 5px; + color: #666; + font-size: 0.95em; +} + +#commitfestSearch { + position: relative; + text-align: right; + padding: 0; + margin: 0; + color: #666; +} + +#commitfestTextSize { + text-align: right; + white-space: nowrap; + margin-top: 7px; + font-size: 0.95em; +} + +#commitfestSearch form { + position: relative; + top: 5px; + right: 0; + margin: 0; /* need for IE Mac */ + text-align: right; /* need for IE Mac */ + white-space: nowrap; /* for Opera */ +} + +#commitfestSearch form label { color: #666; font-size: 0.95em; } +#commitfestSearch form input { font-size: 0.95em; } + +#commitfestSearch form #submit { + font-size: 0.95em; + background: #7A7A7A; + color: #fff; + border-right: 1px solid #7A7A7A; + border-bottom: 1px solid #7A7A7A; + border-top: 1px solid #7A7A7A; + border-left: 1px solid #7A7A7A; + padding: 1px 4px; +} + +#commitfestSearch form #q { + width: 170px; + font-size: 0.95em; + border: 1px solid #7A7A7A; + background: #E1E1E1; + color: #000000; + padding: 2px; +} + +.frmDocSearch { + padding: 0; + margin: 0; + display: inline; +} + +.inpDocSearch { + padding: 0; + margin: 0; + color: #000; +} + +#commitfestContent { + position: relative; + margin-left: 10px; + margin-right: 10px; + margin-top: 20px; +} + +#commitfestFooter { + position: relative; + font-size: 0.9em; + color: #666; + line-height: 1.3em; + margin-left: 10px; + margin-right: 10px; +} + +#commitfestComments { + margin-top: 10px; +} + +#commitfestClear { + clear: both; + margin: 0; + padding: 0; +} + +/* Heading Definitions */ + +h1 { + font-weight: bold; + color: #EC5800; + font-size: 1.4em; +} + +h2 { + font-weight: bold; + color: #666; + font-size: 1.2em; +} + +h3 { + font-weight: bold; + color: #666; + font-size: 1.1em; +} + +/* Text Styles */ + +.txtCurrentLocation { + font-weight: bold; +} + +p, ol, ul, li { + line-height: 1.5em; +} + +.txtCommentsWrap { + border: 2px solid #F5F5F5; + width: 100%; +} + +.txtCommentsContent { + background: #F5F5F5; + padding: 3px; +} + +.txtCommentsPoster { + float: left; +} + +.txtCommentsDate { + float: right; +} + +.txtCommentsComment { + padding: 3px; +} + +/* Link Styles */ + +#commitfestNav a { + font-weight: bold; +} + + +a:link { color:#0066A2; text-decoration: underline; } +a:visited { color:#004E66; text-decoration: underline; } +a:active { color:#0066A2; text-decoration: underline; } +a:hover { color:#000000; text-decoration: underline; } + +#commitfestFooter a:link { color:#666; text-decoration: underline; } +#commitfestFooter a:visited { color:#666; text-decoration: underline; } +#commitfestFooter a:active { color:#666; text-decoration: underline; } +#commitfestFooter a:hover { color:#000000; text-decoration: underline; } + +.error { color: #f00; font-weight: bold } +.controlerror { color: #f00 } diff --git a/html/layout/css/blue/geckofixes.css b/html/layout/css/blue/geckofixes.css new file mode 100644 index 0000000..96313fc --- /dev/null +++ b/html/layout/css/blue/geckofixes.css @@ -0,0 +1,21 @@ +/* Gecko is broken with pre,tt,code sizes */ + +#pgContainer code, #pgContainer pre, #pgContainer tt { + font-size: 1.2em; +} + +#docContainer tt, #docContainer pre, #docContainer code { + font-size: 1.4em; +} + +#docContainer tt tt, #docContainer tt code, #docContainer tt pre { + font-size: 1.0em; +} + +#docContainer pre code, #docContainer pre tt, #docContainer pre pre { + font-size: 1.0em; +} + +#docContainer code code, #docContainer code tt, #docContainer code pre { + font-size: 1.0em; +} diff --git a/html/layout/css/blue/global.css b/html/layout/css/blue/global.css new file mode 100644 index 0000000..16ec0ac --- /dev/null +++ b/html/layout/css/blue/global.css @@ -0,0 +1,80 @@ +/* + PostgreSQL.org - Global Styles +*/ + +body { + margin: 0; + padding: 0; + font-family: verdana, sans-serif; + font-size: 69%; + color: #000; + background-color: #fff; +} + +h1 { + font-size: 1.4em; + font-weight: bold; + margin-top: 0em; + margin-bottom: 0em; +} + +h2 { + font-size: 1.2em; + margin: 1.2em 0em 1.2em 0em; + font-weight: bold; +} + +h3 { + font-size: 1.0em; + margin: 1.2em 0em 1.2em 0em; + font-weight: bold; +} + +h4 { + font-size: 0.95em; + margin: 1.2em 0em 1.2em 0em; + font-weight: normal; +} + +h5 { + font-size: 0.9em; + margin: 1.2em 0em 1.2em 0em; + font-weight: normal; +} + +h6 { + font-size: 0.85em; + margin: 1.2em 0em 1.2em 0em; + font-weight: normal; +} + +img { + border: 0; +} + +ol, ul, li {/* + list-style: none;*/ + font-size: 1.0em; + line-height: 1.2em; + margin-top: 0.2em; + margin-bottom: 0.1em; +} + +p { + font-size: 1.0em; + line-height: 1.2em; + margin: 1.2em 0em 1.2em 0em; +} + +li > p { + margin-top: 0.2em; +} + +pre { + font-family: monospace; + font-size: 1.0em; +} + +strong, b { + font-weight: bold; +} diff --git a/html/layout/css/blue/table.css b/html/layout/css/blue/table.css new file mode 100644 index 0000000..340a242 --- /dev/null +++ b/html/layout/css/blue/table.css @@ -0,0 +1,100 @@ +/* + PostgreSQL.org - Table Styles +*/ + +div.tblBasic h2 { + margin: 25px 0 .5em 0; +} + +div.tblBasic table { + background: #F5F5F5 url(/https/git.postgresql.org/layout/images/nav_tbl_top_lft.png) top left no-repeat; + margin-bottom: 15px; +} + +div.tblBasic table th { + padding-top: 20px; + border-bottom: 1px solid #EFEFEF; + vertical-align: bottom; +} + +div.tblBasic table td { + border-bottom: 1px solid #EFEFEF; +} + +div.tblBasic table th, +div.tblBasic table td { + padding: 8px 11px; + color: #555555; +} + +div.tblBasic table td.indented { + text-indent: 30px; +} + +div.tblBasic table.tblCompact td { + padding: 3px 3px; +} + +div.tblBasic table tr.lastrow td { + border-bottom: none; + padding-bottom: 13px; +} + +div.tblBasic table.tblCompact tr.lastrow td { + padding-bottom: 3px; +} + +div.tblBasic table tr.lastrow td.colFirstT, +div.tblBasic table tr.lastrow td.colFirst { + background: url(/https/git.postgresql.org/layout/images/nav_tbl_btm_lft.png) bottom left no-repeat; +} + +div.tblBasic table.tblBasicGrey th.colLast, +div.tblBasic table.tblCompact th.colLast { + background: #F5F5F5 url(/https/git.postgresql.org/layout/images/nav_tbl_top_rgt.png) top right no-repeat; +} + +div.tblBasic table.tblBasicGrey tr.lastrow td.colLastT, +div.tblBasic table.tblBasicGrey tr.lastrow td.colLast, +div.tblBasic table.tblCompact tr.lastrow td.colLast, +div.tblBasic table.tblCompact tr.lastrow td.colLastT{ + background: #F5F5F5 url(/https/git.postgresql.org/layout/images/nav_tbl_btm_rgt.png) bottom right no-repeat; +} + +div.tblBasic table.tblBasicGrey tr.firstrow td.colLastT, +div.tblBasic table.tblBasicGrey tr.firstrow td.colLast, +div tblBasic table.tblCompact tr.firstrow td.colLast { + background: #F5F5F5 url(/https/git.postgresql.org/layout/images/nav_tbl_top_rgt.png) top right no-repeat; +} + +div.tblBasic table th.colMid, +div.tblBasic table td.colMid, +div.tblBasic table th.colLast, +div.tblBasic table td.colLast { + background-color: #F5F5F5 ; +} + +div.tblBasic table th.colLastC, +div.tblBasic table td.colFirstC, +div.tblBasic table td.colLastC { + text-align: center; +} + +div.tblBasic table th.colLastR, +div.tblBasic table td.colFirstR, +div.tblBasic table td.colLastR { + text-align: right; +} + +div.tblBasic table td.colFirstT, +div.tblBasic table td.colMidT, +div.tblBasic table td.colLastT { + vertical-align: top; +} + +div.tblBasic table th.colLastRT, +div.tblBasic table td.colFirstRT, +div.tblBasic table td.colLastRT { + text-align: right; + vertical-align: top; +} diff --git a/html/layout/css/blue/text.css b/html/layout/css/blue/text.css new file mode 100644 index 0000000..902a118 --- /dev/null +++ b/html/layout/css/blue/text.css @@ -0,0 +1,162 @@ +/* + PostgreSQL.org - Text Styles +*/ + +/* Heading Definitions */ + +h1 { + color: #EC5800; +} + +h2 { + color: #666; +} + +h3 { + color: #666; +} + +h4 { + color: #666; +} + +/* Text Styles */ + +.txtColumn1 { + width: 50%; + line-height: 1.3em; +} + +.txtColumn2 { + width: 50%; + line-height: 1.5em; +} + +.txtCurrentLocation { + font-weight: bold; +} + +.txtDivider { + font-size: 0.8em; + color: #E1E1E1; + padding-left: 4px; + padding-right: 4px; +} + +.txtNewsEvent { + font-size: 0.9em; + color: #0094C7; +} + +.txtDate { + font-size: 0.9em; + color: #666; +} + +.txtMediumGrey { + color: #666; +} + +.txtFormLabel { + color: #666; + font-weight: bold; + text-align: right; + vertical-align: top; +} + +.txtRequiredField { + color: #EC5800; +} + +.txtImportant { + color: #EC5800; +} + +.txtOffScreen { + position: absolute; + left: -1999px; + width: 1990px; +} + +#txtFrontFeatureHeading { + padding-bottom: 1.1em; +} + +#txtFrontFeatureLink a { + font-size: 1.2em; + font-weight: bold; + padding-left: 5px; +} + +#txtFrontUserText { + font-size: 1.0em; + color: #666; + margin-top: 12px; +} + +#txtFrontUserName { + font-size: 0.9em; + color: #666; + margin-top: 9px; + font-weight: bold; +} + +#txtFrontUserLink { + font-size: 0.9em; + color: #666; + margin-top: 11px; + margin-left: 1px; +} + +#txtFrontUserLink img { + padding-right: 5px; +} + +#txtFrontSupportUsText { + font-size: 1.0em; + margin-top: 9px; +} + +#txtFrontSupportUsLink { + font-size: 0.9em; + margin-top: 6px; +} + +#txtFrontSupportUsLink img { + padding-right: 7px; +} + +/* Link Styles */ + +a:link { color:#0085B0; text-decoration: underline; } +a:visited { color:#004E66; text-decoration: underline; } +a:active { color:#0085B0; text-decoration: underline; } +a:hover { color:#000000; text-decoration: underline; } + +#pgFooter a:link { color:#666; text-decoration: underline; } +#pgFooter a:visited { color:#666; text-decoration: underline; } +#pgFooter a:active { color:#666; text-decoration: underline; } +#pgFooter a:hover { color:#000000; text-decoration: underline; } + +#txtFrontUserName a:link { color:#666; text-decoration: underline; } +#txtFrontUserName a:visited { color:#666; text-decoration: underline; } +#txtFrontUserName a:active { color:#666; text-decoration: underline; } +#txtFrontUserName a:hover { color:#000; text-decoration: underline; } + +#txtArchives a:visited { color:#00536E; text-decoration: underline; } +#txtArchives pre { word-wrap: break-word; font-size: 150%; } +#txtArchives tt { word-wrap: break-word; font-size: 150%; } + +#pgFrontUSSContainer h2, #pgFrontUSSContainer h3 { + margin: 0; + padding: 0; +} + +#pgFrontNewsEventsContainer h2, #pgFrontNewsEventsContainer h3 { + margin: 0; + padding: 0; +} + +#pgFrontNewsEventsContainer h3 img { + margin-bottom: 10px; +} diff --git a/html/layout/images/docs/bg_hdr.png b/html/layout/images/docs/bg_hdr.png Binary files differnew file mode 100644 index 0000000..07c3b65 --- /dev/null +++ b/html/layout/images/docs/bg_hdr.png diff --git a/html/layout/images/docs/hdr_logo.png b/html/layout/images/docs/hdr_logo.png Binary files differnew file mode 100644 index 0000000..3e3ef7d --- /dev/null +++ b/html/layout/images/docs/hdr_logo.png diff --git a/html/layout/images/nav_tbl_btm_lft.png b/html/layout/images/nav_tbl_btm_lft.png Binary files differnew file mode 100644 index 0000000..958a364 --- /dev/null +++ b/html/layout/images/nav_tbl_btm_lft.png diff --git a/html/layout/images/nav_tbl_btm_rgt.png b/html/layout/images/nav_tbl_btm_rgt.png Binary files differnew file mode 100644 index 0000000..8db9779 --- /dev/null +++ b/html/layout/images/nav_tbl_btm_rgt.png diff --git a/html/layout/images/nav_tbl_top_lft.png b/html/layout/images/nav_tbl_top_lft.png Binary files differnew file mode 100644 index 0000000..ccdec2f --- /dev/null +++ b/html/layout/images/nav_tbl_top_lft.png diff --git a/html/layout/images/nav_tbl_top_rgt.png b/html/layout/images/nav_tbl_top_rgt.png Binary files differnew file mode 100644 index 0000000..d2a76c3 --- /dev/null +++ b/html/layout/images/nav_tbl_top_rgt.png diff --git a/html/layout/js/geckostyle.js b/html/layout/js/geckostyle.js new file mode 100644 index 0000000..a736f7c --- /dev/null +++ b/html/layout/js/geckostyle.js @@ -0,0 +1,11 @@ +function isGecko() { + var agent = navigator.userAgent.toLowerCase(); + if (agent.indexOf("gecko") != -1) { + return true; + } + return false; +} + +if (isGecko()) { + document.write('<style type="text/css" media="screen">@import "/layout/css/blue/geckofixes.css";</style>\n'); +} diff --git a/perl-lib/PgCommitFest/CommitFest.pm b/perl-lib/PgCommitFest/CommitFest.pm new file mode 100644 index 0000000..e6cadd9 --- /dev/null +++ b/perl-lib/PgCommitFest/CommitFest.pm @@ -0,0 +1,156 @@ +package PgCommitFest::CommitFest; +use strict; +use warnings; + +sub delete { + my ($r) = @_; + $r->authenticate('require_login' => 1); + $r->set_title('Delete CommitFest'); + my $d; + eval { + $d = $r->db->select_one(<<EOM, $r->cgi_required_id); +DELETE FROM commitfest WHERE id = ? RETURNING id +EOM + }; + my $err = $@; + if (! $err) { + $r->error_exit('CommitFest not found.') if !defined $d; + $r->db->commit; + $r->redirect('/'); + } + if ($err =~ /commitfest_topic_commitfest_id_fkey/) { + $r->error_exit(<<EOM); +This CommitFest contains one or more topics and can't be deleted. +EOM + } + $r->error_exit("Internal error: $@"); +} + +sub form { + my ($r) = @_; + $r->authenticate('require_login' => 1); + + # Decide whether this is a new commitfest or an edit of an existing + # commitfest, and if editing reload data from database. + my $d; + my $id = $r->cgi_id(); + if (defined $id) { + $r->set_title('Edit CommitFest'); + $d = $r->db->select_one(<<EOM, $id); +SELECT id, name, commitfest_status_id AS commitfest_status FROM commitfest +WHERE id = ? +EOM + $r->error_exit('CommitFest not found.') if !defined $d; + $r->redirect('/action/commitfest_view?id=' . $id) if $r->cgi('cancel'); + } + else { + $r->set_title('New CommitFest'); + $r->redirect('/') if $r->cgi('cancel'); + } + + # Add controls. + $r->add_control('name', 'text', 'Name', 'required' => 1); + $r->add_control('commitfest_status', 'select', 'Status', 'required' => 1); + $r->control('commitfest_status')->choice($r->db->select(<<EOM)); +SELECT id, name FROM commitfest_status ORDER BY id +EOM + my %value = $r->initialize_controls($d); + + # Handle commit. + if ($r->cgi('go') && ! $r->is_error()) { + if (defined $id) { + $r->db->update('commitfest', { 'id' => $id }, \%value); + } + else { + $id = $r->db->insert_returning_id('commitfest', \%value); + } + $r->db->commit; + $r->redirect('/action/commitfest_view?id=' . $id); + } + + # Display template. + $r->render_template('commitfest_form', { 'id' => $id }); +} + +sub search { + my ($r) = @_; + $r->set_title('CommitFest Index'); + $r->add_link('/action/commitfest_form', 'New CommitFest'); + my $list = $r->db->select(<<EOM); +SELECT id, name, commitfest_status FROM commitfest_view ORDER BY name DESC +EOM + $r->render_template('commitfest_search', { 'list' => $list }); +} + +sub view { + my ($r) = @_; + my $id = $r->cgi_id(); + my $d = $r->db->select_one(<<EOM, $id) if defined $id; +SELECT id, name, commitfest_status FROM commitfest_view WHERE id = ? +EOM + $r->error_exit('CommitFest not found.') if !defined $d; + $r->set_title('CommitFest %s (%s)', $d->{'name'}, + $d->{'commitfest_status'}); + + # Load list of patches. + my %patch_grouping; + my %patch_index; + my $patch_list = $r->db->select(<<EOM, $d->{'id'}); +SELECT id, name, patch_status_id, patch_status, author, reviewers, + commitfest_topic_id, commitfest_topic, commitfest_id, date_closed +FROM patch_view WHERE commitfest_id = ? + ORDER BY date_closed, commitfest_topic, name +EOM + for my $p (@$patch_list) { + if (grep { $_ eq $p->{'patch_status_id'} } qw(4 5 6)) { + push @{$patch_grouping{$p->{'patch_status_id'}}}, $p; + } + else { + push @{$patch_grouping{'p'}}, $p; + } + $patch_index{$p->{'id'}} = $p; + } + + # Load list of comments. + my $comment_list = $r->db->select(<<EOM, $d->{'id'}); +SELECT v.id, v.patch_id, v.patch_comment_type, v.message_id, v.content, + v.creator, to_char(v.creation_time, 'MM/DD/YYYY') AS creation_time +FROM most_recent_comments(?) v +EOM + for my $c (@$comment_list) { + my $p = $patch_index{$c->{'patch_id'}}; + unshift @{$p->{'comment_list'}}, $c; + } + + # Add links and render template. + $r->add_link('/action/patch_form?commitfest=' . $id, 'New Patch'); + $r->add_link('/action/commitfest_topic_search?id=' . $id, + 'CommitFest Topics'); + $r->add_link('/action/commitfest_form?id=' . $id, 'Edit CommitFest'); + $r->add_link('/action/commitfest_delete?id=' . $id, 'Delete CommitFest', + 'Are you sure you want to delete this CommitFest?'); + $r->render_template('commitfest_view', { 'd' => $d, 'patch_grouping' => [ + { + 'name' => 'Pending Patches', + 'patch_list' => $patch_grouping{'p'}, + 'closed' => 0 + }, + { + 'name' => 'Committed Patches', + 'patch_list' => $patch_grouping{'4'}, + 'closed' => 1 + }, + { + 'name' => 'Returned with Feedback', + 'patch_list' => $patch_grouping{'5'}, + 'closed' => 1 + }, + { + 'name' => 'Rejected Patches', + 'patch_list' => $patch_grouping{'6'}, + 'closed' => 1 + }, + ]}); +} + +1; diff --git a/perl-lib/PgCommitFest/CommitFestTopic.pm b/perl-lib/PgCommitFest/CommitFestTopic.pm new file mode 100644 index 0000000..5c2eca8 --- /dev/null +++ b/perl-lib/PgCommitFest/CommitFestTopic.pm @@ -0,0 +1,101 @@ +package PgCommitFest::CommitFestTopic; +use strict; +use warnings; + +sub delete { + my ($r) = @_; + $r->authenticate('require_login' => 1); + $r->set_title('Delete CommitFest Topic'); + my $d; + eval { + $d = $r->db->select_one(<<EOM, $r->cgi_required_id); +DELETE FROM commitfest_topic WHERE id = ? RETURNING commitfest_id +EOM + }; + my $err = $@; + if (! $err) { + $r->error_exit('CommitFest not found.') if !defined $d; + $r->db->commit; + $r->redirect('/action/commitfest_topic_search?id=' + . $d->{'commitfest_id'}); + } + if ($err =~ /patch_commitfest_topic_id_fkey/) { + $r->error_exit(<<EOM); +This CommitFest topic contains one or more patches and can't be deleted. +EOM + } + $r->error_exit("Internal error: $@"); +} + +sub form { + my ($r) = @_; + $r->authenticate('require_login' => 1); + + # Decide whether this is a new commitfest or an edit of an existing + # commitfest, and if editing reload data from database. + my $d; + my $id = $r->cgi_id(); + if (defined $id) { + $r->set_title('Edit CommitFest Topic'); + $d = $r->db->select_one(<<EOM, $id); +SELECT id, commitfest_id, name FROM commitfest_topic WHERE id = ? +EOM + $r->error_exit('CommitFest not found.') if !defined $d; + } + else { + $d = $r->db->select_one(<<EOM, $r->cgi_required_id('commitfest')); +SELECT id AS commitfest_id FROM commitfest WHERE id = ? +EOM + $r->set_title('New CommitFest Topic'); + } + $r->redirect('/action/commitfest_topic_search?id=' . $d->{'commitfest_id'}) + if $r->cgi('cancel'); + + # Add controls. + $r->add_control('name', 'text', 'Name', 'required' => 1); + my %value = $r->initialize_controls($d); + + # Handle commit. + if ($r->cgi('go') && ! $r->is_error()) { + if (defined $id) { + $r->db->update('commitfest_topic', { 'id' => $id }, { + 'name' => $value{'name'}, + }); + } + else { + $id = $r->db->insert_returning_id('commitfest_topic', { + 'commitfest_id' => $d->{'commitfest_id'}, + 'name' => $value{'name'}, + }); + } + $r->db->commit; + $r->redirect('/action/commitfest_topic_search?id=' + . $d->{'commitfest_id'}); + } + + # Display template. + $r->render_template('commitfest_topic_form', { 'id' => $id, + 'd' => $d }); +} + +sub search { + my ($r) = @_; + my $id = $r->cgi_id(); + my $d = $r->db->select_one(<<EOM, $id) if defined $id; +SELECT id, name FROM commitfest_view WHERE id = ? +EOM + $r->error_exit('CommitFest not found.') if !defined $d; + $r->set_title('CommitFest Topics: %s', $d->{'name'}); + + my $topic_list = $r->db->select(<<EOM, $d->{'id'}); +SELECT id, name FROM commitfest_topic WHERE commitfest_id = ? ORDER BY name +EOM + + $r->add_link('/action/commitfest_topic_form?commitfest=' . $id, + 'New Topic'); + $r->add_link('/action/commitfest_view?id=' . $id, 'Back to CommitFest'); + $r->render_template('commitfest_topic_search', + { 'topic_list' => $topic_list }); +} + +1; diff --git a/perl-lib/PgCommitFest/DB.pm b/perl-lib/PgCommitFest/DB.pm new file mode 100644 index 0000000..c0d4e49 --- /dev/null +++ b/perl-lib/PgCommitFest/DB.pm @@ -0,0 +1,149 @@ +package PgCommitFest::DB; +require DBI; +use strict; +use warnings; + +sub connect { + my ($class, $datasource, $username, $password) = @_; + return bless { + 'dbh' => DBI->connect($datasource, $username, $password, + { 'AutoCommit' => 0, 'RaiseError' => 1 }), + 'trace' => 0, + 'dirty' => 0, + }, $class; +} + +sub commit { + my ($self) = @_; + warn "COMMIT" if $self->{'trace'}; + $self->{'dirty'} = 0; + return $self->{'dbh'}->commit(); +} + +sub delete { + my ($self, $table, $criteria) = @_; + my (@where, @bind); + while (my ($k, $v) = each %$criteria) { + if (ref $v) { + push @where, + $self->{'dbh'}->quote_identifier($table) . ' = ' . $$v; + } + else { + push @where, + $self->{'dbh'}->quote_identifier($table) . ' = ?'; + push @bind, $v; + } + } + my $sql = 'DELETE FROM ' . $self->{'dbh'}->quote_identifier($table) + . (@where ? ' WHERE ' . join(' AND ', @where) : ''); + warn $sql if $self->{'trace'}; + $self->{'dirty'} = 1; + return $self->{'dbh'}->do($sql, {}, @bind); +} + +sub disconnect { + my ($self) = @_; + return $self->{'dbh'}->disconnect(); +} + +sub insert { + my ($self, $table, $data) = @_; + my (@values, @bind); + for my $v (values %$data) { + if (ref $v) { + push @values, $$v; + } + else { + push @values, '?'; + push @bind, $v; + } + } + my $sql = 'INSERT INTO ' . $self->{'dbh'}->quote_identifier($table) + . ' (' . join(', ', keys %$data) . ') VALUES (' . join(', ', @values) + . ')'; + warn $sql if $self->{'trace'}; + $self->{'dirty'} = 1; + return $self->{'dbh'}->do($sql, {}, @bind); +} + +sub insert_returning_id { + my ($self, $table, $data) = @_; + my (@values, @bind); + for my $v (values %$data) { + if (ref $v) { + push @values, $$v; + } + else { + push @values, '?'; + push @bind, $v; + } + } + my $sql = 'INSERT INTO ' . $self->{'dbh'}->quote_identifier($table) + . ' (' . join(', ', keys %$data) . ') VALUES (' . join(', ', @values) + . ') RETURNING id'; + warn $sql if $self->{'trace'}; + $self->{'dirty'} = 1; + my $sth = $self->{'dbh'}->prepare($sql); + $sth->execute(@bind); + my $result = $sth->fetchrow_hashref; + return $result->{'id'}; +} + +sub rollback { + my ($self) = @_; + $self->{'dirty'} = 0; + return $self->{'dbh'}->commit(); +} + +sub select { + my ($self, $sql, @bind) = @_; + warn $sql if $self->{'trace'}; + return $self->{'dbh'}->selectall_arrayref($sql, { 'Slice' => {} }, @bind); +} + +sub select_one { + my ($self, $sql, @bind) = @_; + warn $sql if $self->{'trace'}; + my $sth = $self->{'dbh'}->prepare($sql); + $sth->execute(@bind); + my $result = $sth->fetchrow_hashref; + return $result; +} + +sub tidy { + my ($self) = @_; + $self->{'dbh'}->rollback() if $self->{'dirty'}; + return undef; +} + +sub update { + my ($self, $table, $criteria, $data) = @_; + my (@values, @where, @bind); + while (my ($k, $v) = each %$data) { + if (ref $v) { + push @values, $self->{'dbh'}->quote_identifier($k) . ' = ' . $$v; + } + else { + push @values, $self->{'dbh'}->quote_identifier($k) . ' = ?'; + push @bind, $v; + } + } + while (my ($k, $v) = each %$criteria) { + if (ref $v) { + push @where , $self->{'dbh'}->quote_identifier($k) . ' = ' . $$v; + } + else { + push @where, $self->{'dbh'}->quote_identifier($k) . ' = ?'; + push @bind, $v; + } + } + return if ! @values; # Nothing to do? + my $sql = 'UPDATE ' . $self->{'dbh'}->quote_identifier($table) + . ' SET ' . join(', ', @values) + . (@where ? ' WHERE ' . join(' AND ', @where) : ''); + warn $sql if $self->{'trace'}; + $self->{'dirty'} = 1; + return $self->{'dbh'}->do($sql, {}, @bind); +} + +1; diff --git a/perl-lib/PgCommitFest/Handler.pm b/perl-lib/PgCommitFest/Handler.pm new file mode 100644 index 0000000..72b0c4f --- /dev/null +++ b/perl-lib/PgCommitFest/Handler.pm @@ -0,0 +1,128 @@ +package PgCommitFest::Handler; +require Digest::SHA; +require PgCommitFest::CommitFest; +require PgCommitFest::CommitFestTopic; +require PgCommitFest::Patch; +require PgCommitFest::PatchComment; +require PgCommitFest::Request; +use strict; +use warnings; +use FCGI; +use Template; + +our %ACTION = ( + 'login' => \&PgCommitFest::Handler::login, + 'logout' => \&PgCommitFest::Handler::logout, + 'commitfest_delete' => \&PgCommitFest::CommitFest::delete, + 'commitfest_form' => \&PgCommitFest::CommitFest::form, + 'commitfest_search' => \&PgCommitFest::CommitFest::search, + 'commitfest_view' => \&PgCommitFest::CommitFest::view, + 'commitfest_topic_delete' => \&PgCommitFest::CommitFestTopic::delete, + 'commitfest_topic_form' => \&PgCommitFest::CommitFestTopic::form, + 'commitfest_topic_search' => \&PgCommitFest::CommitFestTopic::search, + 'patch_form' => \&PgCommitFest::Patch::form, + 'patch_delete' => \&PgCommitFest::Patch::delete, + 'patch_view' => \&PgCommitFest::Patch::view, + 'patch_comment_form' => \&PgCommitFest::PatchComment::form, + 'patch_comment_delete' => \&PgCommitFest::PatchComment::delete +); + +our $PG = 'dbi:Pg:dbname=commitfest'; +our $PGUSERNAME = 'rhaas'; +our $PGPASSWORD = ''; + +sub main_loop { + $SIG{'PIPE'} = sub { die "SIGPIPE\n"; }; + while (1) { + # Invoke main request handler and save any resulting error message. + my $db = PgCommitFest::DB->connect($PG, $PGUSERNAME, $PGPASSWORD); + my $r = PgCommitFest::Request->new($db); + last if !defined $r; + eval { + handler($r); + if (! $r->response_sent) { + $r->error_exit('No response was generated.'); + } + }; + my $err = $@; + + # Roll back any uncommited database work. + $db->tidy; + + # Print errors to system log. + if ($err && $err ne "SIGPIPE\n" && $err ne "DONE\n") { + print STDERR $err; + if (defined $r && ! $r->response_sent) { + $r->set_title('Internal Server Error'); + $r->render_template('error', { 'error_list' => [ $err ] }); + } + } + $db->disconnect; + } +} + +sub handler { + my ($r) = @_; + my ($action, $extrapath); + my $url = $ENV{'SCRIPT_URL'}; + if ($url eq '/') { + $action = 'commitfest_search'; + } + elsif ($url =~ /^\/action\/([^\/]*)(\/(.*))?$/) { + $action = $1; + $extrapath = $3; + } + if (defined $action && exists $ACTION{$action}) { + $ACTION{$action}->($r, $extrapath); + } + else { + $r->header('Status', '404 Not Found'); + $r->set_title('Page Not Found'); + $r->render_template('404'); + } + return; +} + +sub login { + my ($r) = @_; + + # Prompt for username and password. + $r->set_title('Log in'); + $r->add_control('username', 'text', 'Username', 'required' => 1); + $r->add_control('password', 'password', 'Password', 'required' => 1); + $r->add_control('uri', 'hidden', 'URI'); + my %value = $r->initialize_controls(); + + # Handle cancellation. + $r->redirect('/') if $r->cgi('cancel'); + + # Attempt to validate login. + if ($r->cgi('go') && ! $r->is_error) { + my $u = $r->db->select_one(<<EOM, +SELECT username FROM person WHERE username = ? AND sha512password = ? +EOM + $value{'username'}, + Digest::SHA::sha512_base64($value{'password'})); + if (defined $u) { + my $random_bits; + open(RANDOM_BITS, '</dev/urandom') || die "/dev/urandom: $!"; + sysread(RANDOM_BITS, $random_bits, 16); + close(RANDOM_BITS); + my $session_cookie = unpack("H*", $random_bits); + $r->db->{'trace'} = 1; + $r->db->insert('session', { 'id' => $session_cookie, + 'username' => $u->{'username'} }); + $r->db->commit; + $r->header('Set-Cookie', "session=$session_cookie"); + $r->redirect($value{'uri'} ne '' ? $value{'uri'} : '/'); + } + else { + $r->error('Invalid username or password.'); + } + } + + # Display template. + $r->render_template('login'); +} + +1; diff --git a/perl-lib/PgCommitFest/Patch.pm b/perl-lib/PgCommitFest/Patch.pm new file mode 100644 index 0000000..4b1e323 --- /dev/null +++ b/perl-lib/PgCommitFest/Patch.pm @@ -0,0 +1,147 @@ +package PgCommitFest::Patch; +use strict; +use warnings; + +sub delete { + my ($r) = @_; + $r->authenticate('require_login' => 1); + $r->set_title('Delete Patch'); + my $d; + eval { + $d = $r->db->select_one(<<EOM, $r->cgi_required_id); +DELETE FROM patch AS p + USING commitfest_topic t +WHERE p.commitfest_topic_id = t.id AND p.id = ? RETURNING t.commitfest_id +EOM + }; + my $err = $@; + if (! $err) { + $r->error_exit('Patch not found.') if !defined $d; + $r->db->commit; + $r->redirect('/action/commitfest_view?id=' . $d->{'commitfest_id'}); + } + if ($err =~ /patch_comment_patch_id_fkey/) { + $r->error_exit(<<EOM); +Because this patch has one or more comments, it may not be deleted. +EOM + } + $r->error_exit("Internal error: $@"); +} + +sub form { + my ($r) = @_; + my $aa = $r->authenticate('require_login' => 1); + + # Decide whether this is a new patch or an edit of an existing + # patch, and if editing reload data from database. + my $d; + my $id = $r->cgi_id(); + if (defined $id) { + $r->set_title('Edit Patch'); + $d = $r->db->select_one(<<EOM, $id); +SELECT id, commitfest_topic_id AS commitfest_topic, commitfest_id, name, + patch_status_id AS patch_status, author, reviewers, date_closed +FROM patch_view WHERE id = ? +EOM + $r->error_exit('Patch not found.') if !defined $d; + $r->redirect('/action/patch_view?id=' . $id) if $r->cgi('cancel'); + } + else { + $d = $r->db->select_one(<<EOM, $r->cgi_required_id('commitfest')); +SELECT id AS commitfest_id FROM commitfest WHERE id = ? +EOM + $r->error_exit('CommitFest topic not found.') if !defined $d; + $r->set_title('New Patch'); + $r->redirect('/action/commitfest_view?id=' . $d->{'commitfest_id'}) + if $r->cgi('cancel'); + } + + # Add controls. + $r->add_control('name', 'text', 'Name', 'required' => 1); + $r->add_control('commitfest_topic', 'select', 'CommitFest Topic', + 'required' => 1); + $r->control('commitfest_topic')->choice($r->db->select(<<EOM, +SELECT id, name FROM commitfest_topic WHERE commitfest_id = ? ORDER BY name +EOM + $d->{'commitfest_id'})); + $r->add_control('patch_status', 'select', 'Patch Status', 'required' => 1); + $r->control('patch_status')->choice($r->db->select(<<EOM)); +SELECT id, name FROM patch_status ORDER BY id +EOM + $r->add_control('author', 'text', 'Author', 'required' => 1); + $r->add_control('reviewers', 'text', 'Reviewers'); + $r->add_control('date_closed', 'date', 'Date Closed'); + if (!defined $id) { + $r->add_control('message_id', 'text', + 'Message-ID for Original Patch', 'required' => 1, + 'maxlength' => 255); + } + my %value = $r->initialize_controls($d); + + # Cross-field validation. + if ($r->cgi('go')) { + if (!defined $value{'date_closed'} + && grep { $_ eq $value{'patch_status_id'} } qw(4 5 6)) { + $value{'date_closed'} = \'now()::date'; + } + elsif (defined $value{'date_closed'} + && !grep { $_ eq $value{'patch_status_id'} } qw(4 5 6)) { + $r->error(<<EOM); +Date Closed is permitted only for patches with have been Committed, Returned +with Feedback, or Rejected. +EOM + } + } + + # Handle commit. + if ($r->cgi('go') && ! $r->is_error()) { + if (defined $id) { + $r->db->update('patch', { 'id' => $id }, \%value); + } + else { + my $message_id = $value{'message_id'}; + delete $value{'message_id'}; + $id = $r->db->insert_returning_id('patch', \%value); + $r->db->insert('patch_comment', { + 'patch_id' => $id, + 'patch_comment_type_id' => 2, + 'message_id' => $message_id, + 'content' => 'Initial version.', + 'creator' => $aa->{'username'}, + }); + } + $r->db->commit; + $r->redirect('/action/patch_view?id=' . $id); + } + + # Display template. + $r->render_template('patch_form', { 'id' => $id, 'd' => $d }); +} + +sub view { + my ($r) = @_; + my $id = $r->cgi_id(); + my $d = $r->db->select_one(<<EOM, $id) if defined $id; +SELECT id, name, commitfest_id, commitfest, commitfest_topic_id, + commitfest_topic, patch_status, author, reviewers, date_closed +FROM patch_view WHERE id = ? +EOM + $r->error_exit('Patch not found.') if !defined $d; + $r->set_title('Patch: %s', $d->{'name'}); + + my $patch_comment_list = $r->db->select(<<EOM, $d->{'id'}); +SELECT v.id, v.patch_comment_type, v.message_id, v.content, v.creator, +to_char(v.creation_time, 'MM/DD/YYYY HH:MI:SS AM') AS creation_time +FROM patch_comment_view v WHERE v.patch_id = ? ORDER BY v.creation_time +EOM + + $r->add_link('/action/patch_comment_form?patch=' . $id, + 'New Comment'); + $r->add_link('/action/patch_form?id=' . $id, 'Edit Patch'); + $r->add_link('/action/patch_delete?id=' . $id, 'Delete Patch', + 'Are you sure you want to delete this patch?'); + $r->render_template('patch_view', { 'd' => $d, 'patch_comment_list' + => $patch_comment_list }); +} + +1; diff --git a/perl-lib/PgCommitFest/PatchComment.pm b/perl-lib/PgCommitFest/PatchComment.pm new file mode 100644 index 0000000..6d7476b --- /dev/null +++ b/perl-lib/PgCommitFest/PatchComment.pm @@ -0,0 +1,78 @@ +package PgCommitFest::PatchComment; +use strict; +use warnings; + +sub delete { + my ($r) = @_; + $r->authenticate('require_login' => 1); + $r->set_title('Delete Patch Comment'); + my $d; + eval { + $d = $r->db->select_one(<<EOM, $r->cgi_required_id); +DELETE FROM patch_comment WHERE id = ? RETURNING patch_id +EOM + }; + my $err = $@; + if (! $err) { + $r->error_exit('Patch not found.') if !defined $d; + $r->db->commit; + $r->redirect('/action/patch_view?id=' . $d->{'patch_id'}); + } + $r->error_exit("Internal error: $@"); +} + +sub form { + my ($r) = @_; + my $aa = $r->authenticate('require_login' => 1); + + # Decide whether this is a new comment or an edit of an existing + # comment, and if editing reload data from database. + my $d; + my $id = $r->cgi_id(); + if (defined $id) { + $r->set_title('Edit Patch Comment'); + $d = $r->db->select_one(<<EOM, $id); +SELECT id, patch_id, patch_comment_type_id AS patch_comment_type, message_id, + content FROM patch_comment WHERE id = ? +EOM + $r->error_exit('Patch comment not found.') if !defined $d; + } + else { + $d = $r->db->select_one(<<EOM, $r->cgi_required_id('patch')); +SELECT id AS patch_id FROM patch WHERE id = ? +EOM + $r->error_exit('Patch not found.') if !defined $d; + $r->set_title('New Patch Comment'); + } + $r->redirect('/action/patch_view?id=' . $d->{'patch_id'}) + if $r->cgi('cancel'); + + # Add controls. + $r->add_control('patch_comment_type', 'select', 'Comment Type', + 'required' => 1); + $r->control('patch_comment_type')->choice($r->db->select(<<EOM)); +SELECT id, name FROM patch_comment_type ORDER BY id +EOM + $r->add_control('message_id', 'text', 'Message-ID', 'maxlength' => 255); + $r->add_control('content', 'text', 'Content', 'required' => 1); + my %value = $r->initialize_controls($d); + + # Handle commit. + if ($r->cgi('go') && ! $r->is_error()) { + if (defined $id) { + $r->db->update('patch_comment', { 'id' => $id }, \%value); + } + else { + $value{'patch_id'} = $d->{'patch_id'}; + $value{'creator'} = $aa->{'username'}; + $id = $r->db->insert_returning_id('patch_comment', \%value); + } + $r->db->commit; + $r->redirect('/action/patch_view?id=' . $d->{'patch_id'}); + } + + # Display template. + $r->render_template('patch_comment_form', { 'id' => $id, 'd' => $d }); +} + +1; diff --git a/perl-lib/PgCommitFest/Request.pm b/perl-lib/PgCommitFest/Request.pm new file mode 100644 index 0000000..7f99b2e --- /dev/null +++ b/perl-lib/PgCommitFest/Request.pm @@ -0,0 +1,199 @@ +package PgCommitFest::Request; +require CGI::Fast; +require PgCommitFest::DB; +require PgCommitFest::WebControl; +require Template; +require Template::Plugin::HTML; +use strict; +use warnings; + +our $ROOT = '/home/rhaas/commitfest'; +our $template = Template->new({ 'INCLUDE_PATH' => $ROOT . '/template', + 'FILTERS' => { 'htmlsafe' => \&PgCommitFest::WebControl::escape } }); +$CGI::POST_MAX = 65536; +$CGI::DISABLE_UPLOADS = 1; # No need for uploads at present. + +sub new { + my ($class, $db) = @_; + my $cgi = CGI::Fast->new(); + return undef if !defined $cgi; + bless { + 'cgi' => $cgi, + 'control' => {}, + 'control_list' => [], + 'db' => $db, + 'error_list' => [], + 'header' => { + 'Content-type' => 'text/html', + 'Cache-Control' => 'no-cache', + 'Pragma' => 'no-cache', + }, + 'link' => [], + 'response_sent' => 0, + 'title' => '', + }, $class; +} + +sub add_link { + my ($self, $url, $name, $confirm) = @_; + push @{$self->{'link'}}, [ $url, $name, $confirm ]; +} + +sub add_control { + my ($self, $name, $type, $display_name, %args) = @_; + my $control = PgCommitFest::WebControl->new($name, $type, $display_name, + %args); + push @{$self->{'control_list'}}, $control; + $self->{'control'}{$name} = $control; +} + +sub authenticate { + my ($self, %option) = @_; + if (!defined $self->{'authenticate'} && defined $self->cookie('session')) { + $self->{'authenticate'} = + $self->db->select_one(<<EOM, $self->cookie('session')); +SELECT p.* FROM person p, session s WHERE p.username = s.username AND s.id = ? +EOM + } + if (!defined $self->{'authenticate'} && $option{'require_login'}) { + if ($ENV{'REQUEST_METHOD'} eq 'GET') { + my $uri = $ENV{'REQUEST_URI'}; + $uri =~ s/[^A-Za-z0-9]/sprintf "%%%x", ord($&)/ge; + $self->redirect('/action/login?uri=' . $uri); + } + $self->redirect('/action/login'); + } + return $self->{'authenticate'}; +} + +sub cgi { + my ($self, $n) = @_; + return scalar($self->{'cgi'}->param($n)); +} + +sub cgi_id { + my ($self, $n) = @_; + $n = 'id' if !defined $n; + my $v = $self->cgi($n); + if (defined $v && $v !~ /^\d+$/) { + $self->error_exit('Invalid parameter.'); + } + return $v; +} + +sub cgi_required_id { + my ($self, $n) = @_; + $n = 'id' if !defined $n; + my $v = $self->cgi($n); + if (!defined $v || $v !~ /^\d+$/) { + $self->error_exit('Invalid parameter.'); + } + return $v; +} + +sub cookie { + my ($self, $n) = @_; + return scalar($self->{'cgi'}->cookie($n)); +} + +sub control { + my ($self, $n) = @_; + return $self->{'control'}{$n}; +} + +sub db { + my ($self) = @_; + return $self->{'db'}; +} + +sub error { + my ($self, $fmt, @arg) = @_; + push @{$self->{'error_list'}}, sprintf($fmt, @arg); +} + +sub error_exit { + my ($self, $error) = @_; + $self->render_template('error', { 'error_list' => [ $error ] }); + die "DONE\n"; +} + +sub generate_headers { + my ($self) = @_; + my @header; + while (my ($header, $value) = each %{$self->{'header'}}) { + push @header, "$header: $value"; + } + return join("\r\n", @header) . "\r\n\r\n"; +} + +sub header { + my ($self, $header, $value) = @_; + $self->{'header'}{$header} = $value; +} + +sub is_error { + my ($self) = @_; + return (@{$self->{'error_list'}} != 0); +} + +sub initialize_controls { + my ($self, $default) = @_; + my %value; + # It's important that we process these controls in the same order that they + # were added, so that the user doesn't see the error messages in a funny + # order. + for my $control (@{$self->{'control_list'}}) { + my $n = $control->name; + $n .= '_id' if $control->istype('select'); + $value{$n} = $control->set($self, $default); + } + return %value; +} + +sub redirect { + my ($self, $url) = @_; + $self->header('Status', 302); + $self->header('Location', $url); + print $self->generate_headers(); + $self->{'response_sent'} = 1; + die "DONE\n"; +} + +sub render_template { + my ($self, $file, $vars) = @_; + + # Generate data. We generate the whole response before sending it, + # because an error at this stage is possible, but it's hard to do anything + # reasonable if we've already begun sending the response back to Apache. + my %stash; + my $content = $self->generate_headers(); + %stash = %$vars if defined $vars; + $stash{'control'} = $self->{'control'}; + $template->process('header.tt2', { + 'link' => $self->{'link'}, + 'title' => $self->{'title'}, + 'error_list' => $self->{'error_list'}, + 'script_name' => $ENV{'SCRIPT_NAME'}, + }, \$content) || die $template->error(); + $template->process($file . '.tt2', \%stash, \$content) + || die $template->error(); + $template->process('footer.tt2', {}, \$content) + || die $template->error(); + + # Send the results, unless we already did. + die 'response already sent' if $self->{'response_sent'}; + $self->{'response_sent'} = 1; + print $content; +} + +sub response_sent { + my ($self) = @_; + $self->{'response_sent'}; +} + +sub set_title { + my ($self, $fmt, @args) = @_; + $self->{'title'} = sprintf($fmt, @args); +} + +1; diff --git a/perl-lib/PgCommitFest/WebControl.pm b/perl-lib/PgCommitFest/WebControl.pm new file mode 100644 index 0000000..7f4e48f --- /dev/null +++ b/perl-lib/PgCommitFest/WebControl.pm @@ -0,0 +1,270 @@ +package PgCommitFest::WebControl; +require Date::Calc; +require Template::Plugin::HTML; +use strict; +use warnings; + +my %TYPE_ARGS = ( + '_user' => { + 'required' => qr/^(1|0)$/ + }, + 'date' => { + '_base' => 'text', + }, + 'hidden' => {}, + 'password' => { + '_base' => 'text', + }, + 'select' => { + 'value_tag' => qr/^\w+$/, + 'text_tag' => qr/^\w+$/, + '_base' => '_user', + }, + 'text' => { + 'size' => qr/^\d+$/, + 'maxlength' => qr/^\d+$/, + '_base' => '_user', + }, + 'textarea' => { + 'rows' => qr/^\d+$/, + 'cols' => qr/^\d+$/, + '_base' => '_user', + }, +); + +sub choice { + my ($self, $choice_list) = @_; + $self->{'value_key'} = 'id' if !defined $self->{'value_key'}; + $self->{'text_key'} = 'name' if !defined $self->{'text_key'}; + $self->{'choice'} = $choice_list; + return undef; +} + +sub db_value { + my ($self) = @_; + $self->{'db_value'}; +} + +sub display_name { + my ($self) = @_; + return $self->{'display_name'}; +} + +sub display_name_html { + my ($self) = @_; + my $html = escape($self->{'display_name'}); + if ($self->{'error'}) { + $html = "<span class='controlerror'>$html</span>"; + } + return $html; +} + +my %ESCAPE = ('&' => '&', '<' => '<', '>' => '>', '"' => '"', + '\'' => '''); +sub escape { + my ($text) = @_; + $text =~ s/[&<>"']/$ESCAPE{$&}/ge; + return $text; +} + +sub istype { + my ($self, $n) = @_; + return $self->{'istype'}{$n}; +} + +sub name { + my ($self) = @_; + return $self->{'name'}; +} + +sub new { + my ($class, $name, $type, $display_name, %args) = @_; + + # Initialize object and list of type memberships. + die "type $type not recognized" + if !defined $type || !defined $TYPE_ARGS{$type}; + my $self = bless { + 'db_value' => {}, + 'display_name' => $display_name, + 'error' => 0, + 'istype' => {}, + 'name' => $name, + 'type' => $type, + 'value' => '', + }, $class; + my $istype = $type; + do { + $self->{'istype'}{$istype} = 1; + $istype = $TYPE_ARGS{$istype}->{'_base'}; + } while (defined $istype); + + # Parse arguments. + while (my ($argkey, $argvalue) = each %args) { + my $ctype = $type; + my $validator; + while (1) { + $validator = $TYPE_ARGS{$ctype}->{$argkey}; + last if defined $validator; + $ctype = $TYPE_ARGS{$ctype}->{'_base'}; + last if !defined $ctype; + } + if (!defined $validator) { + die "control $name has bad key $argkey\n"; + } + elsif (!defined $argvalue) { + die "control $name, key $argkey has undefined argument value\n"; + } + elsif ($argvalue !~ /$validator/) { + die "control $name, key $argkey has bad value $argvalue\n"; + } + else { + $self->{$argkey} = $argvalue; + } + } + return $self; +} + +sub render { + my ($self) = @_; + if ($self->{'istype'}{'text'}) { + return sprintf + "<input name='%s' type='%s' size='%d' maxlength='%d' value='%s'>", + $self->{'name'}, + $self->{'istype'}{'password'} ? 'password' : 'text', + defined $self->{'size'} ? $self->{'size'} + : ($self->{'istype'}{'date'} ? 10 : 40), + defined $self->{'maxlength'} ? $self->{'maxlength'} + : ($self->{'istype'}{'date'} ? 10 : 40), + $self->{'istype'}{'password'} ? '' : escape($self->{'value'}); + } + elsif ($self->{'istype'}{'textarea'}) { + return sprintf + "<textarea name='%s' rows='%d' cols='%d'>%s</textarea>", + $self->{'name'}, + defined $self->{'rows'} ? $self->{'text'} : 6, + defined $self->{'cols'} ? $self->{'cols'} : 80, + escape($self->{'value'}); + } + elsif ($self->{'istype'}{'hidden'}) { + return sprintf + "<input name='%s' type='hidden' value='%s'>", + $self->{'name'}, + escape($self->{'value'}); + } + elsif ($self->{'istype'}{'select'}) { + my @html = (sprintf "<select name='%s'>", $self->{'name'}); + die "control $self->{'name'} has not specified choice list" + if !defined $self->{'choice'}; + my $vk = $self->{'value_key'}; + my $tk = $self->{'text_key'}; + for my $choice (@{$self->{'choice'}}) { + push @html, sprintf "<option value='%s'%s>%s</option>", + escape($choice->{$vk}), + $choice->{$vk} eq $self->{'value'} ? " selected" : "", + escape($choice->{$tk}); + } + push @html, "</select>"; + return join('', @html); + } + else { + die "unable to render control $self->{'name'}, type $self->{'type'}"; + } +} + +sub set { + my ($self, $r, $default) = @_; + my $error; + + # If the user is submitting the form (as indicated by the presence of the + # CGI parameter "go") or if the CGI parameter for this field is present + # in any case, we use that value. Otherwise, we fall back to the default + # provided by the user. If the default happens to be a reference, we + # assume it's a hash mapping control names to values. + my $value = + $r->cgi('go') || defined $r->cgi($self->{'name'}) ? + $r->cgi($self->{'name'}) + : ref $default ? $default->{$self->{'name'}} + : $default; + + # Basic sanitization of input. Text fields and text areas lose leading and + # trailing whitespace; text fields that are not text areas also have any + # string of whitespace characters smashed to a single space. Select + # controls can take only the values in the specified list. + if ($self->{'istype'}{'text'}) { + $value = '' if !defined $value; + $value =~ s/^\s+//; + $value =~ s/\s+$//; + $value =~ s/\s+/ /g; + } + elsif ($self->{'istype'}{'textarea'}) { + $value = '' if !defined $value; + $value =~ s/^\s+//; + $value =~ s/\s+$//; + } + elsif ($self->{'istype'}{'hidden'}) { + $value = '' if !defined $value; + } + elsif ($self->{'istype'}{'select'}) { + die "control $self->{'name'} has not specified choice list" + if !defined $self->{'choice'}; + my $found = 0; + my $vk = $self->{'value_key'}; + if (defined $value) { + for my $choice (@{$self->{'choice'}}) { + if (defined $choice->{$vk} && $choice->{$vk} eq $value) { + $found = 1; + last; + } + } + } + if (! $found) { + $value = @{$self->{'choice'}} + && defined $self->{'choice'}[0]{$vk} ? + $self->{'choice'}[0]{$vk} : ''; + } + } + my $db_value = $value; + + # If the field is required, complain if the value is the empty string + # (unless "go" is not set, which means this is the initial form display + # and the user hasn't submitted it yet). + if ($self->{'required'} && $value eq '' && $r->cgi('go')) { + $error = 'is a required field.'; + } + + # If the field is a date, complain if it doesn't look like a valid date. + if ($self->{'istype'}{'date'} && $value ne '' && $r->cgi('go')) { + my $ok = 0; + if ($value =~ /^(\d{4})-(\d{1,2})-(\d{1,2})$/) { + my ($yy, $mm, $dd) = ($1, $2, $3); + if ($yy > 2000 && $mm >= 1 && $mm <= 12 && + $dd >= 1 && $dd <= Date::Calc::Days_in_Month($yy, $mm)) { + $ok = 1; + } + } + $error = 'is not a valid date (use YYYY-MM-DD format).' if ! $ok; + } + + # We store NULL for empty dates. + if ($self->{'istype'}{'date'} && $value eq '') { + $db_value = undef; + } + + # If any error was encountered, post it. + if (defined $error) { + $r->error($self->{'display_name'} . ' ' . $error); + $self->{'error'} = 1; + } + + # Save and return the value. + $self->{'value'} = $value; + $self->{'db_value'} = $db_value; + return $db_value; +} + +sub value { + my ($self) = @_; + $self->{'value'}; +} + +1; diff --git a/template/404.tt2 b/template/404.tt2 new file mode 100644 index 0000000..9597786 --- /dev/null +++ b/template/404.tt2 @@ -0,0 +1 @@ +<p>The page you requested was not found.</p> diff --git a/template/base.tt2 b/template/base.tt2 new file mode 100644 index 0000000..e04bbae --- /dev/null +++ b/template/base.tt2 @@ -0,0 +1,44 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" dir="ltr"> + <head> + <title>{%block title%}{%endblock%} - PostgreSQL Europe</title> + <meta http-equiv="Content-Type" content="text/xhtml; charset=utf-8" /> + <link rel="shortcut icon" href="/https/git.postgresql.org/favicon.ico" /> + <style type="text/css" media="screen" title="Normal Text">@import url("/https/git.postgresql.org/media/css/base.css");</style> + <style type="text/css" media="screen" title="Normal Text">@import url("/https/git.postgresql.org/media/css/geckofixes.css");</style> + <link rel="alternate" type="application/rss+xml" title="PostgreSQL Europe News" href="/https/git.postgresql.org/feeds/news/" /> + <link rel="alternate" type="application/rss+xml" title="PostgreSQL Europe Events" href="/https/git.postgresql.org/feeds/events/" /> + </head> + <body> + <div id="pgContainerWrap"> + <div id="pgContainer"> + <div id="pgHeaderContainer"> + <div id="pgHeader"> + <div id="pgHeaderLogoLeft"> + <a href="http://www.postgresql.eu/"><img alt="PostgreSQL Europe" height="80" width="230" src="/https/git.postgresql.org/media/img/layout/hdr_left.png" /></a> + </div> + <div id="pgHeaderLogoRight"> + <a href="/"><img width="210" height="80" alt="PostgreSQL Europe" src="/https/git.postgresql.org/media/img/layout/hdr_right.png" /></a> + </div> + </div> <!-- pgHeader --> + + <div id="pgTopNav"> + <div id="pgTopNavLeft"><img width="7" height="23" alt="" src="/https/git.postgresql.org/media/img/layout/nav_lft.png" /></div> + <div id="pgTopNavRight"><img width="7" height="23" alt="" src="/https/git.postgresql.org/media/img/layout/nav_rgt.png" /></div> +<!-- + <div id="pgLoginlink">{%if user.is_authenticated%}<a href="/https/git.postgresql.org/logout" title="Log out">Log out</a>{%else%}<a href="/https/git.postgresql.org/login" title="Log in">Log in</a>{%endif%}</div> +--> + <ul id="pgTopNavList"> + <li><a href="/" title="Home">Home</a></li> + <li><a href="/https/git.postgresql.org/about" title="About">About</a></li> + <li><a href="/https/git.postgresql.org/events" title="Events">Events</a></li> + <li><a href="/https/git.postgresql.org/sponsors" title="Sponsors">Sponsors</a></li> + <li><a href="/https/git.postgresql.org/community" title="Community">Community</a></li> + <li><a href="/https/git.postgresql.org/donate" title="Donate">Donate</a></li> + <li><a href="/https/git.postgresql.org/merchandise" title="Merchandise">Merchandise</a></li> + </ul> + </div> <!-- pgTopNav --> + </div> <!-- pgHeaderContainer --> + <div id="pgContent"> +{%block layoutblock%}{%endblock%} diff --git a/template/commitfest_form.tt2 b/template/commitfest_form.tt2 new file mode 100644 index 0000000..6733561 --- /dev/null +++ b/template/commitfest_form.tt2 @@ -0,0 +1,19 @@ +<p></p> + +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr class='firstrow'> + <td class='colFirst'>[% control.name.display_name_html %]</td> + <td class='colLast'>[% control.name.render %]</td> +</tr> +<tr class='lastrow'> + <td class='colFirst'>[% control.commitfest_status.display_name_html %]</td> + <td class='colLast'>[% control.commitfest_status.render %]</td> +</tr> +</table> +</div> + +<div><input type='submit' value='Submit'> +<input name='cancel' type='submit' value='Cancel'> +<input name='go' type='hidden' value='1'> +[% IF id %]<input name='id' type='hidden' value='[% id %]'>[% END %]</div> diff --git a/template/commitfest_search.tt2 b/template/commitfest_search.tt2 new file mode 100644 index 0000000..ab34187 --- /dev/null +++ b/template/commitfest_search.tt2 @@ -0,0 +1,25 @@ +<p>If you want to submit a new patch for review, please visit the <b>Open</b> +CommitFest. If you want to help with the reviewing process for the +CommitFest that's currently taking place (if any), please visit the +CommitFest <b>In Progress</b>. Previous CommitFests are marked as +<b>Closed</b>, and scheduled upcoming CommitFests (other than the one to which +patches should be submitted) are marked as <b>Future</b>.</p> + +[% IF list.size == 0 %] +<p><b>No CommitFests have been defined yet.</b></p> +[% ELSE %] +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr> + <th class='colFirst'>CommitFest Name</th> + <th class='colLast'>Status</th> +</tr> +[% FOREACH x = list %] +<tr[% IF loop.last %] class='lastrow'[% END %]> + <td class='colFirst'><a href='/https/git.postgresql.org/action/commitfest_view?id=[% x.id %]'>[% x.name | htmlsafe %]</a></td> + <td class='colLast'>[% x.commitfest_status | htmlsafe %]</td> +</tr> +[% END %] +</table> +</div> +[% END %] diff --git a/template/commitfest_topic_form.tt2 b/template/commitfest_topic_form.tt2 new file mode 100644 index 0000000..0a5b937 --- /dev/null +++ b/template/commitfest_topic_form.tt2 @@ -0,0 +1,15 @@ +<p></p> + +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr class='firstrow lastrow'> + <td class='colFirst'>[% control.name.display_name_html %]</td> + <td class='colLast'>[% control.name.render %]</td> +</tr> +</table> +</div> + +<div><input type='submit' value='Submit'> +<input name='cancel' type='submit' value='Cancel'> +<input name='go' type='hidden' value='1'> +[% IF id %]<input name='id' type='hidden' value='[% id %]'>[% ELSE %]<input name='commitfest' type='hidden' value='[% d.commitfest_id %]'>[% END %]</div> diff --git a/template/commitfest_topic_search.tt2 b/template/commitfest_topic_search.tt2 new file mode 100644 index 0000000..bd82515 --- /dev/null +++ b/template/commitfest_topic_search.tt2 @@ -0,0 +1,20 @@ +<p></p> +[% IF topic_list.size %] +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr class='firstrow'> + <th class='colFirst'>Topic</th> + <th class='colLast'>Action</th> +</tr> +[% FOREACH t = topic_list %] +<tr[% IF loop.last %] class='lastrow'[% END %]> + <td class='colFirst'>[% t.name | htmlsafe %]</a></td> + <td class='colLast'><a href='/https/git.postgresql.org/action/commitfest_topic_form?id=[% t.id %]'>Edit Topic</a> - + <a href='/https/git.postgresql.org/action/commitfest_topic_delete?id=[% t.id %]' onClick='return confirm("Are you sure you want to delete this topic?");'>Delete Topic</a></td> +</tr> +[% END %] +</table> +</div> +[% ELSE %] +<div>No topics.</div> +[% END %] diff --git a/template/commitfest_view.tt2 b/template/commitfest_view.tt2 new file mode 100644 index 0000000..051838c --- /dev/null +++ b/template/commitfest_view.tt2 @@ -0,0 +1,41 @@ +<p>The most recent three comments for each patch will be displayed below. To +view all the comments for a particular patch, or to add a comment or make other +changes, click on the patch name.</p> + +[% FOREACH g = patch_grouping %] +<h2>[% g.name | htmlsafe %]</h2> + +[% IF g.patch_list.size %] +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey' style='width: 100%'> +<tr class='firstrow'> + <th class='colFirst' style='width: 60%'>Patch Name</th> + <th>Topic</th> + <th>Status</th> + <th>Author</th> + <th>Reviewers</th> + <th class='colLast'>[% IF g.closed %]Date Closed[% ELSE %]Last Activity[% END %]</th> +</tr> +[% FOREACH p = g.patch_list %] +<tr[% IF loop.last %] class='lastrow'[% END %]> + <td class='colFirstT'><a href='/https/git.postgresql.org/action/patch_view?id=[% p.id %]'>[% p.name | htmlsafe %]</a> + <div style='padding-left: 10px'> + [% FOREACH c = p.comment_list %] + <div>[% IF c.message_id != '' %]<a href='http://archives.postgresql.org/message-id/[% c.message_id | htmlsafe %]'>[% END %][% c.patch_comment_type | htmlsafe %][% IF c.message_id != '' %]</a>[% END %]: [% c.content | html %] ([% c.creator | html %]: [% c.creation_time | html %])</div> + [% END %] + </div> + </td> + <td class='colMidT'>[% p.commitfest_topic | htmlsafe %]</td> + <td class='colMidT'>[% p.patch_status | htmlsafe %]</td> + <td class='colMidT'>[% p.author | htmlsafe %]</a></td> + <td class='colMidT'>[% IF p.reviewers != '' %][% p.reviewers %][% ELSE %]<span class='controlerror'>Nobody</span>[% END %]</a></td> + <td class='colLastT'>[% IF g.closed %][% IF p.date_closed.defined %][% p.date_closed %][% ELSE %](None)[% END %][% ELSE %][% IF p.comment_list.defined && p.comment_list.-1.defined %][% p.comment_list.-1.creation_time %][% ELSE %](None)[% END %][% END %]</a></td> +</tr> +[% END %] +</table> +</div> +[% ELSE %] +<div>No patches.</div> +[% END %] + +[% END %] diff --git a/template/error.tt2 b/template/error.tt2 new file mode 100644 index 0000000..6e538fa --- /dev/null +++ b/template/error.tt2 @@ -0,0 +1,3 @@ +[% FOREACH one_error_in_list = error_list %] +<p class='error'>[% one_error_in_list | htmlsafe | html_line_break %]</p> +[% END %] diff --git a/template/footer.tt2 b/template/footer.tt2 new file mode 100644 index 0000000..3f4aad3 --- /dev/null +++ b/template/footer.tt2 @@ -0,0 +1,4 @@ + </form> + </div> +</body> +</html> diff --git a/template/header.tt2 b/template/header.tt2 new file mode 100644 index 0000000..aee22b4 --- /dev/null +++ b/template/header.tt2 @@ -0,0 +1,24 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" dir="ltr"> +<head> + <title>PostgreSQL CommitFest Management[% IF title != '' %]: [% title %][% END %]</title> + <style type="text/css" media="screen" title="Normal Text">@import url("/https/git.postgresql.org/layout/css/blue/commitfest.css");</style> + <script type="text/javascript" src="/https/git.postgresql.org/layout/js/geckostyle.js"></script> +</head> +<body> +<div id="commitfestHeader"> + <div id="commitfestHeaderLogo"> + <a href="/" title="PostgreSQL"><img src="/https/git.postgresql.org/layout/images/docs/hdr_logo.png" width="206" height="80" alt="PostgreSQL" /></a> + </div> +</div> + +<div id="commitfestContent"> + <table cellspacing='0' cellpadding='0' border='0' width='100%'> + <tr> + <td><h1>[% title %]</h1></td> + [% IF link.size != 0 %]<td style='text-align: right; padding-left: 10px'>[% FOREACH l = link %]<a href='[% l.0 %]'[% IF l.2.defined %] onClick='return confirm("[% l.2 | htmlsafe %]")'[% END %]>[% l.1 | html %]</a>[% IF !loop.last %] - [% END %][% END %]</td>[% END %] + </tr> + </table> + <form name='f' method='post' action='[% script_name %]'> +[% INCLUDE error.tt2 %] diff --git a/template/index.tt2 b/template/index.tt2 new file mode 100644 index 0000000..c9dcba4 --- /dev/null +++ b/template/index.tt2 @@ -0,0 +1,4 @@ +<p>This is a test of the emergency broadcast system. The broadcasters of your +area, in voluntary cooperation with federal, state, and local authorities, +have developed this system to keep you informed in the event of an emergency. +</p> diff --git a/template/login.tt2 b/template/login.tt2 new file mode 100644 index 0000000..e99bb8c --- /dev/null +++ b/template/login.tt2 @@ -0,0 +1,19 @@ +<p>Please log in using your username and password.</p> + +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr class='firstrow'> + <td class='colFirst'>[% control.username.display_name | htmlsafe %]</td> + <td class='colLast'>[% control.username.render %]</td> +</tr> +<tr class='lastrow'> + <td class='colFirst'>[% control.password.display_name | htmlsafe %]</td> + <td class='colLast'>[% control.password.render %]</td> +</tr> +</table> +</div> + +<div><input type='submit' value='Submit'> +<input name='cancel' type='submit' value='Cancel'> +<input name='go' type='hidden' value='1'> +[% control.uri.render %]</div> diff --git a/template/patch_comment_form.tt2 b/template/patch_comment_form.tt2 new file mode 100644 index 0000000..a8d1641 --- /dev/null +++ b/template/patch_comment_form.tt2 @@ -0,0 +1,23 @@ +<p></p> + +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr class='firstrow'> + <td class='colFirst'>[% control.patch_comment_type.display_name_html %]</td> + <td class='colLast'>[% control.patch_comment_type.render %]</td> +</tr> +<tr> + <td class='colFirst'>[% control.message_id.display_name_html %]</td> + <td class='colLast'>[% control.message_id.render %]</td> +</tr> +<tr class='lastrow'> + <td class='colFirst'>[% control.content.display_name_html %]</td> + <td class='colLast'>[% control.content.render %]</td> +</tr> +</table> +</div> + +<div><input type='submit' value='Submit'> +<input name='cancel' type='submit' value='Cancel'> +<input name='go' type='hidden' value='1'> +[% IF id %]<input name='id' type='hidden' value='[% id %]'>[% ELSE %]<input name='patch' type='hidden' value='[% d.patch_id %]'>[% END %]</div> diff --git a/template/patch_form.tt2 b/template/patch_form.tt2 new file mode 100644 index 0000000..cb03777 --- /dev/null +++ b/template/patch_form.tt2 @@ -0,0 +1,41 @@ +<p></p> + +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr class='firstrow'> + <td class='colFirst'>[% control.name.display_name_html %]</td> + <td class='colLast'>[% control.name.render %]</td> +</tr> +<tr> + <td class='colFirst'>[% control.commitfest_topic.display_name_html %]</td> + <td class='colLast'>[% control.commitfest_topic.render %]</td> +</tr> +<tr> + <td class='colFirst'>[% control.patch_status.display_name_html %]</td> + <td class='colLast'>[% control.patch_status.render %]</td> +</tr> +<tr> + <td class='colFirst'>[% control.author.display_name_html %]</td> + <td class='colLast'>[% control.author.render %]</td> +</tr> +<tr> + <td class='colFirst'>[% control.reviewers.display_name_html %]</td> + <td class='colLast'>[% control.reviewers.render %]</td> +</tr> +<tr[% IF id %] class='lastrow'[% END %]> + <td class='colFirst'>[% control.date_closed.display_name_html %]</td> + <td class='colLast'>[% control.date_closed.render %] (YYYY-MM-DD)</td> +</tr> +[% IF !id %] +<tr class='lastrow'> + <td class='colFirst'>[% control.message_id.display_name_html %]</td> + <td class='colLast'>[% control.message_id.render %]</td> +</tr> +[% END %] +</table> +</div> + +<div><input type='submit' value='Submit'> +<input name='cancel' type='submit' value='Cancel'> +<input name='go' type='hidden' value='1'> +[% IF id %]<input name='id' type='hidden' value='[% id %]'>[% ELSE %]<input name='commitfest' type='hidden' value='[% d.commitfest_id %]'>[% END %]</div> diff --git a/template/patch_view.tt2 b/template/patch_view.tt2 new file mode 100644 index 0000000..6e586ec --- /dev/null +++ b/template/patch_view.tt2 @@ -0,0 +1,39 @@ +<p></p> + +<div class='tblBasic'> +<table cellspacing='0' class='tblBasicGrey'> +<tr class='firstrow'> + <td class='colFirst'>CommitFest</td> + <td class='colLast'><a href='/https/git.postgresql.org/action/commitfest_view?id=[% d.commitfest_id %]'>[% d.commitfest | htmlsafe %]</a></td> +</tr> +<tr> + <td class='colFirst'>Topic</td> + <td class='colLast'>[% d.commitfest_topic | htmlsafe %]</td> +</tr> +<tr> + <td class='colFirst'>Patch Status</td> + <td class='colLast'>[% d.patch_status | htmlsafe %]</td> +</tr> +<tr> + <td class='colFirst'>Author</td> + <td class='colLast'>[% d.author | htmlsafe %]</td> +</tr> +<tr> + <td class='colFirst'>Reviewers</td> + <td class='colLast'>[% IF d.reviewers != '' %][% d.reviewers | htmlsafe %][% ELSE %]<span class='controlerror'>Nobody</span>[% END %]</td> +</tr> +<tr> + <td class='colFirst'>Close Date</td> + <td class='colLast'>[% IF d.date_closed != '' %][% d.date_closed | htmlsafe %][% ELSE %](None)[% END %]</td> +</tr> +<tr class='lastrow'> + <td class='colFirstT'>Comments</td> + <td class='colLastT'> + [% FOREACH c = patch_comment_list %] + <div>[% IF c.message_id != '' %]<a href='http://archives.postgresql.org/message-id/[% c.message_id | htmlsafe %]'>[% END %][% c.patch_comment_type | htmlsafe %][% IF c.message_id != '' %]</a>[% END %]: [% c.content | html %] ([% c.creator | html %]: [% c.creation_time | html %]) - <a href='/https/git.postgresql.org/action/patch_comment_form?id=[% c.id %]'>Edit</a> - <a href='/https/git.postgresql.org/action/patch_comment_delete?id=[% c.id %]' onClick='return confirm("Are you sure you want to delete this comment?");'>Delete</a></div> + [% END %] + [% IF patch_comment_list.size == 0 %]<div>No comments.</div>[% END %] +</td> +</tr> +</table> +</div> |