summaryrefslogtreecommitdiff
path: root/perl-lib/PgCommitFest/CommitFest.pm
blob: 049bd9991f393e70c9dd50340b2109f6dc5d3b18 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package PgCommitFest::CommitFest;
use strict;
use warnings;
use XML::RSS;

sub activity {
	my ($r, $extrapath) = @_;
	my $d = setup($r, $extrapath);
	my $title = sprintf 'CommitFest %s: Activity Log', $d->{'name'};
	my $rss = '/action/commitfest_activity.rss?id=' . $d->{'id'};
	$r->set_title($title);
	$r->set_rss_alternate($title, $rss);
	$r->add_link($rss, 'RSS');
	$r->add_link('/action/commitfest_view?id=' . $d->{'id'},
		'Back to CommitFest');
	my $activity = $r->db->select(<<EOM, $d->{'id'});
SELECT
	to_char(v.last_updated_time, 'YYYY-MM-DD HH24:MI:SS') AS last_updated_time,
	v.last_updater, v.patch_name, v.patch_id, v.activity_type, v.details
FROM
	commitfest_activity_log v
WHERE
	v.commitfest_id = ?
ORDER BY
	v.last_updated_time DESC
EOM
	$r->render_template('commitfest_activity', { 'd' => $d,
		'activity' => $activity });
}

sub activity_rss {
	my ($r, $extrapath) = @_;
	my $d = setup($r, $extrapath, 1);

	# Fetch most recent 50 updates for relevant CommitFest, or all CommitFests.
	my $sqlbit = '';
	if (defined $d) {
		$sqlbit = "WHERE v.commitfest_id = " . $d->{'id'};
	}
	my $activity = $r->db->select(<<EOM);
SELECT
	v.activity_id, to_char(v.last_updated_time at time zone 'GMT',
		'Dy, DD Mon YYYY HH24:MI:SS') || ' GMT' AS last_updated_time,
	v.last_updater, v.patch_name, v.patch_id, v.activity_type, v.details,
	commitfest.name as commitfest_name
FROM
	commitfest_activity_log v
INNER JOIN
	commitfest ON v.commitfest_id=commitfest.id
$sqlbit
ORDER BY
	v.last_updated_time DESC LIMIT 50
EOM

	# Construct RSS channel.
	my $rssname = defined $d ?
		sprintf('PostgreSQL CommitFest %s: Activity Log', $d->{'name'})
			: 'PostgreSQL CommitFest Activity Log';
	my $rss = XML::RSS->new('version' => '2.0');
	$rss->channel(
		'title'			=> $rssname,
		'link'			=> 'http://commitfest.postgresql.org',
		'language'		=> 'en',
		'description'	=> $rssname,
	);
	foreach my $row (@$activity) {
		my $content;
		$rss->add_item(
			'guid'	=> 'http://commitfest.postgresql.org/activity/' .
					$row->{'activity_id'},
			'title'	=> $row->{'commitfest_name'} . ': ' . $row->{'patch_name'},
			'pubDate' => $row->{'last_updated_time'},
			'description' => $r->eval_template('commitfest_activity_rss',
				{ 'd' => $row })
		);
	}

	print "Content-type: application/xml+rss\n\n";
	print $rss->as_string;
	$r->{'response_sent'} = 1;
}

sub delete {
	my ($r) = @_;
	$r->authenticate('require_login' => 1, 'require_administrator' => 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, 'require_administrator' => 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) = @_;
	my $aa = $r->authenticate();
	$r->set_title('CommitFest Index');
	$r->set_rss_alternate('PostgreSQL CommitFest Activity Log',
		'/action/commitfest_activity.rss');
	if (defined $aa && $aa->{'is_administrator'}) {
		$r->add_link('/action/commitfest_form', 'New CommitFest');
	}
	$r->add_link('/action/commitfest_activity.rss', 'RSS');
	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 setup {
	my ($r, $extrapath, $is_optional) = @_;

	# Target commitfest can be specified either by ID, or we allow special
	# magic to fetch it by 
	my $id = $r->cgi_id();
	my $sqlbit;
	if (defined $id) {
		$sqlbit = "WHERE id = " . $r->db->quote($id);
	}
	elsif (defined $extrapath) {
		if ($extrapath eq 'open') {
			$sqlbit =
				"WHERE commitfest_status_id = 2 ORDER BY name DESC LIMIT 1";
		}
		elsif ($extrapath eq 'inprogress') {
			$sqlbit = <<EOM;
WHERE commitfest_status_id IN (2, 3)
	ORDER BY commitfest_status_id DESC, name DESC LIMIT 1
EOM
		}
		elsif ($extrapath eq 'previous') {
			$sqlbit =
				"WHERE commitfest_status_id = 4 ORDER BY name DESC LIMIT 1";
		}
	}
	if (!defined $sqlbit) {
		return undef if $is_optional;
		$r->error_exit("Unable to identify target CommitFest.");
	}

	# Fetch target commitfest from database.
	my $d = $r->db->select_one(<<EOM);
SELECT id, name, commitfest_status FROM commitfest_view $sqlbit
EOM
	$r->error_exit('CommitFest not found.') if !defined $d;
	return $d;
}

sub view {
	my ($r, $extrapath) = @_;
	my $aa = $r->authenticate();
	my $d = setup($r, $extrapath);
	my $id = $d->{'id'};
	$r->set_title('CommitFest %s (%s)', $d->{'name'},
		$d->{'commitfest_status'});

	# Filtering.
	my $filter_sql = '';
	my @filter_text;
	my $status;
	if (defined $r->cgi_id('status')) {
		$status = $r->db->select_one(<<EOM, $r->cgi_id('status'));
SELECT id, name FROM patch_status WHERE id = ?
EOM
		$r->error_exit('Unknown status.') if !defined $status;
		$filter_sql .= sprintf(' AND patch_status_id = %s', $status->{'id'});
		push @filter_text,
			sprintf 'Filtering on status "%s".', $status->{'name'};
	}

	# Load list of patches.
	my $previous_topic;
	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 = ? $filter_sql
	ORDER BY date_closed, commitfest_topic_sortorder, commitfest_topic, id
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 {
			if (!defined $previous_topic
				|| $previous_topic ne $p->{'commitfest_topic'}) {
				push @{$patch_grouping{'p'}}, {
					'topic_header' => 1,
					'commitfest_topic' => $p->{'commitfest_topic'}
				};
				$previous_topic = $p->{'commitfest_topic'};
			}
			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, 'YYYY-MM-DD') 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;
	}

	# Load status counts.
	my $total_count = 0;
	my $status_count_list = $r->db->select(<<EOM, $d->{'id'});
SELECT patch_status_id, patch_status, sum(1) AS num_patches from patch_view
	WHERE commitfest_id = ? GROUP BY 1, 2 ORDER BY 1
EOM
	for my $scl (@$status_count_list) {
		$total_count += $scl->{'num_patches'};
	}

	# Add links.
	$r->add_link('/action/patch_form?commitfest=' . $id, 'New Patch');
	$r->add_link('/action/commitfest_activity?id=' . $id, 'Activity Log');
	$r->add_link('/action/commitfest_topic_search?id=' . $id,
		'CommitFest Topics');
	if (defined $aa && $aa->{'is_administrator'}) {
		$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?');
	}

	# Render template.
	$r->render_template('commitfest_view', {
		'd' => $d, 'filter_text' => \@filter_text,
		'status_count_list' => $status_count_list,
		'total_count' => $total_count,
		'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;