lfapi

view lfapi/main.js @ 14:fe484d4ca81f

Completed snapshot support for GET /interest, code formatting for GET /event
author bsw
date Fri Nov 04 20:39:19 2011 +0100 (2011-11-04)
parents 7708205ce4c5
children ef5f746f21ad
line source
1 var api_version = '0.2.0';
3 // creates a random string with the given length
4 function randomString(number_of_chars) {
5 var charset, rand, i, ret;
6 charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
7 random_string = '';
9 for (var i = 0; i < number_of_chars; i++) {
10 random_string += charset[parseInt(Math.random() * charset.length)]
11 }
12 return random_string;
13 }
15 var fields = require('./fields.js');
17 var general_params = require('./general_params.js');
19 var config = general_params.config;
20 exports.config = config;
22 var db = require('./db.js');
23 exports.db = db;
25 var selector = db.selector;
27 var nodemailer = require('nodemailer');
30 // check if current session has at least given access level, returns error to client if not.
31 // used by request handlers below
32 function requireAccessLevel(conn, req, res, access_level, callback) {
33 switch (access_level) {
34 case 'anonymous':
35 if (req.current_access_level == 'anonymous') { callback(); return; };
36 case 'pseudonym':
37 if (req.current_access_level == 'pseudonym') { callback(); return; };
38 case 'full':
39 if (req.current_access_level == 'full') { callback(); return; };
40 case 'member':
41 if (req.current_member_id) { callback(); return; };
42 default:
43 respond('json', conn, req, res, 'forbidden', { error: 'Access denied' });
44 }
45 };
47 // callback function, encoding result and sending it to the client
48 function respond(mode, conn, req, res, status, object, err) {
49 var http_status = 200;
50 var command;
52 if (status == 'ok') {
53 command = 'COMMIT';
54 } else {
55 command = 'ROLLBACK';
56 };
58 switch (status) {
59 case 'ok':
60 http_status = 200;
61 break;
62 case 'forbidden':
63 //http_status = 403;
64 break;
65 case 'notfound':
66 http_status = 404;
67 break;
68 case 'unprocessable':
69 //http_status = 422;
70 break;
71 case 'conflict':
72 //http_status = 409;
73 break;
74 };
76 var query;
77 if (mode == 'json' && ! err) query = 'SELECT null';
78 db.query(conn, req, res, query, function(result, conn) {
79 db.query(conn, req, res, command, function (result, conn) {
81 if (mode == 'json') {
82 if (! object) object = {};
83 } else if (mode == 'html') {
84 if (! object) object = 'no content';
85 if (err) object = "Error: " + err;
86 }
88 object.status = status;
89 object.error = err;
91 if (mode == 'json') {
92 var body = JSON.stringify(object);
93 var content_type = 'application/json; charset=UTF-8';
94 if (req.params && req.params.callback) {
95 body = req.params.callback + '(' + body + ')';
96 content_type = 'text/javascript; charset=UTF-8';
97 }
98 res.writeHead(
99 http_status,
100 {
101 'Content-Type': content_type,
102 //'Content-Length': body.length // TODO doesn't work in chrome with JSONP
103 }
104 );
105 res.end(body);
106 } else if (mode == 'html') {
107 var body = ['<html><head><title>lfapi</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><style>body { font-family: sans-serif; }</style></head><body>']
108 body.push(object)
109 body.push('</body></html>')
110 body = body.join('');
111 res.writeHead(
112 http_status,
113 {
114 'Content-Type': 'text/html; charset=UTF-8',
115 'Content-Length': body.length
116 }
117 );
118 res.end(body);
119 }
120 })
121 });
122 };
124 exports.respond = respond;
125 db.error_handler = respond;
127 // add requested related data for requests with include_* parameters
128 function addRelatedData(conn, req, res, result, includes) {
129 if (includes.length > 0) {
130 var include = includes.shift();
131 var class = include.class;
132 var objects = result[include.objects];
134 var query;
136 if (objects) {
137 var objects_exists = false;
138 var ids_hash = {};
139 if (typeof(objects) == 'array') {
140 if (objects.length > 0) {
141 objects_exists = true;
142 objects.forEach( function(object) {
143 if (object[class + "_id"]) {
144 ids_hash[object[class + "_id"]] = true;
145 };
146 });
147 }
148 } else {
149 for (var key in objects) {
150 objects_exists = true;
151 var object = objects[key];
152 if (object[class + "_id"]) {
153 ids_hash[object[class + "_id"]] = true;
154 };
155 };
156 };
158 if (objects_exists) {
159 var ids = [];
160 for (key in ids_hash) {
161 ids.push(key)
162 }
163 if (ids.length > 0) {
164 query = new selector.Selector();
165 query.from(class);
166 query.addWhere([class + '.id IN (??)', ids]);
167 fields.addObjectFields(query, class);
168 }
169 };
170 };
172 db.query(conn, req, res, query, function (result2, conn) {
173 // add result to main result, regarding correct pluralization
174 var tmp = {};
175 if (result2) {
176 result2.rows.forEach( function(row) {
177 tmp[row.id] = row;
178 });
179 };
181 if (class == 'policy') {
182 result['policies'] = tmp;
183 } else {
184 result[class + 's'] = tmp;
185 }
186 addRelatedData(conn, req, res, result, includes);
187 });
188 } else {
189 respond('json', conn, req, res, 'ok', result);
190 };
192 };
194 function lockMemberById(conn, req, res, member_id, callback) {
195 var query = new selector.Selector('member');
196 query.addField('NULL');
197 query.addWhere(['member.id = ?', member_id]);
198 query.forUpdate();
199 db.query(conn, req, res, query, callback);
200 };
202 function requireUnitPrivilege(conn, req, res, unit_id, callback) {
203 var query = new selector.Selector('privilege');
204 query.addField('NULL');
205 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
206 query.addWhere(['privilege.unit_id = ?', unit_id ]);
207 query.addWhere('privilege.voting_right');
208 query.forShareOf('privilege');
209 db.query(conn, req, res, query, function(result, conn) {
210 if (result.rows.length != 1) {
211 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for this unit.');
212 return;
213 }
214 callback();
215 });
216 };
218 function requireAreaPrivilege(conn, req, res, area_id, callback) {
219 var query = new selector.Selector('privilege');
220 query.join('area', null, 'area.unit_id = privilege.unit_id');
221 query.addField('NULL');
222 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
223 query.addWhere(['area.id = ?', area_id ]);
224 query.addWhere('privilege.voting_right');
225 query.forShareOf('privilege');
226 db.query(conn, req, res, query, function(result, conn) {
227 if (result.rows.length != 1) {
228 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for areas in this unit.');
229 return;
230 }
231 callback();
232 });
233 };
235 function requireIssuePrivilege(conn, req, res, issue_id, callback) {
236 var query = new selector.Selector('privilege');
237 query.join('area', null, 'area.unit_id = privilege.unit_id');
238 query.join('issue', null, 'issue.area_id = area.id');
239 query.addField('NULL');
240 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
241 query.addWhere(['issue.id = ?', issue_id ]);
242 query.addWhere('privilege.voting_right');
243 query.forShareOf('privilege');
244 db.query(conn, req, res, query, function(result, conn) {
245 if (result.rows.length != 1) {
246 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for issues in this unit.');
247 return;
248 }
249 callback();
250 });
251 };
253 function requireInitiativePrivilege(conn, req, res, initiative_id, callback) {
254 var query = new selector.Selector('privilege');
255 query.join('area', null, 'area.unit_id = privilege.unit_id');
256 query.join('issue', null, 'issue.area_id = area.id');
257 query.join('initiative', null, 'initiative.issue_id = issue.id');
258 query.addField('NULL');
259 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
260 query.addWhere(['initiative.id = ?', initiative_id ]);
261 query.addWhere('privilege.voting_right');
262 query.forShareOf('privilege');
263 db.query(conn, req, res, query, function(result, conn) {
264 if (result.rows.length != 1) {
265 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for initiatives in this unit.');
266 return;
267 }
268 callback();
269 });
270 };
272 function requireIssueState(conn, req, res, issue_id, required_states, callback) {
273 var query = new selector.Selector('issue');
274 query.addField('NULL');
275 query.addWhere(['issue.id = ?', issue_id]);
276 query.addWhere(['issue.state IN (??)', required_states]);
277 query.forUpdateOf('issue');
278 db.query(conn, req, res, query, function(result, conn) {
279 if (result.rows.length != 1) {
280 respond('json', conn, req, res, 'forbidden', null, 'Issue is in wrong state.');
281 return;
282 }
283 callback();
284 });
285 };
287 function requireIssueStateForInitiative(conn, req, res, initiative_id, required_states, callback) {
288 var query = new selector.Selector('issue');
289 query.join('initiative', null, 'initiative.issue_id = issue.id');
290 query.addField('NULL');
291 query.addWhere(['initiative.id = ?', initiative_id]);
292 query.addWhere(['issue.state IN (??)', required_states]);
293 query.forUpdateOf('issue');
294 db.query(conn, req, res, query, function(result, conn) {
295 if (result.rows.length != 1) {
296 respond('json', conn, req, res, 'forbidden', null, 'Issue is in wrong state.');
297 return;
298 }
299 callback();
300 });
301 }
303 function requireContingentLeft(conn, req, res, is_initiative, callback) {
304 var query = new selector.Selector('member_contingent_left');
305 query.addField('NULL');
306 query.addWhere(['member_contingent_left.member_id = ?', req.current_member_id]);
307 query.addWhere('member_contingent_left.text_entries_left >= 1');
308 if (is_initiative) {
309 query.addWhere('member_contingent_left.initiatives_left >= 1');
310 }
311 db.query(conn, req, res, query, function(result, conn) {
312 if (result.rows.length != 1) {
313 respond('json', conn, req, res, 'forbidden', null, 'Contingent empty.');
314 return;
315 }
316 callback();
317 });
318 }
320 // ==========================================================================
321 // GET methods
322 // ==========================================================================
325 exports.get = {
327 // startpage (html) for users
328 // currently used for implementing public alpha test
329 '/': function (conn, req, res, params) {
331 var html = [];
332 html.push('<h2>welcome to lfapi public developer alpha test</h2>');
333 html.push('<p>This service is provided for testing purposes and is <i><b>dedicated to developers interested in creating applications</b></i> based on LiquidFeedback.</p>');
334 html.push('<h2>how to use</h2>');
335 html.push('<p>The programming interface is described in the <a href="http://dev.liquidfeedback.org/trac/lf/wiki/API">LiquidFeedback API specification</a>.</p>')
336 html.push('<p>The current implementation status of lfapi is published at the <a href="http://dev.liquidfeedback.org/trac/lf/wiki/lfapi">LiquidFeedback API server</a> page in our Wiki.</p>');
337 html.push('<p><b><i>Neither the API specification nor the implementation of lfapi is finished yet.</i></b> This public test should enable developers to join the specification process of the programming interface and makes it possible to start creating applications.</p>');
338 html.push('<h2>questions and suggestions</h2>');
339 html.push('<p>Please use our <a href="http://dev.liquidfeedback.org/cgi-bin/mailman/listinfo/main">public mailing list</a> if you have any questions or suggestions.</p>');
340 html.push('<h2>developer registration</h2>');
341 html.push('<p>To register as developer and receive an account, please submit the following form. You\'ll receive an email with instructions to complete the registration process by verifying your email address.<br />');
342 html.push('<form action="register_test" method="POST">');
343 html.push('<label for="name">Your name:</label> <input type="text" id="name" name="name" /> &nbsp; &nbsp; ');
344 html.push('<label for="email">Email address:</label> <input type="text" id="email" name="email" /> &nbsp; &nbsp; ');
345 html.push('<label for="location">Location:</label> <select name="location" id="location"><option value="earth">Earth</option><option value="moon">Moon</option><option value="mars">Mars</option></select>');
346 html.push('<br />');
347 html.push('<br />');
348 html.push('<div style="border: 2px solid #c00000; background-color: #ffa0a0; padding: 1ex;">');
349 html.push('<b>WARNING:</b> All data you entered above and all data you enter later while using the system and all data you are submitting via the programming interface will be stored in the LiquidFeedback database and published. Every access to the system is subject of tracing and logging for development purposes.<br />Please notice, this is a <b>public alpha test dedicated to developers</b>: serious errors can happen, private data unintentionally published or even <a href="http://en.wikipedia.org/wiki/Grey_goo"> grey goo</a> can appear without further warning. Everything is <b>ON YOUR OWN RISK</b>!');
350 html.push('<br />');
351 html.push('<br />');
352 html.push('<input type="checkbox" name="understood" value="understood" /> I understand the previous warning and I understand that everything is on my own risk.<br />');
353 html.push('</div>');
354 html.push('<br />');
355 html.push('<input type="submit" value="Register account" />');
356 respond('html', null, req, res, 'ok', html.join(''));
357 },
359 // temporary method to implement public alpha test
360 '/register_test_confirm': function (conn, req, res, params) {
361 var secret = params.secret;
363 var query = new selector.Selector('member');
364 query.addField('member.id, member.notify_email_unconfirmed');
365 query.addWhere(['member.notify_email_secret = ?', secret]);
366 db.query(conn, req, res, query, function (result, conn) {
367 var member = result.rows[0];
368 if (member) {
369 var query = new selector.SQLUpdate('member');
370 query.addValues({
371 notify_email: member.notify_email_unconfirmed,
372 notify_email_secret: null,
373 notify_email_unconfirmed: null,
374 active: true,
375 activated: 'now',
376 active: true,
377 last_activity: 'now',
378 locked: false
379 });
380 query.addWhere(['id = ?', member.id]);
381 db.query(conn, req, res, query, function (err, result) {
382 respond('html', conn, req, res, 'ok', 'Account activated: ');
383 });
384 } else {
385 respond('html', conn, req, res, 'forbidden', 'Secret not valid or already used.');
386 }
387 })
388 },
390 '/info': function (conn, req, res, params) {
391 requireAccessLevel(conn, req, res, 'anonymous', function() {
392 var query = new selector.Selector();
393 query.from('"liquid_feedback_version"');
394 query.addField('"liquid_feedback_version".*');
395 db.query(conn, req, res, query, function (result, conn) {
396 var liquid_feedback_version = result.rows[0];
397 var query = new selector.Selector();
398 query.from('"system_setting"');
399 query.addField('"member_ttl"');
400 db.query(conn, req, res, query, function (result, conn) {
401 var member_ttl = null;
402 if (result.rows[0]) {
403 member_ttl = result.rows[0].member_ttl;
404 };
405 respond('json', conn, req, res, 'ok', {
406 core_version: liquid_feedback_version.string,
407 api_version: api_version,
408 current_access_level: req.current_member_id ? 'member' : req.current_access_level,
409 current_member_id: req.current_member_id,
410 member_ttl: member_ttl,
411 settings: config.settings
412 });
413 });
414 });
415 });
416 },
418 '/member_count': function (conn, req, res, params) {
419 requireAccessLevel(conn, req, res, 'anonymous', function() {
420 var query = new selector.Selector();
421 query.from('"member_count"');
422 query.addField('"member_count".*');
423 db.query(conn, req, res, query, function (result, conn) {
424 var member_count = result.rows[0];
425 respond('json', conn, req, res, 'ok', {
426 total_count: member_count.total_count,
427 calculated: member_count.calculated
428 });
429 });
430 });
431 },
433 '/contingent': function (conn, req, res, params) {
434 requireAccessLevel(conn, req, res, 'anonymous', function() {
435 var query = new selector.Selector();
436 query.from('"contingent"');
437 query.addField('"contingent".*');
438 db.query(conn, req, res, query, function (result, conn) {
439 respond('json', conn, req, res, 'ok', { result: result.rows });
440 });
441 });
442 },
444 '/contingent_left': function (conn, req, res, params) {
445 requireAccessLevel(conn, req, res, 'member', function() {
446 var query = new selector.Selector();
447 query.from('"member_contingent_left"');
448 query.addField('"member_contingent_left".text_entries_left');
449 query.addField('"member_contingent_left".initiatives_left');
450 query.addWhere(['member_id = ?', req.current_member_id]);
451 db.query(conn, req, res, query, function (result, conn) {
452 respond('json', conn, req, res, 'ok', { result: result.rows[0] });
453 });
454 });
455 },
457 '/member': function (conn, req, res, params) {
458 requireAccessLevel(conn, req, res, 'pseudonym', function() {
459 var query = new selector.Selector();
460 query.from('"member"');
461 if (req.current_access_level == 'pseudonym' && !req.current_member_id ) {
462 fields.addObjectFields(query, 'member', 'member_pseudonym');
463 } else {
464 fields.addObjectFields(query, 'member');
465 }
466 general_params.addMemberOptions(req, query, params);
467 query.addOrderBy('"member"."id"');
468 general_params.addLimitAndOffset(query, params);
469 db.query(conn, req, res, query, function (result, conn) {
470 respond('json', conn, req, res, 'ok', { result: result.rows });
471 });
472 });
473 },
475 '/member_history': function (conn, req, res, params) {
476 requireAccessLevel(conn, req, res, 'full', function() {
477 var query = new selector.Selector();
478 query.from('"member_history" JOIN "member" ON "member"."id" = "member_history"."member_id"');
479 query.addField('"member_history".*');
480 general_params.addMemberOptions(req, query, params);
481 query.addOrderBy('member_history.id');
482 general_params.addLimitAndOffset(query, params);
483 db.query(conn, req, res, query, function (member_history_result, conn) {
484 var result = { result: member_history_result.rows }
485 includes = [];
486 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
487 addRelatedData(conn, req, res, result, includes);
488 });
489 });
490 },
492 '/member_image': function (conn, req, res, params) {
493 requireAccessLevel(conn, req, res, 'full', function() {
494 var query = new selector.Selector();
495 query.from('"member_image" JOIN "member" ON "member"."id" = "member_image"."member_id"');
496 query.addField('"member_image".*');
497 query.addWhere('member_image.scaled');
498 general_params.addMemberOptions(req, query, params);
499 query.addOrderBy = ['member_image.member_id, member_image.image_type'];
500 db.query(conn, req, res, query, function (result, conn) {
501 respond('json', conn, req, res, 'ok', { result: result.rows });
502 });
503 });
504 },
506 '/contact': function (conn, req, res, params) {
507 requireAccessLevel(conn, req, res, 'pseudonym', function() {
508 var query = new selector.Selector();
509 query.from('contact JOIN member ON member.id = contact.member_id');
510 query.addField('"contact".*');
511 if (req.current_member_id) {
512 // public or own for members
513 query.addWhere(['"contact"."public" OR "contact"."member_id" = ?', req.current_member_id]);
514 } else {
515 // public for everybody
516 query.addWhere('"contact"."public"');
517 }
518 general_params.addMemberOptions(req, query, params);
519 query.addOrderBy('"contact"."id"');
520 general_params.addLimitAndOffset(query, params);
521 db.query(conn, req, res, query, function (result, conn) {
522 respond('json', conn, req, res, 'ok', { result: result.rows });
523 });
524 });
525 },
527 '/privilege': function (conn, req, res, params) {
528 requireAccessLevel(conn, req, res, 'pseudonym', function() {
529 var query = new selector.Selector();
530 query.from('privilege JOIN member ON member.id = privilege.member_id JOIN unit ON unit.id = privilege.unit_id');
531 query.addField('privilege.*');
532 general_params.addUnitOptions(req, query, params);
533 general_params.addMemberOptions(req, query, params);
534 query.addOrderBy('privilege.unit_id, privilege.member_id');
535 general_params.addLimitAndOffset(query, params);
536 db.query(conn, req, res, query, function (privilege_result, conn) {
537 var result = { result: privilege_result.rows }
538 includes = [];
539 if (params.include_units) includes.push({ class: 'unit', objects: 'result'});
540 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
541 addRelatedData(conn, req, res, result, includes);
542 });
543 });
544 },
546 '/policy': function (conn, req, res, params) {
547 requireAccessLevel(conn, req, res, 'anonymous', function() {
548 var query = new selector.Selector();
549 query.from('"policy"');
550 query.addField('"policy".*');
551 general_params.addPolicyOptions(req, query, params);
552 query.addOrderBy('"policy"."index"');
553 general_params.addLimitAndOffset(query, params);
554 db.query(conn, req, res, query, function (result, conn) {
555 respond('json', conn, req, res, 'ok', { result: result.rows });
556 });
557 });
558 },
560 '/unit': function (conn, req, res, params) {
561 requireAccessLevel(conn, req, res, 'anonymous', function() {
562 var query = new selector.Selector();
563 query.from('"unit"');
564 fields.addObjectFields(query, 'unit');
565 general_params.addUnitOptions(req, query, params);
566 query.addOrderBy('unit.id');
567 general_params.addLimitAndOffset(query, params);
568 db.query(conn, req, res, query, function (result, conn) {
569 respond('json', conn, req, res, 'ok', { result: result.rows });
570 });
571 });
572 },
574 '/area': function (conn, req, res, params) {
575 requireAccessLevel(conn, req, res, 'anonymous', function() {
576 var query = new selector.Selector();
577 query.from('area JOIN unit ON area.unit_id = unit.id');
578 fields.addObjectFields(query, 'area');
579 general_params.addAreaOptions(req, query, params);
580 query.addOrderBy('area.id');
581 general_params.addLimitAndOffset(query, params);
582 db.query(conn, req, res, query, function (area_result, conn) {
583 var result = { result: area_result.rows }
584 includes = [];
585 if (params.include_units) includes.push({ class: 'unit', objects: 'result'});
586 addRelatedData(conn, req, res, result, includes);
587 });
588 });
589 },
591 '/allowed_policy': function (conn, req, res, params) {
592 requireAccessLevel(conn, req, res, 'anonymous', function() {
593 var query = new selector.Selector();
594 query.from('allowed_policy');
595 query.join('area', null, 'area.id = allowed_policy.area_id');
596 query.join('unit', null, 'unit.id = area.unit_id');
597 query.addField('allowed_policy.*');
598 general_params.addAreaOptions(req, query, params);
599 query.addOrderBy('allowed_policy.area_id, allowed_policy.policy_id');
600 general_params.addLimitAndOffset(query, params);
601 db.query(conn, req, res, query, function (allowed_policy_result, conn) {
602 var result = { result: allowed_policy_result.rows }
603 includes = [];
604 if (params.include_policies) includes.push({ class: 'policy', objects: 'result'});
605 if (params.include_areas) includes.push({ class: 'area', objects: 'result'});
606 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
607 addRelatedData(conn, req, res, result, includes);
608 });
609 }); },
611 '/membership': function (conn, req, res, params) {
612 requireAccessLevel(conn, req, res, 'pseudonym', function() {
613 var query = new selector.Selector();
614 query.from('membership JOIN member ON membership.member_id = member.id JOIN area ON area.id = membership.area_id JOIN unit ON unit.id = area.unit_id');
615 query.addField('membership.*');
616 general_params.addAreaOptions(req, query, params);
617 general_params.addMemberOptions(req, query, params);
618 query.addOrderBy('membership.area_id, membership.member_id');
619 general_params.addLimitAndOffset(query, params);
620 db.query(conn, req, res, query, function (membership_result, conn) {
621 var result = { result: membership_result.rows }
622 includes = [];
623 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
624 if (params.include_areas) includes.push({ class: 'area', objects: 'result'});
625 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
626 addRelatedData(conn, req, res, result, includes);
627 });
628 });
629 },
631 '/issue': function (conn, req, res, params) {
632 requireAccessLevel(conn, req, res, 'anonymous', function() {
633 var query = new selector.Selector()
634 query.from('issue JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
635 fields.addObjectFields(query, 'issue');
636 general_params.addIssueOptions(req, query, params);
637 query.addOrderBy('issue.id');
638 general_params.addLimitAndOffset(query, params);
639 db.query(conn, req, res, query, function (issue_result, conn) {
640 var result = { result: issue_result.rows }
641 includes = [];
642 if (params.include_areas) includes.push({ class: 'area', objects: 'result'});
643 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
644 if (params.include_policies) includes.push({ class: 'policy', objects: 'result' });
645 addRelatedData(conn, req, res, result, includes);
646 });
647 });
648 },
650 '/interest': function (conn, req, res, params) {
651 requireAccessLevel(conn, req, res, 'pseudonym', function() {
652 var query = new selector.Selector();
653 switch (params.snapshot) {
654 case 'latest':
655 query.from('direct_interest_snapshot', 'interest');
656 query.addWhere('interest.event = issue.latest_snapshot_event');
657 break;
659 case 'end_of_admission':
660 case 'half_freeze':
661 case 'full_freeze':
662 query.from('direct_interest_snapshot', 'interest');
663 query.addWhere('interest.event = ?', params.snapshot);
664 break;
666 case undefined:
667 query.from('interest');
668 break;
670 default:
671 respond('json', conn, req, res, 'unprocessable', { error: 'Invalid snapshot type' });
672 return;
674 };
675 query.addField('interest.*');
676 query.join('member', null, 'member.id = interest.member_id');
677 query.join('issue', null, 'interest.issue_id = issue.id');
678 query.join('policy', null, 'policy.id = issue.policy_id');
679 query.join('area', null, 'area.id = issue.area_id');
680 query.join('unit', null, 'area.unit_id = unit.id');
681 general_params.addMemberOptions(req, query, params);
682 general_params.addIssueOptions(req, query, params);
683 query.addOrderBy('interest.issue_id, interest.member_id');
684 general_params.addLimitAndOffset(query, params);
685 db.query(conn, req, res, query, function (interest_result, conn) {
686 var result = { result: interest_result.rows }
687 includes = [];
688 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
689 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
690 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
691 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
692 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
693 addRelatedData(conn, req, res, result, includes);
694 });
695 });
696 },
698 '/issue_comment': function (conn, req, res, params) {
699 requireAccessLevel(conn, req, res, 'pseudonym', function() {
700 var query = new selector.Selector();
701 query.from('issue_comment JOIN member ON member.id = issue_comment.member_id JOIN issue on issue_comment.issue_id = issue.id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
702 query.addField('issue_comment.*');
703 general_params.addMemberOptions(req, query, params);
704 general_params.addIssueOptions(req, query, params);
705 query.addOrderBy('issue_comment.issue_id, issue_comment.member_id');
706 general_params.addLimitAndOffset(query, params);
707 db.query(conn, req, res, query, function (issue_comment_result, conn) {
708 var result = { result: issue_comment_result.rows }
709 includes = [];
710 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
711 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
712 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
713 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
714 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
715 addRelatedData(conn, req, res, result, includes);
716 });
717 });
718 },
720 '/initiative': function (conn, req, res, params) {
721 requireAccessLevel(conn, req, res, 'anonymous', function() {
722 var query = new selector.Selector();
723 query.from('initiative JOIN issue ON issue.id = initiative.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
724 fields.addObjectFields(query, 'initiative');
725 query.addOrderBy('initiative.id');
726 general_params.addInitiativeOptions(req, query, params);
727 general_params.addLimitAndOffset(query, params);
728 db.query(conn, req, res, query, function (initiative_result, conn) {
729 var result = { result: initiative_result.rows }
730 includes = [];
731 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
732 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
733 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
734 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
735 addRelatedData(conn, req, res, result, includes);
736 });
737 });
738 },
740 '/initiator': function (conn, req, res, params) {
741 requireAccessLevel(conn, req, res, 'pseudonym', function() {
742 var fields = ['initiator.initiative_id', 'initiator.member_id'];
743 var query = new selector.Selector();
744 query.from('initiator JOIN member ON member.id = initiator.member_id JOIN initiative ON initiative.id = initiator.initiative_id JOIN issue ON issue.id = initiative.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
745 query.addWhere('initiator.accepted');
746 fields.forEach( function(field) {
747 query.addField(field, null, ['grouped']);
748 });
749 general_params.addMemberOptions(req, query, params);
750 general_params.addInitiativeOptions(req, query, params);
751 query.addOrderBy('initiator.initiative_id, initiator.member_id');
752 general_params.addLimitAndOffset(query, params);
753 db.query(conn, req, res, query, function (initiator, conn) {
754 var result = { result: initiator.rows }
755 includes = [];
756 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
757 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
758 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
759 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
760 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
761 addRelatedData(conn, req, res, result, includes);
762 });
763 });
764 },
767 '/supporter': function (conn, req, res, params) {
768 requireAccessLevel(conn, req, res, 'pseudonym', function() {
769 var fields = ['supporter.issue_id', 'supporter.initiative_id', 'supporter.member_id', 'supporter.draft_id'];
770 var query = new selector.Selector();
771 query.from('supporter')
772 query.join('member', null, 'member.id = supporter.member_id JOIN initiative ON initiative.id = supporter.initiative_id JOIN issue ON issue.id = initiative.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
773 fields.forEach( function(field) {
774 query.addField(field, null, ['grouped']);
775 });
776 general_params.addMemberOptions(req, query, params);
777 general_params.addInitiativeOptions(req, query, params);
778 query.addOrderBy('supporter.issue_id, supporter.initiative_id, supporter.member_id');
779 general_params.addLimitAndOffset(query, params);
780 db.query(conn, req, res, query, function (supporter, conn) {
781 var result = { result: supporter.rows }
782 includes = [];
783 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
784 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
785 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
786 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
787 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
788 addRelatedData(conn, req, res, result, includes);
789 });
790 });
791 },
793 '/battle': function (conn, req, res, params) {
794 requireAccessLevel(conn, req, res, 'anonymous', function() {
795 var query = new selector.Selector();
796 query.from('battle JOIN initiative ON initiative.id = battle.winning_initiative_id OR initiative.id = battle.losing_initiative_id JOIN issue ON issue.id = battle.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
797 query.addField('battle.*');
798 general_params.addInitiativeOptions(req, query, params);
799 query.addOrderBy('battle.issue_id, battle.winning_initiative_id, battle.losing_initiative_id');
800 general_params.addLimitAndOffset(query, params);
801 db.query(conn, req, res, query, function (result, conn) {
802 var result = { result: result.rows }
803 includes = [];
804 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
805 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
806 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
807 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
808 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
809 addRelatedData(conn, req, res, result, includes);
810 });
811 });
812 },
814 '/draft': function (conn, req, res, params) {
815 requireAccessLevel(conn, req, res, 'anonymous', function() {
816 var fields = ['draft.initiative_id', 'draft.id', 'draft.formatting_engine', 'draft.content', 'draft.author_id'];
817 var query = new selector.Selector();
818 query.from('draft JOIN initiative ON initiative.id = draft.initiative_id JOIN issue ON issue.id = initiative.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
819 fields.forEach( function(field) {
820 query.addField(field, null, ['grouped']);
821 });
822 if (req.current_access_level != 'anonymous' || req.current_member_id) {
823 query.addField('draft.author_id');
824 }
825 if (params.draft_id) {
826 query.addWhere('draft.id = ?', params.draft_id);
827 }
828 if (params.current_draft) {
829 query.join('current_draft', null, 'current_draft.initiative_id = initiative.id AND current_draft.id = draft.id')
830 }
831 general_params.addInitiativeOptions(req, query, params);
832 query.addOrderBy('draft.initiative_id, draft.id');
833 general_params.addLimitAndOffset(query, params);
834 db.query(conn, req, res, query, function (result, conn) {
835 var result = { result: result.rows }
836 includes = [];
837 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
838 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
839 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
840 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
841 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
842 addRelatedData(conn, req, res, result, includes);
843 });
844 });
845 },
847 '/suggestion': function (conn, req, res, params) {
848 requireAccessLevel(conn, req, res, 'anonymous', function() {
849 var query = new selector.Selector();
850 query.from('suggestion JOIN initiative ON initiative.id = suggestion.initiative_id JOIN issue ON issue.id = initiative.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
851 if (req.current_access_level == 'anonymous' && !req.current_member_id ) {
852 fields.addObjectFields(query, 'suggestion', 'suggestion_pseudonym');
853 } else {
854 fields.addObjectFields(query, 'suggestion');
855 }
856 general_params.addSuggestionOptions(req, query, params);
857 query.addOrderBy('suggestion.initiative_id, suggestion.id');
858 general_params.addLimitAndOffset(query, params);
859 db.query(conn, req, res, query, function (result, conn) {
860 var result = { result: result.rows }
861 includes = [];
862 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
863 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
864 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
865 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
866 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
867 addRelatedData(conn, req, res, result, includes);
868 });
869 });
870 },
872 '/opinion': function (conn, req, res, params) {
873 requireAccessLevel(conn, req, res, 'pseudonym', function() {
874 var fields = ['opinion.initiative_id', 'opinion.suggestion_id', 'opinion.member_id', 'opinion.degree', 'opinion.fulfilled']
875 var query = new selector.Selector();
876 query.from('opinion JOIN member ON member.id = opinion.member_id JOIN suggestion ON suggestion.id = opinion.suggestion_id JOIN initiative ON initiative.id = suggestion.initiative_id JOIN issue ON issue.id = initiative.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
877 fields.forEach( function(field) {
878 query.addField(field, null, ['grouped']);
879 });
880 general_params.addMemberOptions(req, query, params);
881 general_params.addSuggestionOptions(req, query, params);
882 query.addOrderBy = ['opinion.initiative_id, opinion.suggestion_id, opinion.member_id'];
883 general_params.addLimitAndOffset(query, params);
884 db.query(conn, req, res, query, function (result, conn) {
885 var result = { result: result.rows }
886 includes = [];
887 if (params.include_suggestions) includes.push({ class: 'suggestion', objects: 'result'});
888 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'suggestions'});
889 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
890 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
891 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
892 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
893 addRelatedData(conn, req, res, result, includes);
894 });
895 });
896 },
898 '/delegation': function (conn, req, res, params) {
899 requireAccessLevel(conn, req, res, 'pseudonym', function() {
900 var fields = ['delegation.id', 'delegation.truster_id', 'delegation.trustee_id', 'delegation.scope', 'delegation.area_id', 'delegation.issue_id', 'delegation.unit_id'];
901 var query = new selector.Selector();
902 query.from('delegation LEFT JOIN issue on delegation.issue_id = issue.id LEFT JOIN policy ON policy.id = issue.policy_id LEFT JOIN area ON area.id = issue.area_id OR area.id = delegation.area_id LEFT JOIN unit ON area.unit_id = unit.id OR unit.id = delegation.unit_id');
903 fields.forEach( function(field) {
904 query.addField(field, null, ['grouped']);
905 });
906 if (params.direction) {
907 switch (params.direction) {
908 case 'in':
909 query.join('member', null, 'member.id = delegation.trustee_id');
910 break;
911 case 'out':
912 query.join('member', null, 'member.id = delegation.truster_id');
913 break;
914 default:
915 respond('json', conn, req, res, 'unprocessable', 'Direction must be "in" or "out" if set.');
916 }
917 } else {
918 query.join('member', null, 'member.id = delegation.truster_id OR member.id = delegation.trustee_id');
919 }
920 general_params.addMemberOptions(req, query, params);
921 general_params.addIssueOptions(req, query, params);
922 if (params.scope) {
923 query.addWhere(['delegation.scope IN (??)', params.scope.split(',')]);
924 };
925 query.addOrderBy = ['delegation.id'];
926 general_params.addLimitAndOffset(query, params);
927 db.query(conn, req, res, query, function (result, conn) {
928 respond('json', conn, req, res, 'ok', { result: result.rows });
929 });
930 });
931 },
933 '/vote': function (conn, req, res, params) {
934 requireAccessLevel(conn, req, res, 'pseudonym', function() {
935 var query = new selector.Selector();
936 query.from('vote JOIN member ON member.id = vote.member_id JOIN initiative ON initiative.id = vote.initiative_id JOIN issue ON issue.id = initiative.issue_id JOIN policy ON policy.id = issue.policy_id JOIN area ON area.id = issue.area_id JOIN unit ON area.unit_id = unit.id');
937 query.addField('vote.*');
938 query.addWhere('issue.closed_at NOTNULL');
939 general_params.addMemberOptions(req, query, params);
940 general_params.addInitiativeOptions(req, query, params);
941 general_params.addLimitAndOffset(query, params);
942 db.query(conn, req, res, query, function (result, conn) {
943 respond('json', conn, req, res, 'ok', { result: result.rows });
944 });
945 });
946 },
948 '/event': function (conn, req, res, params) {
949 requireAccessLevel(conn, req, res, 'anonymous', function() {
950 var fields = ['event.id', 'event.occurrence', 'event.event', 'event.member_id', 'event.issue_id', 'event.state', 'event.initiative_id', 'event.draft_id', 'event.suggestion_id'];
951 var query = new selector.Selector();
952 query.from('event LEFT JOIN member ON member.id = event.member_id LEFT JOIN initiative ON initiative.id = event.initiative_id LEFT JOIN issue ON issue.id = event.issue_id LEFT JOIN policy ON policy.id = issue.policy_id LEFT JOIN area ON area.id = issue.area_id LEFT JOIN unit ON area.unit_id = unit.id');
953 fields.forEach( function(field) {
954 query.addField(field, null, ['grouped']);
955 });
956 general_params.addMemberOptions(req, query, params);
957 general_params.addInitiativeOptions(req, query, params);
958 query.addOrderBy('event.id');
959 general_params.addLimitAndOffset(query, params);
960 db.query(conn, req, res, query, function (events, conn) {
961 var result = { result: events.rows }
962 includes = [];
963 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
964 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
965 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
966 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
967 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
968 addRelatedData(conn, req, res, result, includes);
969 });
970 });
971 },
973 // TODO add interfaces for data structure:
974 // ignored_member requireAccessLevel(conn, req, res, 'member');
975 // ignored_initiative requireAccessLevel(conn, req, res, 'member');
976 // setting requireAccessLevel(conn, req, res, 'member');
978 };
980 // ==========================================================================
981 // POST methods
982 // ==========================================================================
986 exports.post = {
988 '/echo_test': function (conn, req, res, params) { requireAccessLevel(conn, req, res, 'anonymous', function() {
989 respond('json', conn, req, res, 'ok', { result: params });
990 }); },
992 '/register_test': function (conn, req, res, params) {
993 var understood = params.understood;
994 var member_login = randomString(16);
995 var member_name = params.name;
996 var member_password = randomString(16);
997 var member_notify_email = params.email;
998 var member_notify_email_secret = randomString(24);
999 var api_key_member = randomString(24);
1000 var api_key_full = randomString(24);
1001 var api_key_pseudonym = randomString(24);
1002 var api_key_anonymous = randomString(24);
1004 if (understood != 'understood') {
1005 respond('html', conn, req, res, 'unprocessable', null, 'You didn\'t checked the checkbox! Please hit back in your browser and try again.');
1006 return;
1009 // add member
1010 var query = new selector.SQLInsert('member');
1011 query.addValues({
1012 login: member_login,
1013 password: member_password, // TODO hashing of password
1014 notify_email_unconfirmed: member_notify_email,
1015 notify_email_secret: member_notify_email_secret,
1016 name: member_name
1017 });
1018 query.addReturning('id');
1019 db.query(conn, req, res, query, function (result, conn) {
1020 var member_id = result.rows[0].id;
1022 // add privilege for root unit
1023 var query = new selector.SQLInsert('privilege');
1024 query.addValues({ unit_id: 1, member_id: member_id, voting_right: true });
1025 db.query(conn, req, res, query, function (result, conn) {
1027 var location = params.location;
1028 var unit_id;
1029 switch(location) {
1030 case 'earth':
1031 unit_id = 3;
1032 break;
1033 case 'moon':
1034 unit_id = 4;
1035 break;
1036 case 'mars':
1037 unit_id = 5;
1038 break;
1041 // add privilege for selected planet
1042 var query = new selector.SQLInsert('privilege');
1043 query.addValues({ unit_id: unit_id, member_id: member_id, voting_right: true });
1044 db.query(conn, req, res, query, function (result, conn) {
1046 // add application key
1047 var query = new selector.SQLInsert('member_application');
1048 query.addValues({
1049 member_id: member_id,
1050 name: 'member',
1051 comment: 'access_level member',
1052 access_level: 'member',
1053 key: api_key_member
1054 });
1055 query.addReturning('id');
1057 db.query(conn, req, res, query, function (result, conn) {
1059 nodemailer.sendmail = '/usr/sbin/sendmail';
1061 // send email to user
1062 nodemailer.send_mail({
1063 sender: config.mail.from,
1064 subject: config.mail.subject_prefix + "Your LiquidFeedback API alpha test account needs confirmation",
1065 to: member_notify_email,
1066 body: "\
1067 Hello " + member_name + ",\n\
1068 \n\
1069 thank you for registering at the public alpha test of the LiquidFeedback\n\
1070 application programming interface. To complete the registration process,\n\
1071 you need to confirm your email address by opening the following URL:\n\
1072 \n\
1073 " + config.public_url_path + "register_test_confirm?secret=" + member_notify_email_secret + "\n\
1074 \n\
1075 \n\
1076 After you've confirmed your email address, your account will be automatically\n\
1077 activated.\n\
1078 \n\
1079 Your account name is: " + member_name + "\n\
1080 \n\
1081 \n\
1082 You will need the following login and password to register and unregister\n\
1083 applications for your account later. This function is currently not\n\
1084 implemented, but please keep the credentials for future use.\n\
1085 \n\
1086 Account ID: " + member_id + "\n\
1087 Login: " + member_login + "\n\
1088 Password: " + member_password + "\n\
1089 \n\
1090 \n\
1091 To make you able to actually access the API interface, we added the following\n\
1092 application key with full member access privileges to your account:\n\
1093 \n\
1094 API Key: " + api_key_member + "\n\
1095 \n\
1096 \n\
1097 The base address of the public test is: " + config.public_url_path + "\n\
1098 \n\
1099 The programming interface is described in the LiquidFeedback API\n\
1100 specification: http://dev.liquidfeedback.org/trac/lf/wiki/API\n\
1101 \n\
1102 The current implementation status of lfapi is published at the LiquidFeedback\n\
1103 API server page: http://dev.liquidfeedback.org/trac/lf/wiki/lfapi\n\
1104 \n\
1105 If you have any questions or suggestions, please use our public mailing list\n\
1106 at http://dev.liquidfeedback.org/cgi-bin/mailman/listinfo/main\n\
1107 \n\
1108 For issues regarding your test account, contact us via email at\n\
1109 lqfb-maintainers@public-software-group.org\n\
1110 \n\
1111 \n\
1112 Sincerely,\n\
1113 \n\
1114 Your LiquidFeedback maintainers",
1115 },
1116 function(err, result){
1117 if(err){ console.log(err); }
1118 });
1120 respond('html', conn, req, res, 'ok', 'Account created. Please check your mailbox!<br /><br /><br /><a href="/">Back to start page</a>');
1121 });
1122 });
1123 });
1124 });
1125 },
1127 /*
1128 '/register': function (conn, req, res, params) {
1129 var invite_key = params.invite_key;
1130 var login = params.login;
1131 var password = params.password;
1132 var name = params.name;
1133 var notify_email = params.notify_email;
1134 if (!invite_key) {
1135 respond('json', conn, req, res, 'unprocessable', null, 'No invite_key supplied.');
1136 return;
1137 };
1138 if (!login) {
1139 respond('json', conn, req, res, 'unprocessable', null, 'No login supplied.');
1140 return;
1141 };
1142 if (!password) {
1143 respond('json', conn, req, res, 'unprocessable', null, 'No password supplied.');
1144 return;
1145 };
1146 if (!name) {
1147 respond('json', conn, req, res, 'unprocessable', null, 'No name supplied.');
1148 return;
1149 };
1150 if (!notify_email) {
1151 respond('json', conn, req, res, 'unprocessable', null, 'No notify_email supplied.');
1152 return;
1153 };
1154 // check if akey is valid and get member_id for akey
1155 db.query(conn, req, res, { select: ['member.id'], from: ['member'], where: ['NOT member.activation AND member.invite_key = ' + db.pgEncode(invite_key)] }, function (result, conn) {
1156 if (result.rows.length != 1) {
1157 respond('json', conn, req, res, 'forbidden', null, 'Supplied invite_key is not valid.');
1158 return;
1159 };
1160 var member_id = result.rows[0].id;
1161 // check if name is available
1162 db.query(conn, req, res, { select: ['NULL'], from: ['member'], where: ['member.name = ' + db.pgEncode(name)] }, function (result, conn) {
1163 if (result.rows.length > 0) {
1164 respond('json', conn, req, res, 'forbidden', null, 'Login name is not available, choose another one.');
1165 return;
1166 };
1167 // check if login is available
1168 db.query(conn, req, res, { select: ['NULL'], from: ['member'], where: ['member.login = ' + db.pgEncode(login)] }, function (result, conn) {
1169 if (result.rows.length > 0) {
1170 respond('json', conn, req, res, 'forbidden', null, 'Name is not available, choose another one.');
1171 return;
1172 };
1173 var query = { update: 'member', set: { activation: 'now', active: true, } };
1175 });
1176 });
1177 });
1178 },
1179 */
1181 '/session': function (conn, req, res, params) {
1182 var key = params.key;
1183 if (!key) {
1184 respond('json', conn, req, res, 'unprocessable', null, 'No application key supplied.');
1185 return;
1186 };
1187 var query = new selector.Selector();
1188 query.from('member');
1189 query.join('member_application', null, 'member_application.member_id = member.id');
1190 query.addField('member.id');
1191 query.addWhere(['member.active AND member_application.key = ?', key]);
1192 if (params.interactive) {
1193 query.forUpdateOf('member');
1195 db.query(conn, req, res, query, function (result, conn) {
1196 if (result.rows.length != 1) {
1197 respond('json', conn, req, res, 'forbidden', null, 'Supplied application key is not valid.');
1198 return;
1199 };
1200 var member_id = result.rows[0].id;
1201 var session_key = randomString(16);
1202 req.sessions[session_key] = member_id;
1203 var query;
1204 if (params.interactive) {
1205 query = new selector.SQLUpdate('member');
1206 query.addWhere(['member.id = ?', member_id]);
1207 query.addValues({ last_activity: 'now' });
1209 db.query(conn, req, res, query, function (result, conn) {
1210 respond('json', conn, req, res, 'ok', { session_key: session_key });
1211 });
1212 });
1213 },
1215 '/member': function (conn, req, res, params) {
1216 var fields = ['organizational_unit', 'internal_posts', 'realname', 'birthday', 'address', 'email', 'xmpp_address', 'website', 'phone', 'mobile_phone', 'profession', 'external_memberships', 'external_posts', 'statement']
1217 requireAccessLevel(conn, req, res, 'member', function() {
1218 var query = new selector.SQLUpdate('member');
1219 query.addWhere(['member.id = ?', req.current_member_id]);
1220 fields.forEach( function(field) {
1221 if (typeof(params[field]) != 'undefined') {
1222 query.addValues({ field: params[field] });
1223 } else {
1224 query.addValues({ field: null });
1226 });
1227 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1228 });
1229 },
1231 '/membership': function (conn, req, res, params) {
1232 requireAccessLevel(conn, req, res, 'member', function() {
1234 // check if area_id is set
1235 var area_id = parseInt(params.area_id);
1236 if (!area_id) {
1237 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an area_id.');
1238 return;
1241 // delete membership
1242 if (params.delete) {
1243 var query;
1244 query = new selector.SQLDelete('membership');
1245 query.addWhere(['area_id = ?', area_id]);
1246 query.addWhere(['member_id = ?', req.current_member_id]);
1247 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1249 // add membership
1250 } else {
1252 // lock member for upsert
1253 lockMemberById(conn, req, res, req.current_member_id, function() {
1255 // check and lock privilege
1256 requireAreaPrivilege(conn, req, res, area_id, function() {
1258 // upsert membership
1259 var query = new selector.Upserter('membership', ['area_id', 'member_id']);
1260 query.addValues({ area_id: area_id, member_id: req.current_member_id });
1261 db.query(conn, req, res, query, function(result) {
1262 respond('json', conn, req, res, 'ok');
1263 });
1264 });
1265 });
1267 });
1268 },
1270 '/interest': function (conn, req, res, params) {
1271 requireAccessLevel(conn, req, res, 'member', function() {
1272 var query;
1274 // check if issue_id is set
1275 var issue_id = parseInt(params.issue_id);
1276 if (!issue_id) {
1277 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1278 return;
1281 // lock member for upsert
1282 lockMemberById(conn, req, res, req.current_member_id, function() {
1284 // delete interest
1285 if (params.delete) {
1287 // check issue state
1288 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1290 // delete interest
1291 query = new selector.SQLDelete('interest');
1292 query.addWhere(['issue_id = ?', issue_id]);
1293 query.addWhere(['member_id = ?', req.current_member_id]);
1294 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1295 });
1297 // add interest
1298 } else {
1300 // check and lock privilege
1301 requireIssuePrivilege(conn, req, res, issue_id, function() {
1303 // check issue state
1304 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1306 // upsert interest
1307 var query = new selector.Upserter('interest', ['issue_id', 'member_id']);
1308 query.addValues({ issue_id: issue_id, member_id: req.current_member_id });
1309 db.query(conn, req, res, query, function(result) {
1310 respond('json', conn, req, res, 'ok');
1311 });
1312 });
1313 });
1314 };
1315 });
1316 });
1317 },
1319 '/issue_comment': function (conn, req, res, params) {
1320 requireAccessLevel(conn, req, res, 'member', function() {
1322 var issue_id = parseInt(params.issue_id);
1323 var formatting_engine = params.formatting_engine
1324 var content = params.content;
1326 if (!issue_id) {
1327 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1328 return;
1331 // delete issue comment
1332 if (params.delete) {
1333 var query;
1334 query = new selector.SQLDelete('issue_comment');
1335 query.addWhere(['issue_id = ?', params.issue_id]);
1336 query.addWhere(['member_id = ?', req.current_member_id]);
1337 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1339 // upsert issue comment
1340 } else {
1342 // check if formatting engine is supplied and valid
1343 if (!formatting_engine) {
1344 respond('json', conn, req, res, 'unprocessable', null, 'No formatting engine supplied.');
1345 return;
1346 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1347 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1348 return;
1349 };
1351 // check if content is supplied
1352 if (!content) {
1353 respond('json', conn, req, res, 'unprocessable', null, 'No content supplied.');
1354 return;
1357 // lock member for upsert
1358 lockMemberById(conn, req, res, req.current_member_id, function() {
1360 // check and lock privilege
1361 requireIssuePrivilege(conn, req, res, issue_id, function() {
1363 // upsert issue comment
1364 var query = new selector.Upserter('issue_comment', ['issue_id', 'member_id']);
1365 query.addValues({
1366 issue_id: issue_id,
1367 member_id: req.current_member_id,
1368 changed: 'now',
1369 formatting_engine: formatting_engine,
1370 content: content
1371 });
1373 db.query(conn, req, res, query, function(result) {
1374 respond('json', conn, req, res, 'ok');
1375 });
1377 });
1378 });
1382 });
1383 },
1385 '/voting_comment': function (conn, req, res, params) {
1386 requireAccessLevel(conn, req, res, 'member', function() {
1388 var issue_id = parseInt(params.issue_id);
1389 var formatting_engine = params.formatting_engine
1390 var content = params.content;
1392 if (!issue_id) {
1393 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1394 return;
1398 // delete voting comment
1399 if (params.delete) {
1400 var query;
1401 query = new selector.SQLDelete('voting_comment');
1402 query.addWhere(['issue_id = ?', params.issue_id]);
1403 query.addWhere(['member_id = ?', req.current_member_id]);
1404 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1406 // upsert voting comment
1407 } else {
1409 // check if formatting engine is supplied and valid
1410 if (!formatting_engine) {
1411 respond('json', conn, req, res, 'unprocessable', null, 'No formatting engine supplied.');
1412 return;
1413 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1414 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1415 return;
1416 };
1418 // check if content is supplied
1419 if (!content) {
1420 respond('json', conn, req, res, 'unprocessable', null, 'No content supplied.');
1421 return;
1424 // lock member for upsert
1425 lockMemberById(conn, req, res, req.current_member_id, function() {
1427 // check and lock privilege
1428 requireIssuePrivilege(conn, req, res, issue_id, function() {
1430 // check issue state
1431 requireIssueState(conn, req, res, issue_id, ['voting', 'finished_with_winner', 'finished_without_winner'], function() {
1433 // upsert voting comment
1434 var query = new selector.Upserter('voting_comment', ['issue_id', 'member_id']);
1435 query.addValues({
1436 issue_id: issue_id,
1437 member_id: req.current_member_id,
1438 changed: 'now',
1439 formatting_engine: formatting_engine,
1440 content: content
1441 });
1443 db.query(conn, req, res, query, function(result) {
1444 respond('json', conn, req, res, 'ok');
1445 });
1447 });
1448 });
1449 })
1450 };
1451 });
1452 },
1454 '/supporter': function (conn, req, res, params) {
1455 requireAccessLevel(conn, req, res, 'member', function() {
1456 var initiative_id = parseInt(params.initiative_id);
1457 var draft_id = parseInt(params.draft_id);
1459 // check if needed arguments are supplied
1460 if (!initiative_id) {
1461 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an initiative_id.');
1462 return;
1465 if (!draft_id) {
1466 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an draft_id.');
1467 return;
1470 // lock member for upsert
1471 lockMemberById(conn, req, res, req.current_member_id, function() {
1473 // delete supporter
1474 if (params.delete) {
1476 // check issue state
1477 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion', 'verification'], function() {
1479 // delete supporter
1480 var query = new selector.SQLDelete('supporter');
1481 query.addWhere(['initiative_id = ?', initiative_id]);
1482 query.addWhere(['member_id = ?', req.current_member_id]);
1483 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1485 });
1487 // upsert supporter
1488 } else {
1490 // check and lock privilege
1491 requireInitiativePrivilege(conn, req, res, initiative_id, function() {
1493 // check issue state
1494 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion', 'verification'], function() {
1496 // check if given draft is the current one
1497 var query = new selector.Selector('current_draft');
1498 query.addField('NULL');
1499 query.addWhere(['current_draft.initiative_id = ?', initiative_id]);
1500 query.addWhere(['current_draft.id = ?', draft_id]);
1502 db.query(conn, req, res, query, function(result) {
1503 if (result.rows.length != 1) {
1504 respond('json', conn, req, res, 'conflict', null, 'The draft with the supplied draft_id is not the current one anymore!');
1505 return;
1508 // upsert supporter
1509 var query = new selector.Upserter('supporter', ['initiative_id', 'member_id']);
1510 query.addValues({
1511 initiative_id: initiative_id,
1512 member_id: req.current_member_id,
1513 draft_id: draft_id
1514 });
1516 db.query(conn, req, res, query, function(result) {
1517 respond('json', conn, req, res, 'ok');
1518 });
1520 });
1521 });
1522 });
1523 };
1524 });
1525 });
1526 },
1528 '/draft': function (conn, req, res, params) {
1529 requireAccessLevel(conn, req, res, 'member', function() {
1530 var area_id = parseInt(params.area_id);
1531 var policy_id = parseInt(params.policy_id);
1532 var issue_id = parseInt(params.issue_id);
1533 var initiative_id = parseInt(params.initiative_id);
1534 var initiative_name = params.initiative_name;
1535 var initiative_discussion_url = params.initiative_discussion_url;
1536 var formatting_engine = params.formatting_engine;
1537 var content = params.content;
1539 if (!initiative_discussion_url) initiative_discussion_url = null;
1541 // check parameters
1542 if (!formatting_engine) {
1543 respond('json', conn, req, res, 'unprocessable', null, 'No formatting_engine supplied.');
1544 return;
1545 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1546 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1547 return;
1548 };
1550 if (!content) {
1551 respond('json', conn, req, res, 'unprocessable', null, 'No draft content supplied.');
1552 return;
1553 };
1555 lockMemberById(conn, req, res, req.current_member_id, function() {
1557 // new draft in new initiative in new issue
1558 if (area_id && !issue_id && !initiative_id) {
1560 // check parameters for new issue
1561 if (!policy_id) {
1562 respond('json', conn, req, res, 'unprocessable', null, 'No policy supplied.');
1563 return;
1566 if (!initiative_name) {
1567 respond('json', conn, req, res, 'unprocessable', null, 'No initiative name supplied.');
1568 return;
1571 requireAreaPrivilege(conn, req, res, area_id, function() {
1573 // check if policy is allowed in this area and if area and policy are active
1574 var query = new selector.Selector();
1575 query.from('allowed_policy');
1576 query.join('area', null, 'area.id = allowed_policy.area_id AND area.active');
1577 query.join('policy', null, 'policy.id = allowed_policy.policy_id AND policy.active');
1578 query.addField('NULL');
1579 query.addWhere(['area.id = ? AND policy.id = ?', area_id, policy_id]);
1580 db.query(conn, req, res, query, function (result, conn) {
1581 if (result.rows.length != 1) {
1582 respond('json', conn, req, res, 'unprocessable', null, 'Area and/or policy doesn\'t exist, area and/or policy is not active or policy is not allowed in this area.');
1583 return;
1584 };
1586 // check contingent
1587 requireContingentLeft(conn, req, res, true, function() {
1589 // insert new issue
1590 var query = new selector.SQLInsert('issue');
1591 query.addValues({
1592 area_id: area_id,
1593 policy_id: policy_id
1594 });
1595 query.addReturning('id');
1596 db.query(conn, req, res, query, function(result) {
1597 var issue_id = result.rows[0].id;
1599 // insert new initiative
1600 var query = new selector.SQLInsert('initiative');
1601 query.addValues({
1602 issue_id: issue_id,
1603 name: initiative_name,
1604 discussion_url: initiative_discussion_url
1605 });
1606 query.addReturning('id');
1607 db.query(conn, req, res, query, function(result) {
1608 var initiative_id = result.rows[0].id;
1610 // insert initiator
1611 var query = new selector.SQLInsert('initiator');
1612 query.addValues({ initiative_id: initiative_id, member_id: req.current_member_id, accepted: true });
1613 db.query(conn, req, res, query, function(result) {
1615 // insert new draft
1616 var query = new selector.SQLInsert('draft');
1617 query.addValues({
1618 initiative_id: initiative_id,
1619 author_id: req.current_member_id,
1620 formatting_engine: formatting_engine,
1621 content: content
1622 });
1623 query.addReturning('id');
1624 db.query(conn, req, res, query, function (result, conn) {
1625 var draft_id = result.rows[0].id;
1627 respond('json', conn, req, res, 'ok', { issue_id: issue_id, initiative_id: initiative_id, draft_id: draft_id } );
1628 });
1629 });
1630 });
1631 });
1632 });
1633 });
1634 });
1636 // new draft in new initiative in existant issue
1637 } else if (issue_id && !area_id && !initiative_id) {
1639 if (!initiative_name) {
1640 respond('json', conn, req, res, 'unprocessable', null, 'No initiative name supplied.');
1641 return;
1644 // check privilege
1645 requireIssuePrivilege(conn, req, res, issue_id, function() {
1647 // check issue state
1648 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1650 // check contingent
1651 requireContingentLeft(conn, req, res, true, function() {
1653 // insert initiative
1654 var query = new selector.SQLInsert('initiative');
1655 query.addValues({
1656 issue_id: issue_id,
1657 name: initiative_name,
1658 discussion_url: initiative_discussion_url
1659 });
1660 query.addReturning('id');
1661 db.query(conn, req, res, query, function(result) {
1662 var initiative_id = result.rows[0].id;
1664 // insert initiator
1665 var query = new selector.SQLInsert('initiator');
1666 query.addValues({
1667 initiative_id: initiative_id,
1668 member_id: req.current_member_id,
1669 accepted: true
1670 });
1671 db.query(conn, req, res, query, function(result) {
1673 // insert draft
1674 var query = new selector.SQLInsert('draft');
1675 query.addValues({
1676 initiative_id: initiative_id,
1677 author_id: req.current_member_id,
1678 formatting_engine: formatting_engine,
1679 content: content
1680 });
1681 query.addReturning('id');
1682 db.query(conn, req, res, query, function (result, conn) {
1684 var draft_id = result.rows[0].id;
1685 respond('json', conn, req, res, 'ok', { initiative_id: initiative_id, draft_id: draft_id } );
1687 });
1688 });
1689 });
1690 });
1691 });
1692 });
1694 // new draft in existant initiative
1695 } else if (initiative_id && !area_id && !issue_id ) {
1697 // check privilege
1698 requireInitiativePrivilege(conn, req, res, initiative_id, function() {
1700 // check issue state
1701 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion'], function() {
1704 // get initiator
1705 var query = new selector.Selector();
1706 query.from('initiator');
1707 query.addField('accepted');
1708 query.addWhere(['initiative_id = ? AND member_id = ?', initiative_id, req.current_member_id]);
1709 db.query(conn, req, res, query, function (result, conn) {
1711 // if member is not initiator, deny creating new draft
1712 if (result.rows.length != 1) {
1713 respond('json', conn, req, res, 'forbidden', null, 'You are not initiator of this initiative and not allowed to update its draft.');
1714 return;
1716 var initiator = result.rows[0];
1717 if (!initiator.accepted) {
1718 respond('json', conn, req, res, 'forbidden', null, 'You have been invited as initiator, but haven\'t accepted invitation and you are not allowed to update this initiative.');
1719 return;
1720 };
1722 // check contingent
1723 requireContingentLeft(conn, req, res, false, function() {
1725 // insert new draft
1726 var query = new selector.SQLInsert('draft');
1727 query.addValues({
1728 initiative_id: initiative_id,
1729 author_id: req.current_member_id,
1730 formatting_engine: formatting_engine,
1731 content: content
1732 });
1733 query.addReturning('id');
1734 db.query(conn, req, res, query, function (result, conn) {
1736 var draft_id = result.rows[0].id;
1737 respond('json', conn, req, res, 'ok', { draft_id: draft_id } );
1738 });
1739 });
1740 });
1741 });
1742 });
1744 // none of them (invalid request)
1745 } else {
1746 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of area_id, issue_id or initiative_id must be supplied!');
1747 };
1749 });
1750 });
1751 },
1753 '/suggestion': function (conn, req, res, params) {
1754 requireAccessLevel(conn, req, res, 'member', function() {
1755 // TODO
1756 });
1757 },
1759 '/opinion': function (conn, req, res, params) {
1760 requireAccessLevel(conn, req, res, 'member', function() {
1761 // TODO
1762 });
1763 },
1765 '/delegation': function (conn, req, res, params) {
1766 requireAccessLevel(conn, req, res, 'member', function() {
1767 var unit_id = parseInt(params.unit_id);
1768 var area_id = parseInt(params.area_id);
1769 var issue_id = parseInt(params.issue_id);
1770 var trustee_id;
1772 if (params.trustee_id == '') {
1773 trustee_id = null;
1774 } else {
1775 trustee_id = parseInt(params.trustee_id);
1778 lockMemberById(conn, req, res, req.current_member_id, function() {
1780 if (params.delete) {
1781 var query = new selector.SQLDelete('delegation')
1782 if (unit_id && !area_id && !issue_id) {
1783 query.addWhere(['unit_id = ?', unit_id]);
1784 } else if (!unit_id && area_id && !issue_id) {
1785 query.addWhere(['area_id = ?', area_id]);
1786 } else if (!unit_id && !area_id && issue_id) {
1787 query.addWhere(['issue_id = ?', issue_id]);
1788 } else {
1789 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of unit, area_id, issue_id must be supplied!');
1790 return;
1792 query.addWhere(['truster_id = ?', req.current_member_id]);
1793 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1794 } else {
1795 var query = new selector.Upserter('delegation', ['truster_id']);
1796 query.addValues({
1797 truster_id: req.current_member_id,
1798 trustee_id: trustee_id
1799 });
1800 if (unit_id && !area_id && !issue_id) {
1802 // check privilege
1803 requireUnitPrivilege(conn, req, res, unit_id, function() {
1805 query.addKeys(['unit_id'])
1806 query.addValues({ unit_id: unit_id, scope: 'unit' });
1807 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1808 });
1810 } else if (!unit_id && area_id && !issue_id) {
1812 // check privilege
1813 requireAreaPrivilege(conn, req, res, area_id, function() {
1815 query.addKeys(['area_id'])
1816 query.addValues({ area_id: area_id, scope: 'area' });
1817 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1818 });
1820 } else if (!unit_id && !area_id && issue_id) {
1822 // check privilege
1823 requireIssuePrivilege(conn, req, res, issue_id, function() {
1825 // check issue state
1826 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification', 'voting'], function() {
1828 query.addKeys(['issue_id'])
1829 query.addValues({ issue_id: issue_id, scope: 'issue' });
1830 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1831 });
1832 });
1833 } else {
1834 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of unit_id, area_id, issue_id must be supplied!');
1835 return;
1839 });
1841 });
1842 },
1844 };

Impressum / About Us